Skip to content

Commit 11905a4

Browse files
fix(cli)!: os generate binds declared metadata (--object, --flow), never the item name, and every scaffold passes validate, build and lint (#21369)
Fixes #21325 Clause-②: yes (narrowing) The `Clause-②` arm was measured on the real diff, and it is `(narrowing)`, not the `(widening)` the claim expected. `--object` and `--flow` widen the command. The diff also narrows it: an invocation that used to write a scaffold bound by name is now refused. `os g flow customer` in a project that declares several objects is one example, the `cli.mdx` example `os g action customer` another. The changeset carries the BREAKING banner and its ADR-0087 disposition (`not-required (no-migration-prescription)`). `check-adr-0087-registration` and `check-changeset-no-major` both pass on it. ## What changed Triage's direction (`5945870485`): `flow`, `action` and `app` take their target object from an argument or from the stack's existing objects, never from the new item's name. With nothing named and nothing to choose, the command refuses. `view` writes the shape `os lint` requires. Every generator kind, on a fresh scaffold, passes `os validate`, `os build` and `os lint` with zero findings. `packages/cli/src/commands/generate.ts` - **Bindings are resolved, not derived.** Each generator declares `binds` (`ScaffoldBinds`). `view` is named after its object. `flow`, `action` and `app` take the object from `--object`, or the stack's only object. `action` takes its flow from `--flow`, or the stack's only flow. `resolveScaffoldBindings` resolves every reference against the loaded stack before anything is rendered. It runs below the namespace gates and above the render, the parse check and `--dry-run`. It is a pure function: it returns a verdict, and the command prints the verdict and exits. `generate` receives the resolved `ScaffoldBindings`, and a binding scaffold rendered without them throws. Nothing falls back to the item's name. - **Refusals, each listing what the stack declares and naming the next command, with nothing written:** no object or several and no `--object`; an `--object` or `--flow` that names nothing declared; no flow or several and no `--flow`; a view whose name is not a declared object; a binding scaffold outside a project (no config, so no stack to check against); `--object` or `--flow` on a type that takes neither (`unusedBindingFlagRefusal`), including the `types`, `client` and `migration` routes, which now sit in one `SUB_COMMANDS` table. - **Scaffold content.** Each change below removes a finding that was measured. - `object`: no `description` field (`field-no-consumers` on every generated object once the stack had any consumer root). - `view`: no container `name` or `label` (two `liveness-dead-property` warnings). It writes `list.label` (`os lint` `required/label` error), and its columns are every field the bound object declares (a fixed `name` column was a `list-view-field-unknown` error on any object without one). It is sorted by the title field. - `flow`: `status: 'active'` (`flow-draft-status-ambiguous`; draft already fires, so the runtime behaviour is unchanged). - `action`: `locations: ['record_header']` (`action-no-placement`). - `app`: the nav entry is labelled with the bound object's plural label. - `--object` and `--flow` flags. Their help text names the types that read them, read off the roster. `packages/cli/src/utils/project-namespace.ts`: a `loaded` result also carries the evaluated `config`, so the bindings are read off the same load that supplies the namespace. `packages/cli/src/utils/scaffold-wiring.ts`: `registeredItemName` returns the key an item is registered under. For an aggregated views container that key is `deriveViewContainerObject`, the boot registrar's own derivation. `stackCarries` (the reach report) reads it, so a container with no `name` is still found. ## Argument shape, and why - **`--object OBJECT` and `--flow FLOW`** are long flags with a value, the way every other option on `os generate` is spelled (`--dir`, `--output`, `--format`). The object is accepted as declared (`tasks_app_task`), or without the namespace prefix (`task`). In the second case it is looked up through the same prefix rule `os g object` applies, never re-spelled. - **No flag means the stack's only object (or flow), said out loud** in the header: `Object: tasks_app_note (the only object this stack declares; --object binds another)`. This is the non-interactive reading of the direction's "or from the stack's existing objects". With several declared, the command lists them and asks for the flag instead of choosing. No `os generate` code path prompts, and this change adds no prompt (`os migrate` prompts only for a yes/no confirmation). - **A view is named after its object** (`os g view task`), unchanged. A views container is registered under the object it binds, so the name and the object are one fact. `--object` on `view` is refused with that explanation. - **`--flow` goes beyond the single object argument the claim anticipated.** An action's flow was derived from its name in exactly the same way (`complete_task_flow`). `defineStack` refused it whenever flows existed. With no flows declared, the action loaded with a dangling flow reference: measured on `97239c3c8a`, `os g action approve` then exits 0 and `os validate` exits 0, `target: 'approve_flow'`. The same rule (argument, or the stack's only one) is applied to the second reference. ## The view container's `name` and `label`: decided from the code that reads them The scaffold's comment said the server refuses a container whose `name` disagrees. `os validate` said both keys are dead. Read from their readers, **both statements are true and they do not conflict:** - `name`: the boot registrar files a container under `deriveViewContainerObject` (`packages/metadata/src/view-container.ts`): its own `object`, else `list.data.object` / `form.data.object`, and only then `name`. With `object` set, `name` is never the key. Its one reader is `viewContainerNameRefusal` (`packages/objectql/src/view-container-name-refusal.ts`). That function refuses a `name` that is set AND differs from the key, and passes a container that has no `name`. So `name` can only restate the key or contradict it, which is the liveness ledger's `dead` (`packages/spec/liveness/view.json`). The refusal's own remedy is "drop `name`". - `label`: `expandViewContainerWithDiagnostics` (`packages/spec/src/ui/view.zod.ts`) gives every expanded ViewItem the label of its list or form entry, never the container's. The ledger records no Studio reader either. So both keys are dropped, and the label a person sees is `list.label`, which `os lint` requires. ## Measured: the card's scenario `npm create objectstack` (the in-repo on-ramp `bin/`, `--skip-install`, namespace `tasks_app`), then `os g object project` and `os g object task`, run with the in-repo CLI build. | step | before (`97239c3c8a`) | after (`de6107c8e1`) | | --- | --- | --- | | `os g flow task_done` | exit 0, bound `tasks_app_task_done` | exit 1, lists the three objects, asks for `--object` | | `os g flow task_done --object task` | (no such flag) | exit 0, bound `tasks_app_task` | | `os g view task` | exit 0 | exit 0 | | `os g action complete_task` | exit 1 (`defineStack`: object `tasks_app_complete_task`, flow `complete_task_flow`) | exit 1, asks for `--object` | | `os g action complete_task --object task` | (no such flag) | exit 0, runs `task_done_flow` (the only flow) | | `os g app tasks` | exit 1 (`defineStack`: object `tasks_app_tasks`) | exit 1, asks for `--object` | | `os g app tasks --object task` | (no such flag) | exit 0 | | `os validate` | exit 0, 7 warnings | exit 0, 1 warning | | `os build` | exit 0, 7 warnings | exit 0, 1 warning | | `os lint` | **exit 1**: `required/label` at views[0].list.label, plus 7 warnings | exit 0, 1 warning | The remaining warning in the "after" column is `field-no-consumers` at `objects[0].fields.body`. It sits on the STARTER's own `note` object and is not written by `os g`; see Acceptance notes. ## Measured: every generator kind, three gates A fresh starter plus one `os g` invocation per leg. "Before" binds by name (`os g KIND probe_thing`, and the view on the starter's `note`). "After" runs the pin's own chain: the prerequisites `binds` declares, then the kind. Findings are counted from the `--json` output of each gate, at every severity. | kind | before: validate / build / lint | after: findings beyond the starter ledger | | --- | --- | --- | | object | exit 0 / 0 / 0, 0 findings | 0 | | view | exit **1** / **1** / **1**: `list-view-field-unknown`, `sort-field-unknown` and (lint) `required/label` errors; `liveness-dead-property` on `name` and `label` | 0 (`os g view note` also leaves zero, the ledger finding included) | | action | `os g` exit **1** (object derived from its name) | 0 | | flow | exit 0, 3 warnings at each gate (`flow-trigger-unknown-object`, `flow-draft-status-ambiguous`, starter `note.body`) | 0 | | dashboard | exit 0, 1 warning at each gate (starter `note.body`) | 0 | | app | `os g` exit **1** (object derived from its name) | 0 | | skill | exit 0, 1 warning at each gate (starter `note.body`) | 0 | | picklist | exit 0, 0 findings | 0 | ## The pin, family close-out - **Per-PR, in-process: `packages/cli/test/generate-scaffold-validates.test.ts` (extended).** For every kind in `GENERATOR_SCAFFOLD_TARGETS`, each prerequisite named by `binds` (an object, a flow) is generated first. The bindings are resolved through `stackBindingCandidates`, the reader the command uses. The target is deliberately not named like the item. The test then runs what the three commands run: `defineStack`; `normalizeStackInput` with the unknown-key lints, the parse and `runAuthoringRules('validate')`; `runAuthoringRules('build')`; and `lintConfig`. It asserts zero findings at every severity. Before this change it judged only the error half of `os validate`, beside an object named like the item, which is why none of the card's cases showed up there. A kind added later is measured with no edit to the file. Also added: binding scaffolds write exactly the binding they are handed, and throw without one. - **Nightly, real commands: `packages/cli/test/generate-scaffold-gates.e2e.test.ts` (new).** One fresh starter per kind (the on-ramp `bin/`), the prerequisite chain derived from `binds`, then `os validate --json`, `os build --json` and `os lint --json`. Each gate must exit 0 with no finding beyond a one-entry starter ledger. A CONTROL leg (the bare starter) must report zero findings at all three gates. The ledger is shrink-only. Its entry must sit on an object the bare starter declares, read from the compiled artifact, and must still fire in at least one leg. It is in e2e because the run is about forty oclif + tsx cold starts, roughly 4 minutes on this box. That is the same reason `generate-scaffolds-reach-stack.e2e.test.ts` is nightly. - **Per-PR: `packages/cli/test/generate-binds-from-stack.test.ts` (new).** It covers every branch of `resolveScaffoldBindings` and `unusedBindingFlagRefusal`, asserting on the named subjects, not on the prose. Existing tests the behaviour change required updating. Each needed a binding handed over or a flag added; none had an assertion weakened: - `generate-object-namespace-prefix.test.ts`: the set is composed object first. A binding scaffold must write the object it is handed, whatever the namespace. - `generate-scaffold-wiring.test.ts`: the registered key, and the container writes no `name`/`label`. - `generate-emission-parses.test.ts`, `scaffold-emission-typechecks.test.ts` and `generate-refuses-name-outside-charset.test.ts`: the probe bindings from the new `test/helpers/scaffold-bindings.ts`. - `generate-refuses-unimportable-alias.test.ts`: `dashboard` and `skill` carry the alias-layer cases that `view` and `flow` carried. In config-less directories a binding scaffold is now refused first. `dashboard` suffixes its binding the same way, so the layer it pins is unchanged. - `generate-stack-reach.test.ts`: an action whose `--object` names nothing; a view outside a project; the action control generating once an object and a flow exist; the no-config "Not wired" case carried by `dashboard`. - `create-objectstack-stack-reach.test.ts` and `generate-scaffolds-reach-stack.e2e.test.ts`: `--object`. The order is derived from `binds`. ## Ablations The fix was committed first. Each leg went through `scripts/ablation-replace.mjs` (wrap mode: the anchor must hit, the counts are checked on disk, and the file is restored to blob == HEAD with an empty `git diff HEAD`), inside a script with an EXIT/INT/TERM trap. Every leg below ended with the file restored (blob `2c8b969c1f07` for generate.ts, `0d902b75dff7` for scaffold-wiring.ts). | leg | mutation | red | | --- | --- | --- | | A1 | flow start node binds the object derived from its own name | 4 (flow and action legs, the bound-object pin, the start-node pin) | | A2 | flow `status: 'draft'` | 2 (flow and action legs) | | A3 | view container writes `name` and `label` again | 1 (view leg) | | A4 | view `list.label` removed | 1 (view leg) | | A5 | action `locations` removed | 1 (action leg) | | A6 | action object derived from its name | 2 | | A7 | app nav object derived from its name | 2 | | A8 | object scaffold declares `description` again | 3 (flow, action, app legs) | | A9 | resolver picks the first object when several are declared | 1 | | A10 | resolver drops the prefixed `--object` lookup | 1 | | A11 | `--object`/`--flow` on a non-taking type ignored | 8 | | A12 | reach reader identifies a container by `name` only | 2 | | E1 | e2e: flow `status: 'draft'`, through dist | 6 (flow and action legs × three gates) | Two first attempts were void and were redone with new anchors: the tool refused them because the counts did not move. In A3 the replacement re-contained the anchor. In A10 the replacement `;` was already everywhere. **E1's first run was also void, and in a way worth knowing:** the mutation landed in `src/` and the e2e stayed green, 38 of 38. The spawned `bin/run-dev.js` serves `packages/cli/dist` when dist is built. Measured: with `src` mutated to `status: 'draft'`, `tsx bin/run-dev.js g flow … --dry-run` printed `status: 'active'`. E1 was then rerun as a dist ablation: mutate, rebuild the CLI, check with `ablation-dist-preflight` (marker present in `dist/commands/generate.js`), run the e2e (6 red), restore, rebuild, check with `--absent` (clean). See Acceptance notes. ## Verification (on `de6107c8e1`, which merges `origin/main` at `f9bcd08bef`) - The CLI dependency closure was rebuilt after the merge: `turbo run build --filter=@objectstack/cli...`, 59 of 59 tasks. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 245 files, 3485 tests passed. - `--project integration` on `test/generate-` and `test/create-objectstack`: 7 files, 67 tests passed, run against a dist built from this head. - Nightly e2e (`OS_TEST_TIERS=nightly`): `generate-scaffold-gates`, `generate-scaffolds-reach-stack`, `generate-object-namespace-prefix` and `generate-skill`. 4 files, 69 tests passed. - `pnpm --filter @objectstack/cli typecheck`: exit 0. That includes `check:test-typecheck`; the debt ledger did not change. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 95 commands, every one exit 0. Three first exited 3 (PREREQUISITE NOT MET: `check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`) and were rerun after building their prerequisites. `--ran` reconciliation: 95 derived, 95 run, 0 NOT MEASURED, a derived zero. - `pnpm lint` (repo-wide eslint, not narrowed): exit 0. ## Docs and changeset - `content/docs/deployment/cli.mdx`, the `#### os generate` entry under `### Scaffolding`: where bindings come from, the refusals, the new options, the examples, and the view no longer writing `name`. - `.changeset/21325-generate-binds-from-stack.md`: `@objectstack/cli` `minor`, BREAKING banner, `Clause-②: yes (narrowing)`, ADR-0087 `not-required (no-migration-prescription)`. ## Acceptance notes - **Docs edits outside the claimed region.** This change made two examples in `cli.mdx` false, so they were edited too. The Quick Start "Add more metadata" block, `os generate flow customer` / `os generate action customer`, is refused on the starter, which declares two objects once `customer` exists. The "Typical Workflow" step `os g flow opportunity` comes after four objects. Each now passes `--object`. Neither block is in `### Quality`, the region of sibling card #21323. - **`packages/cli/README.md` is fixed in this PR.** The claim fenced it to #21310, which has since closed (landed), so the seat lifted the fence under the same claim, 5946394138. Its Quick Start line `os generate flow task` would be refused after this change, because the `app` template's object plus `task` makes two objects. It now reads `os generate flow task_changed --object task`, measured on a real `os init my-app` project (exit 0, then `os validate` / `os build` / `os lint` exit 0). The Typical Workflow line `os generate view customer` still holds and is unchanged. - **Starter latent finding (the one e2e ledger entry).** The `npm create objectstack` starter's `note` object declares `body`, which nothing reads. `field-no-consumers` stays silent while a stack holds no consumer root. The first view, flow, action, app, dashboard or skill, generated or hand-written, wakes it on the starter's object. `os init -t app`'s `item` object has the same shape (`description`, `status`). `os g` cannot give someone else's field a consumer, so the literal "zero findings on a fresh starter" holds only for kinds that add no consumer root. Each kind's own output is measured at zero, and the pin records the remainder in a shrink-only ledger that turns red when the starter is fixed. The template fix is outside this card's surface. It is reported to the seat for a separate card. - **`bin/run-dev.js` serves `dist/` when it exists.** It is documented in several CLI test headers as the source entry, and the suite says it does not depend on dist. Measured above (E1), it serves the built tree. So a spawned CLI pin reads build state. A test written against an unbuilt change reads stale behaviour, and an ablation of `src/` alone stays green. This PR's spawned tests were rerun against a dist built from this head. No carrier is named for the test infrastructure. - Two new internal-tool edits are within the claim's "generate.ts and the CLI internals it directly references": `project-namespace.ts` and `scaffold-wiring.ts`. Also new: one test helper, `test/helpers/scaffold-bindings.ts`, and two test files. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d78bd01 commit 11905a4

19 files changed

Lines changed: 1785 additions & 359 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli)!: `objectstack generate` binds a view, flow, action or app to an object (and an action to a flow) that you name or that the stack declares, never to one derived from the new item's name, and every scaffold passes `objectstack validate`, `objectstack build` and `objectstack lint` with zero findings
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a change to which `objectstack generate` invocations write a scaffold, and to what the scaffolds contain. No authorable key, spelling, export or stored shape moves: every schema the scaffolds are written against parses exactly what it parsed, the files `objectstack generate` wrote earlier are untouched and still load, and no stored row is read or rewritten. What is narrowed is the command's own argument handling, which no ledger entry can rewrite: the object a scaffold should have bound is the author's to say, which is the whole point of the change. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a CLI argument, and this diff adds none (not `registered` / `already-registered`); and the change is command behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows which `objectstack generate` invocations write a file. It ships as `minor` under the launch-window convention for narrowings. No export or published type changes.
12+
13+
**Why.** `view`, `flow`, `action` and `app` scaffolds took the object they bind from their own name, and an action took its flow the same way. On a fresh `npm create objectstack` project holding `project` and `task`, `objectstack generate flow task_done` wrote a flow triggered by an object called `task_done` that nothing declares (a flow that never fires) and reported success, while `objectstack generate action complete_task` and `objectstack generate app tasks` were refused, because no object was called `complete_task` or `tasks`. Nothing let the author name the object they meant.
14+
15+
**New options.**
16+
17+
- `--object <object>` names the object a `flow`, `action` or `app` binds, as the stack declares it or without the namespace prefix (`--object task` binds `tasks_app_task` under `namespace: 'tasks_app'`). Without it, the scaffold binds the stack's only object.
18+
- `--flow <flow>` names the flow an `action` runs. Without it, the action runs the stack's only flow.
19+
20+
**What is now refused, with nothing written.** In each case the command names what the stack declares and the command to run instead.
21+
22+
- A `flow`, `action` or `app` with no `--object` in a stack that declares no object, or several.
23+
- `--object` or `--flow` naming nothing the stack declares.
24+
- An `action` with no `--flow` in a stack that declares no flow, or several.
25+
- A `view` whose name is not an object the stack declares. A view is still named after the object it binds: `objectstack generate view task` writes the views of `tasks_app_task`.
26+
- Any of these four outside a project, where there is no config and so no stack to check the binding against.
27+
- `--object` or `--flow` on a type that takes neither (`object`, `dashboard`, `skill`, `picklist`, and the `types`, `client` and `migration` routes), instead of reading as honoured.
28+
29+
**What the scaffolds now write.** Each was measured adding at least one finding to `os validate`, `os build` or `os lint`, and now adds none.
30+
31+
- `object`: the record's title field (`name`) and no `description` field. Nothing read the `description` field, so `field-no-consumers` reported it on every generated object as soon as the project held any view, flow, action, app, dashboard or skill.
32+
- `view`: no container `name` or `label`. The container is registered under its `object`, so `name` could only restate that key or contradict it, and no reader reaches a container's `label`. Both were `liveness-dead-property` warnings. The list now carries the `label` that `os lint` requires (`required/label` was an error). Its columns are every field the bound object declares, and it is sorted by the object's title field. It used to show a fixed `name` column, which an object without a `name` field refused.
33+
- `flow`: `status: 'active'` in place of `'draft'`. A draft flow already fires its trigger (only `obsolete` and `invalid` disable one), so the runtime behaviour is unchanged. `flow-draft-status-ambiguous` warned on every scaffold.
34+
- `action`: `locations: ['record_header']`. With no placement, `action-no-placement` warned that the button renders nowhere.
35+
- `app`: its navigation entry opens the bound object and is labelled with that object's plural label.
36+
37+
**What to write instead.** Name the object a flow, action or app binds, for example `objectstack generate flow task_done --object task`. Name the flow an action runs when the stack has more than one, for example `objectstack generate action complete_task --object task --flow task_done_flow`. Run the command in the project's directory. Generate a view under the name of an object the stack declares.
38+
39+
**Unchanged.** `objectstack generate object`, `dashboard`, `skill` and `picklist`, and every name, namespace, parse and import check in front of the bindings. Files generated by earlier releases are not touched.

‎content/docs/deployment/cli.mdx‎

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ This scaffolds a working project with `objectstack.config.ts`, a sample object,
3232
### Add more metadata
3333

3434
```bash
35-
os generate object customer # Add a Customer object
36-
os generate flow customer # Add an automation flow on it
37-
os generate action customer # Add an action on it that runs the flow
35+
os generate object customer # Add a Customer object
36+
os generate flow customer_changed --object customer # Add an automation flow on it
37+
os generate action approve --object customer # Add an action on it that runs the flow
3838
```
3939

4040
Each command writes a file and its export line, and the starter's config already
4141
wires every directory they write into, so all three are part of the stack. The
42-
flow and the action bind to the object named like them, which is why the object
43-
comes first: an action bound to an object the stack does not declare is refused.
42+
flow and the action bind to the object `--object` names, which is why the object
43+
comes first: a scaffold whose object the stack does not declare is refused, with
44+
nothing written. The action runs the stack's only flow.
4445

4546
### Launch the dev server
4647

@@ -1440,10 +1441,30 @@ either. A name in the legacy `<namespace>__<name>` form (`order__line`) is
14401441
refused, because no prefix makes it one `os validate` accepts. The namespace is
14411442
read from the loaded config, the same place `os validate` reads it from. If a
14421443
config exists but does not load, the command refuses and writes nothing.
1443-
Without a `namespace` (or without a config), nothing is prefixed. A view's own
1444-
`name` is the object it binds to, prefix included, because the server registers
1445-
a view under that object and refuses one whose `name` says otherwise. Nothing
1446-
else a scaffold names (an action's, flow's or app's own `name`) is prefixed.
1444+
Without a `namespace` (or without a config), nothing is prefixed. Nothing else a
1445+
scaffold names (an action's, flow's or app's own `name`) is prefixed.
1446+
1447+
**What a scaffold binds comes from you or from the stack, never from its
1448+
name.** A `view`, `action`, `flow` or `app` scaffold refers to metadata the
1449+
project already has, and each reference is checked against the stack the
1450+
config loads before anything is written:
1451+
1452+
- A **view** is named after the object it binds: `os g view customer` writes
1453+
the views of the object `os g object customer` writes, prefix included. The
1454+
container carries no `name` or `label` of its own (the server registers it
1455+
under its object), and its list shows every field the object declares, under
1456+
a `list.label` that `os lint` requires.
1457+
- A **flow**, **action** or **app** binds the object you name with
1458+
`--object` — as declared (`my_app_customer`) or without the namespace prefix
1459+
(`customer`). Without `--object`, it binds the stack's only object.
1460+
- An **action** runs the flow you name with `--flow`, or the stack's only flow.
1461+
1462+
When the stack declares none, or several and you named none, or the one you
1463+
named is not declared, the command refuses, lists what the stack does declare,
1464+
and writes nothing: which object a scaffold acts on is yours to say. Outside a
1465+
project (no config) a scaffold that binds is refused too, because there is no
1466+
stack to check it against. `--object` and `--flow` on a type that takes neither
1467+
are refused rather than ignored.
14471468
14481469
**Every scaffold reaches the stack, or the command says it does not.** The
14491470
`objectstack.config.ts` that `os init` writes for the `app` and `plugin`
@@ -1464,29 +1485,29 @@ After writing, `os g` loads the config again and says which of these holds:
14641485
as it was, and the command prints the import and the `defineStack` key that
14651486
wire the directory.
14661487
- **Refused**: the config loaded before the command wrote anything and no
1467-
longer loads with the new file in place, because the stack refuses it. Two
1468-
examples are an action or app bound to an object nobody declared, and a flow
1469-
in a stack whose `requires` lacks `triggers` or `automation` (`defineStack`
1470-
refuses a record-change flow without both). The command removes what it
1471-
wrote, so the project is as it was, and exits 1 with the stack's own reason.
1472-
Generate the object first (`os g object customer`), then what binds to it,
1473-
and declare `requires: ['automation', 'triggers']` before `os g flow`.
1488+
longer loads with the new file in place, because the stack refuses it — for
1489+
example a flow in a stack whose `requires` lacks `triggers` or `automation`
1490+
(`defineStack` refuses a record-change flow without both). The command
1491+
removes what it wrote, so the project is as it was, and exits 1 with the
1492+
stack's own reason. Declare `requires: ['automation', 'triggers']` before
1493+
`os g flow`.
14741494
14751495
`os g` never edits `objectstack.config.ts`: the config is yours, and the
14761496
command only loads it.
14771497
14781498
```bash
1479-
os g object customer # Generate a Customer object
1480-
os g view customer # Generate a Customer list view
1481-
os g action customer # Generate an action on Customer records
1482-
os g flow customer # Generate a flow that runs when a Customer changes
1483-
os g dashboard sales # Generate a dashboard
1484-
os g app customer # Generate an app whose navigation opens Customer
1485-
os g skill lead_qual # Generate an AI skill
1486-
os g picklist industry # Generate a shared option list select fields name
1487-
1488-
os g object task -d lib/ # Override target directory
1489-
os g object task --dry-run # Preview without writing
1499+
os g object customer # Generate a Customer object
1500+
os g view customer # Generate the Customer list view
1501+
os g flow customer_changed --object customer # Generate a flow that runs when a Customer changes
1502+
os g action approve --object customer # Generate an action on Customer records that runs the flow
1503+
os g dashboard sales # Generate a dashboard
1504+
os g app sales --object customer # Generate an app whose navigation opens Customer
1505+
os g skill lead_qual # Generate an AI skill
1506+
os g picklist industry # Generate a shared option list select fields name
1507+
1508+
os g action escalate --object customer --flow customer_changed_flow # Name the flow when there are several
1509+
os g object task -d lib/ # Override target directory
1510+
os g object task --dry-run # Preview without writing
14901511
```
14911512
14921513
**Available types:**
@@ -1554,9 +1575,11 @@ the rules a JSON Schema can express and names the rest under
15541575
**Options:**
15551576
- `-d, --dir <directory>` — Override target directory
15561577
- `--dry-run` — Preview without writing files
1578+
- `--object <object>` — The object a `flow`, `action` or `app` binds; default: the stack's only object
1579+
- `--flow <flow>` — The flow an `action` runs; default: the stack's only flow
15571580
15581581
**What it does:**
1559-
1. For a type that names an object (`object`, `view`, `action`, `flow`, `app`), reads `manifest.namespace` from the project config (`objectstack.config.ts`, `.js` or `.mjs`) and prefixes the object name with it (see above)
1582+
1. For a type that names an object (`object`, `view`, `action`, `flow`, `app`), reads `manifest.namespace` from the project config (`objectstack.config.ts`, `.js` or `.mjs`) and prefixes the object name with it, and resolves every object or flow the scaffold binds against the stack that config loads (see above)
15601583
2. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; a `skill` declared with `defineSkill({ … })`; a `picklist` declared with `definePicklist({ … })`, which a select field names with `Field.select({ picklist: 'NAME' })` in place of its own `options`, and which the server resolves into that field's `options`; the other types as typed literals (`UI.View`, `UI.Action`, `Automation.Flow`, `UI.Dashboard`, `UI.App`)
15611584
3. Creates or updates the barrel `index.ts` in the target directory
15621585
4. Shows a hint to run `objectstack validate`
@@ -2352,7 +2375,7 @@ os g object contact
23522375
os g object opportunity
23532376

23542377
# 3. Add business logic: a flow that runs when an opportunity changes
2355-
os g flow opportunity
2378+
os g flow opportunity_changed --object opportunity
23562379

23572380
# 4. Validate everything: each file `os g` wrote is counted and checked
23582381
os validate

‎packages/cli/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ os init my-app
2121
# Generate metadata
2222
os generate object task
2323
os generate view task
24-
os generate flow task
24+
os generate flow task_changed --object task
2525

2626
# Validate configuration
2727
os validate

0 commit comments

Comments
 (0)