Skip to content

Commit cf6b671

Browse files
os-litantclaude
andauthored
fix(cli): make os create emit a project that installs outside this monorepo (#15535)
* fix(cli): make `os create` emit a project that installs outside the monorepo `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 developer who followed the docs got a project `pnpm install` refuses. The default emission is now standalone — published semver ranges pinned to the running CLI's own version, a self-contained `tsconfig.json`, a `pnpm-workspace.yaml` carrying the build approvals pnpm 11 requires, and the developer's own directory as the output location. The monorepo-internal placement survives as the explicit `--in-repo` flag. The pin is `scripts/create-scaffold-smoke.sh`, wired into `.github/workflows/os-create-smoke.yml`: it scaffolds every template into a temp directory outside the repository, installs from packed tarballs, and runs the project's own build and typecheck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * fix(cli): emit `init`, hold the docs to the template, and pin the scaffold on CI Three follow-ons measured while building the pin for this card: - the `plugin` template emitted an `initialize` method. `Plugin` carries an index signature, so the excess property was accepted but got no contextual type: the scaffold failed its own `strict` type-check with TS7006, and the kernel loader refuses a plugin without `init` outright. It emits `init` now, and the kernel protocol docs lose the warning that told readers to rename it. - `test/create-plugin-docs-parity.test.ts` holds the template's emitted file set equal to the tree each of the three plugin doc pages prints. Both sides are derived, so a template that grows a file reddens every page that does not say so. The three pages are declared as cross-package test inputs and mirrored into turbo.json; `check-ci-filter-parity`'s rollback pin records them by name. - `scripts/create-scaffold-smoke.sh` avoids `mapfile`, which does not exist on the bash 3.2 floor `check:bash32-floor` holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * fix(cli): restore the `init` emission lost to an ablation restore leg The `initialize` -> `init` change and its header paragraph were uncommitted when an ablation's restore leg ran `git checkout HEAD -- create.ts`, which is a normal, silent, exit-0 discard of a working tree that was the only copy. Both edits are re-applied here, byte for byte, and the tree is committed before the ablation is repeated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7fd5c5 commit cf6b671

13 files changed

Lines changed: 1166 additions & 141 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os create` now emits a project that installs outside this monorepo.
6+
7+
Every project the command scaffolded declared its `@objectstack/*` dependencies
8+
with pnpm's `workspace:*` protocol, extended a `tsconfig.json` two directories
9+
above itself, and was written into this repository's own `packages/plugins/` or
10+
`examples/` by default — so a developer following the documented command got a
11+
project `pnpm install` refuses. The default emission is now standalone:
12+
13+
- `@objectstack/*` dependencies are published semver ranges pinned to the
14+
version of the CLI that generated them;
15+
- the emitted `tsconfig.json` is self-contained and extends nothing;
16+
- the project is written to `./<name>` in the current directory (or `--dir`);
17+
- a `pnpm-workspace.yaml` carries the build approvals a fresh `pnpm install`
18+
needs on pnpm 11.
19+
20+
The `plugin` template also emits `init` where it used to emit `initialize`.
21+
`initialize` is not part of the `Plugin` contract, so the scaffold did not
22+
type-check under its own `strict` config (TS7006 on the untyped `context`
23+
parameter) and `kernel.use()` refused the plugin at load with
24+
`Plugin init function is required` — a defect the kernel protocol docs
25+
previously carried a warning about instead of a fix.
26+
27+
The previous monorepo-internal placement is still available for ObjectStack
28+
platform work as the explicit `--in-repo` flag, which keeps the `workspace:*`
29+
specs and writes into `packages/plugins/` or `examples/`.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# `os create` scaffold smoke — the emitted project installs and builds OUTSIDE
2+
# this monorepo (#14824).
3+
#
4+
# ## What this gate holds
5+
#
6+
# `os create` is presented on four public documentation pages as a user-facing
7+
# scaffolder, and every project it emitted was monorepo-shaped: `workspace:*`
8+
# dependency specs, a `tsconfig.json` extending `'../../tsconfig.json'`, and a
9+
# default output directory inside this repository. A reader who followed the
10+
# docs got a project `pnpm install` refuses. The maintainer ruled that a
11+
# documented developer-facing command must work for the developer who follows
12+
# the docs, and attached an executable criterion: scaffold each template into a
13+
# temporary directory outside the repository, install it with the registry, and
14+
# boot / typecheck it — green outside the monorepo, ON CI, not on a developer
15+
# box. `scripts/create-scaffold-smoke.sh` is that criterion; this workflow is
16+
# the "on CI" half.
17+
#
18+
# ## Why paths-filtered rather than label-gated opt-in
19+
#
20+
# `pack-smoke-optin.yml` covers a different defect class — an author widening
21+
# the unauthenticated surface — where the only reliable trigger is the author
22+
# recognising their own change, so a label is the honest shape and its header
23+
# forbids growing a `paths:` filter. This gate's defect class is the opposite:
24+
# it can only be introduced by editing a bounded, nameable set of files, and
25+
# those files are the `paths:` below. A label would mean a scaffolder change
26+
# could be merged by anyone who did not think to apply it, which is precisely
27+
# how the emitted contract drifted into being uninstallable in the first place.
28+
#
29+
# `init.ts` is in the set even though this gate does not test `os init`. The
30+
# standalone emission CALLS that module — `getCliVersion`, `SCAFFOLD_PNPM_RANGE`
31+
# and `renderPnpmWorkspaceYaml` are its exports — so its build approvals and its
32+
# version resolution decide whether an `os create` scaffold installs. Naming the
33+
# consumer and not the producer is the shape of coupling that lets a gate sit
34+
# green through the change that breaks it.
35+
#
36+
# The nightly run is the backstop for everything the `paths:` set cannot name: a
37+
# scaffolded project resolves the whole `@objectstack/*` graph, so a change in
38+
# any of those packages can break its install or its boot without touching a
39+
# single file listed here.
40+
#
41+
# ⛔ Not a required context — `scripts/check-required-contexts.mjs` owns that
42+
# registry, and a paths-filtered job cannot be required: on a PR that does not
43+
# trip the filter it never reports, and branch protection would block forever.
44+
# It is advisory in the same way `scaffold-e2e.yml` is.
45+
#
46+
# Every step below is part of a build / scaffold / install / build pipeline, not
47+
# a named local verification a dev pre-runs with `pnpm check:x`:
48+
# dispatch-gates: no-check-families -- scaffold + install + build pipeline, no named local check family exists for it
49+
50+
name: OS Create Smoke
51+
52+
on:
53+
pull_request:
54+
branches:
55+
- main
56+
paths:
57+
- 'packages/cli/src/commands/create.ts'
58+
- 'packages/cli/src/commands/init.ts'
59+
- 'scripts/create-scaffold-smoke.sh'
60+
- 'scripts/publish-smoke-pack.mjs'
61+
- '.github/workflows/os-create-smoke.yml'
62+
schedule:
63+
- cron: '41 4 * * *'
64+
workflow_dispatch:
65+
66+
permissions:
67+
contents: read
68+
69+
jobs:
70+
create-scaffold-smoke:
71+
name: Scaffold outside the monorepo, install, build
72+
runs-on: ubuntu-latest
73+
timeout-minutes: 60
74+
concurrency:
75+
group: os-create-smoke-${{ github.event.pull_request.number || github.ref }}
76+
cancel-in-progress: true
77+
steps:
78+
# No `ref:` — on `pull_request` the default checkout is `refs/pull/N/merge`,
79+
# the merge preview, which is what `main` will actually contain.
80+
- name: Checkout
81+
uses: actions/checkout@v7
82+
83+
- name: Setup Node.js
84+
uses: actions/setup-node@v7
85+
with:
86+
node-version: '22'
87+
88+
- name: Setup pnpm
89+
uses: ./.github/actions/setup-pnpm
90+
91+
- name: Get pnpm store directory
92+
shell: bash
93+
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
94+
95+
- name: Setup pnpm cache
96+
uses: actions/cache@v6
97+
with:
98+
path: ${{ env.STORE_PATH }}
99+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
100+
restore-keys: |
101+
${{ runner.os }}-pnpm-store-v3-
102+
103+
- name: Setup turbo cache
104+
uses: actions/cache@v6
105+
with:
106+
path: .turbo/cache
107+
key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }}
108+
restore-keys: |
109+
${{ runner.os }}-turbo-${{ github.job }}-
110+
${{ runner.os }}-turbo-
111+
112+
- name: Install dependencies
113+
run: pnpm install --frozen-lockfile
114+
115+
# The smoke's own prerequisite, asserted by the script itself: it refuses
116+
# to run without packages/cli/dist and the `os` bin.
117+
- name: Build
118+
run: pnpm run build
119+
120+
# ⛔ Do not add flags or env here to make a red go away. A refusal this
121+
# script reports is a refusal a developer following the docs would get.
122+
- name: Scaffold smoke (packed tarballs, outside the repo)
123+
run: bash scripts/create-scaffold-smoke.sh

content/docs/deployment/cli.mdx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,7 @@ only.
11901190
| Command | Alias | Description |
11911191
|---------|-------|-------------|
11921192
| `os generate <type> <name>` | `os g` | Generate metadata files |
1193-
| `os create <type> [name]` | | Create a new package from template |
1193+
| `os create <type> [name]` | | Scaffold a standalone plugin or example project |
11941194

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

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

12641264
#### `os create`
12651265

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

12681269
```bash
1269-
os create plugin analytics # Create packages/plugins/plugin-analytics
1270-
os create example my-app # Create examples/my-app
1270+
os create plugin analytics # Create ./plugin-analytics
1271+
os create example my-app # Create ./my-app
1272+
1273+
cd plugin-analytics
1274+
pnpm install
1275+
pnpm build
12711276
```
12721277

1278+
The emitted `package.json` declares its `@objectstack/*` dependencies as
1279+
published semver ranges pinned to the version of the CLI that generated it, and
1280+
the emitted `tsconfig.json` is self-contained, so the project installs and
1281+
builds anywhere — a workspace around it is neither needed nor assumed.
1282+
1283+
**Options:**
1284+
- `-d, --dir <directory>` — Write the project here instead of `./<name>`
1285+
- `--in-repo` — Scaffold **inside an ObjectStack monorepo checkout** instead
1286+
(`packages/plugins/<name>` for a plugin, `examples/<name>` for an example),
1287+
with `workspace:*` dependencies and a `tsconfig.json` that extends the
1288+
repository root config. For ObjectStack platform work only: the project it
1289+
writes installs nowhere else, and the command refuses the flag when the
1290+
current directory is not a pnpm workspace root.
1291+
12731292
### Quality
12741293

12751294
| Command | Description |

content/docs/plugins/index.mdx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,30 @@ Plugins follow a strict three-phase lifecycle (`init()` → `start()` → `destr
7878
The fastest way to create a plugin is with the CLI scaffolding:
7979

8080
```bash
81-
# Create a new plugin project
81+
# Create a new plugin project in the current directory
8282
os create plugin my-feature
8383

8484
# This creates:
85-
# packages/plugins/plugin-my-feature/
85+
# plugin-my-feature/
8686
# ├── package.json
8787
# ├── tsconfig.json
8888
# ├── README.md
89+
# ├── pnpm-workspace.yaml
8990
# └── src/
9091
# └── index.ts
92+
93+
cd plugin-my-feature
94+
pnpm install
95+
pnpm build
9196
```
9297

98+
The scaffold is a **standalone** project: its `package.json` depends on the
99+
published `@objectstack/*` releases that match the CLI which generated it, and
100+
its `tsconfig.json` extends nothing outside the project — so it installs and
101+
builds wherever you put it. Add `--in-repo` only when you are scaffolding into
102+
a checkout of the ObjectStack monorepo itself; that placement emits
103+
`workspace:*` dependencies and installs nowhere else.
104+
93105
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.
94106

95107
---

content/docs/protocol/kernel/index.mdx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -381,24 +381,31 @@ All system configuration lives in Git:
381381

382382
### Plugin Development
383383
```bash
384-
# Scaffold new plugin (created under packages/plugins/plugin-<name>/)
384+
# Scaffold new plugin (created as ./plugin-<name>/ in the current directory)
385385
os create plugin slack-integration
386386
387387
# Generated structure:
388-
packages/plugins/plugin-slack-integration/
389-
package.json # name, version, dependencies (@objectstack/spec, zod)
390-
tsconfig.json
388+
plugin-slack-integration/
389+
package.json # name, version, dependencies (@objectstack/spec, zod)
390+
tsconfig.json # self-contained, extends nothing outside the project
391+
pnpm-workspace.yaml # the pnpm build approvals a fresh install needs
391392
src/
392-
index.ts # default-export Plugin object (name, version, initialize, destroy)
393+
index.ts # default-export Plugin object (name, version, init, destroy)
393394
README.md
394395
```
395396

396-
<Callout type="warn">
397-
The scaffold still emits an `initialize` method. The kernel's plugin contract
398-
only invokes `init` / `start` / `destroy`, and `init` is **required** — rename
399-
`initialize` to `init` in the generated `src/index.ts` or `kernel.use()`
400-
rejects the plugin outright with
397+
The emitted dependencies are published semver ranges pinned to the CLI that
398+
generated them, so `pnpm install && pnpm build` works in the new directory
399+
without a workspace around it. `os create plugin --in-repo` is the opt-in for
400+
platform work inside an ObjectStack checkout; it emits `workspace:*` instead.
401+
402+
<Callout type="info">
403+
The scaffold emits `init` and `destroy`. `init` is the **required** phase —
404+
`kernel.use()` rejects a plugin without it, with
401405
`Failed to load plugin: slack-integration - Plugin init function is required`.
406+
`start` is optional and is called after every plugin has initialized; add it
407+
when your plugin needs the rest of the graph to be up first. See
408+
[Plugin Anatomy](/docs/plugins/anatomy#plugin-lifecycle) for the phase model.
402409
</Callout>
403410
404411
### Configuration Management

content/docs/protocol/kernel/plugin-spec.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,25 +839,31 @@ describe('CRM Workflow', () => {
839839
os create plugin crm
840840

841841
📁 Creating plugin: crm
842-
📂 Location: packages/plugins/plugin-crm
842+
📂 Location: /home/you/projects/plugin-crm
843843

844844
✓ Created package.json
845845
✓ Created tsconfig.json
846846
✓ Created src/index.ts
847847
✓ Created README.md
848+
✓ Created pnpm-workspace.yaml
848849

849850
✅ Project created successfully!
850851

851852
Next steps:
852-
cd packages/plugins/plugin-crm
853+
cd plugin-crm
853854
pnpm install
854855
pnpm build
855856
```
856857

858+
The project is standalone — published dependency ranges pinned to the CLI that
859+
generated it, and a self-contained `tsconfig.json` — so it installs outside any
860+
workspace. Pass `--in-repo` only to scaffold into an ObjectStack monorepo
861+
checkout.
862+
857863
### 2. Develop Locally
858864

859865
```bash
860-
cd packages/plugins/plugin-crm
866+
cd plugin-crm
861867

862868
# Watch mode (auto-rebuild on changes)
863869
npm run dev

0 commit comments

Comments
 (0)