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
4 changes: 2 additions & 2 deletions .changeset/20197-generate-object-namespace-prefix.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
"@objectstack/cli": patch
---
Expand All @@ -8,6 +8,6 @@

- **Object names are prefixed.** In a project whose manifest declares a `namespace`, every object name a scaffold writes now starts with `<namespace>_`. That covers the `object` scaffold's `name`, a `view`'s `object`, an `action`'s `objectName`, a `flow` start node's `objectName` and an `app` navigation item's `objectName`. Generated scaffolds now also point at each other: `os g view order_line` binds the object `os g object order_line` wrote. The file name and the exported binding still come from the name you typed (`src/objects/order_line.object.ts`, `orderLine`), and the command prints the object name it wrote.
- **No double prefix.** A name that already carries the prefix (`os g object my_app_order_line`) is written as typed. The "already compliant?" check is the namespace-prefix gate's own `validateObjectNamespacePrefix`, so a `sys_*` name, which the gate exempts, is not prefixed either. A name the gate would still refuse after prefixing (the legacy `NS__SHORT` form) is refused before anything is written.
- **One namespace source.** The namespace is `manifest.namespace` of the config as loaded, the value `os validate` checks against. It is never re-derived from the directory or the `package.json` name. With no config, or a manifest without a `namespace`, nothing is prefixed, as before. If a config exists but does not load, a type that names an object is refused and nothing is written, because the namespace is unknown. `dashboard` and `skill` scaffolds name no object and never read the config.
- **Unchanged:** the names the gate does not check against the namespace. A view's, action's, flow's, dashboard's, app's and skill's own `name`, and an action's flow `target`, are written as before.
- **One namespace source.** The namespace is `manifest.namespace` of the config as loaded, the value `os validate` checks against. It is never re-derived from the directory or the `package.json` name. With no config, or a manifest without a `namespace`, nothing is prefixed, as before. If a config exists but does not load, a type that names an object is refused and nothing is written, because the namespace is unknown. `dashboard` and `skill` scaffolds name no object, so a config that does not load does not stop them, but `os g` loads the config after every write, theirs included, to report whether the scaffold reaches the stack.
- **Unchanged:** the names the gate does not check against the namespace. An action's, flow's, dashboard's, app's and skill's own `name`, and an action's flow `target`, are written as before; a view's own `name` now equals the object key it binds to, prefix included.
- `os generate --help` now lists all seven metadata types in the `TYPE` argument. It had omitted `skill`, and the list now comes from the generator table.
23 changes: 23 additions & 0 deletions .changeset/20215-generate-scaffolds-reach-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@objectstack/cli": patch
---

fix(cli): what `os generate` writes now reaches the stack, or the command says it does not

`os init my-app -t app` wrote a config that imported `./src/objects` alone. `os g view`, `action`, `flow`, `dashboard`, `app` and `skill` each wrote a file and a barrel `index.ts` that nothing imported, and `os validate` then exited 0 printing `UI: 0 Apps` and `Logic: 0 Flows`: a green that had judged nothing the command just wrote.

**What `os init` now writes (`app` and `plugin` templates):**

- `objectstack.config.ts` imports every directory `os generate` writes into (`src/objects`, `src/views`, `src/actions`, `src/flows`, `src/dashboards`, `src/apps`, `src/skills`) and hands each barrel's exports to `defineStack` under its key (`objects`, `views`, …). A file `os g` writes there is part of the stack with no edit to the config. The keys read the barrels through a small `exportsOf` helper declared in the config, because `Object.values` on an empty barrel does not type-check against `defineStack`'s collection types.
- An `index.ts` containing only `export {};` for each directory the template puts nothing in. An `index.ts` that already exists is kept as it is and never overwritten.
- `requires: ['automation', 'triggers']`. A flow that starts on a record change is fired by `triggers` and run by `automation`. Without `triggers`, `defineStack` refuses the config as soon as it holds such a flow. Without `automation`, the server loads the flow and never runs it.

**What `os generate` now does:**

- After writing, it loads the project's config again and reports on the new item. Either the stack carries it, or it is **not wired** (the file is written, the config is left untouched, and the command prints the import and `defineStack` key to add), or it **cannot run** (a flow in a stack whose `requires` lacks `automation`: the command prints the whole `requires` list to use). It never edits the config.
- It refuses a write that makes a config that loaded stop loading, for example an action or app bound to an object nobody declared, or a flow in a stack without `triggers`. It removes what it wrote, exits 1, and prints the stack's own reason. Generate the object first (`os g object customer`), then what binds to it. `dashboard` and `skill` now read the config too, so they can report, and they still generate when the config does not load.
- A view's own `name` is now the object it binds to, prefix included (`my_app_order_line`, not `order_line`). The server registers a view under its object and refused, at boot, a scaffold whose `name` disagreed. That never showed while the views barrel was not loaded.
- The barrel step asks the compiler whether the barrel already exports the name, instead of searching the file's text. `os g view order` after `os g view order_line` had found `order` inside `orderLine` and exported nothing.
- The `flow` scaffold's header states the `requires` it needs.

**Projects scaffolded by an earlier release** keep their config. `os g` now tells you when a file it wrote is not wired, and prints the lines to add.
66 changes: 49 additions & 17 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1403,16 +1403,48 @@ either. A name in the legacy `<namespace>__<name>` form (`order__line`) is
refused, because no prefix makes it one `os validate` accepts. The namespace is
read from the loaded config, the same place `os validate` reads it from. If a
config exists but does not load, the command refuses and writes nothing.
Without a `namespace` (or without a config), nothing is prefixed. Nothing else a
scaffold names (a view's, flow's or app's own `name`) is prefixed.
Without a `namespace` (or without a config), nothing is prefixed. A view's own
`name` is the object it binds to, prefix included, because the server registers
a view under that object and refuses one whose `name` says otherwise. Nothing
else a scaffold names (an action's, flow's or app's own `name`) is prefixed.

**Every scaffold reaches the stack, or the command says it does not.** The
`objectstack.config.ts` that `os init` writes for the `app` and `plugin`
templates wires every directory in the table below: it imports each
`src/<dir>/index.ts` barrel and hands its exports to `defineStack` under the key
in the **Collected as** column, so a file `os g` writes there is part of the
stack with no edit to the config. It also declares
`requires: ['automation', 'triggers']`, which a flow needs to load and to run.
After writing, `os g` loads the config again and says which of these holds:

- **Reached**: the stack carries the new item, so `os validate` counts and
checks it.
- **Not wired**: the config loads and its stack does not carry the item, or
there is no config. This is what happens with a config that imports
`./src/objects` alone, as `os init` projects from earlier releases and the
`npm create objectstack` starter do. The file is written, the config is left
as it was, and the command prints the import and the `defineStack` key that
wire the directory.
- **Cannot run**: the stack carries a flow, and its `requires` lacks
`automation`. The config loads and the server never runs the flow. The
command prints the whole `requires` list to use.
- **Refused**: the config loaded before the command wrote anything and no
longer loads with the new file in place, because the stack refuses it. Two
examples are an action or app bound to an object nobody declared, and a flow
in a stack without `triggers`. The command removes what it wrote, so the
project is as it was, and exits 1 with the stack's own reason. Generate the
object first (`os g object customer`), then what binds to it.

`os g` never edits `objectstack.config.ts`: the config is yours, and the
command only loads it.

```bash
os g object customer # Generate a Customer object
os g view customer # Generate a Customer list view
os g action approve # Generate an action
os g flow customer # Generate an automation flow
os g action customer # Generate an action on Customer records
os g flow customer # Generate a flow that runs when a Customer changes
os g dashboard sales # Generate a dashboard
os g app crm # Generate an app definition
os g app customer # Generate an app whose navigation opens Customer
os g skill lead_qual # Generate an AI skill

os g object task -d lib/ # Override target directory
Expand All @@ -1421,15 +1453,15 @@ os g object task --dry-run # Preview without writing

**Available types:**

| Type | Default Directory | Written as | Description |
|------|------------------|------------|-------------|
| `object` | `src/objects/` | `NAME.object.ts` | Business data object with fields |
| `view` | `src/views/` | `NAME.view.ts` | List or form view definition |
| `action` | `src/actions/` | `NAME.action.ts` | Button or batch action |
| `flow` | `src/flows/` | `NAME.flow.ts` | Automation flow |
| `dashboard` | `src/dashboards/` | `NAME.dashboard.ts` | Analytics dashboard |
| `app` | `src/apps/` | `NAME.app.ts` | Application navigation |
| `skill` | `src/skills/` | `NAME.skill.ts` | AI skill — the ADR-0063 extension primitive |
| Type | Default Directory | Written as | Collected as | Description |
|------|------------------|------------|--------------|-------------|
| `object` | `src/objects/` | `NAME.object.ts` | `objects` | Business data object with fields |
| `view` | `src/views/` | `NAME.view.ts` | `views` | List or form view definition |
| `action` | `src/actions/` | `NAME.action.ts` | `actions` | Button or batch action |
| `flow` | `src/flows/` | `NAME.flow.ts` | `flows` | Automation flow |
| `dashboard` | `src/dashboards/` | `NAME.dashboard.ts` | `dashboards` | Analytics dashboard |
| `app` | `src/apps/` | `NAME.app.ts` | `apps` | Application navigation |
| `skill` | `src/skills/` | `NAME.skill.ts` | `skills` | AI skill — the ADR-0063 extension primitive |

<Callout type="info" title="Why generated files carry a type infix">
Every scaffold is written as `NAME.TYPE.ts`, and the infix is read from the
Expand Down Expand Up @@ -2165,10 +2197,10 @@ os g object account
os g object contact
os g object opportunity

# 3. Add business logic
os g flow lead_qualification
# 3. Add business logic: a flow that runs when an opportunity changes
os g flow opportunity

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

# 5. Start development with Console UI
Expand Down
Loading
Loading