@lotics/cli 0.71.0 → 0.74.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
@@ -1,4 +1,13 @@
1
1
  import { LoticsClient } from "./client.js";
2
+ /**
3
+ * Resolve the latest published version of a package from the npm registry.
4
+ * Returns null on any failure (network error, 404, malformed payload) so
5
+ * callers can fall back to a static pin rather than crashing `app create`.
6
+ *
7
+ * 1.5s timeout — npm registry is fast when reachable; the fallback is fine
8
+ * the rare times it isn't, and we don't want to block scaffold on a hang.
9
+ */
10
+ export declare function fetchLatestNpmVersion(packageName: string): Promise<string | null>;
2
11
  /**
3
12
  * Manifest declaration for one workflow alias:
4
13
  * `"alias": { workflow_id, inputs?: { key: { type, … } }, outputs?: { key: { type, … } } }`
@@ -86,6 +95,10 @@ export declare function writeWorkflowFile(projectDir: string, alias: string, sou
86
95
  * real source, not bookkeeping, and must not be silently eaten.
87
96
  */
88
97
  export declare function stripWorkflowHeader(content: string): string;
98
+ /** Run `tar` and resolve when it exits cleanly. Throws with stderr on failure. */
99
+ export declare function runTar(args: string[], cwd: string): Promise<void>;
100
+ /** Run `npm` (run/install/etc.) inheriting stdio so the user sees progress. */
101
+ export declare function runNpm(args: string[], cwd: string): Promise<void>;
89
102
  /**
90
103
  * Heal a pre-existing app's `tsconfig.json` so its generated types load and its
91
104
  * `npm run typecheck` stays honest. Idempotent, run on every pull / codegen /
@@ -32,7 +32,7 @@ import { loadProjectTypescript, checkWorkflowBodies, } from "./app_workflow_chec
32
32
  * 1.5s timeout — npm registry is fast when reachable; the fallback is fine
33
33
  * the rare times it isn't, and we don't want to block scaffold on a hang.
34
34
  */
35
- async function fetchLatestNpmVersion(packageName) {
35
+ export async function fetchLatestNpmVersion(packageName) {
36
36
  try {
37
37
  const controller = new AbortController();
38
38
  const timeout = setTimeout(() => controller.abort(), 1500);
@@ -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,6 +14,7 @@ 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";
@@ -62,6 +63,7 @@ COMMANDS
62
63
  lotics workspace select <id> Switch to a different workspace
63
64
  lotics workspace create <name> Create a new workspace (admin only)
64
65
  lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)
66
+ lotics workspace doctor Report dangling schema references (admin only)
65
67
  lotics tools List all available tools
66
68
  lotics tools <name> Show tool description and input schema
67
69
  lotics run <tool> '<json>' Execute a tool
@@ -94,6 +96,37 @@ COMMANDS
94
96
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
95
97
  lotics app rename "<new name>" Rename the app's display name (launcher title)
96
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)
97
130
  lotics ui link <component> [--ui-src <path>] [--remove]
98
131
  Dev-link @lotics/ui to packages/ui/src (Vite alias
99
132
  + tsc paths) for live HMR + typecheck. Monorepo apps
@@ -122,6 +155,8 @@ FLAGS
122
155
  is_current_member / row-scoping resolve to them (also
123
156
  LOTICS_VIEW_AS env; admin key only; writes stay yours)
124
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"
125
160
  --all (lotics auth logout) Remove every saved credential
126
161
  --version Show version
127
162
 
@@ -536,6 +571,20 @@ async function main() {
536
571
  console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
537
572
  process.exit(1);
538
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
+ }
539
588
  // --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
540
589
  // Reads src/workflows/*.ts + .lotics/workflows/*.globals.d.ts and the app's own
541
590
  // typescript; builds one isolated program per alias. Handled before the auth
@@ -618,7 +667,7 @@ async function main() {
618
667
  return;
619
668
  }
620
669
  // --- Validate command before auth ---
621
- 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") {
622
671
  console.error(`Unknown command: ${command}`);
623
672
  console.error('Run "lotics --help" for usage.');
624
673
  process.exit(1);
@@ -642,6 +691,24 @@ async function main() {
642
691
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
643
692
  process.exit(1);
644
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
+ }
645
712
  if (command === "run" && !subcommand) {
646
713
  console.error("Usage: lotics run <tool> '<json_args>'\n" +
647
714
  " lotics run <tool> @args.json (read args from a file — for large payloads)\n" +
@@ -669,6 +736,24 @@ async function main() {
669
736
  const { client, ctx } = requireClient(flags);
670
737
  // lotics workspace / lotics workspace list / lotics workspace select <id>
671
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
+ }
672
757
  const workspaces = await client.listWorkspaces();
673
758
  const currentWorkspaceId = ctx.workspaceId;
674
759
  if (subcommand === "select") {
@@ -695,11 +780,11 @@ async function main() {
695
780
  if (subcommand === "create") {
696
781
  const name = toolArgs;
697
782
  if (!name) {
698
- console.error('Usage: lotics workspace create <name> [--timezone <tz>]');
783
+ console.error('Usage: lotics workspace create <name> [--timezone <tz>] [--dev]');
699
784
  process.exit(1);
700
785
  }
701
786
  const timezone = flags.timezone;
702
- const created = await client.createWorkspace({ name, timezone });
787
+ const created = await client.createWorkspace({ name, timezone, is_dev: flags.dev || undefined });
703
788
  setSelectedWorkspace(created.id);
704
789
  client.setWorkspaceId(created.id);
705
790
  if (flags.json) {
@@ -768,6 +853,153 @@ async function main() {
768
853
  }
769
854
  // Ensure workspace is resolved for all remaining commands
770
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
+ }
771
1003
  // lotics app create / pull / deploy
772
1004
  if (command === "app") {
773
1005
  if (subcommand === "create") {