@lotics/cli 0.70.0 → 0.73.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/README.md CHANGED
@@ -195,6 +195,44 @@ lotics ui link card --remove # finalize: PR + publish, then drop t
195
195
 
196
196
  `app codegen` reads `package.json#lotics.queries` to decide which tables to put in `app_fields.ts`; widen the set with `package.json#lotics.codegen.tables` (an array of `tbl_…` ids) for tables the app only writes via workflows.
197
197
 
198
+ ## App packages
199
+
200
+ A maintained, versioned app library: author once, install into many workspaces, upgrade per-workspace. Any org authors its own packages (publishing is owner-org-only); Lotics-backed packages carry the `official` badge. See `docs/app_packages.md` for the model.
201
+
202
+ ```bash
203
+ # Author (any org admin; the package is owned by your org)
204
+ lotics package new my-crm # scaffold project + contract.json
205
+ lotics package publish -m "v2: adds deals" # build + publish an immutable version
206
+
207
+ # Dev loop against a dev workspace (created with: lotics workspace create <name> --dev)
208
+ lotics package dev --workspace wsp_dev # scaffold-sync + run the app dev server
209
+ lotics package sync --workspace wsp_dev # migrate the dev installation only
210
+ lotics package reset --workspace wsp_dev # DEV-ONLY: drop scaffolded tables, re-scaffold clean
211
+
212
+ # Operate installations (workspace admin)
213
+ lotics package install apg_... --version 2 # scaffold + materialize + deploy + pin
214
+ lotics package doctor app_... # version pin vs latest + drift + local edits (exit 1 on findings)
215
+ lotics package rebind-role app_... <alias> <grp_id> # re-point a package role at another group (re-materializes)
216
+ lotics package upgrade app_... # preview, then apply (additive; overlay preserved)
217
+ lotics package fleet-upgrade apg_... # upgrade EVERY org installation (clean ones apply; findings skip, exit 1)
218
+ lotics package upgrade app_... --resolve fields.deal.stage=recreate # resolve reported drift
219
+ lotics package upgrade app_... --resolve queries.tasks=keep # consent for a local edit (or =revert)
220
+ lotics package eject app_... # one-way: sever the package link
221
+ lotics workspace doctor # dangling schema references across the workspace (exit 1 on findings)
222
+
223
+ # Promote an EXISTING bespoke app to a package (build it in a real workspace first,
224
+ # distribute once it proves itself — no re-authoring)
225
+ lotics package extract app_... # → draft project: contract + binding + templates
226
+ # prints a findings report; always writes the draft,
227
+ # exits 1 on error findings (fix them, publish re-validates)
228
+ cd <slug-of-app-name> # review contract.json aliases
229
+ lotics package publish -m "v1" # publish the reviewed project
230
+ lotics package adopt app_... # bind the package onto the origin app (same workspace);
231
+ # verifies the binding is faithful, then pins installation #1
232
+ ```
233
+
234
+ Extract writes `.lotics/adopt_binding.json` (the origin pin, excluded from published bundles); `adopt` reads it back and refuses a pin recorded for a different app. After promotion the package project is the master — iterate contract-first (`package dev`/`sync` → `publish`), never extract again.
235
+
198
236
  ## SDK
199
237
 
200
238
  ```typescript
@@ -86,6 +86,10 @@ export declare function writeWorkflowFile(projectDir: string, alias: string, sou
86
86
  * real source, not bookkeeping, and must not be silently eaten.
87
87
  */
88
88
  export declare function stripWorkflowHeader(content: string): string;
89
+ /** Run `tar` and resolve when it exits cleanly. Throws with stderr on failure. */
90
+ export declare function runTar(args: string[], cwd: string): Promise<void>;
91
+ /** Run `npm` (run/install/etc.) inheriting stdio so the user sees progress. */
92
+ export declare function runNpm(args: string[], cwd: string): Promise<void>;
89
93
  /**
90
94
  * Heal a pre-existing app's `tsconfig.json` so its generated types load and its
91
95
  * `npm run typecheck` stays honest. Idempotent, run on every pull / codegen /
@@ -258,7 +258,7 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
258
258
  }
259
259
  }
260
260
  /** Run `tar` and resolve when it exits cleanly. Throws with stderr on failure. */
261
- function runTar(args, cwd) {
261
+ export function runTar(args, cwd) {
262
262
  return new Promise((resolve, reject) => {
263
263
  const proc = spawn("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
264
264
  let stderr = "";
@@ -275,7 +275,7 @@ function runTar(args, cwd) {
275
275
  });
276
276
  }
277
277
  /** Run `npm` (run/install/etc.) inheriting stdio so the user sees progress. */
278
- function runNpm(args, cwd) {
278
+ export function runNpm(args, cwd) {
279
279
  return new Promise((resolve, reject) => {
280
280
  const proc = spawn("npm", args, { cwd, stdio: "inherit", env: ipv4ChildEnv(process.env) });
281
281
  proc.on("error", reject);
package/dist/args.d.ts CHANGED
@@ -30,12 +30,29 @@ export declare function parseArgs(argv: string[]): {
30
30
  message?: string;
31
31
  local: boolean;
32
32
  all: boolean;
33
+ /** `--dev`: mark a workspace as an app-package dev workspace (`workspace create`). */
34
+ dev: boolean;
33
35
  /** `--yes` (alias `-y`): confirm a destructive command (e.g. `workspace delete`) non-interactively. */
34
36
  yes: boolean;
35
37
  /** `--print-created` (alias `--report-effects`): print the honest post-run side-effect harvest. */
36
38
  printCreated: boolean;
37
39
  /** `--cleanup`: also delete the harvested created records (records only). */
38
40
  cleanup: boolean;
41
+ /**
42
+ * `--version <n>`: a package version number (`lotics package install … --version 2`).
43
+ * Distinct from the boolean `version` flag below: `--version` followed by an
44
+ * integer captures the number; a bare `--version` (or `-v`) prints the CLI
45
+ * version. No other command takes a numeric value after `--version`, so the
46
+ * peek is unambiguous.
47
+ */
48
+ packageVersion?: string;
49
+ /**
50
+ * `--resolve <key>=recreate|revert|keep|<id>` (repeatable): upgrade
51
+ * resolutions — drift entries (`<namespace.alias>`) take `recreate` or an
52
+ * existing id; modified-core entries (`<kind>.<alias>`) take `revert` or
53
+ * `keep`.
54
+ */
55
+ resolve: string[];
39
56
  version: boolean;
40
57
  help: boolean;
41
58
  };
package/dist/args.js CHANGED
@@ -25,9 +25,12 @@ export function parseArgs(argv) {
25
25
  message: undefined,
26
26
  local: false,
27
27
  all: false,
28
+ dev: false,
28
29
  yes: false,
29
30
  printCreated: false,
30
31
  cleanup: false,
32
+ packageVersion: undefined,
33
+ resolve: [],
31
34
  version: false,
32
35
  help: false,
33
36
  };
@@ -78,6 +81,9 @@ export function parseArgs(argv) {
78
81
  case "--local":
79
82
  flags.local = true;
80
83
  break;
84
+ case "--dev":
85
+ flags.dev = true;
86
+ break;
81
87
  case "--all":
82
88
  flags.all = true;
83
89
  break;
@@ -92,10 +98,37 @@ export function parseArgs(argv) {
92
98
  case "--cleanup":
93
99
  flags.cleanup = true;
94
100
  break;
101
+ case "--resolve": {
102
+ const value = argv[++i];
103
+ if (value === undefined || value.startsWith("-")) {
104
+ throw new Error("--resolve requires a value: <key>=recreate|revert|keep or <key>=<existing_id> (drift: <namespace.alias>; modified core: <kind>.<alias>).");
105
+ }
106
+ flags.resolve.push(value);
107
+ break;
108
+ }
95
109
  case "--version":
96
- case "-v":
97
- flags.version = true;
110
+ case "-v": {
111
+ // Inside a `package` command, `--version` carries a value (the
112
+ // package version to install) and the value is REQUIRED — a bare or
113
+ // malformed `--version` here must never fall back to the CLI-version
114
+ // early-exit boolean, which would print the CLI version and exit 0
115
+ // (a provisioning script reads that as a successful install). The
116
+ // value is captured raw; the consuming command validates
117
+ // integer-ness with a loud error. Outside `package`, this is the
118
+ // CLI-version boolean.
119
+ const next = argv[i + 1];
120
+ if (command === "package") {
121
+ if (next === undefined || next.startsWith("-")) {
122
+ throw new Error("--version requires a version number for package commands (e.g. --version 2).");
123
+ }
124
+ flags.packageVersion = next;
125
+ i++;
126
+ }
127
+ else {
128
+ flags.version = true;
129
+ }
98
130
  break;
131
+ }
99
132
  case "--help":
100
133
  case "-h":
101
134
  flags.help = true;
package/dist/args.test.js CHANGED
@@ -23,6 +23,49 @@ describe("parseArgs", () => {
23
23
  const r = parseArgs(["app", "deploy"]);
24
24
  expect(r.flags.message).toBeUndefined();
25
25
  });
26
+ it("captures --version <int> as packageVersion, not the boolean version flag", () => {
27
+ const r = parseArgs(["package", "install", "apg_123", "--version", "2"]);
28
+ expect(r.command).toBe("package");
29
+ expect(r.subcommand).toBe("install");
30
+ expect(r.toolArgs).toBe("apg_123");
31
+ expect(r.flags.packageVersion).toBe("2");
32
+ expect(r.flags.version).toBe(false);
33
+ expect(r.restArgs).toEqual([]);
34
+ });
35
+ it("treats a bare --version as the boolean CLI-version flag", () => {
36
+ const r = parseArgs(["--version"]);
37
+ expect(r.flags.version).toBe(true);
38
+ expect(r.flags.packageVersion).toBeUndefined();
39
+ });
40
+ it("treats -v as the boolean CLI-version flag", () => {
41
+ const r = parseArgs(["-v"]);
42
+ expect(r.flags.version).toBe(true);
43
+ expect(r.flags.packageVersion).toBeUndefined();
44
+ });
45
+ it("captures a non-integer --version value under package commands for loud command-level validation", () => {
46
+ const r = parseArgs(["package", "install", "apg_123", "--version", "2.0"]);
47
+ expect(r.flags.packageVersion).toBe("2.0");
48
+ expect(r.flags.version).toBe(false);
49
+ });
50
+ it("collects repeated --resolve values", () => {
51
+ const r = parseArgs([
52
+ "package",
53
+ "upgrade",
54
+ "app_1",
55
+ "--resolve",
56
+ "fields.deal.stage=recreate",
57
+ "--resolve",
58
+ "templates.quote=dtl_abc",
59
+ ]);
60
+ expect(r.flags.resolve).toEqual(["fields.deal.stage=recreate", "templates.quote=dtl_abc"]);
61
+ });
62
+ it("errors on --resolve without a value", () => {
63
+ expect(() => parseArgs(["package", "upgrade", "app_1", "--resolve"])).toThrow(/--resolve requires a value/);
64
+ });
65
+ it("errors loudly on a bare --version under package commands instead of printing the CLI version", () => {
66
+ expect(() => parseArgs(["package", "install", "apg_123", "--version"])).toThrow(/requires a version number/);
67
+ expect(() => parseArgs(["package", "install", "apg_123", "--version", "--json"])).toThrow(/requires a version number/);
68
+ });
26
69
  it("parses --workspace as a value flag", () => {
27
70
  const r = parseArgs(["run", "query_tables", "{}", "--workspace", "wsp_123"]);
28
71
  expect(r.flags.workspace).toBe("wsp_123");
@@ -63,6 +106,12 @@ describe("parseArgs", () => {
63
106
  expect(parseArgs(["app", "workflow", "run", "wf"]).flags.cleanup).toBe(false);
64
107
  expect(parseArgs(["app", "workflow", "run", "wf"]).flags.printCreated).toBe(false);
65
108
  });
109
+ it("parses --dev as a boolean flag (default false)", () => {
110
+ expect(parseArgs(["workspace", "create", "Dev WS", "--dev"]).flags.dev).toBe(true);
111
+ expect(parseArgs(["workspace", "create", "Dev WS"]).flags.dev).toBe(false);
112
+ // The boolean flag does not consume the workspace name positional.
113
+ expect(parseArgs(["workspace", "create", "Dev WS", "--dev"]).toolArgs).toBe("Dev WS");
114
+ });
66
115
  it("parses --yes and -y as the same boolean confirmation flag (default false)", () => {
67
116
  expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).flags.yes).toBe(true);
68
117
  expect(parseArgs(["workspace", "delete", "wsp_1", "-y"]).flags.yes).toBe(true);
package/dist/cli.js CHANGED
@@ -14,10 +14,12 @@ import { LoticsClient, API_BASE_URL } from "./client.js";
14
14
  import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
15
15
  import { VERSION } from "./version.js";
16
16
  import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appQuerySet, appUiLink, } from "./app_commands.js";
17
+ import { packageInstall, packageEject, packageDoctor, packageUpgrade, parseResolveFlags, packageNew, packageBuild, packagePublish, packageDev, packageSync, packageReset, packageExtract, packageAdopt, packageFleetUpgrade, } from "./package_commands.js";
17
18
  import { parseArgs } from "./args.js";
18
19
  import { ingestJsonArgs } from "./inputs.js";
19
20
  import { runXlsxCommand } from "./xlsx.js";
20
21
  import { runDocxCommand } from "./docx.js";
22
+ import { runPreviewCommand } from "./preview.js";
21
23
  function printHelp() {
22
24
  console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
23
25
 
@@ -61,15 +63,12 @@ COMMANDS
61
63
  lotics workspace select <id> Switch to a different workspace
62
64
  lotics workspace create <name> Create a new workspace (admin only)
63
65
  lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)
66
+ lotics workspace doctor Report dangling schema references (admin only)
64
67
  lotics tools List all available tools
65
68
  lotics tools <name> Show tool description and input schema
66
69
  lotics run <tool> '<json>' Execute a tool
67
70
  lotics run <tool> @args.json Read JSON args from a file (large payloads)
68
71
  cat args.json | lotics run <tool> Read JSON args from stdin (large payloads)
69
- lotics upload <file|dir...> Upload files (directories expand to their immediate files)
70
- lotics download <file_id> Download a file by ID
71
- lotics download record <record_id> <field_key>
72
- Download all files on a record file field
73
72
  lotics app create <name> [path] Create a new custom-code app + scaffold locally
74
73
  lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
75
74
  lotics app deploy -m <message> Build + upload current dir as a new version
@@ -97,6 +96,37 @@ COMMANDS
97
96
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
98
97
  lotics app rename "<new name>" Rename the app's display name (launcher title)
99
98
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
99
+ lotics package new <name> [path] Scaffold a package project (contract + app source)
100
+ lotics package build [path] Build the publishable bundle (source + dist)
101
+ lotics package publish [path] Publish a new immutable package version
102
+ lotics package dev [path] Sync the package into a dev workspace + run the app
103
+ dev server (--workspace <dev_ws> selects it)
104
+ lotics package sync [path] Re-sync (additive migrate + materialize) into a dev ws
105
+ lotics package reset [path] DEV-ONLY: drop scaffolded tables + re-scaffold clean
106
+ lotics package install <package> [--version N]
107
+ Install an app package into this workspace
108
+ (scaffolds the data model + deploys + materializes)
109
+ lotics package upgrade <app_id> [--version N] [--resolve <key>=recreate|revert|keep|<id> ...]
110
+ Preview-then-apply a package upgrade; refuses while
111
+ any drift/modified finding lacks a --resolve, and
112
+ hard-stops on breaking contract changes
113
+ lotics package doctor [app_id] Installation health: version pin vs latest, binding
114
+ drift, locally modified core (exit 1 on findings)
115
+ lotics package rebind-role <app_id> <alias> <grp_id>
116
+ Re-point a package role at a different group
117
+ (re-materializes at the pinned version)
118
+ lotics package eject <app_id> Sever an installation's package link
119
+ (re-deploys the pinned source as a bespoke app)
120
+ lotics package extract <app_id> [path] Promote a bespoke app to a draft package
121
+ project (contract + binding + templates); always
122
+ writes the draft, exits 1 on error findings
123
+ lotics package adopt <app_id> [path] Bind the published package project onto the
124
+ origin app (reads .lotics/adopt_binding.json;
125
+ --version N pins a specific version)
126
+ lotics package fleet-upgrade <package_id> [--version N]
127
+ Upgrade EVERY installation of the package across
128
+ your org: applies where the preview is clean,
129
+ skips + reports findings (exit 1 unless all current)
100
130
  lotics ui link <component> [--ui-src <path>] [--remove]
101
131
  Dev-link @lotics/ui to packages/ui/src (Vite alias
102
132
  + tsc paths) for live HMR + typecheck. Monorepo apps
@@ -107,6 +137,12 @@ COMMANDS
107
137
  lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem
108
138
  (uses the bundled Lotics OOXML engine; preserves
109
139
  tables, images, and unknown markup verbatim)
140
+ lotics file upload <file|dir...> Upload files (alias: lotics upload)
141
+ lotics file download <file_id> Download a file by ID (alias: lotics download)
142
+ lotics file download record <record_id> <field_key>
143
+ Download all files on a record file field
144
+ lotics file preview <file> [-o png] Render a .docx/.xlsx to a PNG (frontend engines;
145
+ needs a Chrome/Chromium on the machine)
110
146
 
111
147
  FLAGS
112
148
  --json Full JSON output (default is human-readable text)
@@ -119,6 +155,8 @@ FLAGS
119
155
  is_current_member / row-scoping resolve to them (also
120
156
  LOTICS_VIEW_AS env; admin key only; writes stay yours)
121
157
  --local Pin the current directory (lotics org use / auth api-key)
158
+ --dev (lotics workspace create) Mark a throwaway app-package dev
159
+ workspace — required for "lotics package reset"
122
160
  --all (lotics auth logout) Remove every saved credential
123
161
  --version Show version
124
162
 
@@ -392,7 +430,22 @@ function resolveUploadPaths(rawPaths) {
392
430
  return result;
393
431
  }
394
432
  async function main() {
395
- const { command, subcommand, toolArgs, restArgs, flags } = parseArgs(process.argv.slice(2));
433
+ const parsed = parseArgs(process.argv.slice(2));
434
+ const { restArgs, flags } = parsed;
435
+ let { command, subcommand, toolArgs } = parsed;
436
+ // `file` is a thin noun-group over the file verbs: `lotics file <upload|download|preview>
437
+ // <args…>` strips the prefix and re-dispatches to the same handlers as the top-level
438
+ // verbs (which stay as aliases). Positional shift only — flags are untouched, and
439
+ // `download record <id> <field>` still works as `file download record <id> <field>`.
440
+ if (command === "file") {
441
+ if (!subcommand) {
442
+ printHelp();
443
+ return;
444
+ }
445
+ command = subcommand;
446
+ subcommand = toolArgs;
447
+ toolArgs = restArgs.shift();
448
+ }
396
449
  if (flags.help || (!command && !flags.version)) {
397
450
  printHelp();
398
451
  return;
@@ -494,6 +547,10 @@ async function main() {
494
547
  await runDocxCommand(subcommand, toolArgs, restArgs);
495
548
  return;
496
549
  }
550
+ if (command === "preview") {
551
+ await runPreviewCommand(subcommand, flags);
552
+ return;
553
+ }
497
554
  // --- lotics ui link <component> [--remove] — local vite.config edit, no auth ---
498
555
  if (command === "ui") {
499
556
  if (subcommand === "link") {
@@ -514,6 +571,20 @@ async function main() {
514
571
  console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
515
572
  process.exit(1);
516
573
  }
574
+ // --- lotics package new / build — local scaffold + build, no auth required ---
575
+ if (command === "package" && subcommand === "new") {
576
+ const name = toolArgs;
577
+ if (!name) {
578
+ console.error("Usage: lotics package new <name> [path]");
579
+ process.exit(1);
580
+ }
581
+ await packageNew({ name, targetPath: restArgs[0] });
582
+ return;
583
+ }
584
+ if (command === "package" && subcommand === "build") {
585
+ await packageBuild({ projectDir: toolArgs });
586
+ return;
587
+ }
517
588
  // --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
518
589
  // Reads src/workflows/*.ts + .lotics/workflows/*.globals.d.ts and the app's own
519
590
  // typescript; builds one isolated program per alias. Handled before the auth
@@ -596,7 +667,7 @@ async function main() {
596
667
  return;
597
668
  }
598
669
  // --- Validate command before auth ---
599
- if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
670
+ if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app" && command !== "package") {
600
671
  console.error(`Unknown command: ${command}`);
601
672
  console.error('Run "lotics --help" for usage.');
602
673
  process.exit(1);
@@ -620,6 +691,24 @@ async function main() {
620
691
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
621
692
  process.exit(1);
622
693
  }
694
+ if (command === "package" && !subcommand) {
695
+ console.error("Usage:");
696
+ console.error(" lotics package new <name> [path] Scaffold a package project (contract + app source)");
697
+ console.error(" lotics package build [path] Build the publishable bundle (source + dist)");
698
+ console.error(" lotics package publish [path] [-m <changelog>] Publish a new immutable package version");
699
+ console.error(" lotics package dev [path] [--workspace <ws>] Sync into a dev workspace + run the app dev server");
700
+ console.error(" lotics package sync [path] [--workspace <ws>] Re-sync (additive migrate + materialize) into a dev workspace");
701
+ console.error(" lotics package reset [path] [--workspace <ws>] DEV-ONLY: drop scaffolded tables + re-scaffold clean");
702
+ console.error(" lotics package install <package> [--version N] Install a package into this workspace");
703
+ console.error(" lotics package upgrade <app_id> [--version N] [--resolve ns.alias=recreate|<id>] Preview + apply an upgrade");
704
+ console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding drift");
705
+ console.error(" lotics package rebind-role <app_id> <alias> <grp_id> Re-point a package role at another group");
706
+ console.error(" lotics package eject <app_id> Sever an installation's package link");
707
+ console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
708
+ console.error(" lotics package adopt <app_id> [path] Bind the published project onto the origin app");
709
+ console.error(" lotics package fleet-upgrade <package_id> [--version N] Upgrade every org installation (clean ones apply; findings skip)");
710
+ process.exit(1);
711
+ }
623
712
  if (command === "run" && !subcommand) {
624
713
  console.error("Usage: lotics run <tool> '<json_args>'\n" +
625
714
  " lotics run <tool> @args.json (read args from a file — for large payloads)\n" +
@@ -647,6 +736,24 @@ async function main() {
647
736
  const { client, ctx } = requireClient(flags);
648
737
  // lotics workspace / lotics workspace list / lotics workspace select <id>
649
738
  if (command === "workspace") {
739
+ if (subcommand === "doctor") {
740
+ // The workspace command family runs before the global workspace
741
+ // resolution — doctor needs the same first-workspace fallback every
742
+ // data command gets.
743
+ await resolveWorkspace(client, ctx);
744
+ const dangling = await client.getWorkspaceDanglingReferences();
745
+ if (dangling.length === 0) {
746
+ console.error("Workspace references: healthy — every referenced schema id resolves.");
747
+ return;
748
+ }
749
+ console.error(`Dangling schema references (${dangling.length}):`);
750
+ for (const d of dangling) {
751
+ console.error(` - ${d.referent.kind} "${d.referent.name}" (${d.referent.id}) → ${d.namespace} ${d.id} (missing)`);
752
+ }
753
+ console.error("Each referent points at schema that no longer exists — edit or archive it, or recreate the missing schema.");
754
+ process.exitCode = 1;
755
+ return;
756
+ }
650
757
  const workspaces = await client.listWorkspaces();
651
758
  const currentWorkspaceId = ctx.workspaceId;
652
759
  if (subcommand === "select") {
@@ -673,11 +780,11 @@ async function main() {
673
780
  if (subcommand === "create") {
674
781
  const name = toolArgs;
675
782
  if (!name) {
676
- console.error('Usage: lotics workspace create <name> [--timezone <tz>]');
783
+ console.error('Usage: lotics workspace create <name> [--timezone <tz>] [--dev]');
677
784
  process.exit(1);
678
785
  }
679
786
  const timezone = flags.timezone;
680
- const created = await client.createWorkspace({ name, timezone });
787
+ const created = await client.createWorkspace({ name, timezone, is_dev: flags.dev || undefined });
681
788
  setSelectedWorkspace(created.id);
682
789
  client.setWorkspaceId(created.id);
683
790
  if (flags.json) {
@@ -746,6 +853,153 @@ async function main() {
746
853
  }
747
854
  // Ensure workspace is resolved for all remaining commands
748
855
  await resolveWorkspace(client, ctx);
856
+ // lotics package install <package> [--version N]
857
+ if (command === "package") {
858
+ if (subcommand === "install") {
859
+ const packageId = toolArgs;
860
+ if (!packageId) {
861
+ console.error("Usage: lotics package install <package> [--version N]");
862
+ process.exit(1);
863
+ }
864
+ let version;
865
+ if (flags.packageVersion !== undefined) {
866
+ version = Number(flags.packageVersion);
867
+ if (!Number.isInteger(version) || version <= 0) {
868
+ console.error(`Invalid --version "${flags.packageVersion}" — expected a positive integer.`);
869
+ process.exit(1);
870
+ }
871
+ }
872
+ await packageInstall(client, { package_id: packageId, version });
873
+ return;
874
+ }
875
+ if (subcommand === "eject") {
876
+ const appId = toolArgs;
877
+ if (!appId) {
878
+ console.error("Usage: lotics package eject <app_id>");
879
+ process.exit(1);
880
+ }
881
+ await packageEject(client, { app_id: appId });
882
+ return;
883
+ }
884
+ if (subcommand === "extract") {
885
+ const appId = toolArgs;
886
+ if (!appId) {
887
+ console.error("Usage: lotics package extract <app_id> [path]");
888
+ console.error("Promotes a bespoke app to a draft package project (contract + binding + templates).");
889
+ process.exit(1);
890
+ }
891
+ await packageExtract(client, { app_id: appId, targetPath: restArgs[0] });
892
+ return;
893
+ }
894
+ if (subcommand === "adopt") {
895
+ const appId = toolArgs;
896
+ if (!appId) {
897
+ console.error("Usage: lotics package adopt <app_id> [path]");
898
+ console.error("Binds the published package project (this dir, or [path]) onto the origin app.");
899
+ process.exit(1);
900
+ }
901
+ let version;
902
+ if (flags.packageVersion !== undefined) {
903
+ version = Number(flags.packageVersion);
904
+ if (!Number.isInteger(version) || version <= 0) {
905
+ console.error(`Invalid --version "${flags.packageVersion}" — expected a positive integer.`);
906
+ process.exit(1);
907
+ }
908
+ }
909
+ await packageAdopt(client, { app_id: appId, version, projectDir: restArgs[0] });
910
+ return;
911
+ }
912
+ if (subcommand === "fleet-upgrade") {
913
+ const packageId = toolArgs;
914
+ if (!packageId) {
915
+ console.error("Usage: lotics package fleet-upgrade <package_id> [--version N]");
916
+ process.exit(1);
917
+ }
918
+ let version;
919
+ if (flags.packageVersion !== undefined) {
920
+ version = Number(flags.packageVersion);
921
+ if (!Number.isInteger(version) || version <= 0) {
922
+ console.error(`Invalid --version "${flags.packageVersion}" — expected a positive integer.`);
923
+ process.exit(1);
924
+ }
925
+ }
926
+ await packageFleetUpgrade(client, { package_id: packageId, version });
927
+ return;
928
+ }
929
+ if (subcommand === "doctor") {
930
+ await packageDoctor(client, { app_id: toolArgs });
931
+ return;
932
+ }
933
+ if (subcommand === "rebind-role") {
934
+ const appId = toolArgs;
935
+ const [roleAlias, groupId] = restArgs;
936
+ if (!appId || !roleAlias || !groupId) {
937
+ console.error("Usage: lotics package rebind-role <app_id> <role_alias> <grp_id>");
938
+ process.exit(1);
939
+ }
940
+ const result = await client.rebindAppPackageRole(appId, {
941
+ role_alias: roleAlias,
942
+ group_id: groupId,
943
+ });
944
+ console.error(`Rebound role "${result.role_alias}" → ${result.group_id}` +
945
+ (result.previous_group_id ? ` (was ${result.previous_group_id})` : "") +
946
+ ". Package workflows/agents were re-materialized against the new group.");
947
+ return;
948
+ }
949
+ if (subcommand === "upgrade") {
950
+ const appId = toolArgs;
951
+ if (!appId) {
952
+ console.error("Usage: lotics package upgrade <app_id> [--version N] [--resolve <namespace.alias>=recreate|<id> ...]");
953
+ process.exit(1);
954
+ }
955
+ let version;
956
+ if (flags.packageVersion !== undefined) {
957
+ version = Number(flags.packageVersion);
958
+ if (!Number.isInteger(version) || version <= 0) {
959
+ console.error(`Invalid --version "${flags.packageVersion}" — expected a positive integer.`);
960
+ process.exit(1);
961
+ }
962
+ }
963
+ await packageUpgrade(client, {
964
+ app_id: appId,
965
+ version,
966
+ resolutions: parseResolveFlags(flags.resolve),
967
+ });
968
+ return;
969
+ }
970
+ if (subcommand === "publish") {
971
+ await packagePublish(client, { projectDir: toolArgs, changelog: flags.message });
972
+ return;
973
+ }
974
+ if (subcommand === "sync") {
975
+ await packageSync(client, { projectDir: toolArgs });
976
+ return;
977
+ }
978
+ if (subcommand === "reset") {
979
+ await packageReset(client, { projectDir: toolArgs });
980
+ return;
981
+ }
982
+ if (subcommand === "dev") {
983
+ // First positional is an optional project path; --port / --vite-port
984
+ // override the wrapper / Vite ports (same shape as `lotics app dev`).
985
+ const projectDir = toolArgs;
986
+ let port;
987
+ let vitePort;
988
+ for (const a of restArgs) {
989
+ const portMatch = /^--port=(\d+)$/.exec(a);
990
+ const vitePortMatch = /^--vite-port=(\d+)$/.exec(a);
991
+ if (portMatch)
992
+ port = Number(portMatch[1]);
993
+ else if (vitePortMatch)
994
+ vitePort = Number(vitePortMatch[1]);
995
+ }
996
+ await packageDev(client, { projectDir, port, vitePort });
997
+ return;
998
+ }
999
+ console.error(`Unknown package subcommand: ${subcommand}`);
1000
+ console.error("Run 'lotics package' for usage.");
1001
+ process.exit(1);
1002
+ }
749
1003
  // lotics app create / pull / deploy
750
1004
  if (command === "app") {
751
1005
  if (subcommand === "create") {