@lotics/cli 0.86.1 → 0.86.2

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/cli.js CHANGED
@@ -127,7 +127,7 @@ COMMANDS
127
127
  <alias>=apply|keep|archive|recreate|unbind); an APP package
128
128
  FLEET-upgrades every installation across your org (clean
129
129
  apply, findings skip). --apply-all accepts the package's version
130
- lotics package doctor [app_id] Installation health: version pin vs latest, binding
130
+ lotics package doctor [app_id|package_id] Installation health: version pin vs latest, binding
131
131
  drift, locally modified core, knowledge drift/edits
132
132
  (exit 1 on findings)
133
133
  lotics package config <app_id> [--set key=value ...]
@@ -717,7 +717,7 @@ async function main() {
717
717
  console.error(" lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
718
718
  console.error(" lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content] Uninstall — dispatched by id (app vs content)");
719
719
  console.error(" lotics upgrade <app_id|package_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all] Upgrade — app install / content install / whole app fleet (apg_)");
720
- console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding/knowledge drift");
720
+ console.error(" lotics package doctor [app_id|package_id] Health: version pin vs latest + binding/knowledge drift (apg_ = this workspace's content install)");
721
721
  console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
722
722
  console.error(" lotics package eject <app_id> Sever an installation's package link");
723
723
  console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { KnowledgeUpgradeEntry, ModifiedArtifact, PackageContentBinding, UpgradeResolutions } from "@lotics/shared/schemas/packages";
1
+ import type { ContractConfigEntry, KnowledgeUpgradeEntry, ModifiedArtifact, PackageContentBinding, UpgradeResolutions } from "@lotics/shared/schemas/packages";
2
2
  /**
3
3
  * The error message for a non-ok response. A genuine JSON error (a 4xx carrying
4
4
  * a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
@@ -385,6 +385,7 @@ export declare class LoticsClient {
385
385
  breaking: number;
386
386
  drift: number;
387
387
  modified: number;
388
+ knowledge: number;
388
389
  };
389
390
  message?: string;
390
391
  }>;
@@ -555,6 +556,8 @@ export declare class LoticsClient {
555
556
  alias: string;
556
557
  doc_id: string;
557
558
  }>;
559
+ /** Config knob declarations re-declaring the config (from the manifest). */
560
+ config?: ContractConfigEntry[];
558
561
  }): Promise<{
559
562
  package_id: string;
560
563
  version: number;
@@ -566,6 +569,12 @@ export declare class LoticsClient {
566
569
  removed: string[];
567
570
  changed: string[];
568
571
  };
572
+ /** Absent from a pre-declaration server (deploy skew) — treat as empty delta. */
573
+ config?: {
574
+ added: string[];
575
+ removed: string[];
576
+ changed: string[];
577
+ };
569
578
  findings: ExtractFinding[];
570
579
  }>;
571
580
  /**
@@ -585,6 +594,8 @@ export declare class LoticsClient {
585
594
  alias: string;
586
595
  doc_id: string;
587
596
  }>;
597
+ /** Config knob declarations re-declaring the config (from the manifest). */
598
+ config?: ContractConfigEntry[];
588
599
  }): Promise<{
589
600
  package_id: string;
590
601
  version: number;
@@ -596,6 +607,12 @@ export declare class LoticsClient {
596
607
  removed: string[];
597
608
  changed: string[];
598
609
  };
610
+ /** Absent from a pre-declaration server (deploy skew) — treat as empty delta. */
611
+ config?: {
612
+ added: string[];
613
+ removed: string[];
614
+ changed: string[];
615
+ };
599
616
  }>;
600
617
  /**
601
618
  * Dry-run preview of a first-release — the `GET` behind `lotics app publish`
@@ -615,6 +632,8 @@ export declare class LoticsClient {
615
632
  alias: string;
616
633
  doc_id: string;
617
634
  }>;
635
+ /** Config knob declarations v1 would freeze (from the manifest's `lotics.config`). */
636
+ config?: ContractConfigEntry[];
618
637
  }): Promise<{
619
638
  app_id: string;
620
639
  package_name: string;
@@ -649,6 +668,8 @@ export declare class LoticsClient {
649
668
  alias: string;
650
669
  doc_id: string;
651
670
  }>;
671
+ /** Config knob declarations v1 freezes (from the manifest's `lotics.config`). */
672
+ config?: ContractConfigEntry[];
652
673
  }): Promise<{
653
674
  package_id: string;
654
675
  version: number;
package/dist/client.js CHANGED
@@ -365,6 +365,9 @@ export class LoticsClient {
365
365
  if (opts.knowledge !== undefined && opts.knowledge.length > 0) {
366
366
  params.set("knowledge", JSON.stringify(opts.knowledge));
367
367
  }
368
+ if (opts.config !== undefined) {
369
+ params.set("config", JSON.stringify(opts.config));
370
+ }
368
371
  const query = params.toString();
369
372
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-release${query ? `?${query}` : ""}`);
370
373
  }
@@ -394,6 +397,9 @@ export class LoticsClient {
394
397
  if (opts.knowledge !== undefined && opts.knowledge.length > 0) {
395
398
  params.set("knowledge", JSON.stringify(opts.knowledge));
396
399
  }
400
+ if (opts.config !== undefined && opts.config.length > 0) {
401
+ params.set("config", JSON.stringify(opts.config));
402
+ }
397
403
  if (opts.renames !== undefined && opts.renames.length > 0) {
398
404
  params.set("renames", JSON.stringify(opts.renames));
399
405
  }
@@ -1,13 +1,17 @@
1
1
  import { LoticsClient, type ExtractFinding } from "./client.js";
2
- import { type UpgradeResolutions } from "@lotics/shared/schemas/packages";
2
+ import { type ContractConfigEntry, type UpgradeResolutions } from "@lotics/shared/schemas/packages";
3
3
  /**
4
4
  * Read the local APP project's manifest (`package.json#lotics.app_id` +
5
- * `lotics.knowledge`) — what `lotics app pull` writes. `lotics app publish` /
6
- * `release` run from a pulled app project resolve the app id from it, and
7
- * `publish` forwards the package-managed knowledge declaration to the server
8
- * (agents reference docs by free text, so the author declares which the package
9
- * owns). Returns null when the dir has no package.json. `app_id` is null when the
10
- * manifest is a package/non-app project.
5
+ * `lotics.knowledge` + `lotics.config`) — what `lotics app pull` writes.
6
+ * `lotics app publish` / `release` run from a pulled app project resolve the app
7
+ * id from it and forward the package-managed knowledge declaration AND the config
8
+ * knob declarations to the server both are declarations extract cannot invert
9
+ * from artifacts (agents reference docs by free text; a config knob has no concrete
10
+ * artifact), so the author declares them. Each `lotics.config` entry is validated
11
+ * against the shared `contractConfigEntrySchema` so a malformed knob fails LOUDLY
12
+ * client-side naming the alias, never silently dropped. Returns null when the dir
13
+ * has no package.json. `app_id` is null when the manifest is a package/non-app
14
+ * project.
11
15
  */
12
16
  export declare function readLocalAppManifest(projectDir: string): {
13
17
  app_id: string | null;
@@ -15,6 +19,7 @@ export declare function readLocalAppManifest(projectDir: string): {
15
19
  alias: string;
16
20
  doc_id: string;
17
21
  }>;
22
+ config: ContractConfigEntry[];
18
23
  } | null;
19
24
  /** Parse repeated `--rename old=new` flags into `{ from, to }[]` (first-publish alias fixes). */
20
25
  export declare function parseRenameFlags(renames: string[]): Array<{
@@ -43,7 +48,9 @@ export declare function formatExtractReport(report: ExtractFinding[]): {
43
48
  export declare function parseResolveFlags(resolve: string[]): UpgradeResolutions;
44
49
  /**
45
50
  * Health check: version pin vs. registry latest + binding drift. Exits
46
- * non-zero when drift is found so scripts can gate on it.
51
+ * non-zero when drift is found so scripts can gate on it. An `apg_` PACKAGE id
52
+ * resolves THIS workspace's standalone content installation — the same
53
+ * package-id addressing `upgrade`/`uninstall` speak.
47
54
  */
48
55
  export declare function packageDoctor(client: LoticsClient, args: {
49
56
  app_id?: string;
@@ -21,7 +21,7 @@
21
21
  import fs from "node:fs";
22
22
  import path from "node:path";
23
23
  import "./client.js";
24
- import { knowledgeEntryNeedsConsent, validKnowledgeResolutions, } from "@lotics/shared/schemas/packages";
24
+ import { contractConfigEntrySchema, knowledgeEntryNeedsConsent, validKnowledgeResolutions, } from "@lotics/shared/schemas/packages";
25
25
  function packageJsonPath(projectDir) {
26
26
  return path.join(projectDir, "package.json");
27
27
  }
@@ -30,12 +30,16 @@ function isPlainObject(value) {
30
30
  }
31
31
  /**
32
32
  * Read the local APP project's manifest (`package.json#lotics.app_id` +
33
- * `lotics.knowledge`) — what `lotics app pull` writes. `lotics app publish` /
34
- * `release` run from a pulled app project resolve the app id from it, and
35
- * `publish` forwards the package-managed knowledge declaration to the server
36
- * (agents reference docs by free text, so the author declares which the package
37
- * owns). Returns null when the dir has no package.json. `app_id` is null when the
38
- * manifest is a package/non-app project.
33
+ * `lotics.knowledge` + `lotics.config`) — what `lotics app pull` writes.
34
+ * `lotics app publish` / `release` run from a pulled app project resolve the app
35
+ * id from it and forward the package-managed knowledge declaration AND the config
36
+ * knob declarations to the server both are declarations extract cannot invert
37
+ * from artifacts (agents reference docs by free text; a config knob has no concrete
38
+ * artifact), so the author declares them. Each `lotics.config` entry is validated
39
+ * against the shared `contractConfigEntrySchema` so a malformed knob fails LOUDLY
40
+ * client-side naming the alias, never silently dropped. Returns null when the dir
41
+ * has no package.json. `app_id` is null when the manifest is a package/non-app
42
+ * project.
39
43
  */
40
44
  export function readLocalAppManifest(projectDir) {
41
45
  const pkgPath = packageJsonPath(projectDir);
@@ -54,7 +58,19 @@ export function readLocalAppManifest(projectDir) {
54
58
  }
55
59
  }
56
60
  }
57
- return { app_id, knowledge };
61
+ const config = [];
62
+ if (Array.isArray(lotics.config)) {
63
+ for (const entry of lotics.config) {
64
+ const result = contractConfigEntrySchema.safeParse(entry);
65
+ if (!result.success) {
66
+ const alias = isPlainObject(entry) && typeof entry.alias === "string" ? entry.alias : "?";
67
+ throw new Error(`Invalid lotics.config knob "${alias}" in package.json: ` +
68
+ result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "));
69
+ }
70
+ config.push(result.data);
71
+ }
72
+ }
73
+ return { app_id, knowledge, config };
58
74
  }
59
75
  /** Parse repeated `--rename old=new` flags into `{ from, to }[]` (first-publish alias fixes). */
60
76
  export function parseRenameFlags(renames) {
@@ -115,9 +131,15 @@ export function parseResolveFlags(resolve) {
115
131
  }
116
132
  /**
117
133
  * Health check: version pin vs. registry latest + binding drift. Exits
118
- * non-zero when drift is found so scripts can gate on it.
134
+ * non-zero when drift is found so scripts can gate on it. An `apg_` PACKAGE id
135
+ * resolves THIS workspace's standalone content installation — the same
136
+ * package-id addressing `upgrade`/`uninstall` speak.
119
137
  */
120
138
  export async function packageDoctor(client, args) {
139
+ if (args.app_id !== undefined && args.app_id.startsWith("apg_")) {
140
+ await contentDoctor(client, args.app_id);
141
+ return;
142
+ }
121
143
  const app_id = resolveInstallationAppId(args.app_id);
122
144
  const health = await client.getPackageHealth(app_id);
123
145
  console.error(`${health.package_name} — installation ${health.app_id}` +
@@ -376,6 +398,47 @@ function formatTemplateResolveHint(entry) {
376
398
  : " (a local edit — revert overwrites it with the package's version; keep retains it)";
377
399
  return ` --resolve template.${entry.alias}=revert|keep${note}`;
378
400
  }
401
+ /**
402
+ * Health report for a STANDALONE content installation, addressed by PACKAGE id.
403
+ * Composed client-side from the surfaces the consumer verbs already speak: the
404
+ * list row (pin vs registry latest) and the upgrade preview against latest
405
+ * (which carries per-alias drift + local-edit detection even when the pin is
406
+ * current — the repair view). Exits non-zero on drift, mirroring the app
407
+ * doctor's gate semantics.
408
+ */
409
+ async function contentDoctor(client, package_id) {
410
+ const installation = await resolveWorkspaceContentInstallation(client, package_id);
411
+ const preview = await client.previewContentInstallationUpgrade(installation.id, {});
412
+ const name = installation.package_registry?.name ?? package_id;
413
+ console.error(`${name} — content installation of ${package_id} (this workspace)`);
414
+ console.error(` Installed: v${preview.from_version} Latest: v${preview.to_version}` +
415
+ (preview.to_version > preview.from_version ? " → update available" : ""));
416
+ const drifted = preview.entries.filter((e) => e.change === "drifted");
417
+ if (drifted.length === 0) {
418
+ console.error(" Binding: healthy — every bound doc resolves.");
419
+ }
420
+ else {
421
+ console.error(` Binding drift (${drifted.length}):`);
422
+ for (const entry of drifted) {
423
+ console.error(` - knowledge.${entry.alias} "${entry.name}" (bound doc is gone)`);
424
+ }
425
+ console.error(` Resolve while upgrading:\n lotics upgrade ${package_id}` +
426
+ ` --resolve knowledge.<alias>=recreate|unbind (or --bind-to <alias>=<kdc_id> to re-point)`);
427
+ process.exitCode = 1;
428
+ }
429
+ const editedDocs = preview.entries.filter((e) => e.modified);
430
+ const editedTemplates = preview.templates;
431
+ if (editedDocs.length === 0 && editedTemplates.length === 0) {
432
+ console.error(" Content: pristine — no local edits an upgrade would ask about.");
433
+ }
434
+ else {
435
+ console.error(` Local edits (${editedDocs.length + editedTemplates.length}) — each takes a keep/overwrite consent at upgrade:`);
436
+ for (const entry of editedDocs)
437
+ console.error(` - knowledge.${entry.alias} "${entry.name}"`);
438
+ for (const entry of editedTemplates)
439
+ console.error(` - template.${entry.alias}`);
440
+ }
441
+ }
379
442
  /**
380
443
  * Resolve a STANDALONE content package's installation in the CURRENT workspace
381
444
  * from its PACKAGE id. `UNIQUE (workspace_id, package_id)` means the package id
@@ -816,16 +879,17 @@ export async function appPublish(client, args) {
816
879
  throw new Error("No app id. Run `lotics app publish` from a pulled app project (lotics app pull <app_id>), " +
817
880
  "or pass one: lotics app publish <app_id>.");
818
881
  }
819
- // Forward the app's package-managed knowledge declaration ONLY when the local
820
- // manifest is this app's own project (agents reference docs by free text, so the
821
- // author declares which the package owns). A bare id published from elsewhere
822
- // ships without a bundled corpus — publish from the app dir to include it.
882
+ // Forward the app's package-managed knowledge + config declarations ONLY when the
883
+ // local manifest is this app's own project (both are declarations extract can't
884
+ // invert, so the author declares them). A bare id published from elsewhere ships
885
+ // without them — publish from the app dir to include them.
823
886
  const knowledge = local && local.app_id === appId ? local.knowledge : [];
887
+ const config = local && local.app_id === appId ? local.config : [];
824
888
  const renames = parseRenameFlags(args.renames);
825
889
  // Preview first (GET, no writes) — the same preview→--yes flow as `app release`,
826
890
  // so the aliases v1 freezes forever are never a blind publish and `--rename`
827
891
  // targets are inspectable before committing.
828
- const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
892
+ const preview = await client.previewPublishAppPackage(appId, { renames, knowledge, config });
829
893
  const { lines, hasError } = formatExtractReport(preview.findings);
830
894
  console.error(`Publish preview — ${appId} as new package "${preview.package_name}" (v1):`);
831
895
  const groups = [
@@ -847,6 +911,9 @@ export async function appPublish(client, args) {
847
911
  else {
848
912
  console.error(" No renamable aliases.");
849
913
  }
914
+ if (config.length > 0) {
915
+ console.error(` Config knobs (${config.length}): ${config.map((c) => `${c.alias} (${c.type})`).join(", ")}`);
916
+ }
850
917
  if (lines.length > 0) {
851
918
  console.error(` Findings (${preview.findings.length}):`);
852
919
  for (const line of lines)
@@ -870,6 +937,7 @@ export async function appPublish(client, args) {
870
937
  renames,
871
938
  changelog: args.changelog ?? null,
872
939
  knowledge,
940
+ config,
873
941
  });
874
942
  console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
875
943
  console.error(` The app is now installation #1 — develop it in place, then release the next version:`);
@@ -901,11 +969,12 @@ export async function appRelease(client, args) {
901
969
  if (appId === null) {
902
970
  throw new Error("No app id. Run this from a pulled app project (lotics app pull <app_id>), or pass an app id explicitly.");
903
971
  }
904
- // Forward the manifest's knowledge DECLARATION only from this app's own project
905
- // AND only when it lists docs — an empty/absent declaration is "no change"
906
- // (reconstruct from the pin), never "drop the bundle".
972
+ // Forward the manifest's knowledge + config DECLARATIONS only from this app's own
973
+ // project AND only when non-empty — an empty/absent declaration is "no change"
974
+ // (reconstruct knowledge from the pin / carry config forward), never "drop them".
907
975
  const knowledge = local && local.app_id === appId && local.knowledge.length > 0 ? local.knowledge : undefined;
908
- const preview = await client.previewPackageRelease(appId, { knowledge });
976
+ const config = local && local.app_id === appId && local.config.length > 0 ? local.config : undefined;
977
+ const preview = await client.previewPackageRelease(appId, { knowledge, config });
909
978
  const { lines, hasError } = formatExtractReport(preview.findings);
910
979
  console.error(`Release preview — ${appId} → ${preview.package_id} v${preview.version}:`);
911
980
  if (preview.added_aliases.length > 0) {
@@ -914,7 +983,15 @@ export async function appRelease(client, args) {
914
983
  if (preview.changed_artifacts.length > 0) {
915
984
  console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
916
985
  }
917
- // Deploy-skew boundary: a pre-knowledge-declaration server omits the field.
986
+ // Deploy-skew boundary: a pre-declaration server omits the echo field. Silent
987
+ // skew is dangerous exactly when a declaration was SENT — the server ignored it
988
+ // and the preview would misread as "no changes"; say so loudly, per surface.
989
+ if (preview.knowledge === undefined && knowledge !== undefined) {
990
+ console.error(" WARNING: the server ignored the knowledge declaration (it predates bundled-knowledge releases) — the bundle will NOT change. Retry after the platform deploy completes.");
991
+ }
992
+ if (preview.config === undefined && config !== undefined) {
993
+ console.error(" WARNING: the server ignored the config declaration (it predates manifest config declarations) — the knobs will NOT change. Retry after the platform deploy completes.");
994
+ }
918
995
  const k = preview.knowledge ?? { added: [], removed: [], changed: [] };
919
996
  if (k.added.length > 0 || k.removed.length > 0 || k.changed.length > 0) {
920
997
  const parts = [
@@ -924,11 +1001,23 @@ export async function appRelease(client, args) {
924
1001
  ].filter((p) => p !== null);
925
1002
  console.error(` Knowledge: ${parts.join("; ")}`);
926
1003
  }
1004
+ const cfg = preview.config ?? { added: [], removed: [], changed: [] };
1005
+ if (cfg.added.length > 0 || cfg.removed.length > 0 || cfg.changed.length > 0) {
1006
+ const parts = [
1007
+ cfg.added.length > 0 ? `+${cfg.added.join(", ")}` : null,
1008
+ cfg.removed.length > 0 ? `dropped ${cfg.removed.join(", ")}` : null,
1009
+ cfg.changed.length > 0 ? `changed ${cfg.changed.join(", ")}` : null,
1010
+ ].filter((p) => p !== null);
1011
+ console.error(` Config: ${parts.join("; ")}`);
1012
+ }
927
1013
  if (preview.added_aliases.length === 0 &&
928
1014
  preview.changed_artifacts.length === 0 &&
929
1015
  k.added.length === 0 &&
930
1016
  k.removed.length === 0 &&
931
- k.changed.length === 0) {
1017
+ k.changed.length === 0 &&
1018
+ cfg.added.length === 0 &&
1019
+ cfg.removed.length === 0 &&
1020
+ cfg.changed.length === 0) {
932
1021
  console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
933
1022
  }
934
1023
  if (lines.length > 0) {
@@ -947,7 +1036,7 @@ export async function appRelease(client, args) {
947
1036
  process.exitCode = 1;
948
1037
  return;
949
1038
  }
950
- const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge });
1039
+ const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge, config });
951
1040
  console.error(`Released ${result.package_id} v${result.version}.`);
952
1041
  console.error(` The origin was re-pinned to v${result.version} — verify: lotics package doctor ${appId}`);
953
1042
  }
@@ -990,7 +1079,7 @@ async function packageFleetUpgrade(client, args) {
990
1079
  const from = inst.from_version === null ? "?" : `v${inst.from_version}`;
991
1080
  const line = ` [${inst.outcome}] ${inst.workspace_name} — ${inst.app_name} (${from} → v${result.target_version})`;
992
1081
  if (inst.outcome === "skipped" && inst.blockers) {
993
- console.error(`${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified}`);
1082
+ console.error(`${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified} knowledge=${inst.blockers.knowledge}`);
994
1083
  console.error(` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`);
995
1084
  }
996
1085
  else if (inst.message) {
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { tmpdir } from "node:os";
5
- import { parseResolveFlags, parseBindToFlags, parseRenameFlags, readLocalAppManifest, formatExtractReport, appRelease, packageUpgradeByPackageId, } from "./package_commands.js";
5
+ import { parseResolveFlags, parseBindToFlags, parseRenameFlags, readLocalAppManifest, formatExtractReport, appRelease, packageUpgradeByPackageId, packageDoctor, } from "./package_commands.js";
6
6
  import { parseArgs } from "./args.js";
7
7
  describe("parseRenameFlags", () => {
8
8
  it("parses old=new pairs", () => {
@@ -20,21 +20,42 @@ describe("parseRenameFlags", () => {
20
20
  });
21
21
  });
22
22
  describe("readLocalAppManifest", () => {
23
- it("reads app_id + knowledge from a pulled app project", () => {
23
+ function withManifest(lotics, fn) {
24
24
  const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-app-test-"));
25
25
  try {
26
- fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({
27
- name: "crm",
28
- lotics: { app_id: "app_x", knowledge: [{ alias: "sop", doc_id: "kdc_1" }] },
29
- }));
30
- expect(readLocalAppManifest(dir)).toEqual({
31
- app_id: "app_x",
32
- knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
33
- });
26
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "crm", lotics }));
27
+ return fn(dir);
34
28
  }
35
29
  finally {
36
30
  fs.rmSync(dir, { recursive: true, force: true });
37
31
  }
32
+ }
33
+ it("reads app_id + knowledge + config from a pulled app project", () => {
34
+ withManifest({
35
+ app_id: "app_x",
36
+ knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
37
+ config: [{ alias: "page_size", label: "Page size", type: "number", default: 25 }],
38
+ }, (dir) => {
39
+ expect(readLocalAppManifest(dir)).toEqual({
40
+ app_id: "app_x",
41
+ knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
42
+ config: [{ alias: "page_size", label: "Page size", type: "number", default: 25 }],
43
+ });
44
+ });
45
+ });
46
+ it("defaults knowledge + config to empty arrays when the manifest omits them", () => {
47
+ withManifest({ app_id: "app_x" }, (dir) => {
48
+ expect(readLocalAppManifest(dir)).toEqual({ app_id: "app_x", knowledge: [], config: [] });
49
+ });
50
+ });
51
+ it("validates each config knob against the shared schema, throwing NAMING the bad alias", () => {
52
+ withManifest({
53
+ app_id: "app_x",
54
+ // A select knob with no `options` — the shared schema rejects it.
55
+ config: [{ alias: "theme", label: "Theme", type: "select", default: "light" }],
56
+ }, (dir) => {
57
+ expect(() => readLocalAppManifest(dir)).toThrow(/lotics\.config knob "theme"/);
58
+ });
38
59
  });
39
60
  it("returns null when the dir has no package.json", () => {
40
61
  const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-empty-"));
@@ -146,27 +167,30 @@ describe("parseArgs — package list-content", () => {
146
167
  expect(flags.workspace).toBe("wsp_dev");
147
168
  });
148
169
  });
149
- describe("appRelease — knowledge declaration forwarding", () => {
170
+ describe("appRelease — knowledge + config declaration forwarding", () => {
171
+ const pageSizeKnob = { alias: "page_size", label: "Page size", type: "number", default: 25 };
150
172
  const previewResult = {
151
173
  package_id: "apg_1",
152
174
  version: 2,
153
175
  added_aliases: [],
154
176
  changed_artifacts: [],
155
177
  knowledge: { added: ["guide"], removed: [], changed: [] },
178
+ config: { added: ["page_size"], removed: [], changed: [] },
156
179
  findings: [],
157
180
  };
158
- /** A mock client capturing the knowledge forwarded to preview + release. */
159
- function mockClient() {
181
+ /** A mock client capturing the knowledge + config forwarded to preview + release. */
182
+ function mockClient(overridePreview) {
160
183
  const preview = [];
161
184
  const release = [];
185
+ const result = { ...previewResult, ...overridePreview };
162
186
  const client = {
163
187
  previewPackageRelease: async (app_id, opts = {}) => {
164
188
  preview.push({ app_id, opts });
165
- return previewResult;
189
+ return result;
166
190
  },
167
191
  releasePackage: async (app_id, body) => {
168
192
  release.push({ app_id, body });
169
- return { ...previewResult, findings: undefined };
193
+ return { ...result, findings: undefined };
170
194
  },
171
195
  };
172
196
  return { client, preview, release };
@@ -176,33 +200,53 @@ describe("appRelease — knowledge declaration forwarding", () => {
176
200
  fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "app", lotics }));
177
201
  return fn(dir).finally(() => fs.rmSync(dir, { recursive: true, force: true }));
178
202
  }
179
- it("forwards the manifest's knowledge declaration from the app's own project", async () => {
203
+ it("forwards the manifest's knowledge + config declarations from the app's own project", async () => {
180
204
  vi.spyOn(console, "error").mockImplementation(() => undefined);
181
205
  const { client, preview, release } = mockClient();
182
- await withManifestDir({ app_id: "app_x", knowledge: [{ alias: "guide", doc_id: "kdc_1" }] }, async (dir) => {
206
+ await withManifestDir({ app_id: "app_x", knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] }, async (dir) => {
183
207
  await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
184
208
  });
185
- expect(preview[0]).toEqual({ app_id: "app_x", opts: { knowledge: [{ alias: "guide", doc_id: "kdc_1" }] } });
186
- expect(release[0].body).toEqual({ changelog: "ship", knowledge: [{ alias: "guide", doc_id: "kdc_1" }] });
209
+ expect(preview[0]).toEqual({
210
+ app_id: "app_x",
211
+ opts: { knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] },
212
+ });
213
+ expect(release[0].body).toEqual({
214
+ changelog: "ship",
215
+ knowledge: [{ alias: "guide", doc_id: "kdc_1" }],
216
+ config: [pageSizeKnob],
217
+ });
187
218
  vi.restoreAllMocks();
188
219
  });
189
- it("omits knowledge (reconstruct from the pin) when the manifest declares none", async () => {
220
+ it("omits knowledge + config (reconstruct/carry-forward) when the manifest declares neither", async () => {
190
221
  vi.spyOn(console, "error").mockImplementation(() => undefined);
191
222
  const { client, preview, release } = mockClient();
192
223
  await withManifestDir({ app_id: "app_x" }, async (dir) => {
193
224
  await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
194
225
  });
195
- expect(preview[0].opts).toEqual({ knowledge: undefined });
196
- expect(release[0].body).toEqual({ changelog: "ship", knowledge: undefined });
226
+ expect(preview[0].opts).toEqual({ knowledge: undefined, config: undefined });
227
+ expect(release[0].body).toEqual({ changelog: "ship", knowledge: undefined, config: undefined });
197
228
  vi.restoreAllMocks();
198
229
  });
199
- it("omits knowledge for a bare app id whose manifest is a DIFFERENT app", async () => {
230
+ it("omits knowledge + config for a bare app id whose manifest is a DIFFERENT app", async () => {
200
231
  vi.spyOn(console, "error").mockImplementation(() => undefined);
201
232
  const { client, preview } = mockClient();
202
- await withManifestDir({ app_id: "app_other", knowledge: [{ alias: "guide", doc_id: "kdc_1" }] }, async (dir) => {
233
+ await withManifestDir({ app_id: "app_other", knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] }, async (dir) => {
203
234
  await appRelease(client, { app_id: "app_x", changelog: "ship", yes: true, projectDir: dir });
204
235
  });
205
- expect(preview[0]).toEqual({ app_id: "app_x", opts: { knowledge: undefined } });
236
+ expect(preview[0]).toEqual({ app_id: "app_x", opts: { knowledge: undefined, config: undefined } });
237
+ vi.restoreAllMocks();
238
+ });
239
+ it("warns LOUDLY when a SENT config declaration is ignored by a pre-declaration server (deploy skew)", async () => {
240
+ const errs = [];
241
+ vi.spyOn(console, "error").mockImplementation((msg) => {
242
+ errs.push(String(msg));
243
+ });
244
+ // A pre-declaration server omits `config` from the preview echo.
245
+ const { client } = mockClient({ config: undefined });
246
+ await withManifestDir({ app_id: "app_x", config: [pageSizeKnob] }, async (dir) => {
247
+ await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
248
+ });
249
+ expect(errs.some((e) => /server ignored the config declaration/.test(e))).toBe(true);
206
250
  vi.restoreAllMocks();
207
251
  });
208
252
  });
@@ -307,3 +351,65 @@ describe("formatExtractReport", () => {
307
351
  expect(formatExtractReport([])).toEqual({ lines: [], hasError: false });
308
352
  });
309
353
  });
354
+ describe("packageDoctor — content package-id form", () => {
355
+ function mockClient(preview) {
356
+ return {
357
+ getWorkspaceId: () => "wsp_1",
358
+ listContentInstallations: async () => [
359
+ {
360
+ id: "pci_1",
361
+ package_id: "apg_corpus",
362
+ workspace_id: "wsp_1",
363
+ package_version: preview.from_version,
364
+ app_id: null,
365
+ binding: { knowledge: {}, templates: {} },
366
+ installed_by: null,
367
+ created_at: "",
368
+ updated_at: "",
369
+ package_registry: {
370
+ name: "Ops corpus",
371
+ kind: "content",
372
+ is_official: false,
373
+ latest_version: preview.to_version,
374
+ update_available: preview.to_version > preview.from_version,
375
+ },
376
+ },
377
+ ],
378
+ previewContentInstallationUpgrade: async () => preview,
379
+ };
380
+ }
381
+ it("reports a healthy, pristine install without failing the check", async () => {
382
+ const lines = [];
383
+ vi.spyOn(console, "error").mockImplementation((msg) => void lines.push(msg));
384
+ const before = process.exitCode;
385
+ await packageDoctor(mockClient({ from_version: 3, to_version: 3, entries: [], templates: [] }), { app_id: "apg_corpus" });
386
+ expect(lines.join("\n")).toContain("Ops corpus — content installation of apg_corpus");
387
+ expect(lines.join("\n")).toContain("Installed: v3 Latest: v3");
388
+ expect(lines.join("\n")).toContain("Binding: healthy");
389
+ expect(lines.join("\n")).toContain("Content: pristine");
390
+ expect(process.exitCode).toBe(before);
391
+ vi.restoreAllMocks();
392
+ });
393
+ it("reports drift (non-zero exit) and local edits with their consent framing", async () => {
394
+ const lines = [];
395
+ vi.spyOn(console, "error").mockImplementation((msg) => void lines.push(msg));
396
+ await packageDoctor(mockClient({
397
+ from_version: 2,
398
+ to_version: 3,
399
+ entries: [
400
+ { alias: "sop", name: "SOP", change: "drifted", modified: false, doc_id: null },
401
+ { alias: "pricing", name: "Pricing", change: "changed", modified: true, doc_id: "kdc_2" },
402
+ ],
403
+ templates: [{ kind: "template", alias: "bien_ban" }],
404
+ }), { app_id: "apg_corpus" });
405
+ const out = lines.join("\n");
406
+ expect(out).toContain("→ update available");
407
+ expect(out).toContain('knowledge.sop "SOP" (bound doc is gone)');
408
+ expect(out).toContain("--resolve knowledge.<alias>=recreate|unbind");
409
+ expect(out).toContain('knowledge.pricing "Pricing"');
410
+ expect(out).toContain("template.bien_ban");
411
+ expect(process.exitCode).toBe(1);
412
+ process.exitCode = 0;
413
+ vi.restoreAllMocks();
414
+ });
415
+ });
package/dist/src/cli.js CHANGED
@@ -29960,6 +29960,9 @@ var LoticsClient = class {
29960
29960
  if (opts.knowledge !== void 0 && opts.knowledge.length > 0) {
29961
29961
  params.set("knowledge", JSON.stringify(opts.knowledge));
29962
29962
  }
29963
+ if (opts.config !== void 0) {
29964
+ params.set("config", JSON.stringify(opts.config));
29965
+ }
29963
29966
  const query = params.toString();
29964
29967
  return this.request(
29965
29968
  "GET",
@@ -29992,6 +29995,9 @@ var LoticsClient = class {
29992
29995
  if (opts.knowledge !== void 0 && opts.knowledge.length > 0) {
29993
29996
  params.set("knowledge", JSON.stringify(opts.knowledge));
29994
29997
  }
29998
+ if (opts.config !== void 0 && opts.config.length > 0) {
29999
+ params.set("config", JSON.stringify(opts.config));
30000
+ }
29995
30001
  if (opts.renames !== void 0 && opts.renames.length > 0) {
29996
30002
  params.set("renames", JSON.stringify(opts.renames));
29997
30003
  }
@@ -50377,7 +50383,20 @@ function readLocalAppManifest(projectDir) {
50377
50383
  }
50378
50384
  }
50379
50385
  }
50380
- return { app_id, knowledge };
50386
+ const config2 = [];
50387
+ if (Array.isArray(lotics.config)) {
50388
+ for (const entry of lotics.config) {
50389
+ const result = contractConfigEntrySchema.safeParse(entry);
50390
+ if (!result.success) {
50391
+ const alias = isPlainObject2(entry) && typeof entry.alias === "string" ? entry.alias : "?";
50392
+ throw new Error(
50393
+ `Invalid lotics.config knob "${alias}" in package.json: ` + result.error.issues.map((i2) => `${i2.path.join(".") || "(root)"}: ${i2.message}`).join("; ")
50394
+ );
50395
+ }
50396
+ config2.push(result.data);
50397
+ }
50398
+ }
50399
+ return { app_id, knowledge, config: config2 };
50381
50400
  }
50382
50401
  function parseRenameFlags(renames) {
50383
50402
  return renames.map((entry) => {
@@ -50416,6 +50435,10 @@ function parseResolveFlags(resolve2) {
50416
50435
  return resolutions;
50417
50436
  }
50418
50437
  async function packageDoctor(client, args) {
50438
+ if (args.app_id !== void 0 && args.app_id.startsWith("apg_")) {
50439
+ await contentDoctor(client, args.app_id);
50440
+ return;
50441
+ }
50419
50442
  const app_id = resolveInstallationAppId(args.app_id);
50420
50443
  const health = await client.getPackageHealth(app_id);
50421
50444
  console.error(
@@ -50635,6 +50658,38 @@ function formatTemplateResolveHint(entry) {
50635
50658
  const note = entry.baseline_unknown ? " (can't verify the local edit \u2014 older package version; revert overwrites, keep retains)" : " (a local edit \u2014 revert overwrites it with the package's version; keep retains it)";
50636
50659
  return ` --resolve template.${entry.alias}=revert|keep${note}`;
50637
50660
  }
50661
+ async function contentDoctor(client, package_id) {
50662
+ const installation = await resolveWorkspaceContentInstallation(client, package_id);
50663
+ const preview = await client.previewContentInstallationUpgrade(installation.id, {});
50664
+ const name = installation.package_registry?.name ?? package_id;
50665
+ console.error(`${name} \u2014 content installation of ${package_id} (this workspace)`);
50666
+ console.error(
50667
+ ` Installed: v${preview.from_version} Latest: v${preview.to_version}` + (preview.to_version > preview.from_version ? " \u2192 update available" : "")
50668
+ );
50669
+ const drifted = preview.entries.filter((e) => e.change === "drifted");
50670
+ if (drifted.length === 0) {
50671
+ console.error(" Binding: healthy \u2014 every bound doc resolves.");
50672
+ } else {
50673
+ console.error(` Binding drift (${drifted.length}):`);
50674
+ for (const entry of drifted) {
50675
+ console.error(` - knowledge.${entry.alias} "${entry.name}" (bound doc is gone)`);
50676
+ }
50677
+ console.error(
50678
+ ` Resolve while upgrading:
50679
+ lotics upgrade ${package_id} --resolve knowledge.<alias>=recreate|unbind (or --bind-to <alias>=<kdc_id> to re-point)`
50680
+ );
50681
+ process.exitCode = 1;
50682
+ }
50683
+ const editedDocs = preview.entries.filter((e) => e.modified);
50684
+ const editedTemplates = preview.templates;
50685
+ if (editedDocs.length === 0 && editedTemplates.length === 0) {
50686
+ console.error(" Content: pristine \u2014 no local edits an upgrade would ask about.");
50687
+ } else {
50688
+ console.error(` Local edits (${editedDocs.length + editedTemplates.length}) \u2014 each takes a keep/overwrite consent at upgrade:`);
50689
+ for (const entry of editedDocs) console.error(` - knowledge.${entry.alias} "${entry.name}"`);
50690
+ for (const entry of editedTemplates) console.error(` - template.${entry.alias}`);
50691
+ }
50692
+ }
50638
50693
  async function resolveWorkspaceContentInstallation(client, package_id) {
50639
50694
  const workspaceId = client.getWorkspaceId();
50640
50695
  if (!workspaceId) {
@@ -50992,8 +51047,9 @@ async function appPublish(client, args) {
50992
51047
  );
50993
51048
  }
50994
51049
  const knowledge = local && local.app_id === appId ? local.knowledge : [];
51050
+ const config2 = local && local.app_id === appId ? local.config : [];
50995
51051
  const renames = parseRenameFlags(args.renames);
50996
- const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
51052
+ const preview = await client.previewPublishAppPackage(appId, { renames, knowledge, config: config2 });
50997
51053
  const { lines, hasError } = formatExtractReport(preview.findings);
50998
51054
  console.error(`Publish preview \u2014 ${appId} as new package "${preview.package_name}" (v1):`);
50999
51055
  const groups = [
@@ -51015,6 +51071,11 @@ async function appPublish(client, args) {
51015
51071
  } else {
51016
51072
  console.error(" No renamable aliases.");
51017
51073
  }
51074
+ if (config2.length > 0) {
51075
+ console.error(
51076
+ ` Config knobs (${config2.length}): ${config2.map((c) => `${c.alias} (${c.type})`).join(", ")}`
51077
+ );
51078
+ }
51018
51079
  if (lines.length > 0) {
51019
51080
  console.error(` Findings (${preview.findings.length}):`);
51020
51081
  for (const line of lines) console.error(line);
@@ -51039,7 +51100,8 @@ Re-run with --yes to publish v1:`);
51039
51100
  const result = await client.publishAppAsPackage(appId, {
51040
51101
  renames,
51041
51102
  changelog: args.changelog ?? null,
51042
- knowledge
51103
+ knowledge,
51104
+ config: config2
51043
51105
  });
51044
51106
  console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
51045
51107
  console.error(` The app is now installation #1 \u2014 develop it in place, then release the next version:`);
@@ -51058,7 +51120,8 @@ async function appRelease(client, args) {
51058
51120
  );
51059
51121
  }
51060
51122
  const knowledge = local && local.app_id === appId && local.knowledge.length > 0 ? local.knowledge : void 0;
51061
- const preview = await client.previewPackageRelease(appId, { knowledge });
51123
+ const config2 = local && local.app_id === appId && local.config.length > 0 ? local.config : void 0;
51124
+ const preview = await client.previewPackageRelease(appId, { knowledge, config: config2 });
51062
51125
  const { lines, hasError } = formatExtractReport(preview.findings);
51063
51126
  console.error(`Release preview \u2014 ${appId} \u2192 ${preview.package_id} v${preview.version}:`);
51064
51127
  if (preview.added_aliases.length > 0) {
@@ -51067,6 +51130,16 @@ async function appRelease(client, args) {
51067
51130
  if (preview.changed_artifacts.length > 0) {
51068
51131
  console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
51069
51132
  }
51133
+ if (preview.knowledge === void 0 && knowledge !== void 0) {
51134
+ console.error(
51135
+ " WARNING: the server ignored the knowledge declaration (it predates bundled-knowledge releases) \u2014 the bundle will NOT change. Retry after the platform deploy completes."
51136
+ );
51137
+ }
51138
+ if (preview.config === void 0 && config2 !== void 0) {
51139
+ console.error(
51140
+ " WARNING: the server ignored the config declaration (it predates manifest config declarations) \u2014 the knobs will NOT change. Retry after the platform deploy completes."
51141
+ );
51142
+ }
51070
51143
  const k = preview.knowledge ?? { added: [], removed: [], changed: [] };
51071
51144
  if (k.added.length > 0 || k.removed.length > 0 || k.changed.length > 0) {
51072
51145
  const parts = [
@@ -51076,7 +51149,16 @@ async function appRelease(client, args) {
51076
51149
  ].filter((p) => p !== null);
51077
51150
  console.error(` Knowledge: ${parts.join("; ")}`);
51078
51151
  }
51079
- if (preview.added_aliases.length === 0 && preview.changed_artifacts.length === 0 && k.added.length === 0 && k.removed.length === 0 && k.changed.length === 0) {
51152
+ const cfg = preview.config ?? { added: [], removed: [], changed: [] };
51153
+ if (cfg.added.length > 0 || cfg.removed.length > 0 || cfg.changed.length > 0) {
51154
+ const parts = [
51155
+ cfg.added.length > 0 ? `+${cfg.added.join(", ")}` : null,
51156
+ cfg.removed.length > 0 ? `dropped ${cfg.removed.join(", ")}` : null,
51157
+ cfg.changed.length > 0 ? `changed ${cfg.changed.join(", ")}` : null
51158
+ ].filter((p) => p !== null);
51159
+ console.error(` Config: ${parts.join("; ")}`);
51160
+ }
51161
+ if (preview.added_aliases.length === 0 && preview.changed_artifacts.length === 0 && k.added.length === 0 && k.removed.length === 0 && k.changed.length === 0 && cfg.added.length === 0 && cfg.removed.length === 0 && cfg.changed.length === 0) {
51080
51162
  console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
51081
51163
  }
51082
51164
  if (lines.length > 0) {
@@ -51095,7 +51177,7 @@ Re-run with --yes to publish v${preview.version}:`);
51095
51177
  process.exitCode = 1;
51096
51178
  return;
51097
51179
  }
51098
- const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge });
51180
+ const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge, config: config2 });
51099
51181
  console.error(`Released ${result.package_id} v${result.version}.`);
51100
51182
  console.error(` The origin was re-pinned to v${result.version} \u2014 verify: lotics package doctor ${appId}`);
51101
51183
  }
@@ -51126,7 +51208,7 @@ async function packageFleetUpgrade(client, args) {
51126
51208
  const line = ` [${inst.outcome}] ${inst.workspace_name} \u2014 ${inst.app_name} (${from} \u2192 v${result.target_version})`;
51127
51209
  if (inst.outcome === "skipped" && inst.blockers) {
51128
51210
  console.error(
51129
- `${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified}`
51211
+ `${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified} knowledge=${inst.blockers.knowledge}`
51130
51212
  );
51131
51213
  console.error(
51132
51214
  ` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`
@@ -68016,7 +68098,7 @@ COMMANDS
68016
68098
  <alias>=apply|keep|archive|recreate|unbind); an APP package
68017
68099
  FLEET-upgrades every installation across your org (clean
68018
68100
  apply, findings skip). --apply-all accepts the package's version
68019
- lotics package doctor [app_id] Installation health: version pin vs latest, binding
68101
+ lotics package doctor [app_id|package_id] Installation health: version pin vs latest, binding
68020
68102
  drift, locally modified core, knowledge drift/edits
68021
68103
  (exit 1 on findings)
68022
68104
  lotics package config <app_id> [--set key=value ...]
@@ -68545,7 +68627,7 @@ async function main() {
68545
68627
  console.error(" lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
68546
68628
  console.error(" lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content] Uninstall \u2014 dispatched by id (app vs content)");
68547
68629
  console.error(" lotics upgrade <app_id|package_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all] Upgrade \u2014 app install / content install / whole app fleet (apg_)");
68548
- console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding/knowledge drift");
68630
+ console.error(" lotics package doctor [app_id|package_id] Health: version pin vs latest + binding/knowledge drift (apg_ = this workspace's content install)");
68549
68631
  console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
68550
68632
  console.error(" lotics package eject <app_id> Sever an installation's package link");
68551
68633
  console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.86.1",
3
+ "version": "0.86.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {