Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/os-create-emits-an-installable-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@objectstack/cli": minor
---

`os create` now emits a project that installs outside this monorepo.

Every project the command scaffolded declared its `@objectstack/*` dependencies
with pnpm's `workspace:*` protocol, extended a `tsconfig.json` two directories
above itself, and was written into this repository's own `packages/plugins/` or
`examples/` by default — so a developer following the documented command got a
project `pnpm install` refuses. The default emission is now standalone:

- `@objectstack/*` dependencies are published semver ranges pinned to the
version of the CLI that generated them;
- the emitted `tsconfig.json` is self-contained and extends nothing;
- the project is written to `./<name>` in the current directory (or `--dir`);
- a `pnpm-workspace.yaml` carries the build approvals a fresh `pnpm install`
needs on pnpm 11.

The `plugin` template also emits `init` where it used to emit `initialize`.
`initialize` is not part of the `Plugin` contract, so the scaffold did not
type-check under its own `strict` config (TS7006 on the untyped `context`
parameter) and `kernel.use()` refused the plugin at load with
`Plugin init function is required` — a defect the kernel protocol docs
previously carried a warning about instead of a fix.

The previous monorepo-internal placement is still available for ObjectStack
platform work as the explicit `--in-repo` flag, which keeps the `workspace:*`
specs and writes into `packages/plugins/` or `examples/`.
123 changes: 123 additions & 0 deletions .github/workflows/os-create-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# `os create` scaffold smoke — the emitted project installs and builds OUTSIDE
# this monorepo (#14824).
#
# ## What this gate holds
#
# `os create` is presented on four public documentation pages as a user-facing
# scaffolder, and every project it emitted was monorepo-shaped: `workspace:*`
# dependency specs, a `tsconfig.json` extending `'../../tsconfig.json'`, and a
# default output directory inside this repository. A reader who followed the
# docs got a project `pnpm install` refuses. The maintainer ruled that a
# documented developer-facing command must work for the developer who follows
# the docs, and attached an executable criterion: scaffold each template into a
# temporary directory outside the repository, install it with the registry, and
# boot / typecheck it — green outside the monorepo, ON CI, not on a developer
# box. `scripts/create-scaffold-smoke.sh` is that criterion; this workflow is
# the "on CI" half.
#
# ## Why paths-filtered rather than label-gated opt-in
#
# `pack-smoke-optin.yml` covers a different defect class — an author widening
# the unauthenticated surface — where the only reliable trigger is the author
# recognising their own change, so a label is the honest shape and its header
# forbids growing a `paths:` filter. This gate's defect class is the opposite:
# it can only be introduced by editing a bounded, nameable set of files, and
# those files are the `paths:` below. A label would mean a scaffolder change
# could be merged by anyone who did not think to apply it, which is precisely
# how the emitted contract drifted into being uninstallable in the first place.
#
# `init.ts` is in the set even though this gate does not test `os init`. The
# standalone emission CALLS that module — `getCliVersion`, `SCAFFOLD_PNPM_RANGE`
# and `renderPnpmWorkspaceYaml` are its exports — so its build approvals and its
# version resolution decide whether an `os create` scaffold installs. Naming the
# consumer and not the producer is the shape of coupling that lets a gate sit
# green through the change that breaks it.
#
# The nightly run is the backstop for everything the `paths:` set cannot name: a
# scaffolded project resolves the whole `@objectstack/*` graph, so a change in
# any of those packages can break its install or its boot without touching a
# single file listed here.
#
# ⛔ Not a required context — `scripts/check-required-contexts.mjs` owns that
# registry, and a paths-filtered job cannot be required: on a PR that does not
# trip the filter it never reports, and branch protection would block forever.
# It is advisory in the same way `scaffold-e2e.yml` is.
#
# Every step below is part of a build / scaffold / install / build pipeline, not
# a named local verification a dev pre-runs with `pnpm check:x`:
# dispatch-gates: no-check-families -- scaffold + install + build pipeline, no named local check family exists for it

name: OS Create Smoke

on:
pull_request:
branches:
- main
paths:
- 'packages/cli/src/commands/create.ts'
- 'packages/cli/src/commands/init.ts'
- 'scripts/create-scaffold-smoke.sh'
- 'scripts/publish-smoke-pack.mjs'
- '.github/workflows/os-create-smoke.yml'
schedule:
- cron: '41 4 * * *'
workflow_dispatch:

permissions:
contents: read

jobs:
create-scaffold-smoke:
name: Scaffold outside the monorepo, install, build
runs-on: ubuntu-latest
timeout-minutes: 60
concurrency:
group: os-create-smoke-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
steps:
# No `ref:` — on `pull_request` the default checkout is `refs/pull/N/merge`,
# the merge preview, which is what `main` will actually contain.
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Setup pnpm
uses: ./.github/actions/setup-pnpm

- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-

- name: Setup turbo cache
uses: actions/cache@v6
with:
path: .turbo/cache
key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.job }}-
${{ runner.os }}-turbo-

- name: Install dependencies
run: pnpm install --frozen-lockfile

# The smoke's own prerequisite, asserted by the script itself: it refuses
# to run without packages/cli/dist and the `os` bin.
- name: Build
run: pnpm run build

# ⛔ Do not add flags or env here to make a red go away. A refusal this
# script reports is a refusal a developer following the docs would get.
- name: Scaffold smoke (packed tarballs, outside the repo)
run: bash scripts/create-scaffold-smoke.sh
27 changes: 23 additions & 4 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,7 @@ only.
| Command | Alias | Description |
|---------|-------|-------------|
| `os generate <type> <name>` | `os g` | Generate metadata files |
| `os create <type> [name]` | | Create a new package from template |
| `os create <type> [name]` | | Scaffold a standalone plugin or example project |

#### `os generate` (alias: `os g`)

Expand Down Expand Up @@ -1263,13 +1263,32 @@ third-party extension primitive, authored as `src/skills/<name>.skill.ts` with

#### `os create`

Creates new packages from built-in templates (for monorepo-level scaffolding):
Scaffolds a **standalone** project — a plugin, or an example application — into
the current directory:

```bash
os create plugin analytics # Create packages/plugins/plugin-analytics
os create example my-app # Create examples/my-app
os create plugin analytics # Create ./plugin-analytics
os create example my-app # Create ./my-app

cd plugin-analytics
pnpm install
pnpm build
```

The emitted `package.json` declares its `@objectstack/*` dependencies as
published semver ranges pinned to the version of the CLI that generated it, and
the emitted `tsconfig.json` is self-contained, so the project installs and
builds anywhere — a workspace around it is neither needed nor assumed.

**Options:**
- `-d, --dir <directory>` — Write the project here instead of `./<name>`
- `--in-repo` — Scaffold **inside an ObjectStack monorepo checkout** instead
(`packages/plugins/<name>` for a plugin, `examples/<name>` for an example),
with `workspace:*` dependencies and a `tsconfig.json` that extends the
repository root config. For ObjectStack platform work only: the project it
writes installs nowhere else, and the command refuses the flag when the
current directory is not a pnpm workspace root.

### Quality

| Command | Description |
Expand Down
16 changes: 14 additions & 2 deletions content/docs/plugins/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,30 @@ Plugins follow a strict three-phase lifecycle (`init()` → `start()` → `destr
The fastest way to create a plugin is with the CLI scaffolding:

```bash
# Create a new plugin project
# Create a new plugin project in the current directory
os create plugin my-feature

# This creates:
# packages/plugins/plugin-my-feature/
# plugin-my-feature/
# ├── package.json
# ├── tsconfig.json
# ├── README.md
# ├── pnpm-workspace.yaml
# └── src/
# └── index.ts

cd plugin-my-feature
pnpm install
pnpm build
```

The scaffold is a **standalone** project: its `package.json` depends on the
published `@objectstack/*` releases that match the CLI which generated it, and
its `tsconfig.json` extends nothing outside the project — so it installs and
builds wherever you put it. Add `--in-repo` only when you are scaffolding into
a checkout of the ObjectStack monorepo itself; that placement emits
`workspace:*` dependencies and installs nowhere else.

For the full walkthrough — implementing the `Plugin` interface, registering services and hooks, testing, and registering with the kernel — see the [Plugin Development](/docs/plugins/development) tutorial.

---
Expand Down
27 changes: 17 additions & 10 deletions content/docs/protocol/kernel/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -381,24 +381,31 @@ All system configuration lives in Git:

### Plugin Development
```bash
# Scaffold new plugin (created under packages/plugins/plugin-<name>/)
# Scaffold new plugin (created as ./plugin-<name>/ in the current directory)
os create plugin slack-integration

# Generated structure:
packages/plugins/plugin-slack-integration/
package.json # name, version, dependencies (@objectstack/spec, zod)
tsconfig.json
plugin-slack-integration/
package.json # name, version, dependencies (@objectstack/spec, zod)
tsconfig.json # self-contained, extends nothing outside the project
pnpm-workspace.yaml # the pnpm build approvals a fresh install needs
src/
index.ts # default-export Plugin object (name, version, initialize, destroy)
index.ts # default-export Plugin object (name, version, init, destroy)
README.md
```

<Callout type="warn">
The scaffold still emits an `initialize` method. The kernel's plugin contract
only invokes `init` / `start` / `destroy`, and `init` is **required** — rename
`initialize` to `init` in the generated `src/index.ts` or `kernel.use()`
rejects the plugin outright with
The emitted dependencies are published semver ranges pinned to the CLI that
generated them, so `pnpm install && pnpm build` works in the new directory
without a workspace around it. `os create plugin --in-repo` is the opt-in for
platform work inside an ObjectStack checkout; it emits `workspace:*` instead.

<Callout type="info">
The scaffold emits `init` and `destroy`. `init` is the **required** phase —
`kernel.use()` rejects a plugin without it, with
`Failed to load plugin: slack-integration - Plugin init function is required`.
`start` is optional and is called after every plugin has initialized; add it
when your plugin needs the rest of the graph to be up first. See
[Plugin Anatomy](/docs/plugins/anatomy#plugin-lifecycle) for the phase model.
</Callout>

### Configuration Management
Expand Down
12 changes: 9 additions & 3 deletions content/docs/protocol/kernel/plugin-spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -839,25 +839,31 @@ describe('CRM Workflow', () => {
os create plugin crm

📁 Creating plugin: crm
📂 Location: packages/plugins/plugin-crm
📂 Location: /home/you/projects/plugin-crm

✓ Created package.json
✓ Created tsconfig.json
✓ Created src/index.ts
✓ Created README.md
✓ Created pnpm-workspace.yaml

✅ Project created successfully!

Next steps:
cd packages/plugins/plugin-crm
cd plugin-crm
pnpm install
pnpm build
```

The project is standalone — published dependency ranges pinned to the CLI that
generated it, and a self-contained `tsconfig.json` — so it installs outside any
workspace. Pass `--in-repo` only to scaffold into an ObjectStack monorepo
checkout.

### 2. Develop Locally

```bash
cd packages/plugins/plugin-crm
cd plugin-crm

# Watch mode (auto-rebuild on changes)
npm run dev
Expand Down
Loading
Loading