@lotics/cli 0.237.0 → 0.238.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/clauses.md CHANGED
@@ -62,14 +62,16 @@ of them, over the rows of your own model, before a table exists.
62
62
  | `acts.import` | Turn a file into rows — mapped, validated per row, previewed, then upserted by the entity's natural key | `{"kind":"import","label":"Import the statement","entity":"payment","key":"reference","columns":["date","amoun…` |
63
63
  | `acts.import.columns` | The fields the file may fill, in this order — absent, every field a person states | `["date","amount","direction","state"]` |
64
64
  | `section_acts` | Acts on the RECORD's own sections, keyed by the alias each section is derived from — a field alias (its progress, its prose, its set, its charge, its files) or a child entity's alias (its register, its desk, its run, its log) | `{"document":[{"label":"Chase the papers","template":"chaser"}]}` |
65
- | `sections` | How a section of the RECORD is DRAWN, keyed by the child entity alias its register is derived from — or `{"heading": …}`, `{"facts": […]}` or both alone to rename any section of the rows it owns or head it with the record's facts | `{"order_line":{"draw":"worksheet","cost":"line_cost","sell":"line_total"}}` |
66
- | `sections[] as Worksheet section` | Priced lines the reader works down in place, each part footed and the sheet closing under them | `{"draw":"worksheet","cost":"line_cost","sell":"line_total"}` |
65
+ | `sections` | The RECORD's sections, drawn in exactly this order — each the alias a section is derived from (a child entity's rows; a field's files, prose, charge, set or ladder), or `{"of": <child>, …}` with how those rows are drawn; a section left out is not drawn, `[]` draws none, and a counted child is named after every open section. Absent, every section the record derives, in the recipe's order. The facts are banded by `facts`, and a closing step always ends the record | `[]` |
66
+ | `sections[] as alias` | A section drawn as the roles derive it — the alias it is derived from | `"stage"` |
67
+ | `sections[] as Worksheet section` | Priced lines the reader works down in place, each part footed and the sheet closing under them | `{"of":"order_line","draw":"worksheet","cost":"line_cost","sell":"line_total"}` |
67
68
  | `sections[].heading` | What this section is headed on the record; absent, the child entity's own label | `"Touches"` |
68
69
  | `sections[].facts` | Fields of THIS record drawn at the head of the section, above its rows — filed here, and so in no band of the record's facts | `["outstanding"]` |
69
70
  | `sections[].cost` | The child's field holding what a line COSTS — the base the margin is taken against | `"line_cost"` |
70
71
  | `sections[].sell` | The child's field holding what it SELLS for — the figure the sheet is read for | `"line_total"` |
71
- | `sections[] as Ledger section` | A book of movements — closes on its total, read against the record's rollup that sums it | `{"draw":"ledger","facts":["outstanding"]}` |
72
- | `sections[] as Itinerary section` | A run of stops read a day at a time — stated to place the child's fields on the stop where the roles' placement is not the reading | `{"draw":"itinerary","lines":[["window","fitting_hours","buyer"],["line_total"]],"badge":"status"}` |
72
+ | `sections[] as Ledger section` | A book of movements — closes on its total, read against the record's rollup that sums it | `{"of":"payment","draw":"ledger","facts":["outstanding"],"columns":["date","amount","reference"]}` |
73
+ | `sections[].columns` | The child's fields this register draws as its columns, in this order — at most 4, each one its role or its type draws as a column; absent, the child's roles decide | `["date","amount","reference"]` |
74
+ | `sections[] as Itinerary section` | A run of stops read a day at a time — stated to place the child's fields on the stop where the roles' placement is not the reading | `{"of":"order_line","draw":"itinerary","lines":[["window","fitting_hours","buyer"],["line_total"]],"badge":"st…` |
73
75
  | `sections[].name` | The child's field that NAMES the entry and leads it; absent, its `identity` | `"subject"` |
74
76
  | `sections[].words` | The child's text field the entry SAYS; absent, its `body`, else (on a log) its first markdown text | `"words"` |
75
77
  | `sections[].lines` | The entry's supporting lines, at most 2, each the child's fields it reads in order, at most 4; absent, a log reads its selects, the parties it was with and the address it reached them at, and a stop reads its slot and its party, with its amount on a second line | `[["with_party"]]` |
@@ -77,31 +79,30 @@ of them, over the rows of your own model, before a table exists.
77
79
  | `sections[].figure` | The child's number worn at the trailing end of the entry's head; absent, a log's amount, and nothing on a stop | `"hours_open"` |
78
80
  | `sections[].evidence` | The child's fields drawn under the words as what the entry shows, a playable file played; absent, its `recording` | `["photos"]` |
79
81
  | `sections[].detail` | The child's fields kept behind one fold under the words; absent, its `verbatim` | `["desk_note"]` |
80
- | `sections[] as Claim section` | Lines claimed against a priced schedule, one period at a time — the contract, before, now, to date and what is left | `{"draw":"claim","quantity":"now","contract":"contract_qty","price":"price","previous":"before","unit":"unit",…` |
82
+ | `sections[] as Claim section` | Lines claimed against a priced schedule, one period at a time — the contract, before, now, to date and what is left | `{"of":"claim_line","draw":"claim","quantity":"now","contract":"contract_qty","price":"price","previous":"befo…` |
81
83
  | `sections[].unit` | The child's field naming what the line is counted in | `"unit"` |
82
84
  | `sections[].retention` | A percentage field of THIS record — the share held back from what is claimed | `"retention"` |
83
- | `sections[] as Tree section` | The rows nested under each other, each money figure summed up the tree | `{"draw":"tree","nest":"part_of"}` |
84
- | `sections[] as Gantt section` | The rows as bars across the calendar — from each row's `when`, for the days its measure counts | `{"draw":"gantt","until":"finishes","nest":"part_of","depends_on":"after","baseline":{"start":"planned_start",…` |
85
+ | `sections[] as Tree section` | The rows nested under each other, each money figure summed up the tree | `{"of":"claim_line","draw":"tree","nest":"part_of"}` |
86
+ | `sections[] as Gantt section` | The rows as bars across the calendar — from each row's `when`, for the days its measure counts | `{"of":"works_task","draw":"gantt","until":"finishes","nest":"part_of","depends_on":"after","baseline":{"start…` |
85
87
  | `sections[].until` | The child's date field each bar is drawn TO; absent, a bar runs for the days its measure counts | `"finishes"` |
86
88
  | `sections[].nest` | The child's one-row link to its own entity — folds a row's bars under the row it sits under | `"part_of"` |
87
89
  | `sections[].depends_on` | The child's link to its own entity naming the rows that must finish before a row starts | `"after"` |
88
90
  | `sections[].baseline` | The child's date fields each row was PLANNED to start and end on, drawn under its bar | `{"start":"planned_start","end":"planned_end"}` |
89
- | `sections[] as Curve section` | Two figures of the rows, each added up to date along the rows' `when` and drawn as two lines | `{"draw":"curve","planned":"planned_value","actual":"earned_value"}` |
90
- | `sections[] as Log section` | Dated entries read in the words each states — stated to place the child's fields on the entry where the roles' placement is not the reading | `{"draw":"log","name":"subject","words":"message","evidence":["photos"],"detail":["desk_note"],"lines":[["with…` |
91
- | `sections[] as Publish section` | One row per destination this record stands on — the strip, the preview, and the press that sends them | `{"draw":"publish","states":{"queued":"queued","published":"out","failed":"refused","by_hand":"by_hand"},"text…` |
91
+ | `sections[] as Curve section` | Two figures of the rows, each added up to date along the rows' `when` and drawn as two lines | `{"of":"works_task","draw":"curve","planned":"planned_value","actual":"earned_value"}` |
92
+ | `sections[] as Log section` | Dated entries read in the words each states — stated to place the child's fields on the entry where the roles' placement is not the reading | `{"of":"order_message","draw":"log","name":"subject","words":"message","evidence":["photos"],"detail":["desk_n…` |
93
+ | `sections[] as Publish section` | One row per destination this record stands on — the strip, the preview, and the press that sends them; the record's facts read after it, unless a band is placed `first` or `last` | `{"of":"posting","draw":"publish","states":{"queued":"queued","published":"out","failed":"refused","by_hand":"…` |
92
94
  | `sections[].text` | A plain text on the child — what this one destination goes out with instead of the body | `"wording"` |
93
95
  | `sections[].permalink` | A link-formatted text on the child — where the post landed | `"address"` |
94
96
  | `sections[].error` | A text on the child holding the platform's own refusal | `"fault"` |
95
97
  | `sections[].link` | A link-formatted text of THIS entity — the address the post carries as its card | `"source"` |
96
98
  | `sections[].when` | Offer the desk's Publish only while THIS record stands at one of these stages | `{"field":"state","in":["ready"]}` |
97
- | `sections[] as Acts section` | Acts waiting for a person — the words, where to reach them, and the sheet that records what came of it | `{"draw":"acts","heading":"Next steps","states":{"queued":"waiting","done":"done","skipped":"passed"},"verb":"…` |
99
+ | `sections[] as Acts section` | Acts waiting for a person — the words, where to reach them, and the sheet that records what came of it | `{"of":"follow_up","draw":"acts","heading":"Next steps","states":{"queued":"waiting","done":"done","skipped":"…` |
98
100
  | `sections[].why` | A text on the child — why this is worth doing now | `"why_now"` |
99
101
  | `sections[].reason` | A text or a single select on the child — why it was passed over | `"pass_reason"` |
100
102
  | `sections[].reach` | Verb option → where that act is done; a verb this map leaves out is done wherever the reader already is | `{"reply":{"at":"enquiry.source","by":"link"},"message":{"at":"page","by":"link"},"call":{"at":"phone","by":"p…` |
101
103
  | `sections[].withhold` | A yes/no of THIS entity — while it reads yes, no act reaches out, and the reach verb says why | `"quiet"` |
102
104
  | `sections[].cta` | The register's row wears this record's NEXT waiting act as its verb — the one press that reaches the person | `true` |
103
- | `sections[] as Section heading` | Rename a section of the rows this record owns, or head it with the record's facts that frame them, however the roles draw it | `{"heading":"Touches"}` |
104
- | `children` | The child entities whose rows the RECORD draws in this app, in this order — only these; absent, every one its links derive | `["works_task","claim_line"]` |
105
+ | `sections[] as Section heading` | Rename a section of the rows this record owns, head it with the record's facts that frame them, or name its columns, however the roles draw it | `{"of":"conversation","heading":"Touches"}` |
105
106
  | `filters` | The chips beside the search — a single-select or select_member field's alias, `{"field": …, "default": "mine"}` to open a member field on the reader's own rows, or a lens this model states as predicates over the entity's fields | `["kind"]` |
106
107
  | `filters[] as Lens` | A lens the MODEL states as predicates over the entity's fields, where no select holds the answer | `{"label":"Shelf","predicates":[{"label":"Empty","tone":"rose","where":{"node_type":"group","logic":"and","chi…` |
107
108
  | `filters[].sort` | The order the rows are read in while the lens is on | `{"field":"next_due","order":"asc"}` |
@@ -44,7 +44,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
44
44
  | `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` — one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |
45
45
  | `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** — the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches — while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as "everything". |
46
46
  | `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
47
- | `lotics app create <name> [path] [--api]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1. **`--api`** scaffolds an app with NO screens instead: the app's declared queries and workflows are the whole of what it offers, called over HTTP by the customer's own site, server or agent (`POST /v1/apps/{app_id}/queries/{alias}` and `/workflows/{alias}/execute`). It writes `package.json` (the `lotics` block, `typescript` as its only devDependency, `typecheck` as its only script), `tsconfig.json`, the brief in `src/workflows/`, a CI workflow and the two READMEs — no `index.html`, no `src/App.tsx`, no `vite.config.ts`, no kit. Nothing is built and nothing is deployed, so `current_version_id` stays null: `lotics app query set` and `lotics app workflow set` publish each declaration on their own, and `lotics app api publish` snapshots what they promise. The npm registry is not consulted (there is no `@lotics/app-runtime` range to resolve), `npm install` still runs for `typescript` (which `app workflow check` loads), and, for want of a bundle, `app dev` and `app check --screens` (no `vite.config.*`) and `app deploy` (no `build` script) are refused on such a project in one sentence, before a binding is pushed; `app pull` says its project lives in version control, where `app codegen` rebuilds `.lotics/`. `--api` beside `--from` is refused before anything is written — a plan describes screens. The generated `package.json#name` is the app name FOLDED to ASCII (`Đơn hàng` → `don-hang`), never stripped of it — dropping the marks would treat each accented vowel as a separator and can slug a name away to nothing. The app's display name is unaffected; this is the npm field only. **`--from <model.json>#<app>` builds the app a PLAN describes, and it is JSON.** The file is checked as `scaffold check` checks it, the named app (or the only one) is taken, every screen's entity is found as the live table this WORKSPACE remembers the alias became — and BY LABEL only for an alias nothing has bound, which is a new entity or a workspace nothing has applied this model to — so a table or a column relabelled since the apply is still the same one, while a bound id the workspace no longer serves is refused by name rather than re-bound to its namesake; then every field the model declares on it — the record shows the ones the list leaves out — and, for each child entity a record section is over, its table and the fields that section draws, and, for each one-row link the record's facts let a reader RE-POINT, the target entity's table and the column its rows are picked by (`display_field_aliases`, else the target's `identity`) — a table the workspace lacks is refused first, naming every missing one and `scaffold apply`; then a field a table lacks, a label two tables or two fields share, or a field whose live type is not the model's — all before anything is created. **The app is BOUND to the plan's alias** (`PUT /v1/workspaces/model/binding/apps`) the moment its row exists, so a `sibling` act naming that alias in another app opens this app; an alias already bound to another app is refused before the row is made — `lotics app pull <app_id>` works on that one, `lotics scaffold unbind app <alias>` first makes a new one the plan's — and a bound app the workspace no longer holds is refused the same way, never replaced in silence. **What it writes is `app.json`**: the bound plan, whole — every screen with its registry shape and the fields its slots read, its strip, lenses, summary and acts; the RECORD that shape opens, off the same roles (`recordSections`), with its header, its band, its facts in bands and its sections in the archetype's job order — a progress over the lifecycle, an expected set per required set, a children register per child entity, a files pile per files field; and the create panels, each with the inputs its workflow declares. Every id in it is live (`tbl_`, `fld_`, `opt_`). Beside it: a five-line `src/main.tsx` that mounts `@lotics/app-runtime` over the spec, `src/components/index.ts` seeded empty (the one hatch — a screen or a section the plan has no word for names a component there), one `src/workflows/<alias>.ts` per write, and the README. There is no screen source: **the runtime draws the spec**, so a kit correction reaches the app with its next `npm install` rather than with a regeneration, and `@lotics/app-runtime` is installed as a dependency for a plan-built app only. The manifest declares one `project` query per screen (every column the screen and its record read, a files cell whole, a dated book newest first) plus one per child entity, filtered to the record through whichever of those links names it and taking it as a declared `{{params.<entity>_id}}`, plus one per re-pointable link — the target's rows under their naming column alone, sorted by it, carrying the target's own `read_scope` and a `{{params.search}}` the picker narrows by server-side; the `.lotics` companions and `app_fields.ts` are written before the first build, and the deploy pushes the queries as it pushes any. **And it declares what the app WRITES** — `package.json#lotics.writes`, table alias → the field aliases each record surface's editor changes, derived from the same `update_<entity>` declarations it emits beside the bodies — **and what it DELETES**, `package.json#lotics.deletes`, the table aliases whose ROWS its bodies take (a delete names no column, so no field entry could carry it — a publish desk's child is one, since withdrawing a publication deletes its row). Seeded once: from then on the declaration is the app's, and `app check` refuses a body that writes or deletes outside it. A screen the plan marks `writes: false` contributes none. |
47
+ | `lotics app create <name> [path] [--api]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1. **`--api`** scaffolds an app with NO screens instead: the app's declared queries and workflows are the whole of what it offers, called over HTTP by the customer's own site, server or agent (`POST /v1/apps/{app_id}/queries/{alias}` and `/workflows/{alias}/execute`). It writes `package.json` (the `lotics` block, `typescript` as its only devDependency, `typecheck` as its only script), `tsconfig.json`, the brief in `src/workflows/`, a CI workflow and the two READMEs — no `index.html`, no `src/App.tsx`, no `vite.config.ts`, no kit. Nothing is built and nothing is deployed, so `current_version_id` stays null: `lotics app query set` and `lotics app workflow set` publish each declaration on their own, and `lotics app api publish` snapshots what they promise. The npm registry is not consulted (there is no `@lotics/app-runtime` range to resolve), `npm install` still runs for `typescript` (which `app workflow check` loads), and, for want of a bundle, `app dev` and `app check --screens` (no `vite.config.*`) and `app deploy` (no `build` script) are refused on such a project in one sentence, before a binding is pushed; `app pull` says its project lives in version control, where `app codegen` rebuilds `.lotics/`. `--api` beside `--from` is refused before anything is written — a plan describes screens. The generated `package.json#name` is the app name FOLDED to ASCII (`Đơn hàng` → `don-hang`), never stripped of it — dropping the marks would treat each accented vowel as a separator and can slug a name away to nothing. The app's display name is unaffected; this is the npm field only. **`--from <model.json>#<app>` builds the app a PLAN describes, and it is JSON.** The file is checked as `scaffold check` checks it, the named app (or the only one) is taken, every screen's entity is found as the live table this WORKSPACE remembers the alias became — and BY LABEL only for an alias nothing has bound, which is a new entity or a workspace nothing has applied this model to — so a table or a column relabelled since the apply is still the same one, while a bound id the workspace no longer serves is refused by name rather than re-bound to its namesake; then every field the model declares on it — the record shows the ones the list leaves out — and, for each child entity a record section is over, its table and the fields that section draws, and, for each link the record's facts let a reader pick, the target entity's table and the column its rows are picked by (`display_field_aliases`, else the target's `identity`) — a table the workspace lacks is refused first, naming every missing one and `scaffold apply`; then a field a table lacks, a label two tables or two fields share, or a field whose live type is not the model's — all before anything is created. **The app is BOUND to the plan's alias** (`PUT /v1/workspaces/model/binding/apps`) the moment its row exists, so a `sibling` act naming that alias in another app opens this app; an alias already bound to another app is refused before the row is made — `lotics app pull <app_id>` works on that one, `lotics scaffold unbind app <alias>` first makes a new one the plan's — and a bound app the workspace no longer holds is refused the same way, never replaced in silence. **What it writes is `app.json`**: the bound plan, whole — every screen with its registry shape and the fields its slots read, its strip, lenses, summary and acts; the RECORD that shape opens, off the same roles (`recordSections`), with its header, its band, its facts in bands and its sections in the archetype's job order — a progress over the lifecycle, an expected set per required set, a children register per child entity, a files pile per files field; and the create panels, each with the inputs its workflow declares. Every id in it is live (`tbl_`, `fld_`, `opt_`). Beside it: a five-line `src/main.tsx` that mounts `@lotics/app-runtime` over the spec, `src/components/index.ts` seeded empty (the one hatch — a screen or a section the plan has no word for names a component there), one `src/workflows/<alias>.ts` per write, and the README. There is no screen source: **the runtime draws the spec**, so a kit correction reaches the app with its next `npm install` rather than with a regeneration, and `@lotics/app-runtime` is installed as a dependency for a plan-built app only. The manifest declares one `project` query per screen (every column the screen and its record read, a files cell whole, a dated book newest first) plus one per child entity, filtered to the record through whichever of those links names it and taking it as a declared `{{params.<entity>_id}}`, plus one per pickable link — the target's rows under their naming column alone, sorted by it, carrying the target's own `read_scope` and a `{{params.search}}` the picker narrows by server-side; the `.lotics` companions and `app_fields.ts` are written before the first build, and the deploy pushes the queries as it pushes any. **And it declares what the app WRITES** — `package.json#lotics.writes`, table alias → the field aliases each record surface's editor changes, derived from the same `update_<entity>` declarations it emits beside the bodies — **and what it DELETES**, `package.json#lotics.deletes`, the table aliases whose ROWS its bodies take (a delete names no column, so no field entry could carry it — a publish desk's child is one, since withdrawing a publication deletes its row). Seeded once: from then on the declaration is the app's, and `app check` refuses a body that writes or deletes outside it. A screen the plan marks `writes: false` contributes none. |
48
48
  | `lotics app regenerate [--from <model.json>#<app>] [--dry-run] [--bind-new] [--screens]` | **Re-run the plan over an app that already exists, and rewrite its spec.** Run inside the app directory. The plan is whatever `package.json#lotics.plan` remembers — written there by `app create --from` and by this command — unless `--from` names another, which is then remembered in its place; no plan and no flag is a refusal, as is a directory with no `lotics.app_id`, a model that does not check (every finding printed), and a plan that names no such app. It resolves against the LIVE workspace through the same function `app create --from` resolves with, so the refusals and the output are that command's. **Files. The generator owns what it emits.** `app.json` is derived from the plan and rewritten whole — there is nothing in a spec to merge — the entry beside it never varies, and a workflow body is the generator's too: what it replaced goes into `.lotics/regenerate-dropped.patch`, file by file, rather than into a three-way merge, because a conflict marker inside a body is a file the server has to parse. A body is compared as its BODY, since `app codegen` wraps every one on disk in the header and `__workflow` envelope the server verifies against, so comparing bytes would read that wrapper as your edit on every app. **`src/components/` is the exception and the only one**: seeded where it is absent, named back where you have changed it, never rewritten — it is the hatch, so it is the app's code. A body whose alias the generator has retired is deleted. **Manifest.** `.lotics/generated/manifest.json` records the alias sets each generation declared, and that is what the reconciliation reads: queries and workflow declarations the generator emits replace their counterparts (each `workflow_id` carried over — the server minted it), aliases the last generation emitted and this one does not are removed and listed, and aliases you added by hand are kept. `lotics.writes` gains the generator's columns and `lotics.deletes` its tables, and each loses an entry on two facts only, each named in the summary: a column the live table no longer carries, and one nothing in the app WRITES any more (a delete retires on the second rule alone — it names no column for a rename to strand) (the bodies it holds after the run, plus this generation's own declaration — a column a screen only READS is not a write, and `app check` can never find one, because a declaration covering more than the bodies write refuses nothing). A body the subset refuses, or one calling a tool this CLI's registry does not know, suspends that second rule for the run: nothing is dropped on a guess. **It pushes NOTHING.** What the live app RUNS changes at `lotics app deploy` and nowhere else, because the bundle production serves was built against the bindings it has: a regeneration that replaced a live workflow body or a live picker query left a deployed create dialog posting inputs its workflow no longer declared, and a picker answering nothing. Instead the summary NAMES what a deploy will do to the live app — `add` for an alias it does not have, `change` for one this tree is ahead of, `remove` for one the generator retired that this checkout bound and the app still serves (the next deploy unbinds it; the retirement is recorded in `.lotics/generated/manifest.json` until one does) — read through the same detector `app check` reports from and `app deploy` pushes from, so the preview cannot disagree with the deploy. **`--bind-new`** is the one live effect left: it binds the aliases the app does not have YET and refuses, naming them, to touch one that already exists, because `lotics app dev` forwards its queries to production and a new alias cannot be exercised until something binds it, while adding one the deployed bundle never calls cannot change what that bundle does. **The runtime follows the spec.** The spec is written for the `@lotics/app-runtime` line this CLI generates for, so the directory's `@lotics/app-runtime` moves to the runtime's latest line and a `@lotics/ui` the app lists to the range that runtime depends on, in one install, and the summary names each move; a range already on its line is left as written, an app still listing `@lotics/app-sdk` is migrated as `lotics app kit --published` migrates it, and a runtime installed from a checkout (`lotics app kit`) is left and named. The plan's icon and colour are compared with the live app's and a difference is named — `workspace build` sets it, since this command changes nothing live. Then `app codegen` runs, the summary prints — files written / kept / deleted, what a deploy will change, what was bound ahead, and `lotics app deploy` as the next command — and the fast `app check` runs (`--screens` passes through), with the bindings this run deliberately left ahead reported by the summary rather than failed by the check. `--dry-run` decides the whole run, prints it, and writes nothing: not a file, not a binding; it cannot be combined with `--bind-new`. |
49
49
  | `lotics app eject <screen\|<section key>\|<act label>>` | **Hand ONE part of a JSON app's spec to the app — one-way, per part.** Run inside the app directory; it is local and offline, and touches no workspace. It writes `src/components/<Name>.tsx` whose whole body renders what `@lotics/app-runtime` was rendering for that part — `ScreenView` over the screen the spec still states, `RecordSectionView` over the node the section WAS, `ActView` over the act — points `app.json` at it (`component` on the screen, the section node replaced by `{"kind": "custom", …}`, or `component` on the act), and adds the import and the map entry to `src/components/index.ts`, which is what the app's entry reads its components from. **The first eject lists the kit**: a JSON app lists `@lotics/app-runtime` alone, and a component of its own is where it starts importing `@lotics/ui` itself, so the first one adds `@lotics/ui` to `package.json` — and to the lockfile's root, so `npm ci` still agrees — at the range the INSTALLED runtime depends on, and says so. The node a section was travels INTO the file, because the spec no longer states it — which also takes that section out of what `app check` proves, since the spec no longer names the columns it reads; the plan still declares the query behind it, so the manifest keeps the alias. The name is derived (`<Screen>Register`, `<Entity><Key>`, `<Address>Act`), so two ejects can never collide. **Targets**: `screen` (or the register's alias) for the register; a section by its `key`, or `<entity>.<key>` where two records carry the same one; an ACT by its label, its key, or its address (`screen#<key>` for the row's ⋯, the selection bar or the export, `<entity>#<key>` for a record's own header menu, `<entity>.<section>#<key>` for a verb on a section — the `#` is what keeps an act's address off a section's). **An act's eject ADDS a surface rather than taking one over**: a paper act's default is to make the document on the press, so the component is the PANEL it now opens instead — the runtime owns the dialog, the file starts at the runtime's own confirm-and-run body, and `props.run({ … })` makes the paper with whatever the panel asked for beside the rows it was pressed on. An AGENT act is refused: the panel a run is reviewed in is the kit's — started once, reviewed before it is applied, cancelled by closing — so there is nothing to hand over without handing those over too. **Refusals, all before anything is written**: a directory that is not a JSON app; an `app.json` that does not read (every finding printed); a part already ejected, naming the component it points at; a `src/components/<Name>.tsx` that already exists, because that file IS the ejected part and rewriting it would be the generator taking your screen away; a word that reaches two parts, naming every address; on the first eject, a runtime that is not installed or a lockfile holding no `@lotics/ui` at the top of `node_modules` (run `npm install`, then eject again); a name something under `src/components/` already exports; and a `src/components/index.ts` that no longer states `export const components: RuntimeComponents = { … }`, which names the two lines to add by hand rather than guessing at the author's own file. **It is one-way because the file is.** Dropping the clause by hand does not put the component back — delete the file too. `lotics app regenerate` keeps both: `app.json` is rewritten whole, and every `component` clause an eject wrote is folded back onto the fresh spec, so the parts you did NOT eject go on taking every ruling the runtime makes. A part whose key the plan later retires has no node left to point at, and its clause goes with it; the file stays. |
50
50
  | `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, install dependencies (`npm ci --ignore-scripts` when a lockfile is present, else `npm install --ignore-scripts`), stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir. **A pull never overwrites a file that differs from what it is about to write** — it writes only what is ABSENT or already identical, keeps the rest, and reports which files it kept plus the commands that close the gap. The same rule covers `src/workflows/<alias>.ts` and `src/agents/<alias>.md`, so an unpushed body or prompt survives too. Those two are written from the LIVE App row (`apps.workflows` / `apps.agents`), which owns them, and the archive's own copy of them is deliberately SKIPPED on extract: a deploy tars the whole source directory, so the tarball holds a deploy-time snapshot that is stale for anything authored since. The comparison is against the app's own content, not git, so it holds for a project that was never a repo. `--force` takes the app's copy and DISCARDS local edits; there is no other way to lose them. **For a KEPT workflow body or agent prompt the pull records the server's fingerprint only when the server has not moved** — that token is `set_app_workflow`/`set_app_agent`'s lost-update precondition, so recording one for text the author has not seen would clear the next push's refusal by disarming the guard, and silently overwrite whoever edited it. When the live text HAS moved, the pull writes it beside the checkout (`.lotics/agents/<alias>.live.md`, `.lotics/workflows/<alias>.live.ts`), names it, and leaves the token stale: the next deploy is refused, which is correct, and the text to merge is now on disk. A file whose prose/body already reads back AS the live text is never "kept" at all — the baselines are healed from it, so a checkout whose prose was pushed out of band (chat, `lotics run set_app_agent`) converges instead of latching. **A pull also REPORTS the files it restored** when refreshing a tree that already claimed a version: a pull mirrors the last DEPLOYED source, so a file deleted locally comes back until the deletion itself ships, and saying so is the only honest fix — nothing can read a deletion off the disk. **When a pull ACROSS versions keeps files, the manifest is left on the OLDER of the two versions** — the tree is then part one and part the other, and claiming the newer would make `deploy`'s `prev_version_id` check pass and ship a half-and-half bundle. Older, not "the one it had": `--from-version` pulls a deliberately old revision, so the version it had is the NEWER side, and holding that would match what the server serves and let the old source ship. Either way a deploy from that tree is refused until you reconcile the listed files by hand and pull again, or take the app's copy with `--force`. The report says which version each side is on, because a kept file is your unshipped work when the project was already current and merely the OLD version when it was behind — and nothing in a byte comparison can tell those apart. **`--from-version <apv_…>`** pulls an OLDER revision instead of the current one (`lotics app versions` lists the ids) — point it at a NEW path to read a previous revision without disturbing the project you are in. The manifest records the version actually written, never the live pointer, so a deploy from that checkout is refused by the version guard rather than shipping old source over newer. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed, AND — AFTER `npm install`, so `node_modules/@lotics/ui` actually exists to read — `.lotics/tsconfig.link.json`'s peer pins and, for a kit old enough to ship one, its `react-native` augmentation (a kit that ships none has the previously-written copy deleted); a pulled project's own `tsc` used to fail until `app codegen` was run by hand, because nothing had regenerated either one after install populated node_modules. AND the runtime `.lotics/app_fields.ts` (the same generation `app codegen` runs, off the app row already fetched). That one is not optional: `app deploy` tars source with `--exclude=.lotics`, so no archive can carry it, and a pulled project whose `src/` imports `F`/`OPT` would fail to build with `Could not resolve "../../.lotics/app_fields"` until `app codegen` was run by hand. Skipped under `--view-as` (the schema is read as that member and silently drops tables they cannot see — a narrowed `F` map compiles and then throws at runtime, worse than the missing module). A schema fetch failure is non-fatal and names the right recovery for what is on disk: an existing file is kept, an ABSENT one warns about the build error and points at `app codegen`. Pull GENERATES but never RECONCILES `.lotics/` — deleting a companion whose alias the manifest no longer declares is `app codegen`'s alone, since pull's authority is the server's alias set and a declared-but-not-yet-`set` alias is supported. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file. A pull writes it from live UNLESS the local file holds unpushed work, in which case it is kept and the live text is parked beside the checkout — the same rule the rest of this row describes. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_tier`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.237.0",
3
+ "version": "0.238.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {