@lotics/cli 0.172.1 → 0.175.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/dist/src/cli.js CHANGED
@@ -45696,7 +45696,7 @@ function resultSideEffects(result) {
45696
45696
  }
45697
45697
 
45698
45698
  // src/version.ts
45699
- var VERSION = "0.172.1";
45699
+ var VERSION = "0.175.0";
45700
45700
 
45701
45701
  // src/timezone.ts
45702
45702
  function machineTimezone() {
@@ -45708,6 +45708,9 @@ function machineTimezone() {
45708
45708
  }
45709
45709
  }
45710
45710
 
45711
+ // src/cli.ts
45712
+ import { spawn as spawn4 } from "node:child_process";
45713
+
45711
45714
  // src/docs_command.ts
45712
45715
  import fs5 from "node:fs";
45713
45716
  import path5 from "node:path";
@@ -45919,6 +45922,12 @@ var COMMANDS = [
45919
45922
  " these rows ship to everyone who copies the starter"
45920
45923
  ]
45921
45924
  },
45925
+ {
45926
+ verbs: ["upgrade"],
45927
+ help: [
45928
+ " lotics upgrade Update this CLI to the latest version"
45929
+ ]
45930
+ },
45922
45931
  {
45923
45932
  verbs: ["docs"],
45924
45933
  help: [
@@ -48393,7 +48402,7 @@ function walk(node, visit) {
48393
48402
  }
48394
48403
  }
48395
48404
 
48396
- // src/generate_app_fields.ts
48405
+ // ../shared/src/generate_app_fields.ts
48397
48406
  var HEADER = `// Auto-generated by 'lotics app codegen' and 'lotics app pull'.
48398
48407
  // DO NOT EDIT \u2014 regenerated from the workspace schema.
48399
48408
  //
@@ -48511,6 +48520,17 @@ export type AppFields = typeof F;
48511
48520
  export type AppOptions = typeof OPT;
48512
48521
  `;
48513
48522
  }
48523
+ function codegenTableIds(queries, allowlist) {
48524
+ const ids = new Set(allowlist);
48525
+ for (const declaration of Object.values(queries)) {
48526
+ for (const id of collectQueryTableIds(declaration.ast)) ids.add(id);
48527
+ }
48528
+ return [...ids];
48529
+ }
48530
+ function readCodegenTablesAllowlist(pkg) {
48531
+ const tables = pkg?.lotics?.codegen?.tables;
48532
+ return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
48533
+ }
48514
48534
 
48515
48535
  // src/app_workflow_check.ts
48516
48536
  import fs7 from "node:fs";
@@ -71970,18 +71990,13 @@ function writeAppDts(projectDir, manifest) {
71970
71990
  ensureAppVitestSetup(projectDir);
71971
71991
  return written.map(([file2]) => file2);
71972
71992
  }
71973
- function readCodegenTablesAllowlist(projectDir) {
71974
- const pkg = JSON.parse(fs8.readFileSync(path9.join(projectDir, "package.json"), "utf-8"));
71975
- const tables = pkg.lotics?.codegen?.tables;
71976
- return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
71977
- }
71978
71993
  function resolveCodegenTableIds(projectDir, queries) {
71979
- const ids = /* @__PURE__ */ new Set();
71980
- for (const decl of Object.values(queries)) {
71981
- for (const id of collectQueryTableIds(decl.ast)) ids.add(id);
71994
+ let manifest = null;
71995
+ try {
71996
+ manifest = JSON.parse(fs8.readFileSync(path9.join(projectDir, "package.json"), "utf-8"));
71997
+ } catch {
71982
71998
  }
71983
- for (const id of readCodegenTablesAllowlist(projectDir)) ids.add(id);
71984
- return [...ids];
71999
+ return codegenTableIds(queries, readCodegenTablesAllowlist(manifest));
71985
72000
  }
71986
72001
  function writeAppFields(projectDir, tables) {
71987
72002
  const dotLotics = path9.join(projectDir, ".lotics");
@@ -74023,15 +74038,19 @@ function reportKnowledgeWarnings(warnings) {
74023
74038
  answering from nowhere. Create them with those names, or edit the agent.`
74024
74039
  );
74025
74040
  }
74041
+ function instantiateBody(args) {
74042
+ return {
74043
+ ...args.noSampleData === true ? { no_sample_data: true } : {},
74044
+ ...args.adopt === true ? { adopt: true } : {},
74045
+ build_on_server: true
74046
+ };
74047
+ }
74026
74048
  async function starterInit(client, args) {
74027
74049
  const starter = await client.getPackage(args.starter_id);
74028
74050
  note(
74029
74051
  `Copying ${starter.name}${starter.is_official ? " (official)" : ""} into this workspace\u2026`
74030
74052
  );
74031
- const result = await client.instantiateStarter(args.starter_id, {
74032
- ...args.noSampleData === true ? { no_sample_data: true } : {},
74033
- ...args.adopt === true ? { adopt: true } : {}
74034
- });
74053
+ const result = await client.instantiateStarter(args.starter_id, instantiateBody(args));
74035
74054
  const tables = Object.keys(result.binding.entities ?? {}).sort();
74036
74055
  const knowledgeDocs = Object.keys(result.binding.knowledge ?? {}).sort();
74037
74056
  const templates = Object.keys(result.binding.templates ?? {}).sort();
@@ -74094,19 +74113,42 @@ Done \u2014 these are yours now, with no link back to the starter.`);
74094
74113
  stamp: { id: null, number: null },
74095
74114
  kept
74096
74115
  });
74116
+ const serverDeployed = result.deployed ?? null;
74097
74117
  if (starter.owned_by_caller !== true) {
74098
74118
  warn(
74099
- `
74119
+ serverDeployed !== null ? `
74120
+ Built ${starter.name} on Lotics \u2014 its package scripts and vite config are the
74121
+ publisher's code, run in an isolated container rather than on your machine.${starter.is_official ? " Lotics reviewed this starter." : ""}` : `
74100
74122
  Building ${starter.name} \u2014 this runs its build on your machine (its package scripts
74101
74123
  and vite config are the publisher's code, executed as you).${starter.is_official ? " Lotics reviewed this starter." : ""}`
74102
74124
  );
74103
- } else {
74125
+ } else if (serverDeployed === null) {
74104
74126
  note(`Building and deploying\u2026`);
74105
74127
  }
74106
- await appDeploy(client, {
74107
- projectDir: targetPath,
74108
- message: `Copied from starter ${starter.name}`
74109
- });
74128
+ const resumeDir = path10.relative(process.cwd(), targetPath) || ".";
74129
+ try {
74130
+ if (serverDeployed !== null) {
74131
+ note(`Deployed v${serverDeployed.version_number} \u2014 built on Lotics, no Node needed here.`);
74132
+ } else {
74133
+ await appDeploy(client, {
74134
+ projectDir: targetPath,
74135
+ message: `Copied from starter ${starter.name}`
74136
+ });
74137
+ }
74138
+ } catch (error52) {
74139
+ const reason = error52 instanceof Error ? error52.message : String(error52);
74140
+ throw new Error(
74141
+ `${reason}
74142
+
74143
+ The copy itself is DONE and nothing needs repeating: the tables, the sample
74144
+ records and the project all landed. Only the deploy is left.
74145
+
74146
+ Finish it: cd ${resumeDir} && lotics app deploy -m "Copied from starter ${starter.name}"
74147
+
74148
+ Do NOT re-run the copy \u2014 it would adopt the tables this one just created and
74149
+ insert the sample records again.`
74150
+ );
74151
+ }
74110
74152
  let signInUrl = null;
74111
74153
  try {
74112
74154
  const link = await client.login({
@@ -103302,6 +103344,31 @@ function alreadySetUp(email3) {
103302
103344
  if (config2?.email !== email3) return false;
103303
103345
  return Object.keys(config2.profiles ?? {}).length > 0;
103304
103346
  }
103347
+ async function runUpgrade() {
103348
+ const cmd = updateCommand();
103349
+ console.error(`Upgrading ${VERSION} \u2192 latest
103350
+ ${cmd}
103351
+ `);
103352
+ const proc = spawn4(cmd, {
103353
+ // The command is one of three literals this binary chooses between, never
103354
+ // anything a caller supplied — and each is a pipeline that needs a shell.
103355
+ shell: true,
103356
+ stdio: "inherit",
103357
+ env: process.env
103358
+ });
103359
+ const code = await new Promise((resolve2, reject2) => {
103360
+ proc.on("error", reject2);
103361
+ proc.on("exit", (exitCode) => resolve2(exitCode ?? 1));
103362
+ });
103363
+ if (code !== 0) {
103364
+ console.error(
103365
+ `
103366
+ Upgrade failed (exit ${code}). Run it yourself to see why:
103367
+ ${cmd}`
103368
+ );
103369
+ process.exit(1);
103370
+ }
103371
+ }
103305
103372
  async function handleSignup(positionalEmail, flags) {
103306
103373
  const email3 = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
103307
103374
  if (!email3) {
@@ -103650,6 +103717,10 @@ async function main() {
103650
103717
  docsCommand({ area: subcommand });
103651
103718
  return;
103652
103719
  }
103720
+ if (command === "upgrade") {
103721
+ await runUpgrade();
103722
+ return;
103723
+ }
103653
103724
  if (command === "report") {
103654
103725
  let input = subcommand ?? "";
103655
103726
  if (input.startsWith("@")) {
@@ -403,11 +403,25 @@ export declare class LoticsClient {
403
403
  version?: number;
404
404
  no_sample_data?: boolean;
405
405
  adopt?: boolean;
406
+ /** Ask the server to build and deploy the copy, so this machine needs no Node. */
407
+ build_on_server?: boolean;
406
408
  }): Promise<{
407
409
  app_id: string | null;
408
410
  starter_id: string;
409
411
  version: number;
410
412
  bundle_url: string | null;
413
+ /**
414
+ * The version the SERVER deployed, when it did.
415
+ *
416
+ * Optional in this type on purpose: a server that predates the field omits
417
+ * it entirely, and it is absent rather than null. Callers must treat "not
418
+ * there" and "null" alike and build locally — assuming the request was
419
+ * honoured would report success over an app nobody built.
420
+ */
421
+ deployed?: {
422
+ version_id: string;
423
+ version_number: number;
424
+ } | null;
411
425
  binding: Record<string, Record<string, string>>;
412
426
  sample_record_ids: Record<string, string[]>;
413
427
  knowledge_warnings: {
@@ -46,6 +46,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
46
46
  | `lotics starter show <starter_id>` | One starter's name, description, current version and trust standing (`official` — reviewed by Lotics; `your organization's own`). Read it before copying a starter you did not publish. |
47
47
  | `lotics starter init <starter_id> [path]` | **Copy a starter into this workspace.** Server-side it scaffolds the tables and fields, creates the document templates and knowledge docs, inserts the sample records, creates a BESPOKE app and materializes its queries, workflows and agents onto it. Then it stops: a starter ships **source only**, with no prebuilt bundle, so the app source is downloaded here, hydrated against the live app (the same steps `app pull` runs), built, and deployed — which is why this needs node and a few minutes, and why the app is not servable until the deploy lands. **What you get is yours outright**: an ordinary app plus ordinary tables, with no link back to the starter, nothing pinned, and nothing to upgrade. Edit any of it. **It builds the publisher's code on your machine** — the copy has to build where your workspace's field ids are, so `npm run build` runs their build script and vite loads their `vite.config.ts` in Node, as you. Dependencies install with `npm ci --ignore-scripts` — the lockfile's exact tree (so a starter published months ago resolves the same packages today), and no lifecycle scripts (the path that fires before you run anything). Neither changes the build itself. That is why provenance is the gate: **copyable only if the starter is Lotics-reviewed or your own organization published it** — copying runs the author's code (its bundle, its workflows, its agents) under YOUR authority, so provenance is the gate, enforced server-side. **Refuses a workspace that already has tables** unless `--adopt`: scaffold matches an entity by DISPLAY NAME, so a starter declaring `Contacts` would bind to yours. `--json` prints one object on stdout instead of progress (the shape is under `lotics start`) and captures npm/vite output unless the build fails. `--no-sample-data` skips the sample records; with them, the created record ids are reported — they are ordinary records, delete them whenever. Resolves and ANNOUNCES its workspace first (`lotics → <org> / <workspace>` on stderr). Admin-only. Authoring the registry (`starter publish/release/unpublish`) stays operator-only. |
48
48
  | `lotics starter 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 starter 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). **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. |
49
+ | `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. |
49
50
  | `lotics docs` \| `lotics docs <area>` | The index of the reference docs, **resolved out of the packages installed beside this project** — never carried by this CLI. **Both levels are discovered by looking**: every `@lotics/*` package carrying an `AGENTS.md` or a `docs/` in any `node_modules/@lotics` from the current directory UPWARD (nearest wins, so a hoisted root copy never shadows the one a project's own imports resolve to), and within each, every area it actually ships. Titles come from each file's own `# heading` and the version from the installed `package.json`, so a doc OR a whole package added upstream appears with no change to this CLI, and a skewed install is visible rather than reassuring. A package's index is named after the package (`lotics docs ui`), never `index`. `@lotics/app-sdk`, `@lotics/ui` and `@lotics/cli` sort first as a reading ORDER, not a filter. Both the index and `<area>` print to **stdout** — the index is the payload of a bare `lotics docs`, so `lotics docs | grep -i excel` works — with only the provenance line on stderr, so `lotics docs ai > ai.md` is the doc alone; a name two packages share is refused with both qualified forms (`lotics docs ui/templates`) rather than resolved silently. Needs no auth. Outside a project only `@lotics/cli`'s own resolve, and it says so. |
50
51
  | `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. |
51
52
  | `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 off the app row it already fetched, so a stale tree fails before it pushes a binding or builds, instead of after the upload arrives and the server 409s. 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 the app serves that the source names nowhere, 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__`, a `window.open` in the app's own source, and an INSTALLED `@lotics/app-sdk` below the version that understands the host's realtime push — read from `node_modules`, not the dependency range, because a caret is minor-locked below 1.0 so `^0.79.x` can never resolve `0.80` and `npm update` does nothing (all three fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent holding `run_app_query`/`run_app_workflow` with an EMPTY `query_aliases`/`workflow_aliases` (the tool is the capability, the alias list is the reach — empty 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 kit this app builds against has fallen behind what is published** — `@lotics/ui` and `@lotics/app-sdk`, read from `node_modules` for the same reason as the floor check above: a range keeps accepting, so an app pinned `^44.x` reads healthy for a year, and even an in-range one sits on the lockfile's older patch until `npm update` (never `npm install`, which honours the lock). A MAJOR behind is loud and names the packages actually behind — plus `@lotics/ui`'s `MIGRATION.md`, when ui is one of them, since it is the only half that keeps one; anything smaller is one quiet line, because a warning that fires on every deploy is one the reader stops seeing. The registry lookup is bounded and every failure — offline, slow, private — is silence: a version check must never become a new way for a deploy to fail. Adds no rule of its own — each finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **Exits 1 on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.172.1",
3
+ "version": "0.175.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {