@lotics/cli 0.86.0 → 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/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
  }
@@ -31598,7 +31604,13 @@ function buildWrapperPage(args) {
31598
31604
 
31599
31605
  // The iframe SDK sends one "upload" op carrying a File. A File can't
31600
31606
  // cross the JSON /_rpc hop, so the upload runs here in the browser:
31601
- // mint a presigned URL, PUT the bytes to storage, then finalize.
31607
+ // mint an upload URL, PUT the bytes, then finalize.
31608
+ //
31609
+ // In dev the minted URL is same-origin (/_upload/<file_id>) \u2014 the dev
31610
+ // server relays the bytes to storage on our behalf, because the prod
31611
+ // bucket's CORS does not admit a localhost origin. Production PUTs the
31612
+ // presigned storage URL directly. Same three lines either way: the page
31613
+ // PUTs wherever the mint points.
31602
31614
  //
31603
31615
  // Retry policy parity with the production SDK
31604
31616
  // (packages/app-sdk/src/upload/transport.ts): 3 PUT attempts with
@@ -31803,6 +31815,85 @@ function escapeHtml2(s) {
31803
31815
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31804
31816
  }
31805
31817
 
31818
+ // src/dev/upload_relay.ts
31819
+ var PRESIGN_TTL_MS = 15 * 60 * 1e3;
31820
+ function createUploadRelay(now = Date.now) {
31821
+ const minted = /* @__PURE__ */ new Map();
31822
+ const prune = (at) => {
31823
+ for (const [id, entry] of minted) {
31824
+ if (at - entry.mintedAt > PRESIGN_TTL_MS) minted.delete(id);
31825
+ }
31826
+ };
31827
+ return {
31828
+ rewriteMint(result) {
31829
+ const mint = result;
31830
+ if (typeof mint?.file_id !== "string" || typeof mint?.upload_url !== "string") return result;
31831
+ const at = now();
31832
+ prune(at);
31833
+ minted.set(mint.file_id, { url: mint.upload_url, mintedAt: at });
31834
+ return { ...mint, upload_url: `/_upload/${encodeURIComponent(mint.file_id)}` };
31835
+ },
31836
+ destinationFor(fileId) {
31837
+ const entry = minted.get(fileId);
31838
+ if (!entry) return null;
31839
+ if (now() - entry.mintedAt > PRESIGN_TTL_MS) {
31840
+ minted.delete(fileId);
31841
+ return null;
31842
+ }
31843
+ return entry.url;
31844
+ },
31845
+ settle(fileId) {
31846
+ minted.delete(fileId);
31847
+ },
31848
+ size: () => minted.size
31849
+ };
31850
+ }
31851
+
31852
+ // src/dev/file_relay.ts
31853
+ import { createHash } from "node:crypto";
31854
+ var TTL_MS = 24 * 60 * 60 * 1e3;
31855
+ var MAX_ENTRIES = 5e3;
31856
+ var PRESIGNED_KEYS = /* @__PURE__ */ new Set(["url", "thumbnail_url", "preview_url"]);
31857
+ var isFileObject = (o) => typeof o.filename === "string" && typeof o.mime_type === "string";
31858
+ var isRemoteUrl = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
31859
+ function createFileRelay(wrapperOrigin, now = Date.now) {
31860
+ const seen = /* @__PURE__ */ new Map();
31861
+ const tokenFor = (url2) => createHash("sha256").update(url2).digest("hex").slice(0, 24);
31862
+ const remember = (url2) => {
31863
+ const token = tokenFor(url2);
31864
+ seen.set(token, { url: url2, at: now() });
31865
+ if (seen.size > MAX_ENTRIES) {
31866
+ const oldest = seen.keys().next();
31867
+ if (!oldest.done) seen.delete(oldest.value);
31868
+ }
31869
+ return token;
31870
+ };
31871
+ const walk2 = (node) => {
31872
+ if (Array.isArray(node)) return node.map(walk2);
31873
+ if (!node || typeof node !== "object") return node;
31874
+ const obj = node;
31875
+ const file2 = isFileObject(obj);
31876
+ const out = {};
31877
+ for (const [key, value] of Object.entries(obj)) {
31878
+ out[key] = file2 && PRESIGNED_KEYS.has(key) && isRemoteUrl(value) ? `${wrapperOrigin}/_file/${remember(value)}` : walk2(value);
31879
+ }
31880
+ return out;
31881
+ };
31882
+ return {
31883
+ rewrite: (result) => walk2(result),
31884
+ destinationFor(token) {
31885
+ const entry = seen.get(token);
31886
+ if (!entry) return null;
31887
+ if (now() - entry.at > TTL_MS) {
31888
+ seen.delete(token);
31889
+ return null;
31890
+ }
31891
+ return entry.url;
31892
+ },
31893
+ size: () => seen.size
31894
+ };
31895
+ }
31896
+
31806
31897
  // src/dev/server.ts
31807
31898
  var DEFAULT_PORT = 5174;
31808
31899
  var DEFAULT_VITE_PORT = 5173;
@@ -31849,6 +31940,24 @@ async function startDevServer(args) {
31849
31940
  }
31850
31941
  };
31851
31942
  process.once("exit", killViteOnExit);
31943
+ const uploads = createUploadRelay();
31944
+ const wrapperOrigin = `http://localhost:${wrapperPort}`;
31945
+ const viteOrigin = `http://localhost:${vitePort}`;
31946
+ const files = createFileRelay(wrapperOrigin);
31947
+ const fileCors = {
31948
+ "Access-Control-Allow-Origin": viteOrigin,
31949
+ "Access-Control-Allow-Headers": "range, content-type",
31950
+ "Access-Control-Expose-Headers": "content-length, content-range, accept-ranges, content-type, content-disposition, etag"
31951
+ };
31952
+ const PASS_THROUGH = [
31953
+ "content-type",
31954
+ "content-length",
31955
+ "content-range",
31956
+ "accept-ranges",
31957
+ "etag",
31958
+ "last-modified",
31959
+ "content-disposition"
31960
+ ];
31852
31961
  const server = http.createServer(async (req, res) => {
31853
31962
  const url2 = req.url ?? "/";
31854
31963
  const pathname = url2.split("?")[0];
@@ -31877,7 +31986,11 @@ async function startDevServer(args) {
31877
31986
  process.stderr.write(`[rpc] ${body.op} ${ms}ms
31878
31987
  `);
31879
31988
  res.writeHead(200, { "Content-Type": "application/json" });
31880
- res.end(serializeRpcResult(result));
31989
+ res.end(
31990
+ serializeRpcResult(
31991
+ body.op === "upload_url" ? uploads.rewriteMint(result) : files.rewrite(result)
31992
+ )
31993
+ );
31881
31994
  } catch (err2) {
31882
31995
  const message = err2 instanceof Error ? err2.message : String(err2);
31883
31996
  process.stderr.write(`[rpc] ERROR ${message}
@@ -31930,12 +32043,115 @@ async function startDevServer(args) {
31930
32043
  }
31931
32044
  return;
31932
32045
  }
32046
+ if (pathname.startsWith("/_file/")) {
32047
+ if (req.method === "OPTIONS") {
32048
+ res.writeHead(204, fileCors);
32049
+ res.end();
32050
+ return;
32051
+ }
32052
+ if (req.method !== "GET" && req.method !== "HEAD") {
32053
+ res.writeHead(405, { ...fileCors, Allow: "GET, HEAD, OPTIONS" });
32054
+ res.end();
32055
+ return;
32056
+ }
32057
+ const token = decodeURIComponent(pathname.slice("/_file/".length));
32058
+ const destination = files.destinationFor(token);
32059
+ if (!destination) {
32060
+ process.stderr.write(`[file] ERROR unknown or expired token ${token}
32061
+ `);
32062
+ res.writeHead(404, { ...fileCors, "Content-Type": "application/json" });
32063
+ res.end(JSON.stringify({ message: "No file for this token" }));
32064
+ return;
32065
+ }
32066
+ try {
32067
+ const range = req.headers.range;
32068
+ const upstream = await fetch(destination, {
32069
+ method: req.method,
32070
+ headers: range ? { Range: range } : void 0
32071
+ });
32072
+ const headers = { ...fileCors };
32073
+ for (const name of PASS_THROUGH) {
32074
+ const value = upstream.headers.get(name);
32075
+ if (value) headers[name] = value;
32076
+ }
32077
+ const size = headers["content-length"] ?? "?";
32078
+ process.stderr.write(`[file] ${token} ${upstream.status} ${size}B
32079
+ `);
32080
+ res.writeHead(upstream.status, headers);
32081
+ if (req.method === "HEAD" || !upstream.body) {
32082
+ res.end();
32083
+ return;
32084
+ }
32085
+ const reader = upstream.body.getReader();
32086
+ for (; ; ) {
32087
+ const { value, done } = await reader.read();
32088
+ if (done) break;
32089
+ res.write(Buffer.from(value));
32090
+ }
32091
+ res.end();
32092
+ } catch (err2) {
32093
+ const message = err2 instanceof Error ? err2.message : String(err2);
32094
+ process.stderr.write(`[file] ERROR ${token} ${message}
32095
+ `);
32096
+ if (!res.headersSent) {
32097
+ res.writeHead(502, { ...fileCors, "Content-Type": "application/json" });
32098
+ res.end(JSON.stringify({ message }));
32099
+ } else {
32100
+ res.end();
32101
+ }
32102
+ }
32103
+ return;
32104
+ }
32105
+ if (req.method === "PUT" && pathname.startsWith("/_upload/")) {
32106
+ const fileId = decodeURIComponent(pathname.slice("/_upload/".length));
32107
+ const destination = uploads.destinationFor(fileId);
32108
+ if (!destination) {
32109
+ process.stderr.write(`[upload] ERROR unknown or expired file_id ${fileId}
32110
+ `);
32111
+ res.writeHead(404, { "Content-Type": "application/json" });
32112
+ res.end(JSON.stringify({ message: `No presigned upload pending for ${fileId}` }));
32113
+ return;
32114
+ }
32115
+ try {
32116
+ const chunks = [];
32117
+ for await (const chunk of req) chunks.push(chunk);
32118
+ const bytes = Buffer.concat(chunks);
32119
+ const startedAt = Date.now();
32120
+ const upstream = await fetch(destination, {
32121
+ method: "PUT",
32122
+ body: bytes,
32123
+ headers: { "Content-Type": req.headers["content-type"] ?? "application/octet-stream" }
32124
+ });
32125
+ const ms = Date.now() - startedAt;
32126
+ if (upstream.ok) {
32127
+ uploads.settle(fileId);
32128
+ process.stderr.write(`[upload] ${fileId} ${bytes.length}B ${ms}ms
32129
+ `);
32130
+ } else {
32131
+ process.stderr.write(`[upload] ERROR ${fileId} storage returned ${upstream.status}
32132
+ `);
32133
+ }
32134
+ res.writeHead(upstream.status);
32135
+ res.end();
32136
+ } catch (err2) {
32137
+ const message = err2 instanceof Error ? err2.message : String(err2);
32138
+ process.stderr.write(`[upload] ERROR ${fileId} ${message}
32139
+ `);
32140
+ if (!res.headersSent) {
32141
+ res.writeHead(502, { "Content-Type": "application/json" });
32142
+ res.end(JSON.stringify({ message }));
32143
+ } else {
32144
+ res.end();
32145
+ }
32146
+ }
32147
+ return;
32148
+ }
31933
32149
  res.writeHead(404, { "Content-Type": "text/plain" });
31934
32150
  res.end("Not Found");
31935
32151
  });
31936
32152
  await new Promise((resolve2, reject2) => {
31937
32153
  server.once("error", reject2);
31938
- server.listen(wrapperPort, () => {
32154
+ server.listen(wrapperPort, "127.0.0.1", () => {
31939
32155
  server.off("error", reject2);
31940
32156
  resolve2();
31941
32157
  });
@@ -50167,7 +50383,20 @@ function readLocalAppManifest(projectDir) {
50167
50383
  }
50168
50384
  }
50169
50385
  }
50170
- 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 };
50171
50400
  }
50172
50401
  function parseRenameFlags(renames) {
50173
50402
  return renames.map((entry) => {
@@ -50206,6 +50435,10 @@ function parseResolveFlags(resolve2) {
50206
50435
  return resolutions;
50207
50436
  }
50208
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
+ }
50209
50442
  const app_id = resolveInstallationAppId(args.app_id);
50210
50443
  const health = await client.getPackageHealth(app_id);
50211
50444
  console.error(
@@ -50425,6 +50658,38 @@ function formatTemplateResolveHint(entry) {
50425
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)";
50426
50659
  return ` --resolve template.${entry.alias}=revert|keep${note}`;
50427
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
+ }
50428
50693
  async function resolveWorkspaceContentInstallation(client, package_id) {
50429
50694
  const workspaceId = client.getWorkspaceId();
50430
50695
  if (!workspaceId) {
@@ -50782,8 +51047,9 @@ async function appPublish(client, args) {
50782
51047
  );
50783
51048
  }
50784
51049
  const knowledge = local && local.app_id === appId ? local.knowledge : [];
51050
+ const config2 = local && local.app_id === appId ? local.config : [];
50785
51051
  const renames = parseRenameFlags(args.renames);
50786
- const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
51052
+ const preview = await client.previewPublishAppPackage(appId, { renames, knowledge, config: config2 });
50787
51053
  const { lines, hasError } = formatExtractReport(preview.findings);
50788
51054
  console.error(`Publish preview \u2014 ${appId} as new package "${preview.package_name}" (v1):`);
50789
51055
  const groups = [
@@ -50805,6 +51071,11 @@ async function appPublish(client, args) {
50805
51071
  } else {
50806
51072
  console.error(" No renamable aliases.");
50807
51073
  }
51074
+ if (config2.length > 0) {
51075
+ console.error(
51076
+ ` Config knobs (${config2.length}): ${config2.map((c) => `${c.alias} (${c.type})`).join(", ")}`
51077
+ );
51078
+ }
50808
51079
  if (lines.length > 0) {
50809
51080
  console.error(` Findings (${preview.findings.length}):`);
50810
51081
  for (const line of lines) console.error(line);
@@ -50829,7 +51100,8 @@ Re-run with --yes to publish v1:`);
50829
51100
  const result = await client.publishAppAsPackage(appId, {
50830
51101
  renames,
50831
51102
  changelog: args.changelog ?? null,
50832
- knowledge
51103
+ knowledge,
51104
+ config: config2
50833
51105
  });
50834
51106
  console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
50835
51107
  console.error(` The app is now installation #1 \u2014 develop it in place, then release the next version:`);
@@ -50848,7 +51120,8 @@ async function appRelease(client, args) {
50848
51120
  );
50849
51121
  }
50850
51122
  const knowledge = local && local.app_id === appId && local.knowledge.length > 0 ? local.knowledge : void 0;
50851
- 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 });
50852
51125
  const { lines, hasError } = formatExtractReport(preview.findings);
50853
51126
  console.error(`Release preview \u2014 ${appId} \u2192 ${preview.package_id} v${preview.version}:`);
50854
51127
  if (preview.added_aliases.length > 0) {
@@ -50857,6 +51130,16 @@ async function appRelease(client, args) {
50857
51130
  if (preview.changed_artifacts.length > 0) {
50858
51131
  console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
50859
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
+ }
50860
51143
  const k = preview.knowledge ?? { added: [], removed: [], changed: [] };
50861
51144
  if (k.added.length > 0 || k.removed.length > 0 || k.changed.length > 0) {
50862
51145
  const parts = [
@@ -50866,7 +51149,16 @@ async function appRelease(client, args) {
50866
51149
  ].filter((p) => p !== null);
50867
51150
  console.error(` Knowledge: ${parts.join("; ")}`);
50868
51151
  }
50869
- 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) {
50870
51162
  console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
50871
51163
  }
50872
51164
  if (lines.length > 0) {
@@ -50885,7 +51177,7 @@ Re-run with --yes to publish v${preview.version}:`);
50885
51177
  process.exitCode = 1;
50886
51178
  return;
50887
51179
  }
50888
- const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge });
51180
+ const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge, config: config2 });
50889
51181
  console.error(`Released ${result.package_id} v${result.version}.`);
50890
51182
  console.error(` The origin was re-pinned to v${result.version} \u2014 verify: lotics package doctor ${appId}`);
50891
51183
  }
@@ -50916,7 +51208,7 @@ async function packageFleetUpgrade(client, args) {
50916
51208
  const line = ` [${inst.outcome}] ${inst.workspace_name} \u2014 ${inst.app_name} (${from} \u2192 v${result.target_version})`;
50917
51209
  if (inst.outcome === "skipped" && inst.blockers) {
50918
51210
  console.error(
50919
- `${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}`
50920
51212
  );
50921
51213
  console.error(
50922
51214
  ` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`
@@ -67806,7 +68098,7 @@ COMMANDS
67806
68098
  <alias>=apply|keep|archive|recreate|unbind); an APP package
67807
68099
  FLEET-upgrades every installation across your org (clean
67808
68100
  apply, findings skip). --apply-all accepts the package's version
67809
- 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
67810
68102
  drift, locally modified core, knowledge drift/edits
67811
68103
  (exit 1 on findings)
67812
68104
  lotics package config <app_id> [--set key=value ...]
@@ -68335,7 +68627,7 @@ async function main() {
68335
68627
  console.error(" lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
68336
68628
  console.error(" lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content] Uninstall \u2014 dispatched by id (app vs content)");
68337
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_)");
68338
- 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)");
68339
68631
  console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
68340
68632
  console.error(" lotics package eject <app_id> Sever an installation's package link");
68341
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.0",
3
+ "version": "0.86.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {