@lotics/cli 0.263.0 → 0.264.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/AGENTS.md +30 -50
- package/README.md +49 -205
- package/dist/src/cli.js +31069 -84675
- package/dist/src/cli.js.LEGAL.txt +0 -16
- package/dist/src/client.d.ts +18 -1231
- package/dist/src/client.js +14 -727
- package/dist/src/invocation.d.ts +1 -1
- package/docs/building_an_app.md +88 -409
- package/docs/cli_reference.md +9 -42
- package/docs/knowledge_docs.md +0 -8
- package/docs/migration.md +64 -97
- package/package.json +1 -1
- package/dist/probe_page.js +0 -2381
- package/dist/render_page.js +0 -67559
- package/dist/render_page.js.LEGAL.txt +0 -14
package/docs/cli_reference.md
CHANGED
|
@@ -6,7 +6,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
6
6
|
|---|---|
|
|
7
7
|
| `lotics` / `lotics --help` | Show full help with capabilities, tool categories, workflow |
|
|
8
8
|
| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |
|
|
9
|
-
| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open — `https://lotics.ai/cli_login/<request_id>`, also mailed — and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential — `
|
|
9
|
+
| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open — `https://lotics.ai/cli_login/<request_id>`, also mailed — and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential — `docs` among them — claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |
|
|
10
10
|
| `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
|
|
11
11
|
| `lotics auth web` | Send a magic link email to access the web app (requires auth) |
|
|
12
12
|
| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key — read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |
|
|
@@ -22,14 +22,12 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
22
22
|
| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone — `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |
|
|
23
23
|
| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** — `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too — every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |
|
|
24
24
|
| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` — every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> "<name>" (<id>) → <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |
|
|
25
|
-
| `lotics workspace build <model.json> [--dry-run] [--deploy]` | **A model to live apps in one command**, composed out of the verbs that already own each step — it authors nothing, so every refusal a reader sees is the refusal the underlying command writes. In order: `scaffold check` (a model that does not check stops the run with its findings and nothing is written); the `scaffold diff` join against this workspace, printed; `scaffold apply` **only when that diff found something apply acts on** — what the workspace lacks, or a disagreement it settles or refuses — because the apply is additive and idempotent but costs a round trip per table and the common case is a model that has not moved; a difference apply leaves as the workspace has it (what only the workspace holds, a relabel, a date's format) is printed and never runs it, or it would run on every build and change nothing; then, per app the model declares, in the model's order, `app create --from <model.json>#<alias>` into `<dir-of-model>/<alias-with-dashes>` when that directory does not exist, else `app regenerate` there — which also sets the icon and colour the model states where the live tile shows otherwise — then `app check`, then `app deploy` under `--deploy` — which unbinds every alias the regeneration retired (see `app deploy`). **One app's failure is not the run's.** A refusal or a red check is recorded and the next app still runs — the author is going to fix that one and run this again, and an app that never ran is an app whose state nobody knows — so the summary at the end carries a line per app (created/regenerated, files rewritten, the check verdict, the version deployed) and the exit code is 1 if any line is bad. **`--dry-run` writes nothing, locally or remotely** — it checks the model, prints the diff and names which apps it would create and which it would regenerate. The check names a regenerated app's bindings as what its deploy will push, never as a refusal, and a run without `--deploy` ends by naming how many bindings still await one. Admin-only, like the apply it runs. |
|
|
26
|
-
| `lotics field rename <table> <field> "<new label>" [--model <file>] [--apps <dir>]` | **One field renamed everywhere it is addressed.** `<table>` and `<field>` each take a name or an id/key. ONE `update_table` call, then the places that call the old name: the label inside a `--model` file (rewritten as JSON, addressed by the entity and field the rename names, so a namesake label elsewhere is left alone), and every `--apps` project (repeatable) whose `src/**/*.{ts,tsx}` addresses `T.<field>`, `F.<TABLE>.<field>` or `OPT.<TABLE>.<field>.*` — each rewritten and then re-codegened, in that order, so no project is left holding new source against the old map. `T.` is rewritten only in a file that binds `const T = F.<TABLE>;` for THIS table: unscoped it would rename a namesake field on whatever table that file is about, which still compiles and reads the wrong column. **The old→new alias pair is read off the table's schema BEFORE and AFTER the write**, never off slugifying the new label alone: an alias is deduped against its neighbours (`ngay`, `ngay_2`), so a rename that frees a slug moves a field nobody touched — and computing it in isolation would leave that one addressed by a key the map no longer has. Everything after the one write reads its result, so a refused rename leaves the file and every project as they were. Admin-only. |
|
|
27
25
|
| `lotics tools` | List tools by category with descriptions |
|
|
28
26
|
| `lotics tools <name>` | Full description + JSON Schema for one tool |
|
|
29
27
|
| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \| lotics run <tool> -`) — both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once; without the `-` it used to read "not a TTY" as "the payload is coming" and block until the CALLER's timeout, which under any agent harness is always (a harness's stdin is never a TTY). `lotics report` takes the same sentinel for the same reason. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone — the error names both escapes. |
|
|
30
28
|
| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
|
|
31
29
|
| `lotics run <tool>` — **file cells** | A file in a tool's result carries its `fil_…` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file — this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |
|
|
32
|
-
| — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`)
|
|
30
|
+
| — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_query`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`, the `sandbox_*` tools). A command exists only for work that touches a local file: `model apply`, `model pull`, `app create --custom`, `app deploy`. |
|
|
33
31
|
| — | **The exit code reports the WORK, not just the call — for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> → <status>: <message>` to stderr, so `lotics run … && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE — an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` — `press_button`'s included — is data, and exits 0. |
|
|
34
32
|
| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |
|
|
35
33
|
| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes — harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |
|
|
@@ -44,44 +42,13 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
44
42
|
| `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
43
|
| `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
44
|
| `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
|
|
48
|
-
| `lotics
|
|
49
|
-
| `lotics
|
|
50
|
-
| `lotics app
|
|
51
|
-
| `lotics app
|
|
52
|
-
| `lotics
|
|
53
|
-
| `lotics app api publish [app_id]` \| `unpublish` \| `status` \| `spec [-o <file.json>]` | **The app's API — what its declared queries, workflows and agents promise to a caller OUTSIDE it** (a customer's own site or server, an integration, another system). An agent is in the contract as its alias plus whichever of `inputs` / `outputs` it declares, and the ABSENCE of either half is part of the promise: no `inputs` means a run accepts any object, no `outputs` means it answers free text. So declaring an input schema where there was none is breaking (a caller's own keys start being refused), dropping one is additive, and declaring or dropping `outputs` is breaking either way, because the result changes kind. `publish` (`POST /v1/apps/{id}/api/publish`) snapshots that promise as a numbered contract version and prints the version, when it was taken, and every warning about what the published surface exposes — a query anyone holding the public link can reach, a field an owner may not have meant to hand out. It is REFUSED (400) while a query does not name the columns it returns: those field names come from the table and would change under the consumer whenever the table does, so they are not the app's to promise — the refusal names each such alias and the `project` that fixes it, and nothing is written. **From the publish onward a manifest write is a release**: additive changes re-snapshot silently, and one that breaks what is promised is refused with every breaking change named, unless the write carries the acknowledgment (`--acknowledge-breaking-api` on `app deploy` / `app query set` / `app workflow set` / `app agent set` / `app upgrade`). There is no `app agent remove` verb, so removing a published agent is `app deploy --prune --acknowledge-breaking-api` once the bundle stops naming it. `unpublish` ends the promise; the superseded snapshot stays, so a later publish continues the numbering rather than reusing a version. `status` says whether one is published and which version its callers hold. `spec` prints the OpenAPI 3.1 document — rendered by the server from the SNAPSHOT rather than from the manifest, so it describes what the app has promised — to stdout, or to the file `-o` names; 404 while nothing is published. It carries one operation per query, per workflow and per agent, plus, whenever the contract holds an agent at all, the single `GET /v1/apps/{id}/agent-runs/{run_id}` where every run's result is read: an agent's own operation answers a `text/event-stream`, so its `output` shape is stated there and nowhere else. Every `description` the contract declares — on an input, a param, an output, and every nested field and array item — is that node's `description` in the document, so a generated client carries what each value means. The app_id comes from the local manifest, or pass one to act on any app in the workspace. **`--json` answers `publish` / `unpublish` / `status` with one object on stdout and nothing else**, every warning carried in it rather than printed away; `spec` already prints a document there. **Who may run which**: starting and ending the promise is an organization ADMIN's — what an app hands outside the workspace is the same capability that declared it. `status` is the app's AUTHOR's (its owner, or an admin): whether their own app publishes anything is theirs to see. `spec` is reachable by whoever may USE the app, which on a publicly-shared app is anyone holding the link — it is the document a consumer generates their client from. |
|
|
54
|
-
| `lotics app codegen [path]` | Regenerate `.lotics/*` from the manifest + workspace schema **without a deploy**. The three `.d.ts` companions (`app_{workflows,queries,agents}.d.ts`) are always rewritten (synchronous, no network). When credentials resolve, also rewrites the **runtime** `.lotics/app_fields.ts` — a real `.ts` exporting `F` (table→field→`"fld_…"`), `OPT` (table→select-field→option→`"opt_…"`), `TBL` (table→`"tbl_…"`) and `GRP` (member group→`"grp_…"`) keyed by display-name aliases, for every table the app's queries READ plus every table its bound workflows WRITE (each binding's recorded `table_ids`, so a table no screen reads is still addressable by alias and a rename fails `tsc` instead of the body), plus every member group in the organization — which one a screen names is not knowable from the manifest, and a `GRP` narrowed by a failed read is a map that is wrong rather than absent, so a failure writes nothing. **There is one form, and that is what makes a starter's source portable**: the keys are slugified DISPLAY NAMES and a starter carries its labels verbatim, so running codegen in a copy's own workspace emits the same keys pointing at that workspace's ids — no binding fetched at load, no prebuilt bundle to keep in step. Beside it, `.lotics/app_siblings.ts` exports `APP` — every app the workspace's model declares, by the model's alias for it (`APP.<alias>` → `"app_…"`), read off the workspace's model binding — so a hand-written app opens a sibling as `openApp(APP.<alias>, route)` and holds no `app_` id; an app archived here is absent, so a call naming it fails `tsc`. The binding is an admin's read: a key that cannot read it keeps the file on disk and says so. Also refreshes each bound workflow's `.lotics/workflows/<alias>.globals.d.ts` + re-wraps its EXISTING `src/workflows/<alias>.ts` body in the current envelope (strips + re-wraps; never re-fetches the body, so local edits survive). **`.lotics/` is reconciled to the manifest, not merely added to** — a `<alias>.globals.d.ts` whose alias the manifest no longer declares is DELETED. Only that exact filename shape is removed; anything else in the directory is left alone. The reconcile runs before the credential branch, so it happens offline too. The authored counterpart is never deleted — a `src/workflows/<alias>.ts` the manifest does not declare is NAMED instead (`check` and `set` both take their alias set from the manifest, so editing an undeclared body is a silent no-op). A getApp / binding / schema / dts-fetch failure is non-fatal (warns, keeps the last-generated files). **Re-silvers `package.json#lotics.agents`** from the live app row whenever its `inputs`/`outputs` disagree, then rewrites the agent `.d.ts` from the refreshed block: that block is a mirror AND the offline seed for `useAgentRun` typings, so a stale copy types the app against an agent that does not exist. The write is surgical and order-preserving, so it changes only the fields that actually differ. A hand edit to that block is therefore reverted — it never changed the agent anyway; to change one, `set_app_agent`. **Types are written for every DECLARED alias, body file or not** — the dts is rendered from the declaration, which is the whole point of the declare → codegen → write → check loop — and codegen NAMES each alias it wrote types for without a body, with the next step. It used to return early on a missing or blank `src/workflows/<alias>.ts` and say nothing, which left `workflow check` pointing at `app workflow pull` (which writes nothing for an alias the server has never bound) on one path and at `app codegen` — the command that had just declined to write them — on the other. **A body whose helper sits ABOVE the `__workflow` wrapper is refused by name and line** rather than wrapped a second time: the strip peels a wrapper only when it is the first line after the header, so a top-level declaration between the two used to leave the whole file read as the body and the re-wrap nested an envelope per run. Move the helper inside the wrapper — the body is one expression sequence. |
|
|
55
|
-
| `lotics app kit <path-to-packages/app-runtime>` \| `lotics app kit --published` | **The app's ONE range, `@lotics/app-runtime`, from a CHECKOUT, or back to the range it listed before**, so a runtime change is proven on a real app before it is published. Run inside the app directory; the path names `packages/app-runtime` in your clone (a kit change reaches an app through the runtime that depends on it — `LOTICS_UI_SRC` links the kit's source into `app dev`). It runs the runtime's own `npm run build`, `npm pack`s it into the app's `.lotics/kit/` (replacing any earlier pack, so the directory holds one tarball), removes the installed copy and the lockfile's entry for it, installs by file specifier, and then **proves the install**: the first non-dot file of the directory the package's own `package.json#files` publishes is hashed on both sides, and a mismatch — or an absence — is a refusal naming the file. That proof is the point. A repack under the SAME name is the normal case (a working copy's version does not move between builds) and the lockfile pins the first tarball's integrity, so on some npm versions the install is a no-op that reports success while the app keeps building against an hour-old kit; nothing else in the loop can tell. **Either way the rest of the manifest follows the runtime**: `@lotics/ui`, where the app lists it, moves to the range the runtime depends on — two ranges are two copies of the kit — and an app still on `@lotics/app-sdk` (the package the SDK was before it moved into the runtime) is MIGRATED: every `@lotics/app-sdk` and `@lotics/app-sdk/router` specifier under `src/` and in the Vite config becomes `@lotics/app-runtime/sdk` and `@lotics/app-runtime/router`, the dependency is dropped, and the summary names each file rewritten. **Refusals, all before anything is written**: a directory that is not a Lotics app (no `package.json#lotics.workspace_id`); a path that is not `@lotics/app-runtime`; a runtime that declares no `@lotics/ui` range while the app lists the kit; `--published` given a path; a build that failed, named with npm's own output tail. **It records what it did in `package.json#lotics.kit.@lotics/app-runtime`** (`version`, `tarball`, the `source` checkout, `packed_at`), which is what `lotics app check` warns about and what `lotics app deploy` REFUSES on unless you pass `--allow-local-kit` — `.lotics/` is excluded from the source archive a deploy uploads, so an app whose manifest names `file:.lotics/kit/…` ships a bundle that runs and a source tree the next clone cannot install. The record also keeps what the app listed before the FIRST checkout went in (`replaced` — a repack keeps it), and `--published` undoes the trial: the runtime and the kit go back to exactly those specs, so a kit trial never moves an app to another line on its way out; an app with no such record (never given a checkout, or given one by a CLI that recorded none, which the run says) moves to `^<latest>` from the registry and the kit to the range that published version depends on. Either way the tarball and the record are deleted, the lock entry and the installed copy are cleared, and a plain install resolves the published package. Needs no credential — npm, the filesystem and (for `--published` with no prior range) the registry. |
|
|
56
|
-
| `lotics setup <apg_id \| model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes — `--name` and `--timezone` apply), then fills its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** — it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the copy or the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as — that address IS the account it holds, not a second one. **The argument decides which of the two forms this is, by SHAPE**: a `*.json` file is a workspace MODEL — in either of ITS two forms, spelled out or `{"from": "<preset-slug>", …}` — and anything else is a package id copied through `library init`. The suffix decides it alone — asking the filesystem would answer a long library id with `ENAMETOOLONG` instead of with a verdict — and a model is checked OFFLINE before an account is created, because a file with a typo in it must not leave an organization behind. The model form creates no apps, so its sign-in link lands on the first table it made. It sends no `adopt`: an entity whose `label` already names a table in the workspace is REFUSED with every collision named, and the refusal adds the line the server cannot — `lotics scaffold apply <model.json>`, the verb that adds to the workspace you already have. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently copies a package into an org the caller did not name — the message says how to do each thing on purpose. Without it, `setup` copies into the account you already have and is a pure alias for `library init`. A path positional is accepted and IGNORED with a warning — `setup` writes nothing to disk — so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** — `organization_id`, `workspace_id`, `app_ids` (alias → id), `apps` (each app's `version_number`, or its `error`), `signin_url`, and `created` — which NAMES what landed (`tables`, `templates` and `knowledge_docs` are alias arrays; `sample_records` is a row count, since rows are not named things). Aliases rather than counts because the next question is about a particular artifact: a copied template carries the publisher's wording and a copied knowledge doc describes how they work, so "which of these should be mine?" is the conversation a copy starts, and a count cannot begin it. **The model form emits `entities`, `roles`, `record_ids` and `rows_skipped`** in place of `app_ids` / `apps` / `created` — a model creates no apps and nothing named for a copier to review. **The model form also runs the file's `apply` list** — each named package copied in after the tables exist, with that entry's `bind`, in order, stopping at a refusal with everything before it kept — and emits `applied: [{package, apps}]` beside them; **the sign-in link then lands on the FIRST app any applied package created**, falling back to the first table when the model applied none. Plus a `warnings` array carrying everything the prose form would have said out of band — an unbindable knowledge doc, a sign-in link that could not be minted, the publisher's-code disclosure, an app that landed without a version. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup …`. |
|
|
57
|
-
| `lotics scaffold check <model.json> [--json]` | **Prove a model before anyone sees it — no network, no credential**, unless the file names a preset. ONE parse of the whole file against the model schema (strict, so `tabels` or `row` is an error rather than a silently dropped key, and a model cannot express what only a starter bundle carries: `fixtures`, `knowledge`, `knowledge_expects`, a file-backed `excel`/`word`/`pdf-form` template; its `apps` are stated over the model's entities, never built code; a retired key — `field_roles`, `table_workflows`, `connections` — is refused by name with what replaced it), then `validateWorkspaceModel` — the caps, every cross-reference, and the first rows themselves (a field the entity does not declare, an unknown option alias, a link naming no row in the file, a duplicate `ref`, a date that is not one, a value on a platform-computed field, a files cell that is not a relative path beside the model or a `fil_` id, a document path with no file beside the model), then `write_rules`, then `records` and `apps`: an entity an app lists, opens or picks with no `records` entry, a title that is an autonumber or a code, the `record.facts` or `record.blocks` that `sections` replaced, a section holding nothing, a field, a block or an act placed twice on one record, an `at` on a record with no status, an act at a staged section's foot offered while the section is not the work, a `where` on a field that is not a single select, a block whose entity does not link to the record, a `when` on a field that is not a single select, an act setting a field or option that does not exist, a check on a field that is not a yes/no or text formula, a register or rows-block entity with no `singular`. **Reports EVERY problem in one run**, each as `<path>: <message>` in the file's own keys (`entities.0.fields.1.type`, `apps.0.acts.1.set.stage`), so fixing a model is not a round trip per mistake — one of the model's own rules ending with the rule's id (`[register.filter]`), and each rule named printed once more under the list in the one sentence `lotics docs model/<rule>` states it in. Exits 1 when there is one; exits 0 with the counts (`N tables, N fields, N links, N views, N roles, N rows`, plus `N apps` when the file states any), then **each app as its reader will see it** — the register's row, columns, filters, add and order; the record's door, header and the buttons beside its ⋯, checks, its sections as the reader scrolls them — the stage each is the work of, its fields, its blocks and the acts at its foot — its thread, and its acts with every `when`, `requires` and blocking check — with every value a default supplied marked `(default)`, and a line naming `lotics app preview <model.json>#<app>`, all on stdout. `--json` replaces both with one object and nothing else: `{ok: true, …counts, resolved: {<app>: …}, notes}` or `{ok: false, findings: [{path, message, severity, rule}]}` (`rule` on a finding of the model's own rules). **A `preset` is checked as N models, not one** — every variant merged onto the base (its added entities, and its added fields keyed by entity) and put through the same rules, each finding addressed `preset.variants.<slug>.<path>`, so a preset ships with every branch proven: the branch nobody took is the one that fails in the workspace of whoever takes it, who is the one reader who cannot fix it. A variant's `fields` key naming no declared entity is a finding too — the merge keys on the entity, so a typo'd alias adds those fields to nothing. Same verdict the server reaches, because it runs the server's own functions out of `@lotics/shared` rather than a second implementation of them. **A file written as `{"from": "<preset-slug>", …}` is resolved first** — one GET of that preset's file on the website — and that read is the one step on this path that needs the network; it says so when it cannot make it, and a slug nothing serves is answered with the slugs there ARE, read from the listing, rather than with a 404 the author cannot spell their way out of. Resolution is pure (`resolveModelFrom` in `@lotics/shared`): the named variants merged onto the preset's base in order, then `rename` through the same `applyBinding` a `--bind` goes through, then the file's own `entities` appended, and the preset's `records` and `write_rules` merged with the file's, entity by entity (the file's entry replacing the preset's). What comes out is the full form and goes through everything above unchanged, so a `from` file cannot reach a workspace by a route the full form does not. A variant slug the preset does not declare, an alias `rename` names that it does not declare, and a renamed label that is already another table's are each a finding rather than a silent drop — a branch quietly ignored scaffolds the base and looks like it worked. |
|
|
58
|
-
| `lotics scaffold apply <model.json> [--json]` | **Create the model in this workspace**: its tables, fields, select options, links, views, roles and first rows, through `POST /v1/workspaces/scaffold`. Runs `check` first, so a bad file never reaches the network, then resolves and ANNOUNCES its workspace (`lotics → <org> / <workspace>` on stderr) before writing — it is a destructive path. **Additive and re-runnable**: it is the verb that sends `adopt`, so an entity whose `label` already names a table here BINDS to that table and gains the fields, options and views it is missing, while `setup` refuses that same label. No stored value is ever changed or deleted. A field the workspace already had takes the model's `default`, the `format` of its text or number and a number's `unit` — how a new row starts and how its values read — and keeps everything else; a date's format is left, since changing it would rewrite every stored date, and so is a unit moved between two of one dimension (`kg` to `t`), since it would convert every stored figure — `scaffold diff` names both as what apply leaves. So applying the same model twice changes nothing the second time — and a declared PAIRING (`sync_both_ways` / `paired_field_alias`) over a link this workspace already has one-way is REFUSED before the first write, naming the alias and `update_fields sync_both_ways`, because a pairing is only ever created with the link and adoption would otherwise finish clean over a half-paired link. **After the first run the WORKSPACE remembers what each alias became**, so every later run binds entity, field, select option, template and role BY ID and a relabel on either side is a RENAME rather than a second table: the run reports each moved name with the command that reconciles it (`lotics field rename` moves the platform, the file and every bound app together; `update_table` moves a table's name), and applies anyway — which of the two names is right is the author's to decide, and the run bound the thing the model has always meant either way. A bound target the workspace no longer holds is refused by name, with `restore_table` and `lotics scaffold unbind` as the two ways out. **Rows land only where every bound table is empty**: one bound table already holding records and none are written anywhere, because sample rows landing among a customer's real ones cannot be told apart from them — it says so and reports `rows_skipped`. Prints `created`/`adopted` per entity with its table id **and the delta that landed on it** (`+8 fields, +2 options, +1 view, 2 fields set to the model's default or format`, and nothing where the run changed nothing) — `adopted` alone cannot report the columns, options and views a later version of a model puts on a table that is already the owner's, and the only other proof was a full re-export and a diff — marking `(bound by name)` the one case where a NAME decided which table the model points at; the same `created`/`adopted` per ROLE with its group id — an adopted role binds a group that already exists, which is how one silently inherits another workspace's members — and rows written per entity. **The binding is the workspace's, not the file's**: a model file is never committed, so memory kept beside it is one author's disk — absent for a teammate, on a second machine or after a delete, and each of those falls silently back to the label join that grows the twin. It holds what each entity, field, option, role and template alias became (`tbl_`/`fld_`/`opt_`/`grp_`/`tpl_`), plus one entry per first ROW under its own `<entity>:<ref>` and `documents` (path → `fil_`), and is read by `diff`, `--documents`, `app create --from`, `app regenerate --from` and `workspace build` before any of them reads a label. **Then it copies in every package the file's `apply` list names, in order** — each one a `library init` with that entry's `bind` and `no_sample_data`, and each sending `adopt`, because by then the workspace holds exactly the tables this same run just created. Order is load-bearing: a later entry may bind onto a table an earlier one made. **A refused entry stops the run and the entries before it stay** — they are separate copies, committed as they land — so the refusal carries the server's own message plus what already landed and the one-package command to retry with. `--json` prints one object and nothing else (`entities`, each with `bound_by`: `created` by this run, `id` through the binding, or `label` for an alias nothing had bound; `roles`, `record_ids`, `rows_skipped`, `drift`, `applied: [{package, apps}]` — always present, empty included, so a reader cannot mistake "applied nothing" for "too old to say" — plus `organization_id`/`workspace_id` and a `warnings` array). Admin-only. A model STATES its apps and `apply` builds none of them — `lotics app create --from <model.json>#<app>` or `lotics workspace build` does; a published package named in `apply` brings its own. |
|
|
59
|
-
| `lotics scaffold apply <model.json> --documents` | **The `files` cells of `rows`, and nothing else** — no table, no field, no row. The half a re-apply cannot redo: rows land only into empty tables, so a model whose binaries were missed the first time has no other way back, and a model whose records are pictured ships with empty pictures until this runs. Each distinct path is uploaded once, recorded against the file it became, and attached with `update_records add_to` onto the record the binding names — joined by the row's own REF (`<entity>:<ref>`), so a row added to or removed from the file since the apply changes nothing about the rest. **A ref this workspace holds no record for is a refusal**, naming it: the alternative is filing its document under whichever row happened to sit at that position. **Idempotent by construction**: an uploaded path is reused from the binding's `documents` map and `add_to` is a set union, so running it twice leaves one id in the cell. The field is bound by the id the binding recorded, and by LABEL only for a field added since — a label that has moved with nothing bound to it is refused, pointing at `lotics scaffold diff`. Admin-only. |
|
|
60
|
-
| `lotics scaffold diff <model.json>` | **Where the file and this workspace have come apart.** Runs `scaffold export` against the selected workspace and prints what the model has and the workspace lacks, the reverse, and every field the two disagree about — a type, the option labels one carries and the other does not, where a new row starts (`default`, none where the model states none) and how its values read (`format`, the type's plain one where none is stated; a date's is named and left, since `apply` would rewrite its dates; a number's `unit`, one moved between two of one dimension named and left, since `apply` would convert its figures), or a PAIRING the model declares over a link that is one-way here, which `apply` refuses and nothing else named — and each `unique` set one side declares and the other lacks, compared as an unordered set of the workspace's fields. **Joined on the ids this workspace remembers**, entity then field then select option, and on LABEL only for an alias nothing has bound — a new entity, or a workspace this model has never been applied to, which is the join the first apply itself makes. A relabel on either side therefore prints as one rename under the alias (`order.state: label: model "Stage" · workspace "Giai đoạn"`) rather than as one thing the workspace lacks and one the model lacks, which is what made an additive apply add a second one. A bound id this workspace no longer serves is named with the command that puts it back, never silently re-bound by label. **Exits 1 on any difference**, so it is a gate: a starter published from a model that has drifted would ship the FILE's labels while the workspace uses others, and nothing else compares them. Checks the file offline first. Admin-only (the export is). |
|
|
61
|
-
| `lotics scaffold export [--tables <tbl_id,…>]` | **This workspace, read back as a model file** — `GET /v1/workspaces/model`. Prints the tables it has (or only the ids `--tables` names) with their fields, options and views, plus its roles and its html/email templates when it has any (a file-backed template is named on stderr and left out), as pretty JSON on **stdout**: exactly the file `lotics scaffold check` reads, so `lotics scaffold export > model.json && lotics scaffold check model.json` is the round trip. Resolves and ANNOUNCES its workspace first (`lotics → <org> / <workspace>` on stderr), like every other verb that reads one. **Findings go to stderr, each led by its severity** (`• <severity> <area>: <message>`, and one line counting the errors underneath) — a workspace holds things a model cannot express, and a file that dropped them silently would read as the whole workspace; the model is printed either way, and the exit is 1 when any finding is an `error`, because a file with a hole in it is still worth having on disk. **`--tables` names the closure it returned**, on stderr above the findings: the walk takes the transitive closure over links, so one seed in a connected workspace comes back with nearly all of it, and this file is what a preset is written from — a pull-in nobody stated is a preset nobody chose. **What comes out is a STARTING POINT, never a source of truth**: it carries one business's labels and stops describing that workspace the moment either changes. Edit the labels into the trade's words, add the `preset` block with its questions and variants (`lotics docs model/presets`), and prove every branch with `lotics scaffold check` before it is published. Admin-only. |
|
|
62
|
-
| `lotics scaffold unbind <kind> <alias>` | **Make this workspace forget what one of a model's aliases is bound to** — `DELETE /v1/workspaces/model/binding`. `kind` is `entity`, `field`, `option`, `template`, `role`, `row`, `document` or `app`; `alias` is spelled in that kind's own grammar (`order`, `order.state`, `order.state:open`, `order:first`, a document's relative path, or an app's alias in the model). **`scaffold unbind app <alias>`** forgets which app `app create --from` made for that model alias: the next `app create --from <model.json>#<alias>` then binds the app it makes. **The escape from a binding whose target was deleted**: every verb refuses that alias by name rather than quietly creating a second thing beside it, and this is where somebody who is sure says so. Deliberately a command of its own and never a flag on `apply` — forgetting means the next apply CREATES a new one, and nothing afterwards joins it to whatever was there. An alias is a namespace, so forgetting an entity forgets its fields, its options and its rows with it, and the count of what went is what it prints. 404 when nothing was bound. Admin-only. |
|
|
63
|
-
| `lotics library list` | **Works with no account**, and that is the point: whether to start from a preset, copy a package or build from scratch is decided before one exists, so requiring a key would mean signing up to learn the answer was no. **Two shelves, printed under their own headings and never merged**, because they are different kinds of thing and end in different commands. **Presets** are a trade's MODEL, served as static files on the website (`GET <site>/presets/index.json`, no credential, no server that knows what a preset is): each row is `slug · name`, the sentence, how many tables the base carries, and every branch as `slug · when`. The `when` rides the listing rather than waiting for a `show`, because it is what an answer is matched against — two trades whose names sound alike are told apart by which one has a branch describing the business in front of the reader. A preset is READ and turned into a `model.json`; nothing is copied. **Packages** are apps plus the tables they stand on, COPIED in whole. Unauthenticated it lists what Lotics publishes (`GET /v1/starters/official`, public); authenticated it lists the org shelf — the packages this organization can copy, Lotics-reviewed ones plus its own, each with at least one released version, deliberately NOT a catalogue of everything published: the server returns exactly what a copy would be allowed to take, so the list can never offer something that then refuses (admin-only). Both render through one function, and each row names WHAT IS INSIDE it — its apps and how many tables — because that is the fact the choice turns on: a name and a sentence leave a chooser guessing, and an agent matching what someone said they manage has nothing else to match against. Nothing fitting on either shelf is a real answer: `lotics docs model` is where that goes. |
|
|
64
|
-
| `lotics library show <slug\|apg_id> [--json]` | **The argument says which shelf**, and both forms are allowlists rather than a fallback: an `apg_` id is a package, anything else is a preset slug (`^[a-z0-9_]+$`, refused before any request — a slug reaches a URL). **A SLUG** reads the preset's own file off the website with no credential and no account, which is the whole timing argument for serving it as a file: it prints the preset's name and sentence, the questions it may ask (at most two), every table as `alias · label` with each field as `alias:type`, and every branch as `slug · when` followed by the tables and fields taking it ADDS — a slug picked off its `when` alone cannot say whether the branch brings the column the person was asked about. It closes with the `{"from": …}` file to write. The file is proven as a MODEL on the way through (the same `readPresetModel` this repo's own test runs over these files), so a preset that would fail in the workspace of whoever takes a branch is refused here, named in the preset's own keys — ours to fix, not the reader's. **An `apg_` id** prints the package: name, description, current version, shelf tile and trust standing (`official` — reviewed by Lotics; `your organization's own`; otherwise `not copyable from this organization`), plus the date it was unpublished once it has been, then the same COMPACT table listing — each table as `alias · label`, each field as `alias:type` — which is exactly what a `--bind` is typed from. Read it before copying a package you did not publish. **Works with no account for anything Lotics publishes**, falling to `GET /v1/starters/official/{id}` the way `list` falls to the public shelf; signed in, the prose form is admin-only and readable by id from any org, but an unpublished package 404s for every org except the one that published it. `--json` prints the preset FILE for a slug, and the published contract read whole — views, labels and all — for a package: ONE shape whichever credentials the caller holds, because the reader of that object is a program writing a model from it. |
|
|
65
|
-
| `lotics library init <apg_id> [--bind <entity>=<Label> ...] [--connection <alias>=<cac_id> ...]` | **Copy a package into this workspace.** Server-side it scaffolds the tables and fields, creates the document templates and knowledge docs, inserts the sample records, creates every app the package carries and materializes each one's queries, workflows and agents onto it — then deploys each app from the dist the package was published with, rewriting the publisher's sentinel field keys to this workspace's. No build runs anywhere, nothing is written to this machine, and nothing here needs node: the apps are live when the command returns. **What you get is yours outright**: ordinary apps plus ordinary tables, with no link back to what it came from and nothing pinned. It does STAMP what delivered it (`apps.origin`), which nothing resolves through and only `lotics app upgrade <app_id>` reads. Edit any of it — `lotics app pull <app_id>` is how an app's code is edited afterwards. **The publisher's code runs in your workspace as you** — its apps, workflows and agents — which is why provenance is the gate: **copyable only if the package is Lotics-reviewed or your own organization published it**, enforced server-side; the disclosure is printed (and carried in `--json`'s `warnings`) whenever the package is not your own. **Refuses a workspace that already has tables** unless `--adopt`: scaffold matches an entity by DISPLAY NAME, so a package declaring `Contacts` would bind to yours. An app whose deploy failed is reported by name with its reason and the exit is non-zero, but the copy is complete around it — the tables, the records and the app row exist — so it must not be run again; the publisher fixes the package and it is copied into a fresh workspace. The sign-in link lands on the app when there is one, else on the workspace's app list. `--json` prints one object on stdout instead of progress (the shape is under `lotics setup`). `--no-sample-data` skips the sample records, and a copy that ADOPTS an existing table writes none either — that table already holds real rows, and the fixture set links to itself, so it is all-or-nothing; with them, how many landed is reported. They are ordinary records, delete them whenever. **`--bind <entity>=<Label>` says which of YOUR tables the package's entities are, and `--bind <entity>.<field>=<Label>` which of your fields** — repeatable, and split on the FIRST `=` so a label may contain one. Scaffold adopts by DISPLAY LABEL, so a bind renames the contract to what you already call things and the copy lands on your tables instead of creating a second set beside them: this is how a package of apps lands on a workspace that already has its tables. Only naming moves — a bound field must be the TYPE the package declares, or the copy is refused (409). A bound entity needs no `--adopt`. The same target named twice is refused rather than overwritten, because the caller then believes one of the two took. `lotics library show <apg_id>` lists the aliases to bind. Resolves and ANNOUNCES its workspace first (`lotics → <org> / <workspace>` on stderr). Admin-only. Authoring the registry (`opctl library publish/unpublish`) stays operator-only. **`--connection <alias>=<cac_id>`** (repeatable) names the account a connection pushes through, where you can use more than one account of its provider — two accounts of one service are two sets of books, so the refusal that asks for it names each and nothing is ever picked for you; the account must be this workspace's, of the provider the model declares, and one you can use. |
|
|
66
|
-
| `lotics library fixtures capture [--entity <alias> ...] [--limit <n>]` | **(authoring)** Write this app's live records into the project as `fixtures/<entity-alias>.json` — the sample data a package carries, so a copy lands with something in it. Run from an app project; the app id comes from its manifest. The alias-keyed shape is produced server-side, because the aliases are minted when the starter is extracted and exist nowhere a project can read them. `--entity` is repeatable and comma-separated; omitted, every table the app declares is captured. **Capture a linked set in ONE call** — a link between two rows only resolves within a single capture, so taking companies and contacts separately drops the edge between them (it says so when it happens). `--limit` bounds rows per table (default 10, max 200 — a higher one is clamped, not refused). **A captured row is written into a copy exactly as it reads here**: a cell the origin left empty stays empty, because the copy's inserts do not apply field defaults. **READ WHAT IT WROTE before committing**: these rows are created verbatim in every workspace that copies the starter, so a real customer name, price or address captured here is published. Files, formulas, rollups, lookups and autonumbers are never captured — the platform writes those. Admin-only; writes nothing to the workspace. |
|
|
45
|
+
| `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes — `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** — it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as — that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created, because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name — the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning — `setup` writes nothing to disk — so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** — what `lotics model apply --json` does (`apps`, each with `alias`, `app_id`, `version_id`, `origin`, `findings`, and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup …`. |
|
|
46
|
+
| `lotics model apply <model.json> [--app <alias> ...] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked with the validator the server runs, every problem in one run, before anything is uploaded. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. Then the tool adopts or creates every table (an existing table of the same label is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares — `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app — alias, `app_id`, and `created`/`updated` with the version minted, or `unchanged` — and every finding. **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, apps, findings}` on stdout, or `{ok: false, findings, notes}` when the file does not check. |
|
|
47
|
+
| `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** — the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version — through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. Applying what it wrote changes nothing. |
|
|
48
|
+
| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty — before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`get_app_types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |
|
|
49
|
+
| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged — those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live; to ship this directory over it, set `current_version_id` to that version and deploy again. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |
|
|
50
|
+
| `lotics docs` \| `lotics docs <area>[/<section>]` | **This CLI's own references, carried inside the binary** — the model reference, this index, and every doc under `docs/` — so the doc a reader opens always describes the binary answering. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |
|
|
67
51
|
| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script — the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe — a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |
|
|
68
|
-
| `lotics docs` \| `lotics docs
|
|
69
|
-
| `lotics docs model` \| `lotics docs model/<section>[/…]` | **The model reference, from inside the binary** — the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs → entities and fields → `records` → one app per job → `scaffold check` → `app preview` → `workspace build`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<entity-alias>:<ref>"`), the rules, the `apply` list (packages copied in after the model's own tables, each with an optional `bind` onto them), the `preset` block (a published model's branches and its at-most-two questions), the **`from` form** — `{from, variants, rename, entities, rows, records, write_rules, apps, apply}`, which names a preset by SLUG instead of restating it — and one complete worked example. **Offline, no account.** |
|
|
52
|
+
| `lotics docs model` \| `lotics docs model/<section>[/…]` | **The model reference, from inside the binary** — the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs → entities and fields → `records` → one app per job → `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<entity-alias>:<ref>"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |
|
|
70
53
|
| `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** — a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |
|
|
71
|
-
| `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping. **First, whether this project is even based on the served version** — the one thing a deploy REFUSES outright rather than pushing (the server 409s a stale `prev_version_id`), and the one finding that invalidates every other: a stale tree and the live app are two different apps, so comparing them reports nothing trustworthy. Stale exits 1 naming both versions and stops before the rest; a project with no stamp at all — or an app with no version yet — is a first deploy, not a conflict. `deploy` runs the SAME assertion, so a stale tree fails before it pushes or builds. Then: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings this bundle stopped calling (the same transition — and the same baseline — `deploy` reports, so the two cannot disagree), capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__` **in a project that still ships react-native** (read off its own `package.json` — an app on the React DOM kit bundles none of it and the check would be advice to define two globals nothing reads), and a `window.open` in the app's own source (each fails ONLY in the deployed app: dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green), an agent whose capability and its reach disagree, in EITHER direction, over any of the five declaration-bound tools (`run_app_query`/`run_app_workflow` against `query_aliases`/`workflow_aliases`; `grep_knowledge`/`read_knowledge`/`list_knowledge` against `knowledge_doc_ids`) — the tool is the capability, the list is the reach, and a tool with no reach means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb, and a notice for any alias the source computes at runtime (invisible to every check here and to `--prune`'s unbind guard). **And whether the runtime this app builds against has fallen behind what is published** — `@lotics/app-runtime` alone, since the kit an app lists sits at the runtime's range, read from `node_modules` rather than the range because a caret is minor-locked below 1.0 (`^0.13.x` can never resolve `0.14`, and `npm update` does nothing). A release LINE behind is loud and names `lotics app kit --published` and `@lotics/ui`'s `MIGRATION.md`; anything smaller is one quiet `npm update` line, because a warning that fires on every deploy is one the reader stops seeing. An app still listing `@lotics/app-sdk` is told it moved into the runtime and which verb migrates it. The lookup is bounded and every failure is silence: a version check must never become a new way for a deploy to fail. Every deploy finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **And the PORTABILITY gate, one of three rules `check` runs that a deploy does not** (the others are the undeclared call site below and `package.json#lotics.writes`/`deletes`, held against what every workflow body writes and deletes) — the ids an app cannot carry into another workspace, over the working tree, with the same exclusions the deploy tar applies. Two rules. **An id this workspace MINTED**, written into `src/`, a `.md` or the app's own docs — it resolves to nothing in a copy, and in prose it is an instruction the copier's agent follows; this is the one a `library publish` also refuses, on the uploaded archive. **And an id-shaped STAND-IN** too short for the generator that mints its prefix (`"opt_X"`, `"fld_a"` — quoted or in a code span, so a bare `opt_in` stays legal, and never in a test file), which only `check` runs, and which additionally reads a workflow body and the manifest: those two are exempt from the first rule because a publish INVERTS a real id there and cannot invert a fake. **And an `app_` id anywhere in `app.json`**: the spec names no other app, so a pasted one is re-pointed by nothing. Each is reported as `<file>:<line> — <id>` with the one edit that fixes it. This gate reads only the files, but the command around it still needs a resolvable credential and the live app row, so it is not an offline check. **And every bound workflow body, checked as `app workflow check` checks it** — the same isolated per-alias program against the pulled `.lotics/workflows/<alias>.globals.d.ts`, then the server's `verify_only` verdict on each body that passed — and on each body no local pass could type (no globals, no `typescript`), since that is what a push would meet — which exits 1 on any issue and warns when the server is not reached. The server verifies a body once, at the save that wrote it, so a body whose declared types have since moved stays stored, matching what is live, and is refused by the next writer — a starter copy, in somebody else's workspace. The verdict is as fresh as those types, which `pull`, `workflow pull` and `codegen` refresh. **And the app's own `npm run typecheck`**, after regenerating the `.lotics/*.d.ts` companions from the manifest — the same run a deploy makes before building, so a filter or sort key the query does not project fails here rather than at the first member's request. **Exits 1 on that, on a body the types refuse, on a failing typecheck, and on what a `deploy` would REFUSE** — **every manifest query the server would refuse** — held to the server's own query gate (the declaration, each query it runs as, the tables it reads through `get_table`, its filters and the columns it outputs; the owner's reach and the SQL compile stay the server's), in the server's words — and a query or workflow `description` over the 300-character capability cap, which is a binding no `set` and no deploy will take (the rule is `@lotics/shared`'s, the same one the server refuses with, and `workflow set` / `query set` ask it before sending anything — so a long line costs one edit rather than a failed push per alias). **And every manifest query declared with NO description**, named in one line: that sentence is what a chat or MCP caller chooses between aliases by, and an alias is a JS identifier. `app create --from` deliberately writes none — a template over a shape's own English reads like a line about the business while saying nothing — so a generated app is told once, here, which lines are the author's to write. Both are things a deploy would refuse, so CI gating on a green check means a deploy will not refuse. **Every binding the project has ahead of the app** — an agent schema that disagrees with the live app, an edited workflow body or declaration, edited agent prose, a changed query — **is named as what the deploy will push**, each with the one-alias `set` verb, and never fails the check: the deploy pushes it itself, so a check red over it disagreed with the deploy it stands in for (and with the check `app regenerate` runs). Genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. **`--screens` adds the rendered surface, and is its own entry below.** It runs after the typecheck and only when it passed — a type error renders nothing to measure — and its findings, and any write it refused, fold into this command's exit code. **It also refuses a call site the manifest no longer declares.** `useQuery`/`useWorkflow` keep a bare-string overload for a computed alias, so deleting or renaming an alias leaves every call site compiling and failing only when the screen renders — the one edit most likely to orphan a call site is the one the generated types cannot catch. The alias literals in `src/` are matched against `package.json#lotics.queries`/`.workflows` (the manifest, not the live row: the server still SERVES an alias whose declaration was just deleted, because a deploy never unbinds), and an undeclared one exits 1. `app codegen` prints the same finding as a warning, since it is the command an author runs right after editing the manifest. **A failing body that is byte-identical to the one the server is running is labelled as such**: its `lotics.synced.workflows.<alias>.content` baseline proves the file has not been edited since it was pushed or pulled, so the failure is a grammar migration the stored body is owed rather than a stale checkout — a link field reads as an id array, so drop `.id` or descend with `linked(…)`. Until the body is edited and `set`, the stored one keeps running as it always has. **And a query that reads past a table's ROW RULE.** A table's `private_filters` bind the CALLER, and an app query's caller is the app's OWNER — the viewer needs no table access, `app:use` is the grant — so the rule passes and every row is served. For each table a declared query reads (`GET /v1/tables/{id}`, the one surface that serves the rule), the viewer predicate — `current_member in_any_group`, or a member field's `is_current_member` / `is_not_current_member` — is attributed to the SCAN it guards: a `from_table`'s own `filter`, or an enclosing `filter` node's predicate, whose rows are the ones that scan produced. So a clause written over one table never silences the finding for the ruled table joined beside it, and every scan no clause covers is named with its table. A table this credential may not read (403, or 404 for one that is gone) is dropped — a rule that cannot be read is not evidence of one — while any other failure of that read fails the command, since "I could not ask" must never render as "there is no rule". Advisory, never part of the exit code: an app that deliberately serves the whole table to a desk of people who may all read it is legitimate, and nothing here can tell the two apart. **And a scan no index can narrow**, on a table of 2,000 records or more: each `from_table` is judged with only the required params set, then with each optional param its filter reads set alone, pruned as the runtime prunes an omitted param, and named with the params that leave it unserved and the conditions at fault. A filter is served when it cannot hold without an index-served condition — any branch of an AND, every branch of an OR — or its source carries a `search`; an index serves an exact match (`equals`/`is_any_of` on text, comparisons on numbers and dates, built at deploy) `has_any_of`/`has_all_of` on a select, member or link, and `is_current_member`, never a substring match. A call whose filter keeps no condition asked for every record and is not named; a scan beneath a `filter` over its projection is not judged, since the compiler may move that filter into it. A push prints the same advisory for the queries it sends. Advisory, never part of the exit code. **And it names the workflows a chat or MCP caller is offered with no description** (one `get_app_capabilities` read, the reader's own view of the app): that text is what those callers choose between aliases by, and without it they choose by the alias. It cannot be counted from the manifest — a workflow bound out of band is not declared there at all. Advisory, never part of the exit code. **It regenerates `.lotics/app_fields.ts` from the live schema before typechecking**, as a deploy does — a gitignored map from a moved schema otherwise passes. **And three claims the app makes**: a bound body's writes against `package.json#lotics.writes`, read as the step tree the server would store, exit 1 on a field no entry covers, and declaring none only warns; a description carrying `<placeholder>` syntax exits 1, since the catalogue escapes angle brackets — state the format as an example; so does one naming a desk `query_apps` does not list. |
|
|
72
|
-
| `lotics app check --screens [--screen <label>] [--width <n>] [--shots <dir>] [--changed]` | **`--screens` adds the rendered surface**: the app is served the way `app dev` serves it (its real data, this key), rendered headless in Chrome (`CHROME_PATH`/`LOTICS_CHROME`, then Playwright's, then system) at 1280 and 375. **The screens are its navigation's destinations** — a `nav` landmark's `a[href]` or `role="link"` (an app's route lives in its router, so the kit's shell renders each screen as a button carrying the link role and no `href`), else the first tab strip, else the root — in that order, because a screen's own lifecycle desk draws a tablist too, and reaching for one first walks a screen's STAGES as if they were the app's. **Each screen is then WALKED THROUGH ITS OWN DOORS**, nothing configured per app: every door is named by something the document states. A record: a row stamped `data-opens="page"`, a real `a[href]`, or an EMPTY DOOR — a named control with no text and no child element, the only legal whole-row press target, which reaches a drawer register, a Schedule row and a hand-written row alike. On a record — a page, or a drawer standing over its register, whose doors are the drawer's and never the register's behind it: the acts menu (`aria-haspopup`), the first child row, and every tab (a drawer's sections, the companion) and every section that is a page of its own, each measured and walked for the dialogs its acts and adds raise, a dialog the record already opened not twice. On any surface: every dialog a visible primary or secondary act raises — nothing announces one, so a press is kept only where an overlay appeared. **THE RUN IS A READ, AND THE NETWORK IS WHERE THAT IS ENFORCED — never a rule about what the page may draw.** The app frame holds no credential and Chrome runs on a throwaway profile, so every call the app makes reaches the workspace through this CLI and nowhere else, and this CLI serves an ALLOWLIST of reads. Everything outside it is refused before the call leaves the machine — a workflow run, a record write, an upload, an agent run, a comment, and any op this CLI does not know, which is refused because it is not on the list rather than because anyone listed it. A refused call HEADS the report, naming the surface, the control the app had focused and the RPC by its alias (never a payload value), and fails the run on its own: a walk that provoked a write left the app in a state no reader could have put it in, so nothing measured under it means anything. What the walk does is bounded on the page as well: nothing inside an open overlay but the record drawer it opened, nothing typed, no submit and nothing in a form, no act the kit marks costly (`data-tone` danger/warning) and none whose own name is the write, in either language — and nothing is FOCUSED, because focus cannot be taken from one field without leaving another, and a field that saves itself when the reader leaves it saves on exactly that. Where a reading needs the focused state, as the focus-ring rule does, Chrome is asked to PAINT `:focus-visible` and release it again: the cascade answers, focus never moves and no event is dispatched. Each overlay closes with Escape, confirmed closed; a record is descended at most twice; the walk stops at twenty-four surfaces per screen and NAMES each door it left. Each surface is measured once no request is in flight, and the measurable probes of `@lotics/ui` docs/reviewing.md run over the DOM, each finding printing the rule it IS — its law, its section and the one edit that answers it — so the numbers need no key. What they exempt is what the screen itself declares: a register's ordinal gutter (a column counting to the row count is the shape's numbering, which no app can treat), a strip whose list carries `data-order="sequence"` (a lifecycle rail, which composition.md permits under a screen's tabs), a hairline or `clip-path`-clipped leaf (the visually-hidden node a control plants for a screen reader), and a leaf whose own computed line clamp states a count over a sentence. **A meter counts as an encoding only where it draws a POSITION** — `aria-valuenow` inside a range with room left. A bar pinned at its own maximum — what a meter alarmed AT its maximum draws on every alarmed row — reads the same as every value above it, and one with no maximum states none; both are counted in the census's `devices` and out of its `encoded`, so "nothing drawn" and "drawn and saying nothing" never read alike. The bare values that remain are grouped into columns, each named by the heading over it, so a finding says WHICH slot draws its figures as words. **Two finding classes read what geometry cannot.** `clutter` (docs/hierarchy.md): a second primary act, a value in two places on a record, a box reserving more lines than it holds, markers on over half a form's fields, a second accent. `right_form` (docs/screen.md's device index): a boolean as a two-option select, a day run as a repeated date column where the kit ships `Schedule`. Each prints a line per rule fired, with three offenders. **Four read room and counts** (reviewing.md §8l–§8o): `reserved_blank`, a register column 40px or wider that is blank (no text, no control) on more than half the rows in view, or a row-act gutter leaving 40px beside the acts on them — a row a complete register draws for nothing stored (`data-ghost`, a day nobody filed) is no row of the set; `shed_with_room`, a column the register's width gave up (the kit states them as `data-shed`) that, with the gap between two columns, fits in what its slots hold beyond what each must seat — its heading and widest ink, a fixed column's whole width, the acts most rows have; `action_off_heading`, a section's create (a control stating `data-create`) drawn in its body rather than its heading row; and `partial_count`, a count or `n of N` equal to the rows in hand of a read the server cut on the screen being walked, where the server's own count of the same alias, params and filter — asked by this CLI, a read — says otherwise. **Three read a record's anatomy** (docs/hierarchy.md), over each record on the surface with the title heading it: `record_primary`, a primary act in a block's heading row — a block's add is secondary beside the record's own acts (two under one heading are `clutter`'s); `fact_repeats`, a field whose value reads the same as the header's own words, whatever its case; and `empty_block`, a block that draws nothing under its heading, not even the line saying it is empty. A census line per screen (text runs, money strings, bare values against the devices reading, tab strips) prints first, so a clean verdict over a screen that rendered nothing cannot pass; a screen that renders no text is itself a finding, and one still changing after fifteen seconds is measured as it is. **A cold dependency optimisation is waited out, not measured**: the first paint has its own bound, far longer than settle's, since an app's modules load after its document completes and Vite holds them; only a MOUNTED, idle, textless frame is blank at once, and the finding names the wait and its blocker. It also RELOADS the page under the probe, so a width is measured again once; a second reload is a page that keeps moving and fails. **`--screen <label>` and `--width <n>` narrow a run** (repeatable, comma-separated; the label matches case-insensitively as a substring), for the author iterating on one screen who would otherwise pay a typecheck, a Vite boot and every screen at both widths on every edit; the clean verdict then names only the widths covered, and a `--screen` matching nothing is refused. **`--shots <dir>` writes what the run measured** — a `<surface>@<width>.png` and a `<surface>@<width>.json` per surface, off the SAME settled frame the probes read, so a shot and a finding can never describe different pixels. The PNG is the WHOLE surface: an app scrolls inside a box of its own, so the window is grown to the height the probe measured and put back, and an overlay a resize dismissed is shot as the viewport, the sidecar saying so (`app dev`'s header band is in it — the band the app was laid out under). The JSON is the half a picture cannot carry: the nav's first item's left edge, the title and first section heading, the primary acts by name, label/value pairs against what the folds state, values drawn twice, money that wraps, text the layout cut, and the console errors and uncaught exceptions the frame raised. **An uncaught exception is a finding** (`crash`, its first line, on the surface it was thrown reaching); a console error is not, since every React development warning is one. **A register reading the app states and a screen drew nowhere is printed under "Stated and not drawn:"** with the runtime's reason (narrowed by a chip, no part holding more than one row, no rows in view), and is not a finding. Named `<nn>-<slug>` plus one `__<step>` per door taken (`__record`, `__menu`, `__child`, `__fold`, `__dialog-<n>`, `__tab-<its name>`, `__section-<its name>`); `<nn>` is the walk's index, which lists the directory in reading order and keeps two labels that fold to one ASCII slug apart. The row a record surface opened from is the sidecar's `opened_from` and that screen's census line, never a file name — it is a person's data. The directory is created if missing, and refused before the dev server boots when it cannot be; `--shots` without `--screens` is refused. Findings exit 1 like the rest. **Where it renders**: the app's files are copied into the render project of its DEPENDENCY SET — `~/.lotics/render/<key>/`, keyed by the manifest's `dependencies` and `devDependencies` as written, a `file:` tarball by its bytes — the one `app preview` renders in, so the install and Vite's dependency optimisation are paid once per set rather than per app; one render holds a project at a time (`<key>.lock` — a second run waits, naming the app and pid, and takes over a dead hold). Where that install resolved another runtime or kit than the app's own, the run says which, since a deploy builds the app's own. The run ends with what each phase cost: `preflight · install|reuse · walk`. **`--changed`** walks only when something the screens READ has moved since this checkout's last walk — each part of `app.json` (every record apart), each declared query, each bundled source file, the field map, the ranges, the kit the render resolved and the CLI version, whose probes measure them — read against `.lotics/screens_check.json`, which every walk writes; with nothing moved, that walk's report is printed again under its timestamp and still fails the run. An app's screens all read its one spec, so a move walks the app whole. It walks, saying why, with no earlier walk, a different `--screen`/`--width`, or `LOTICS_UI_SRC` set (a linked working copy has no fingerprint); the workspace's rows are not an input. `--changed` without `--screens`, or beside `--shots`, is refused. |
|
|
73
|
-
| `lotics app preview <model.json>#<app> [--shots <dir>] [--width <n>] [--screen <label>] [--kit <path>]` | **The app a model states, RENDERED — before a table exists, and with no credential in the process.** The model is checked offline, the named app is compiled against the workspace the model WOULD become (synthetic `tbl_`/`fld_`/`opt_`/`grp_` ids, minted positionally off the file), and every read the app makes is answered from the model's own `rows` — projected under the columns the query names, narrowed by the params it declares and sorted the way it states, so a child block under a record holds that record's rows and not every parent's. An entity the app reads that states no `rows` gets three synthesized, so a register never measures clean over nothing. Then the SAME headless walk `app check --screens` takes: the register, the record its first row opens in its door, and the add dialog, at 1280 and 375, measured by the same probes. `--shots <dir>` writes a PNG and a sidecar per surface into `<dir>/<app>/`, so apps previewed into one directory never overwrite each other. **Exits 1 on any finding** — an exception the app throws is one — exactly as the check does, and prints what each phase cost — bind, install-or-reuse, walk. **Nothing is created and nothing is reached** — no app row, no table, no workspace; the only network it needs is the npm registry, and only when the kit has moved. It renders in the cached render project of its dependency set (`~/.lotics/render/<key>/`, shared with `app check --screens` and with every model whose app lists the same set, and held by one render at a time — a second run waits, naming the app and pid holding it, and takes over a hold whose process is gone): the runtime and its kit are PUBLISHED packages, so a project is what a preview needs to install them into — the CLI has no renderer of its own. **It installs the runtime this CLI was built for**, and the kit through it, never `latest`: the spec this binary's generator writes is the one that runtime reads, and a version that published mid-session is a runtime nobody checked the model against. A version the registry does not serve yet is refused before npm runs, naming `--kit` — needed only until that runtime is published. **`--kit <checkout>`** (repeatable) — a checkout's ROOT, which names the app-runtime and the ui it holds, or one of those two packages — renders against that checkout instead — built, packed and proven the way `lotics app kit` installs one into an app — and the run prints that it rendered against a LOCAL CHECKOUT, first and last, so the render is never read as the published one. The project is a CACHE and never a source: `npm install` runs on the first render of a set and again only when a checkout's tarball did, and every file in it is rewritten from the model on each run. **A document a row names by path is served** from the project's own static root, as the named file a workspace would hold, so a files block and a record's picture read what the model states; a `fil_` id is carried by name. **`--record <ref>`** opens that row's record by its own address — a row of the app's entity, by the ref the model gives it, open or closed — where the walk otherwise opens the first row the register lists; a ref naming no such row is refused with the refs there are. **Every read is answered at the BRIDGE**, not inside the page. The app SDK's design-time fixture (`registerMockFixture` + `?__mock=1`) is the wrong half of this: the flag lives in the app's own url, and the first record page is a navigation the app's router performs — so from that surface onwards the flag is gone and every read falls through anyway. The bridge is the one place every call arrives whatever the url says, so `query`, `field_options`, `members` and `context` are answered there from one reading of the rows, and the generated entry ships EXACTLY as `app create --from` writes it. **A preview writes nothing**: a workflow, an upload or an agent run meets the same read gate `app check --screens` uses, so the app draws its own refusal path rather than reporting a save nothing moved for, and the call is reported above the census the way the check reports one. **What came out empty, it names**: a `formula`, `rollup`, `lookup` or `autonumber` is computed here rather than stated in the model, and the census lists each computed column that stayed empty on EVERY row of a table that has rows — named by outcome, since a total drawn blank is either the app's own answer or a hole and only the run can say which. The census reads the CELLS and not the field types, so nothing is exempt by kind. **A computed column that came out `#ERROR:` is named too, and the run refuses it** before anything is installed: a wall of red measures clean, and a run that drew it and exited 0 would tell its author the app is fine. **It computes in UTC**, because a model states no timezone — a row's `@today`, an autonumber's `{YEAR}` and a record's own clock all land on the run's own date at UTC midnight, so two runs of one model draw the same register wherever they are made. |
|
|
74
|
-
| `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. A deploy runs this same verb for every alias whose declaration or body is ahead of the app, so this command is the one-alias spelling of what a release does, not a step a release leaves to a person. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. A push also prints any non-blocking verify warnings, including an input the alias declares that the body never reads. A first bind MINTS the workflow row, and the id it echoes is written back into `package.json#lotics.workflows.<alias>.workflow_id` — the same surgical write the derived `outputs` gets. Without it a hand-declared alias ended up shaped unlike its siblings, so anything reading the manifest (an audit, a port to another workspace, a person comparing two blocks) had to treat a missing id as normal, which is exactly how a genuinely missing one stops being visible. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version; without it such a write is refused and every breaking change is named (see `app api`). |
|
|
75
|
-
| `lotics app agent set <alias> [--acknowledge-breaking-api]` | Push `src/agents/<alias>.md` — plus `inputs`/`outputs` when `package.json#lotics.agents.<alias>` declares them — through `set_app_agent`. The agent mirror of `app workflow set`, and the deploy-free authoring path for an agent's prose and its typed edges. **It sends only those fields.** Everything else is absent, and absent means unchanged, so a declaration this CLI does not model cannot be reverted by a push from a checkout that predates it — the chat authoring agent's `knowledge_doc_ids`, another operator's `query_aliases` grant. To change one of those, call `set_app_agent` with just that field (`lotics run set_app_agent '{"app_id":…,"alias":…,"tool_names":[…]}'` — it merges), then `app pull` to bring the manifest back in step. **CREATES the alias when the app has not bound one yet**, so a new agent is authored the same way a new workflow is: write the prose, declare the typed half, push. A create needs the prose file (an agent without instructions is not an agent); it is gated on nothing else, because what keeps a binding alive is a `useAppAgentRun("<alias>")` call site in the shipped bundle — a deploy prunes an agent the bundle never names, manifest entry or not. The prose push is a conditional write against the fingerprint this project last saw, so it is refused rather than allowed to overwrite prose someone else changed. Clear error + non-zero exit when there is no prose file and nothing declared to push instead, when a create has no prose to create from, or when the file is empty once the header is stripped. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version: an agent's alias and its declared `inputs`/`outputs` are in that contract too, so renaming one or changing what it accepts breaks a caller nobody here can redeploy. Without the flag such a write is refused and every breaking change is named (see `app api`). |
|
|
76
|
-
| `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy pushes a DRIFTED declaration through this same verb before it ships (see `app deploy`), so this is the explicit single-alias path, not the only way a query reaches the app. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted — after holding every one to the server's own query gate and pushing none when any is refused; a refusal past that stops at the first failure and names what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. **The manifest is the WHOLE declaration**: `set_app_query` merges, so the push sends every optional key, `null` for one the manifest no longer carries. After the push it regenerates `.lotics/app_queries.d.ts` from the manifest, so the types the next `npm run typecheck` reads match what was just pushed. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version; without it such a write is refused and every breaking change is named (see `app api`). |
|
|
77
|
-
| `lotics app query run <alias> [--params <json>] [--json]` | **One BOUND query, run against the workspace this app is bound in, through the call a screen makes** (`POST /v1/apps/{id}/query`, the op `lotics app dev` forwards), so the rows printed are the rows the screen receives — one JSON object per line on stdout, the count on stderr, and a read cut short says so; `--json` prints the whole result. `--params` is a JSON object keyed by the query's declared params; text that is not JSON, or JSON that is not an object, is refused. An alias the app has not bound is refused before the query route is asked — naming `app query set <alias>` where this checkout declares it, else the aliases that are bound. Where this checkout's declaration differs from the bound one, the run names the differing keys first: the server runs what is BOUND. |
|
|
78
|
-
| `lotics app query move <alias> --to <dir|app_id>` \| `lotics app workflow move <alias> --to <dir|app_id>` | **One alias, out of this app and into another of the same workspace — both halves, no release either side.** Run it in the SOURCE project. In order, and the order is the safety: the alias is refused while anything here still reaches it (a `useQuery`/`useWorkflow` call site in `src/`, or an alias this app computes at run time — the same scan `deploy --prune` defers on), and that gate runs before the first write; then the target gains the binding; then the source loses it. A failure anywhere leaves the alias bound SOMEWHERE, where the other order would leave a capability that exists nowhere. `--to` is an ALLOWLIST of two shapes: a DIRECTORY holding the target's `package.json` (the declaration lands in its manifest, a workflow's `src/workflows/<alias>.ts` lands beside it, its `.lotics/*.d.ts` are regenerated, and its baseline is recorded — so its next deploy ships exactly what is live; all of it AFTER the push, because declared-and-unbound is a state a deploy would BIND, so a manifest written ahead of a refused push would have the target take the alias while the source still owns it), or an `app_…` id with no checkout here (only the live binding moves, and the command names the `lotics app pull <app_id>` that project owes). Anything else is refused by name. **A workflow's target binding is a NEW workflow row**: `workflow_id` is the SOURCE app's and is dropped on the way in, which is the mistake a hand-copied declaration makes — the second app then edits the first app's workflow. The source unbind is `remove_app_query` / `remove_app_workflow`, the same tools `deploy --prune` calls, so it needs no version; the declaration is parked under `.lotics/pruned/<kind>/<alias>.json` and removed from the manifest, because leaving it declared is how the next plain deploy re-creates the binding just retired. If the unbind is REFUSED (the server's guard on an alias the workspace has RUN, say) the target half and the manifest half still stand and the command names the one thing the source still owes: `lotics app deploy --prune [--prune-invoked <alias>] -m "…"`. `--even-if-invoked` lifts both guards — the call-site scan here and, for a workflow, the server's recorded-run refusal. Refused with nothing written: an alias this project does not declare, a workflow whose body was never pulled, a target that already declares the alias or already has that body file (a move never overwrites either), a target in another workspace, and `--to` naming this same app. |
|
|
79
|
-
| `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Each alias's `description` is folded back into `package.json#lotics.workflows.<alias>` from the same read — the alias binding the manifest is otherwise stamped from carries `inputs`/`outputs` but not the description, which lives on the workflow ROW, so without this a pull would erase an authored one. The server's GENERATED default is skipped, so an app that never described its workflows gains no manifest noise. Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. **A body the app's row has not moved on is left exactly as it is**, and the aliases skipped are named. A pull writes the server's RE-RENDER of a stored step tree, which is not the text that made it — a comment inside an object literal does not survive the round trip — and the local hash cannot catch that, because `workflow set` recorded this checkout's own text (comments included) as `synced.workflows.<alias>.content`, so nothing reads as unpushed. The fact that can is the server's own fingerprint: when `synced.workflows.<alias>.live` still equals the `body_sha` the read returns, there is nothing to deliver and the baseline is left where it is. `--force` takes the app's rendering anyway. |
|
|
80
|
-
| `lotics app workflow diff [alias...]` | Print how `src/workflows/<alias>.ts` differs from the body the SERVER is running, line by line (`-` is live, `+` is the file, three lines of context, the unchanged middle elided). Name the aliases to diff them whether or not they read as drifted; name none and it diffs every alias the baseline says has moved. Exits 1 when anything differs, so a script can gate on it. It is the companion the drift signal never had: `workflow set` pushes the file and `workflow pull --force` takes the server's, but nothing could say what the difference WAS short of pulling into a throwaway directory. **The two hashes under `package.json#lotics.synced` are not a comparison**: `content` hashes the local text and `live` is the server's own fingerprint of a body stored as steps, so they can never be equal and nothing compares them — reading `content != live` as drift is a misreading the block's shape invites. |
|
|
81
|
-
| `lotics app workflow check [alias...]` | Check the editable workflow bodies locally — **every alias you name**, or all of them when you name none; an alias that is not bound is refused BEFORE any body is checked, so a green ✓ never sits under an exit 1 — locally first, in the **server's own order** — parse, then type-check — then on the server. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias. All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there, and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. **Then every body the local passes found clean goes to the server**: `set_app_workflow` with `verify_only: true`, sent exactly what `set` sends (the manifest's `inputs`/`outputs`/`description` and the synced `expected_body_sha`), which runs every check a save runs — names, lint, structural validation, table reach, the published-API guard — and writes nothing. Its issues print in the same `<file>:<line>:<col> - <source>/<code>` form at the physical line; one tied to a step rather than a position prints against the file, naming the step; a refusal (a stale baseline, a draft, a break) prints as `server/<code>`. All of them exit 1. A server it cannot reach — no credentials, the network, a server that predates `verify_only` — is a warning naming each alias and why, and the exit is then the local verdict's, so a green run says which of the two it is. A bound alias with no body file yet warns + skips; a body with no globals errors (naming `lotics app codegen`, which refreshes types WITHOUT touching the body — a pull would overwrite it). **It also keeps the types honest.** Each alias's `.lotics/workflows/<alias>.globals.d.ts` carries a `// lotics:declaration <hash>` stamp of the manifest declaration it was rendered from; `check` compares it to `package.json#lotics.workflows.<alias>` and, when they differ, re-renders that alias's dts from the LOCAL declaration before compiling. Without it the verdict was confidently wrong in the exact case an author needs it — declare an input, run `check`, and get `TS2339: Property 'x' does not exist` pointing at your body for a schema the types have never been told about. The server renders a dts from a SUPPLIED declaration, so this works before the manifest has ever been deployed, which is when it matters (the order is edit → check → set). That refresh is skipped when the stamps match, and with no credentials or a failed fetch it WARNS and checks against the older types rather than blocking. A file written before the stamp existed reads as unknown, never as matching, so a pre-existing checkout heals on its first run. |
|
|
82
|
-
| `lotics app subdomain <new-subdomain>` | Rename the app's public address under the instance's apps domain via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
|
|
83
|
-
| `lotics app rename "<new name>" [--description <d>] [--icon <lucide-name>] [--theme <color>]` | **The app's display metadata, live and on disk, in one verb** — via the `update_app` tool (the single setter for name/description/icon/theme). app_id comes from the local `package.json` manifest; the public address (`subdomain`) and the code (`deploy`) are unchanged. **It also writes `package.json#name`**, folded from the new display name by the scaffold's own rule (`starter_template.ts` — diacritics are FOLDED, never dropped, so `Điều xe` is `dieu-xe` and not `i-u`). Nothing else folds it, so a rename that skipped it left every `npm` line, every CI log and every reader of the project calling the app by its old name. Written only after the server took the rename, and surgically: no other manifest key moves. The three flags set the branding `app check` warns about — a missing icon or colour draws a generic tile, a missing description gives the app's chat agent a roster of aliases and no brief — through the same call, so setting them is never a second `lotics run update_app` that forgets the manifest write. `--theme` takes the COLOUR (`theme.color` is the whole of what the launcher reads), not a JSON object. A flag you omit changes nothing: `update_app` merges, and absent means unchanged. CLEARING one is still `lotics run update_app` with an explicit `null` — a CLI flag has no spelling for that a shell cannot produce by accident. |
|
|
84
|
-
| `lotics app dev [path] [--port <n>] [--vite-port <n>] [--view-as=<member_id>]` | Spawn Vite dev server + an RPC-forwarding HTTP server. **Refused in one sentence on a project with no `vite.config.*`** — an app created with `--api` serves no bundle, and without the config Vite fails on a missing entry document in a bundler's words about a file the author never expected to have. `app check --screens` is refused on the same project for the same reason, rather than reporting "nothing blocking" for a pass it never ran; plain `app check` runs everything else. **`--port` is the wrapper you open and `--vite-port` is the module server, each as `--port <n>` or `--port=<n>`; pin both to run several apps at once.** A value that is not a port number is refused. With no `--vite-port`, `vite.config`'s own `server.port` is used when it states a literal one — the CLI passes `--port … --strictPort` to Vite, and a CLI flag beats the config in Vite's precedence, so the config's value could otherwise never win. `--vite-port` still outranks it and says so. **The wrapper serves every path that is not one of its own `/_…` routes**, so `http://localhost:PORT/lo/rec_…` opens that screen directly; `?_loc=<url-encoded path>` still works and wins. With `LOTICS_UI_SRC` set, Vite is started with `--force`: its optimizer cache survives a restart, so a NEW file added to the linked kit tree otherwise left the browser running the previous build of the module that imported it, silently. The wrapper page embeds the iframe with `sandbox="allow-scripts allow-same-origin"` matching production; postMessage ops (query / workflow / members / context / upload / openExternal / urlState / agentRun) are forwarded to api.lotics.ai using the CLI's API key — file bytes move in **both** directions through the dev server's own relays, never browser↔storage: dev runs against the PROD bucket, whose CORS admits `https://*.lotics.app` and not `http://localhost:<port>`, so a direct browser transfer is blocked — no upload could complete and no preview engine (PDF/Word/Excel all FETCH the bytes) could read a file. `upload` mints a presigned URL and PUTs it **to `PUT /_upload/<file_id>`** from the wrapper page — same-origin, so no preflight and no CORS — and Node forwards it on; every presigned `url`/`thumbnail_url`/`preview_url` on a **file object** in an RPC result is rewritten to **`GET /_file/<token>`** (absolute — the iframe would resolve a relative path against Vite), which streams the bytes back with `Range` passthrough (206s intact, so PDF seeking works) and an `Access-Control-Allow-Origin` for the Vite origin (the one cross-origin hop left is OUR response to allow). Neither relay ever takes a destination from the client — it gets a `file_id`/token and transfers only to/from a URL it minted or observed itself, so there is no client-controlled target and no SSRF surface. A URL in a record's own text cell is NOT rewritten. Production is unchanged (direct-to-storage, no bytes through the API server); `openExternal` and `urlState.get/set` are handled locally (the latter read/write the wrapper page's own address bar — `set` writes in place via `replaceState` and browser back/forward broadcast a `url-state` message back, so `useUrlState` survives refresh and is shareable in the dev loop; in-app *routing* is the app's own (the iframe owns its url via `@lotics/app-runtime/router`), and the wrapper bakes the saved screen (`_loc`) into the iframe src on load so a refresh restores it, mirroring production); `agentRun` (streaming) is proxied through `POST /_agent_run`, which opens the run's SSE with the CLI key and pipes chunks back to the iframe (`stream-chunk`* → `stream-end`), so `useAgentRun` works in the dev loop just like production; `context` resolves the viewer (`member_id` from `cli/whoami` + `comments_enabled` from the local manifest) and fetches the app's stored `config` live from the app row, so `useConfig()` renders the same values as production. `--view-as` (global flag; also `LOTICS_VIEW_AS`) threads `x-view-as-member-id` so `is_current_member` + `context` resolve to that member — **admin key only** (the server 403s a non-admin), writes stay attributed to the key owner. Hot reload via Vite; full DevTools / Playwright access via plain localhost. **Every forwarded op logs one line naming its ALIAS** — `[rpc] query applicants 231ms` — and `query applicants (count)` for a count request, which is a SECOND full execution of the same query rather than a cheap lookup. When requests overlap the line carries `· N in flight`. That number is the one to watch: the server bounds how many app queries run at once, so requests past the bound wait and the wait lands inside each request's own duration — a burst reads as "every query got slower", which looks like a slow database and is not one. A screen firing its list plus three facet counts on one keystroke shows up here as eight lines over one or two aliases; see `@lotics/app-runtime` `docs/data_fetching.md` (`total`, and `useQueries` for several counts at once) and `docs/queries.md` §10 for collapsing them. **Holds no realtime connection** — push belongs to the product frontend, so an app previewed here never updates on an external write (a CLI run, another tab, an agent): reload to see it. Deliberate rather than missing, since the alternative is a second implementation of the channel in the wrapper page, and a blanket poll here would hide an app whose queries do not declare their tables — the one mistake the real host punishes. The startup banner says `realtime: off` so this is visible without reading this table. The scaffold's `vite.config.ts` states `optimizeDeps: loticsOptimizeDeps()` — every published kit subpath, DERIVED from the kit's own `exports` rather than copied, and empty under `LOTICS_UI_SRC`. Vite's scanner reaches a subpath the moment something imports it, and meeting one mid-session re-optimizes, reloads, and inside this sandboxed iframe leaves two Reacts ("Invalid hook call") until a cold restart. `dev` and `codegen` heal that line, and the `server.fs.allow` one beside it, into a config scaffolded before them. Binds **loopback only** (`127.0.0.1`) — `/_rpc` dispatches with the developer's API key, so a socket on every interface would hand anyone on the network full read/write on the workspace. |
|
|
85
|
-
| `LOTICS_UI_SRC=<abs path to packages/ui/src>` (env, not a command) | Dev-link `@lotics/ui` to a monorepo checkout for the length of ONE command, **for every tool at once**. The app's `vite.config.ts` gets its whole `resolve` block from the kit (`resolve: loticsResolve()` — `@lotics/ui/vite`), which reads the variable at call time and adds the `@lotics/ui/*` → working-copy alias, so kit edits go live under `lotics app dev` (HMR) and bundle under `lotics app deploy`. In the same breath, every command that regenerates types (`create`/`pull`/`dev`/`deploy`/`codegen`, all via `writeAppDts`) writes **`.lotics/tsconfig.link.json`** — the matching `paths`, which the app's `tsconfig.json` `extends` — so `tsc`, vitest, eslint and your EDITOR resolve the same copy Vite does. Unset ⇒ every one of them goes back to `node_modules`, and the generated file is rewritten inert. **Why `paths` and not `npm link`:** under the dev-link a kit file sits OUTSIDE the app's `node_modules` and resolves its OWN `react` from the monorepo — two copies in one program and every shared type stops matching ("Two different types with this name exist, but they are unrelated"). The generated file therefore also pins every peer @lotics/ui declares to the APP's copy, types-package first (`react` → `@types/react`; pinning the runtime package instead strands tsc on a `.js` with no declarations). The pin set is derived from the installed kit's `peerDependencies`, so it tracks the kit rather than rotting. **The one hand-written file edited is `vite.config.ts`**, by `dev` and `codegen`, and only for the two blocks that are CALLS into `@lotics/ui/vite` (`optimizeDeps: loticsOptimizeDeps()`, `loticsFsAllow()` in `server.fs.allow`), spliced at the scaffold's own anchors in a config that already imports the subpath; a key the author answers themselves is left as typed and named back instead. Everything else generated lives in `.lotics/` (the CLI's own dir). Identical for a monorepo app and an EXTERNAL one (e.g. `~/lotics_apps`). `app deploy` still warns whenever the variable is set — that the bundle carries kit code from your working copy, or that the app's config predates `loticsResolve()` and never reads it, so the PUBLISHED kit is going out. An app whose `tsconfig.json` already `extends` something else is told rather than rewritten: add `./.lotics/tsconfig.link.json` to the array yourself. |
|
|
86
|
-
| `lotics file preview <file\|fil_id> [-o out.png]` | (also `lotics preview`) A PDF is refused and the refusal names the route: `lotics file download <fil_id>`, then `pdftoppm -png -r 150` (poppler-utils) for one PNG per page — "open it" is an instruction for a person at a screen, and the caller here is usually an agent. A `.html` renders as the page it is — served from its own directory so what it refers to beside it resolves, read once its images have loaded, captured at its content size — which is how a demo's paper props (an official letter, a stamped minute, a supplier's bill) are looked at before they go into an `html` template. Otherwise render a .docx/.xlsx to a PNG using the SAME engines the frontend FilePreview uses (`@lotics/docx` `loadDocxIntoElement` / `@lotics/xlsx` `drawSpreadsheet`) — so what you see matches an operator. Accepts a **local path** OR a stored **`fil_…` id** (a bare id, no extension): an id is first downloaded to a temp dir (the same presign path as `lotics file download`), rendered, then the transient source is removed; with no `-o` the PNG lands in cwd under the stored file's base name. Drives a headless Chrome over **CDP with only Node built-ins** — zero npm deps, the CLI stays a single bundled binary. The browser render logic is a separate browser bundle shipped at `dist/render_page.js`, served over a throwaway localhost http server and screenshotted full-page. **Requires a Chrome/Chromium on the machine** — detected from `CHROME_PATH`/`LOTICS_CHROME`, then Playwright's installed chromium, then system paths — inherent to rendering these browser formats; a clear "install a browser" error otherwise. PDFs need no render (open them directly). |
|
|
87
54
|
|
package/docs/knowledge_docs.md
CHANGED
|
@@ -214,14 +214,6 @@ statement about discovery, so it cannot silently break an app that depends on a
|
|
|
214
214
|
Reach for it instead of `rm` whenever the material still matters: last year's tariff schedule, a
|
|
215
215
|
handbook a newer one replaced, the raw source a curated doc was written from.
|
|
216
216
|
|
|
217
|
-
## Knowledge from a starter
|
|
218
|
-
|
|
219
|
-
A knowledge doc can also arrive with a **starter** — a published snapshot of a workspace setup
|
|
220
|
-
that carries a corpus of docs (and document templates) along with it. Copying a starter creates
|
|
221
|
-
the docs in your workspace as ordinary knowledge docs: yours outright, edited and deleted like
|
|
222
|
-
any other, with no link back to where they came from. Authoring, sharing and retrieval work the
|
|
223
|
-
same either way.
|
|
224
|
-
|
|
225
217
|
## Reaching the tools
|
|
226
218
|
|
|
227
219
|
```bash
|