@lotics/cli 0.75.0 → 0.76.1

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
@@ -197,7 +197,7 @@ lotics ui link card --remove # finalize: PR + publish, then drop t
197
197
 
198
198
  ## App packages
199
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.
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/packages.md` for the model.
201
201
 
202
202
  ```bash
203
203
  # Author (any org admin; the package is owned by your org)
@@ -210,13 +210,19 @@ lotics package sync --workspace wsp_dev # migrate the dev installation only
210
210
  lotics package reset --workspace wsp_dev # DEV-ONLY: drop scaffolded tables, re-scaffold clean
211
211
 
212
212
  # Operate installations (workspace admin)
213
+ lotics package show apg_... # registry metadata + version history (trust badge, channel, yank)
213
214
  lotics package install apg_... --version 2 # scaffold + materialize + deploy + pin
215
+ lotics package install apg_... --config heading="Ops board" # per-knob overrides over contract defaults
216
+ lotics package config app_... --set show_done=false # edit a live installation's config (partial merge)
217
+ lotics package uninstall app_... [--archive-tables] # remove an installation; workflows stop firing
218
+ lotics package retire apg_... [--undo] # owner-org: refuse NEW installs (existing keep working + upgrading)
214
219
  lotics package doctor app_... # version pin vs latest + drift + local edits (exit 1 on findings)
215
220
  lotics package rebind-role app_... <alias> <grp_id> # re-point a package role at another group (re-materializes)
216
221
  lotics package upgrade app_... # preview, then apply (additive; overlay preserved)
217
222
  lotics package fleet-upgrade apg_... # upgrade EVERY org installation (clean ones apply; findings skip, exit 1)
218
223
  lotics package upgrade app_... --resolve fields.deal.stage=recreate # resolve reported drift
219
224
  lotics package upgrade app_... --resolve queries.tasks=keep # consent for a local edit (or =revert)
225
+ lotics package upgrade app_... --resolve template.quote=revert # a locally-edited template (clean ones auto-update; or =keep)
220
226
  lotics package eject app_... # one-way: sever the package link
221
227
  lotics workspace doctor # dangling schema references across the workspace (exit 1 on findings)
222
228
 
@@ -233,6 +239,35 @@ lotics package adopt app_... # bind the package onto the origin ap
233
239
 
234
240
  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
241
 
242
+ ### Content packages
243
+
244
+ A **content** package ships **knowledge docs and/or document templates** — no app, just the artifacts. Knowledge is the reference corpus an agent looks up by name; templates are standalone document templates. Content comes two ways: bundled *with an app* (installing the app delivers its agents' docs) or as a standalone content package (a versioned corpus). Install delivers, upgrade refreshes — preserving stable `kdc_`/`tmpl_` ids + a version snapshot per change. See `docs/packages.md` § Content.
245
+
246
+ ```bash
247
+ # Author a content package (scaffolds a knowledge/ dir + manifest; add templates/ + a
248
+ # lotics.package.templates entry per template — inline html/email or file-backed excel/word/pdf-form)
249
+ lotics package new my-sops --kind content
250
+ lotics package publish -m "v1: SOP corpus + templates"
251
+
252
+ # Install into a workspace (a same-named local DOC collides — consent with --bind-to; templates never collide)
253
+ lotics package install apg_... # app OR content, kind-branched
254
+ lotics package install apg_... --bind-to policy=kdc_... # adopt an existing doc as package-managed
255
+
256
+ # Discover installed content (the source for a pci_ id — install prints it once)
257
+ lotics package list-content # id · package · version · standalone vs app-bundled
258
+
259
+ # Refresh a standalone content install (pci_ id from install / list-content) — one gate over docs + templates
260
+ lotics package upgrade pci_... # preview, then apply
261
+ lotics package upgrade pci_... --resolve policy=apply # a doc: apply|keep|archive|recreate|unbind
262
+ lotics package upgrade pci_... --resolve quote=revert # a locally-edited template: revert|keep (clean ones auto-update)
263
+ lotics package upgrade pci_... --apply-all # accept upstream for every doc + template (OVERWRITES local edits)
264
+ lotics package uninstall pci_... --keep-content # remove the pin; keep docs + templates (default archives them)
265
+ ```
266
+
267
+ A package's knowledge can also arrive **bundled with an app**. Upgrading such an app (`lotics package upgrade <app_id>`) previews its bundled-knowledge changes alongside the plan and resolves them with the SAME flags: a `--resolve <alias>=apply|keep|archive|recreate|unbind` naming a bundled doc routes to the doc (anything else stays a core-artifact resolution; a name that is both errors), `--bind-to` consents an added-doc collision, `--apply-all` takes the package's version for every consent-requiring doc.
268
+
269
+ To package the knowledge an existing app's agents route to, declare it in the app project's `package.json#lotics.knowledge` (`[{ alias, doc_id }]`, + `lotics.knowledge_expects` for docs the agents assume but the package doesn't own), then `lotics package extract` / `adopt` as usual.
270
+
236
271
  ## SDK
237
272
 
238
273
  ```typescript
@@ -117,6 +117,22 @@ export declare function runNpm(args: string[], cwd: string): Promise<void>;
117
117
  * the author fixes the config.
118
118
  */
119
119
  export declare function ensureAppTsconfig(projectDir: string): void;
120
+ /**
121
+ * Write the three `.lotics/app_{workflows,queries,agents}.d.ts` companions from
122
+ * the manifest's maps, then heal the app's tsconfig so they actually load. Called
123
+ * from `app create / pull / dev / deploy / codegen`, so the augmented `AppWorkflows`
124
+ * / `AppQueries` / `AppAgents` types stay in sync with the manifest.
125
+ *
126
+ * The heal is at the write boundary on purpose: a `.d.ts` written but not loaded is
127
+ * useless (a bare `.lotics` include is skipped by TypeScript's include-glob walk and
128
+ * loads zero of them), so `ensureAppTsconfig` couples "wrote the types" with "the
129
+ * program can see them" — no caller can do one without the other.
130
+ */
131
+ export declare function writeAppDts(projectDir: string, manifest: {
132
+ workflows?: Record<string, AppWorkflowDeclaration>;
133
+ queries?: Record<string, AppQueryDeclaration>;
134
+ agents?: Record<string, AppAgentDeclaration>;
135
+ }): string[];
120
136
  /**
121
137
  * `lotics app codegen [path]` — regenerate every `.lotics/` artifact from the
122
138
  * manifest + workspace schema, WITHOUT a deploy. The `.d.ts` companions are
@@ -386,7 +386,7 @@ export function ensureAppTsconfig(projectDir) {
386
386
  * loads zero of them), so `ensureAppTsconfig` couples "wrote the types" with "the
387
387
  * program can see them" — no caller can do one without the other.
388
388
  */
389
- function writeAppDts(projectDir, manifest) {
389
+ export function writeAppDts(projectDir, manifest) {
390
390
  const dotLotics = path.join(projectDir, ".lotics");
391
391
  fs.mkdirSync(dotLotics, { recursive: true });
392
392
  const written = [
package/dist/args.d.ts CHANGED
@@ -50,9 +50,30 @@ export declare function parseArgs(argv: string[]): {
50
50
  * `--resolve <key>=recreate|revert|keep|<id>` (repeatable): upgrade
51
51
  * resolutions — drift entries (`<namespace.alias>`) take `recreate` or an
52
52
  * existing id; modified-core entries (`<kind>.<alias>`) take `revert` or
53
- * `keep`.
53
+ * `keep`. For a standalone KNOWLEDGE upgrade, `<alias>=apply|keep|archive|
54
+ * recreate|unbind`.
54
55
  */
55
56
  resolve: string[];
57
+ /**
58
+ * `--bind-to <alias>=<kdc_id>` (repeatable): consent to adopt an existing
59
+ * same-named doc as package-managed — resolves a knowledge name collision at
60
+ * install / a created-doc collision at upgrade.
61
+ */
62
+ bindTo: string[];
63
+ /** `--keep-content` (`package uninstall` of a content install): retain the package-bound docs instead of archiving them. */
64
+ keepContent: boolean;
65
+ /** `--apply-all` (knowledge `package upgrade`): accept the package's version for every consent entry. */
66
+ applyAll: boolean;
67
+ /** `--kind <app|content>` (`package new`): the package project kind to scaffold (default app). */
68
+ kind?: string;
69
+ /** `--config key=value` (repeatable): per-knob config overrides at `package install`. */
70
+ config: string[];
71
+ /** `--set key=value` (repeatable): per-knob config edits at `package config`. */
72
+ set: string[];
73
+ /** `--archive-tables` (`package uninstall` of an app install): also archive the scaffolded tables. */
74
+ archiveTables: boolean;
75
+ /** `--undo`: reverse a `package retire` / `package yank`. */
76
+ undo: boolean;
56
77
  version: boolean;
57
78
  help: boolean;
58
79
  };
package/dist/args.js CHANGED
@@ -31,6 +31,14 @@ export function parseArgs(argv) {
31
31
  cleanup: false,
32
32
  packageVersion: undefined,
33
33
  resolve: [],
34
+ bindTo: [],
35
+ keepContent: false,
36
+ applyAll: false,
37
+ kind: undefined,
38
+ config: [],
39
+ set: [],
40
+ archiveTables: false,
41
+ undo: false,
34
42
  version: false,
35
43
  help: false,
36
44
  };
@@ -106,6 +114,45 @@ export function parseArgs(argv) {
106
114
  flags.resolve.push(value);
107
115
  break;
108
116
  }
117
+ case "--bind-to": {
118
+ const value = argv[++i];
119
+ if (value === undefined || value.startsWith("-")) {
120
+ throw new Error("--bind-to requires a value: <alias>=<kdc_id>.");
121
+ }
122
+ flags.bindTo.push(value);
123
+ break;
124
+ }
125
+ case "--keep-content":
126
+ flags.keepContent = true;
127
+ break;
128
+ case "--apply-all":
129
+ flags.applyAll = true;
130
+ break;
131
+ case "--kind":
132
+ flags.kind = argv[++i];
133
+ break;
134
+ case "--config": {
135
+ const value = argv[++i];
136
+ if (value === undefined || value.startsWith("-")) {
137
+ throw new Error("--config requires a value: key=value (repeatable).");
138
+ }
139
+ flags.config.push(value);
140
+ break;
141
+ }
142
+ case "--set": {
143
+ const value = argv[++i];
144
+ if (value === undefined || value.startsWith("-")) {
145
+ throw new Error("--set requires a value: key=value (repeatable).");
146
+ }
147
+ flags.set.push(value);
148
+ break;
149
+ }
150
+ case "--archive-tables":
151
+ flags.archiveTables = true;
152
+ break;
153
+ case "--undo":
154
+ flags.undo = true;
155
+ break;
109
156
  case "--version":
110
157
  case "-v": {
111
158
  // Inside a `package` command, `--version` carries a value (the
package/dist/cli.js CHANGED
@@ -14,7 +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, packageYank, } from "./package_commands.js";
17
+ import { packageInstall, packageUninstall, packageListContent, packageConfig, packageRetire, packageShow, parseInstallConfigFlags, packageEject, packageDoctor, packageUpgrade, packageUpgradeKnowledge, parseBindToFlags, packageNew, packageBuild, packagePublish, packageDev, packageSync, packageReset, packageExtract, packageAdopt, packageFleetUpgrade, packageYank, } from "./package_commands.js";
18
18
  import { parseArgs } from "./args.js";
19
19
  import { ingestJsonArgs } from "./inputs.js";
20
20
  import { runXlsxCommand } from "./xlsx.js";
@@ -96,22 +96,41 @@ COMMANDS
96
96
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
97
97
  lotics app rename "<new name>" Rename the app's display name (launcher title)
98
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)
99
+ lotics package new <name> [path] [--kind app|content]
100
+ Scaffold a package project — an app (contract + source)
101
+ or a content corpus (docs-only, no app source)
100
102
  lotics package build [path] Build the publishable bundle (source + dist)
101
103
  lotics package publish [path] Publish a new immutable package version
102
104
  lotics package dev [path] Sync the package into a dev workspace + run the app
103
105
  dev server (--workspace <dev_ws> selects it)
104
106
  lotics package sync [path] Re-sync (additive migrate + materialize) into a dev ws
105
107
  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
108
+ lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id> ...] [--config key=value ...]
109
+ Install a package (app: scaffolds/deploys/materializes,
110
+ --config sets its knobs; content: installs the doc corpus,
111
+ --bind-to consents to adopt a same-named doc on a collision)
112
+ lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content]
113
+ Uninstall dispatched by id. App (app_id): archives
114
+ artifacts (+ --archive-tables also archives scaffolded
115
+ tables). Content (pci_): archives its package-bound docs
116
+ unless --keep-content
117
+ lotics package config <app_id> [--set key=value ...]
118
+ Show or edit an installation's config knobs
119
+ lotics package show <package_id> Registry metadata + version history
120
+ lotics package retire <package_id> [--undo]
121
+ Retire a package (refuse new installs, hide from other
122
+ orgs); existing installations keep working + upgrade
123
+ lotics package list-content List the workspace's content installations
124
+ (standalone pci_ + app-bundled) — source for a pci_ id
125
+ lotics package upgrade <app_id|pci_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all]
126
+ Preview-then-apply an upgrade. App: refuses while any
127
+ drift/modified/bundled-knowledge finding lacks a --resolve
128
+ (a knowledge-alias --resolve routes to the doc). Content
129
+ (pci_): --resolve <alias>=apply|keep|archive|recreate|unbind;
130
+ --apply-all accepts the package's version for every doc
113
131
  lotics package doctor [app_id] Installation health: version pin vs latest, binding
114
- drift, locally modified core (exit 1 on findings)
132
+ drift, locally modified core, knowledge drift/edits
133
+ (exit 1 on findings)
115
134
  lotics package rebind-role <app_id> <alias> <grp_id>
116
135
  Re-point a package role at a different group
117
136
  (re-materializes at the pinned version)
@@ -158,6 +177,12 @@ FLAGS
158
177
  --local Pin the current directory (lotics org use / auth api-key)
159
178
  --dev (lotics workspace create) Mark a throwaway app-package dev
160
179
  workspace — required for "lotics package reset"
180
+ --kind <k> (lotics package new) Package kind: app (default) or content
181
+ --bind-to <a>=<id> (lotics package install/upgrade, repeatable) Adopt an existing
182
+ same-named doc as package-managed (resolves a knowledge collision)
183
+ --keep-content (lotics package uninstall) Keep the docs instead of archiving them
184
+ --apply-all (lotics package upgrade, knowledge) Accept the package's version
185
+ for every doc (overwrites local edits)
161
186
  --all (lotics auth logout) Remove every saved credential
162
187
  --version Show version
163
188
 
@@ -576,10 +601,18 @@ async function main() {
576
601
  if (command === "package" && subcommand === "new") {
577
602
  const name = toolArgs;
578
603
  if (!name) {
579
- console.error("Usage: lotics package new <name> [path]");
604
+ console.error("Usage: lotics package new <name> [path] [--kind app|content]");
580
605
  process.exit(1);
581
606
  }
582
- await packageNew({ name, targetPath: restArgs[0] });
607
+ if (flags.kind !== undefined && flags.kind !== "app" && flags.kind !== "content") {
608
+ console.error(`Invalid --kind "${flags.kind}" — expected "app" or "content".`);
609
+ process.exit(1);
610
+ }
611
+ await packageNew({
612
+ name,
613
+ targetPath: restArgs[0],
614
+ kind: flags.kind === "content" ? "content" : "app",
615
+ });
583
616
  return;
584
617
  }
585
618
  if (command === "package" && subcommand === "build") {
@@ -694,15 +727,20 @@ async function main() {
694
727
  }
695
728
  if (command === "package" && !subcommand) {
696
729
  console.error("Usage:");
697
- console.error(" lotics package new <name> [path] Scaffold a package project (contract + app source)");
730
+ console.error(" lotics package new <name> [path] [--kind app|content] Scaffold a package project (app source, or docs-only)");
698
731
  console.error(" lotics package build [path] Build the publishable bundle (source + dist)");
699
732
  console.error(" lotics package publish [path] [-m <changelog>] Publish a new immutable package version");
700
733
  console.error(" lotics package dev [path] [--workspace <ws>] Sync into a dev workspace + run the app dev server");
701
734
  console.error(" lotics package sync [path] [--workspace <ws>] Re-sync (additive migrate + materialize) into a dev workspace");
702
735
  console.error(" lotics package reset [path] [--workspace <ws>] DEV-ONLY: drop scaffolded tables + re-scaffold clean");
703
- console.error(" lotics package install <package> [--version N] Install a package into this workspace");
704
- console.error(" lotics package upgrade <app_id> [--version N] [--resolve ns.alias=recreate|<id>] Preview + apply an upgrade");
705
- console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding drift");
736
+ console.error(" lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
737
+ console.error(" lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content] Uninstall dispatched by id (app vs content)");
738
+ console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
739
+ console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
740
+ console.error(" lotics package retire <package_id> [--undo] Retire a package (refuse new installs; installs keep working)");
741
+ console.error(" lotics package list-content List the workspace's content installations (standalone + app-bundled)");
742
+ console.error(" lotics package upgrade <app_id|pci_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all] Preview + apply an upgrade");
743
+ console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding/knowledge drift");
706
744
  console.error(" lotics package rebind-role <app_id> <alias> <grp_id> Re-point a package role at another group");
707
745
  console.error(" lotics package eject <app_id> Sever an installation's package link");
708
746
  console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
@@ -738,16 +776,6 @@ async function main() {
738
776
  const { client, ctx } = requireClient(flags);
739
777
  // lotics workspace / lotics workspace list / lotics workspace select <id>
740
778
  if (command === "workspace") {
741
- if (subcommand === "yank") {
742
- const [packageId, versionRaw, maybeUndo] = toolArgs ? toolArgs.split(/\s+/) : [];
743
- const version = Number(versionRaw);
744
- if (!packageId || !Number.isInteger(version) || version <= 0) {
745
- console.error("Usage: lotics package yank <package_id> <version> [--undo]");
746
- process.exit(1);
747
- }
748
- await packageYank(client, { package_id: packageId, version, undo: maybeUndo === "--undo" });
749
- return;
750
- }
751
779
  if (subcommand === "doctor") {
752
780
  // The workspace command family runs before the global workspace
753
781
  // resolution — doctor needs the same first-workspace fallback every
@@ -870,7 +898,7 @@ async function main() {
870
898
  if (subcommand === "install") {
871
899
  const packageId = toolArgs;
872
900
  if (!packageId) {
873
- console.error("Usage: lotics package install <package> [--version N]");
901
+ console.error("Usage: lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id> ...]");
874
902
  process.exit(1);
875
903
  }
876
904
  let version;
@@ -881,7 +909,60 @@ async function main() {
881
909
  process.exit(1);
882
910
  }
883
911
  }
884
- await packageInstall(client, { package_id: packageId, version });
912
+ const config = flags.config.length > 0 ? parseInstallConfigFlags(flags.config) : undefined;
913
+ await packageInstall(client, {
914
+ package_id: packageId,
915
+ version,
916
+ bind_to: parseBindToFlags(flags.bindTo),
917
+ ...(config !== undefined ? { config } : {}),
918
+ });
919
+ return;
920
+ }
921
+ if (subcommand === "uninstall") {
922
+ const id = toolArgs;
923
+ if (!id) {
924
+ console.error("Usage: lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content]");
925
+ console.error("Dispatched by id: an app installation (app_id; --archive-tables to also archive its " +
926
+ "scaffolded tables) or a standalone content installation (pci_ id from install / " +
927
+ "lotics package list-content; --keep-content to retain its docs).");
928
+ process.exit(1);
929
+ }
930
+ await packageUninstall(client, {
931
+ id,
932
+ keep_content: flags.keepContent,
933
+ archive_tables: flags.archiveTables,
934
+ });
935
+ return;
936
+ }
937
+ if (subcommand === "list-content") {
938
+ await packageListContent(client);
939
+ return;
940
+ }
941
+ if (subcommand === "config") {
942
+ const appId = toolArgs;
943
+ if (!appId) {
944
+ console.error("Usage: lotics package config <app_id> [--set key=value ...]");
945
+ process.exit(1);
946
+ }
947
+ await packageConfig(client, { app_id: appId, sets: flags.set });
948
+ return;
949
+ }
950
+ if (subcommand === "retire") {
951
+ const packageId = toolArgs;
952
+ if (!packageId) {
953
+ console.error("Usage: lotics package retire <package_id> [--undo]");
954
+ process.exit(1);
955
+ }
956
+ await packageRetire(client, { package_id: packageId, undo: flags.undo });
957
+ return;
958
+ }
959
+ if (subcommand === "show") {
960
+ const packageId = toolArgs;
961
+ if (!packageId) {
962
+ console.error("Usage: lotics package show <package_id>");
963
+ process.exit(1);
964
+ }
965
+ await packageShow(client, { package_id: packageId });
885
966
  return;
886
967
  }
887
968
  if (subcommand === "eject") {
@@ -921,6 +1002,16 @@ async function main() {
921
1002
  await packageAdopt(client, { app_id: appId, version, projectDir: restArgs[0] });
922
1003
  return;
923
1004
  }
1005
+ if (subcommand === "yank") {
1006
+ const packageId = toolArgs;
1007
+ const version = Number(restArgs[0]);
1008
+ if (!packageId || !Number.isInteger(version) || version <= 0) {
1009
+ console.error("Usage: lotics package yank <package_id> <version> [--undo]");
1010
+ process.exit(1);
1011
+ }
1012
+ await packageYank(client, { package_id: packageId, version, undo: flags.undo });
1013
+ return;
1014
+ }
924
1015
  if (subcommand === "fleet-upgrade") {
925
1016
  const packageId = toolArgs;
926
1017
  if (!packageId) {
@@ -949,7 +1040,7 @@ async function main() {
949
1040
  console.error("Usage: lotics package rebind-role <app_id> <role_alias> <grp_id>");
950
1041
  process.exit(1);
951
1042
  }
952
- const result = await client.rebindAppPackageRole(appId, {
1043
+ const result = await client.rebindPackageRole(appId, {
953
1044
  role_alias: roleAlias,
954
1045
  group_id: groupId,
955
1046
  });
@@ -959,9 +1050,9 @@ async function main() {
959
1050
  return;
960
1051
  }
961
1052
  if (subcommand === "upgrade") {
962
- const appId = toolArgs;
963
- if (!appId) {
964
- console.error("Usage: lotics package upgrade <app_id> [--version N] [--resolve <namespace.alias>=recreate|<id> ...]");
1053
+ const target = toolArgs;
1054
+ if (!target) {
1055
+ console.error("Usage: lotics package upgrade <app_id | pci_installation_id> [--version N] [--resolve <key>=... ...]");
965
1056
  process.exit(1);
966
1057
  }
967
1058
  let version;
@@ -972,10 +1063,24 @@ async function main() {
972
1063
  process.exit(1);
973
1064
  }
974
1065
  }
1066
+ // A `pci_` id is a standalone content installation (its own upgrade
1067
+ // pipeline); everything else is an app installation.
1068
+ if (target.startsWith("pci_")) {
1069
+ await packageUpgradeKnowledge(client, {
1070
+ installation_id: target,
1071
+ version,
1072
+ resolve: flags.resolve,
1073
+ bind_to: parseBindToFlags(flags.bindTo),
1074
+ applyAll: flags.applyAll,
1075
+ });
1076
+ return;
1077
+ }
975
1078
  await packageUpgrade(client, {
976
- app_id: appId,
1079
+ app_id: target,
977
1080
  version,
978
- resolutions: parseResolveFlags(flags.resolve),
1081
+ resolve: flags.resolve,
1082
+ bindTo: flags.bindTo,
1083
+ applyAll: flags.applyAll,
979
1084
  });
980
1085
  return;
981
1086
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Dispatch smoke test over the BUILT CLI artifact — every `package` subcommand
3
+ * must route to its own handler, proven by its usage/refusal line appearing on
4
+ * a bare invocation. The dispatch in cli.ts is one hand-rolled argv walk with
5
+ * subcommand names repeated across command families (`doctor` exists in both
6
+ * `workspace` and `package`) and split positional conventions (`toolArgs` +
7
+ * `restArgs`), and nothing else tests it: `package yank` shipped dispatched
8
+ * inside the WORKSPACE family and surfaced only on the first live invocation.
9
+ *
10
+ * The fake API key satisfies `requireClient` (client construction is offline);
11
+ * every asserted path exits before any network call. Spawns run in an empty
12
+ * temp cwd so path-defaulting subcommands fail on the missing project file —
13
+ * which equally proves their dispatch reached the right handler.
14
+ */
15
+ import { beforeAll, describe, expect, it } from "vitest";
16
+ import { execFileSync, spawnSync } from "node:child_process";
17
+ import fs from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
22
+ const cliBin = path.join(packageRoot, "dist", "src", "cli.js");
23
+ const emptyCwd = fs.mkdtempSync(path.join(os.tmpdir(), "lotics-cli-dispatch-"));
24
+ function runCli(args) {
25
+ const result = spawnSync(process.execPath, [cliBin, ...args], {
26
+ cwd: emptyCwd,
27
+ env: {
28
+ ...process.env,
29
+ LOTICS_API_KEY: "ltk_dispatch_smoke_fake_key_never_used_on_wire",
30
+ // A pinned (fake) workspace skips the pre-dispatch auto-resolution,
31
+ // which would otherwise hit the network (`listWorkspaces`) before any
32
+ // usage gate. Nothing below ever reaches the wire.
33
+ LOTICS_WORKSPACE: "wsp_dispatch_smoke_fake",
34
+ LOTICS_ORG: "",
35
+ },
36
+ encoding: "utf-8",
37
+ timeout: 30_000,
38
+ });
39
+ return { status: result.status, stderr: result.stderr ?? "" };
40
+ }
41
+ beforeAll(() => {
42
+ // Bundle the real artifact from src (esbuild only — no tsgo; vitest's
43
+ // transform already type-agnostic). ~1s.
44
+ execFileSync(process.execPath, ["scripts/build_cli.mjs"], { cwd: packageRoot });
45
+ }, 120_000);
46
+ describe("lotics package <subcommand> dispatch", () => {
47
+ // Positional-requiring subcommands: a bare invocation must print the
48
+ // subcommand's OWN usage line — reaching it proves family routing AND that
49
+ // the usage gate fires before any network/filesystem work.
50
+ const usageCases = [
51
+ ["install", /Usage: lotics package install/],
52
+ ["uninstall", /Usage: lotics package uninstall/],
53
+ ["config", /Usage: lotics package config/],
54
+ ["retire", /Usage: lotics package retire <package_id>/],
55
+ ["show", /Usage: lotics package show <package_id>/],
56
+ ["upgrade", /Usage: lotics package upgrade/],
57
+ ["yank", /Usage: lotics package yank <package_id> <version>/],
58
+ ["rebind-role", /Usage: lotics package rebind-role/],
59
+ ["fleet-upgrade", /Usage: lotics package fleet-upgrade/],
60
+ ["extract", /Usage: lotics package extract/],
61
+ ["adopt", /Usage: lotics package adopt/],
62
+ ["new", /Usage: lotics package new/],
63
+ ];
64
+ for (const [sub, usage] of usageCases) {
65
+ it(`routes "package ${sub}" to its handler (usage on missing args)`, () => {
66
+ const { status, stderr } = runCli(["package", sub]);
67
+ expect(stderr).toMatch(usage);
68
+ expect(stderr).not.toMatch(/Unknown package subcommand/);
69
+ expect(status).toBe(1);
70
+ });
71
+ }
72
+ // Path-defaulting subcommands: in an empty cwd they must fail on the missing
73
+ // package project — their handler's message, not the dispatcher's. (`sync`
74
+ // is excluded: it validates the dev workspace over the network BEFORE
75
+ // reading the project, so it can't be smoke-tested offline.)
76
+ const projectCases = ["build", "publish"];
77
+ for (const sub of projectCases) {
78
+ it(`routes "package ${sub}" to its handler (missing project in empty cwd)`, () => {
79
+ const { status, stderr } = runCli(["package", sub]);
80
+ expect(stderr).toMatch(/No package\.json in|not a package project/);
81
+ expect(stderr).not.toMatch(/Unknown package subcommand/);
82
+ expect(status).toBe(1);
83
+ });
84
+ }
85
+ it("still rejects a genuinely unknown subcommand", () => {
86
+ const { status, stderr } = runCli(["package", "frobnicate"]);
87
+ expect(stderr).toMatch(/Unknown package subcommand/);
88
+ expect(status).toBe(1);
89
+ });
90
+ });