@lotics/cli 0.86.1 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1007 +0,0 @@
1
- /**
2
- * The package CONSUMER + registry surface (see docs/packages.md). Nothing starts
3
- * as a package, and nothing is authored as a local package PROJECT: an app is
4
- * first-released with `lotics app publish`, and a content package (a SET of live
5
- * knowledge docs + templates) is published with the `publish_content` /
6
- * `release_content` tools (`lotics run publish_content …`). This module
7
- * implements only what operates published packages + installations:
8
- *
9
- * install / uninstall / upgrade the high-traffic consumer verbs, dispatched
10
- * from the TOP LEVEL (`lotics install|uninstall|upgrade`). `upgrade` folds in
11
- * the whole-fleet path (a `package_id` upgrades every org installation).
12
- * doctor / eject / config / list-content / yank / show the low-traffic
13
- * ops, under `lotics package <verb>`. (Re-pointing a live role rides
14
- * `upgrade --resolve roles.<alias>=<grp_id>` — no separate verb.)
15
- *
16
- * Author verbs live on `lotics app` (`publish` / `release` / `unpublish`), and the
17
- * app-development loop lives entirely in `lotics app *`: pull an installed origin
18
- * (`lotics app pull <app_id>`), edit + `lotics app deploy`, then
19
- * `lotics app release <app_id>` cuts the next version.
20
- */
21
- import fs from "node:fs";
22
- import path from "node:path";
23
- import "./client.js";
24
- import { knowledgeEntryNeedsConsent, validKnowledgeResolutions, } from "@lotics/shared/schemas/packages";
25
- function packageJsonPath(projectDir) {
26
- return path.join(projectDir, "package.json");
27
- }
28
- function isPlainObject(value) {
29
- return typeof value === "object" && value !== null && !Array.isArray(value);
30
- }
31
- /**
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.
39
- */
40
- export function readLocalAppManifest(projectDir) {
41
- const pkgPath = packageJsonPath(projectDir);
42
- if (!fs.existsSync(pkgPath))
43
- return null;
44
- const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
45
- if (!isPlainObject(parsed))
46
- return null;
47
- const lotics = isPlainObject(parsed.lotics) ? parsed.lotics : {};
48
- const app_id = typeof lotics.app_id === "string" ? lotics.app_id : null;
49
- const knowledge = [];
50
- if (Array.isArray(lotics.knowledge)) {
51
- for (const entry of lotics.knowledge) {
52
- if (isPlainObject(entry) && typeof entry.alias === "string" && typeof entry.doc_id === "string") {
53
- knowledge.push({ alias: entry.alias, doc_id: entry.doc_id });
54
- }
55
- }
56
- }
57
- return { app_id, knowledge };
58
- }
59
- /** Parse repeated `--rename old=new` flags into `{ from, to }[]` (first-publish alias fixes). */
60
- export function parseRenameFlags(renames) {
61
- return renames.map((entry) => {
62
- const eq = entry.indexOf("=");
63
- if (eq <= 0 || eq === entry.length - 1) {
64
- throw new Error(`Invalid --rename "${entry}" — expected old=new (an alias to rename before v1 freezes it).`);
65
- }
66
- return { from: entry.slice(0, eq), to: entry.slice(eq + 1) };
67
- });
68
- }
69
- /**
70
- * Render a package-extract report grouped by severity (errors, then warnings,
71
- * then info), one ` [<severity>] <area>: <message>` line each, and classify
72
- * whether any `error` finding is present. Shared by the release preview display.
73
- * Pure — the command prints `lines` to stderr and gates on `hasError`.
74
- */
75
- export function formatExtractReport(report) {
76
- const order = ["error", "warning", "info"];
77
- const lines = order.flatMap((severity) => report
78
- .filter((f) => f.severity === severity)
79
- .map((f) => ` [${f.severity}] ${f.area}: ${f.message}`));
80
- return { lines, hasError: report.some((f) => f.severity === "error") };
81
- }
82
- /**
83
- * Resolve the installation to operate on: an explicit app id is required (there
84
- * is no dev-workspace pin to fall back to).
85
- */
86
- function resolveInstallationAppId(explicit) {
87
- if (explicit)
88
- return explicit;
89
- throw new Error("Pass an app id — e.g. lotics package doctor <app_id>.");
90
- }
91
- const RESOLVE_VERBS = new Set(["recreate", "revert", "keep", "apply", "archive", "unbind"]);
92
- /**
93
- * Parse repeated `--resolve <key>=<value>` flags into the ONE namespaced
94
- * resolutions map every upgrade wire takes. Keys pass through verbatim — a
95
- * drifted binding entry (`<namespace>.<alias>`), a modified artifact
96
- * (`<kind>.<alias>`), a bundled/standalone knowledge doc (`knowledge.<alias>`),
97
- * or a live-role re-point (`roles.<alias>`). A value that is one of the
98
- * resolution verbs stays a verb; anything else is a `{ bind_to }` id. The
99
- * server validates value-kind against what each key resolves.
100
- */
101
- export function parseResolveFlags(resolve) {
102
- const resolutions = {};
103
- for (const entry of resolve) {
104
- const eq = entry.indexOf("=");
105
- if (eq <= 0 || eq === entry.length - 1) {
106
- throw new Error(`Invalid --resolve "${entry}" — expected <key>=<verb> (recreate|revert|keep|apply|archive|unbind) or <key>=<existing_id>.`);
107
- }
108
- const key = entry.slice(0, eq);
109
- const value = entry.slice(eq + 1);
110
- resolutions[key] = RESOLVE_VERBS.has(value)
111
- ? value
112
- : { bind_to: value };
113
- }
114
- return resolutions;
115
- }
116
- /**
117
- * Health check: version pin vs. registry latest + binding drift. Exits
118
- * non-zero when drift is found so scripts can gate on it.
119
- */
120
- export async function packageDoctor(client, args) {
121
- const app_id = resolveInstallationAppId(args.app_id);
122
- const health = await client.getPackageHealth(app_id);
123
- console.error(`${health.package_name} — installation ${health.app_id}` +
124
- (health.is_origin ? " (origin — this app IS installation #1, the release working copy)" : ""));
125
- console.error(` Installed: v${health.installed_version} Latest: v${health.latest_version}` +
126
- (health.update_available ? " → update available" : ""));
127
- if (health.drift.length === 0) {
128
- console.error(" Binding: healthy — every bound alias resolves.");
129
- }
130
- else {
131
- console.error(` Binding drift (${health.drift.length}):`);
132
- for (const d of health.drift) {
133
- console.error(` - ${d.namespace}.${d.alias} → ${d.id} (missing from the workspace)`);
134
- }
135
- console.error(` Resolve while upgrading:\n lotics upgrade ${app_id}` +
136
- ` --resolve <namespace.alias>=recreate (or =<existing_id> to re-point)`);
137
- process.exitCode = 1;
138
- }
139
- if (health.modified.length === 0) {
140
- console.error(health.is_origin
141
- ? " Package artifacts: no changes since the last release.\n" +
142
- ` (schema additions aren't fingerprinted — preview them with: lotics app release ${app_id})`
143
- : " Package artifacts: pristine — no local edits an upgrade would revert.");
144
- }
145
- else if (health.is_origin) {
146
- // The origin IS the author's working copy — edits since the last release are
147
- // the NEXT release's payload, not consumer drift, and never fail the check.
148
- console.error(` Changed since v${health.installed_version} (${health.modified.length}) — a release will publish these:`);
149
- for (const m of health.modified) {
150
- console.error(` - ${m.kind}.${m.alias}`);
151
- }
152
- console.error(` Cut the next version: lotics app release ${app_id} -m "<what changed>"`);
153
- }
154
- else {
155
- console.error(` Locally modified package artifacts (${health.modified.length}):`);
156
- for (const m of health.modified) {
157
- console.error(` - ${m.kind}.${m.alias} (an upgrade overwrites this unless kept)`);
158
- }
159
- console.error(` Consent while upgrading:\n lotics upgrade ${app_id}` +
160
- ` --resolve <kind.alias>=revert (or =keep to retain the edit)`);
161
- process.exitCode = 1;
162
- }
163
- // Package-managed knowledge (bundled with this app install): drift + local edits
164
- // + unmet expects. All advisory; resolved through the app's package upgrade.
165
- if (health.knowledge_drift.length > 0) {
166
- console.error(` Knowledge binding drift (${health.knowledge_drift.length}):`);
167
- for (const d of health.knowledge_drift) {
168
- console.error(` - ${d.alias} "${d.name}" → ${d.doc_id ?? "(unbound)"} (missing from the workspace)`);
169
- }
170
- }
171
- if (health.knowledge_modified.length > 0) {
172
- if (health.is_origin) {
173
- console.error(` Knowledge changed since v${health.installed_version} (${health.knowledge_modified.length}) — a release will re-snapshot these:`);
174
- for (const m of health.knowledge_modified) {
175
- console.error(` - ${m.alias} "${m.name}"`);
176
- }
177
- }
178
- else {
179
- console.error(` Locally edited package knowledge (${health.knowledge_modified.length}):`);
180
- for (const m of health.knowledge_modified) {
181
- console.error(` - ${m.alias} "${m.name}" (an upgrade overwrites this unless kept)`);
182
- }
183
- }
184
- }
185
- if (health.missing_expected_docs.length > 0) {
186
- console.error(` Missing expected knowledge docs (${health.missing_expected_docs.length}):`);
187
- for (const name of health.missing_expected_docs) {
188
- console.error(` - "${name}" (the package's agents route to this name; no matching doc exists)`);
189
- }
190
- }
191
- // Drift is genuine breakage on the origin too (a bound id vanished); edited
192
- // knowledge on the origin is staged release work, not a failure.
193
- if (health.knowledge_drift.length > 0 || (health.knowledge_modified.length > 0 && !health.is_origin)) {
194
- process.exitCode = 1;
195
- }
196
- if (health.knowledge_drift.length === 0 &&
197
- health.knowledge_modified.length === 0 &&
198
- health.missing_expected_docs.length === 0) {
199
- console.error(" Knowledge: healthy — bound docs resolve, none locally edited, expects met.");
200
- }
201
- if (health.update_available) {
202
- console.error(` Upgrade: lotics upgrade ${app_id}`);
203
- }
204
- }
205
- /**
206
- * Preview-then-apply an app-installation upgrade. Prints the additive plan +
207
- * informational removals + any bundled-knowledge changes; refuses (exit 1, with
208
- * the exact --resolve syntax) while any binding drift, modified core artifact, or
209
- * consent-requiring bundled-knowledge doc lacks a resolution.
210
- *
211
- * `--resolve` speaks ONE namespaced grammar, passed to the server verbatim: a
212
- * drifted binding entry (`<namespace>.<alias>`), a modified artifact
213
- * (`<kind>.<alias>`), a bundled knowledge doc (`knowledge.<alias>` —
214
- * apply|keep|archive|recreate|unbind), or a live-role re-point
215
- * (`roles.<alias>=<grp_id>`). `--bind-to` consents an added-knowledge-doc name
216
- * collision; `--apply-all` accepts the package's version for every
217
- * consent-requiring knowledge doc (overwriting local edits).
218
- */
219
- export async function packageUpgrade(client, args) {
220
- const preview = await client.previewPackageUpgrade(args.app_id, {
221
- ...(args.version !== undefined ? { version: args.version } : {}),
222
- });
223
- console.error(`Upgrade v${preview.from_version} → v${preview.to_version}` +
224
- (preview.changelog ? ` — ${preview.changelog}` : ""));
225
- const summarize = (sets) => Object.entries(sets)
226
- .filter(([, entries]) => entries.length > 0)
227
- .map(([kind, entries]) => `${entries.length} ${kind}`)
228
- .join(", ");
229
- const added = summarize(preview.diff.added);
230
- const removed = summarize(preview.diff.removed);
231
- if (added)
232
- console.error(` Adds: ${added}`);
233
- if (removed)
234
- console.error(` Unbinds (workspace data kept): ${removed}`);
235
- if (preview.knowledge.length > 0) {
236
- console.error(` Knowledge docs (${preview.knowledge.length}):`);
237
- for (const entry of preview.knowledge) {
238
- console.error(` ${formatKnowledgeEntryLine(entry)}`);
239
- }
240
- }
241
- // Breaking contract changes are NOT resolvable via --resolve — scaffold would
242
- // refuse them on a bound alias, and there is no in-flow remediation. Report
243
- // every entry and the only two real remedies, then hard-stop.
244
- if (preview.diff.breaking.length > 0) {
245
- console.error(" Breaking contract changes — NOT resolvable via --resolve (a bound alias's shape changed):");
246
- for (const b of preview.diff.breaking) {
247
- console.error(` - ${b.entity}.${b.alias} (${b.kind}): ${b.from} → ${b.to}`);
248
- }
249
- console.error(" There is no in-flow resolution for this class. The package author must publish a version\n" +
250
- " that keeps these aliases' shape stable, or eject this app (severing the package link)\n" +
251
- ` before making the change: lotics package eject ${args.app_id}`);
252
- process.exit(1);
253
- }
254
- // ONE namespaced grammar — the flags pass to the server verbatim. `--bind-to`
255
- // is sugar for `knowledge.<alias>={bind_to}`; `--apply-all` fills the
256
- // remaining consent-requiring knowledge entries with the accept verb.
257
- const resolutions = parseResolveFlags(args.resolve);
258
- for (const [alias, id] of Object.entries(parseBindToFlags(args.bindTo))) {
259
- resolutions[`knowledge.${alias}`] = { bind_to: id };
260
- }
261
- if (args.applyAll) {
262
- for (const entry of preview.knowledge) {
263
- if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined) {
264
- resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
265
- }
266
- }
267
- }
268
- const unresolvedDrift = preview.drift.filter((d) => resolutions[`${d.namespace}.${d.alias}`] === undefined);
269
- const unresolvedModified = preview.modified.filter((m) => resolutions[`${m.kind}.${m.alias}`] === undefined);
270
- const unresolvedKnowledge = preview.knowledge.filter((e) => knowledgeEntryNeedsConsent(e) && resolutions[`knowledge.${e.alias}`] === undefined);
271
- if (unresolvedDrift.length > 0 ||
272
- unresolvedModified.length > 0 ||
273
- unresolvedKnowledge.length > 0) {
274
- if (unresolvedDrift.length > 0) {
275
- console.error(" Binding drift must be resolved before upgrading:");
276
- for (const d of unresolvedDrift) {
277
- console.error(` --resolve ${d.namespace}.${d.alias}=recreate (or =<existing_id> to re-point; was ${d.id})`);
278
- }
279
- }
280
- if (unresolvedModified.length > 0) {
281
- console.error(" Locally modified package artifacts need consent before upgrading:");
282
- for (const m of unresolvedModified) {
283
- console.error(` --resolve ${m.kind}.${m.alias}=revert (overwrite the local edit; or =keep to retain it)`);
284
- }
285
- }
286
- if (unresolvedKnowledge.length > 0) {
287
- console.error(" Bundled knowledge docs need consent before upgrading (or --apply-all to accept the package's version for all):");
288
- for (const entry of unresolvedKnowledge) {
289
- console.error(formatKnowledgeResolveHint(entry));
290
- }
291
- }
292
- process.exit(1);
293
- }
294
- const app = await client.upgradePackage(args.app_id, {
295
- ...(args.version !== undefined ? { version: args.version } : {}),
296
- ...(Object.keys(resolutions).length > 0 ? { resolutions } : {}),
297
- });
298
- console.error(`Upgraded ${app.name} → v${app.package_version} (${app.id}).`);
299
- }
300
- /** `--apply-all`'s accept-the-package resolution for a consent-requiring entry. */
301
- function knowledgeAcceptResolution(change) {
302
- switch (change) {
303
- case "changed":
304
- return "apply";
305
- case "removed":
306
- return "archive";
307
- case "drifted":
308
- return "recreate";
309
- case "added":
310
- return "apply"; // unreachable (added never needs consent), kept total.
311
- }
312
- }
313
- /** The install-consent trust line: official > your own org > third-party. */
314
- function trustBadge(pkg) {
315
- if (pkg.is_official)
316
- return "official Lotics package";
317
- if (pkg.owned_by_caller === true)
318
- return "your organization's package";
319
- return "third-party package (runs under your authority once installed)";
320
- }
321
- /**
322
- * `lotics package show <package_id>` — registry metadata + version history
323
- * (trust badge, retirement, per-version channel/yank/changelog). The read
324
- * surface for "what is this package and what shipped when". Kind-agnostic — a
325
- * content package shows here the same way.
326
- */
327
- export async function packageShow(client, args) {
328
- const pkg = await client.getPackage(args.package_id);
329
- const { versions } = await client.listPackageVersions(args.package_id);
330
- console.error(`${pkg.name} (${pkg.id}) — ${trustBadge(pkg)}`);
331
- if (pkg.description)
332
- console.error(` ${pkg.description}`);
333
- if (pkg.retired_at)
334
- console.error(` RETIRED ${pkg.retired_at}`);
335
- console.error(` latest installable: ${pkg.latest_version > 0 ? `v${pkg.latest_version}` : "none"}`);
336
- console.error("");
337
- for (const v of versions) {
338
- const marks = [
339
- v.version === pkg.latest_version ? "*" : " ",
340
- v.channel === "dev" ? "dev" : " ",
341
- v.yanked_at ? "YANKED" : " ",
342
- ].join(" ");
343
- console.log(`${marks} v${v.version} ${v.created_at} ${v.changelog ?? ""}`.trimEnd());
344
- }
345
- if (versions.length === 0)
346
- console.error(" (no published versions)");
347
- }
348
- /**
349
- * One preview line for a knowledge upgrade entry — `[change] alias "name"` with
350
- * `modified` / `needs consent` marks. Shared verbatim by the standalone (`pci_`)
351
- * and the app-install upgrade previews so both speak one vocabulary; the caller
352
- * owns the leading indent.
353
- */
354
- function formatKnowledgeEntryLine(entry) {
355
- const marks = [
356
- entry.modified ? "modified" : null,
357
- knowledgeEntryNeedsConsent(entry) ? "needs consent" : null,
358
- ].filter((m) => m !== null);
359
- return `[${entry.change}] ${entry.alias} "${entry.name}"${marks.length ? ` (${marks.join(", ")})` : ""}`;
360
- }
361
- /**
362
- * The exact `--resolve <alias>=<valid options>` remediation line for a
363
- * consent-requiring knowledge entry (options from the shared
364
- * `validKnowledgeResolutions`). Shared by both upgrade paths' gate output.
365
- */
366
- function formatKnowledgeResolveHint(entry) {
367
- const note = entry.change === "changed" || entry.change === "removed"
368
- ? " (a local edit — apply/archive overwrites it; keep retains it)"
369
- : " (bound doc is gone; recreate from the package, or unbind)";
370
- return ` --resolve knowledge.${entry.alias}=${validKnowledgeResolutions(entry.change).join("|")}${note}`;
371
- }
372
- /** The `--resolve template.<alias>=revert|keep` remediation line for a consent-requiring template. */
373
- function formatTemplateResolveHint(entry) {
374
- const note = entry.baseline_unknown
375
- ? " (can't verify the local edit — older package version; revert overwrites, keep retains)"
376
- : " (a local edit — revert overwrites it with the package's version; keep retains it)";
377
- return ` --resolve template.${entry.alias}=revert|keep${note}`;
378
- }
379
- /**
380
- * Resolve a STANDALONE content package's installation in the CURRENT workspace
381
- * from its PACKAGE id. `UNIQUE (workspace_id, package_id)` means the package id
382
- * fully determines the anchor row, so consumers address content by package id and
383
- * the `pci_` resource id never surfaces (the server keeps it — resource identity —
384
- * and the CLI resolves it here through the list endpoint; zero backend change).
385
- * Throws a clear "not installed" error (pointing at `lotics install`) when the
386
- * package has no content installation in this workspace.
387
- */
388
- async function resolveWorkspaceContentInstallation(client, package_id) {
389
- const workspaceId = client.getWorkspaceId();
390
- if (!workspaceId) {
391
- throw new Error("No workspace selected. Pass --workspace <ws> (or select one) to operate on its content installation.");
392
- }
393
- const installations = await client.listContentInstallations(workspaceId);
394
- const match = installations.find((inst) => inst.package_id === package_id);
395
- if (!match) {
396
- throw new Error(`Package ${package_id} is not installed in this workspace — install it first: lotics install ${package_id}`);
397
- }
398
- return match;
399
- }
400
- /**
401
- * CLEAN BREAK for an explicit `pci_` argument on `upgrade` / `uninstall`: the
402
- * `pci_` resource id is retired from human sight — content installs are addressed
403
- * by their PACKAGE id. Prints a loud redirect, best-effort resolving the `pci_`
404
- * back to its package id (via list-content) so the exact command is spelled out.
405
- * The header prints synchronously first, so the redirect is observable even when
406
- * the best-effort resolution can't reach the registry.
407
- */
408
- export async function redirectContentPciForm(client, pci_id, verb) {
409
- console.error(`Content installations are addressed by their PACKAGE id now — ${pci_id} is a server-internal resource id.`);
410
- const workspaceId = client.getWorkspaceId();
411
- const match = workspaceId
412
- ? (await client.listContentInstallations(workspaceId)).find((inst) => inst.id === pci_id)
413
- : undefined;
414
- if (match) {
415
- console.error(` Run: lotics ${verb} ${match.package_id}`);
416
- }
417
- else {
418
- console.error(" Find the package id: lotics package list-content");
419
- console.error(` Then run: lotics ${verb} <package_id>`);
420
- }
421
- process.exit(1);
422
- }
423
- /**
424
- * `lotics upgrade <apg_>` — the package-id upgrade path, kind-branched. `kind` is a
425
- * DERIVED display hint (`contractHasAppSurface`): `'content'` means no app surface.
426
- * - a CONTENT package upgrades THIS workspace's standalone content installation
427
- * (resolved from the package id — the anchor is unique per workspace, so the
428
- * `pci_` never surfaces), through the same content review gate.
429
- * - an APP-surface package FLEET-upgrades every installation across the org
430
- * (unchanged) — the resolve/bind/apply-all flags don't apply to a fleet run.
431
- */
432
- export async function packageUpgradeByPackageId(client, args) {
433
- const pkg = await client.getPackage(args.package_id);
434
- if (pkg.kind === "content") {
435
- const installation = await resolveWorkspaceContentInstallation(client, args.package_id);
436
- await packageUpgradeKnowledge(client, {
437
- installation_id: installation.id,
438
- package_id: pkg.id,
439
- package_name: pkg.name,
440
- version: args.version,
441
- resolve: args.resolve,
442
- bind_to: parseBindToFlags(args.bindTo),
443
- applyAll: args.applyAll,
444
- });
445
- return;
446
- }
447
- await packageFleetUpgrade(client, {
448
- package_id: args.package_id,
449
- ...(args.version !== undefined ? { version: args.version } : {}),
450
- });
451
- }
452
- /**
453
- * Preview-then-apply a STANDALONE content installation upgrade — knowledge docs
454
- * AND document templates behind one review gate. Prints the per-alias plan;
455
- * refuses (exit 1, with the exact `--resolve` syntax) while any consent-requiring
456
- * entry lacks a resolution — a modified knowledge change/removal or drift, or a
457
- * locally-edited changed template. `--apply-all` auto-resolves EVERY consent entry
458
- * by accepting the package's version (knowledge changed→apply, removed→archive,
459
- * drifted→recreate; template→revert) — an explicit bulk "take upstream" that
460
- * discards local edits. `--resolve knowledge.<alias>=<value>` /
461
- * `--resolve template.<alias>=revert|keep` and `--bind-to <alias>=<kdc_id>`
462
- * resolve entries individually — the ONE namespaced grammar, passed to the
463
- * server verbatim.
464
- */
465
- async function packageUpgradeKnowledge(client, args) {
466
- const preview = await client.previewContentInstallationUpgrade(args.installation_id, {
467
- ...(args.version !== undefined ? { version: args.version } : {}),
468
- });
469
- console.error(`Content upgrade v${preview.from_version} → v${preview.to_version}` +
470
- (preview.changelog ? ` — ${preview.changelog}` : ""));
471
- const apply = async (resolutions) => {
472
- const updated = await client.applyContentInstallationUpgrade(args.installation_id, {
473
- ...(args.version !== undefined ? { version: args.version } : {}),
474
- resolutions,
475
- });
476
- console.error(`Upgraded ${args.package_name} (${args.package_id}) → v${updated.package_version}.`);
477
- };
478
- if (preview.entries.length === 0 && preview.templates.length === 0) {
479
- if (preview.to_version === preview.from_version) {
480
- console.error(" Already up to date — no content changes.");
481
- return;
482
- }
483
- console.error(" No changes needing consent — advancing the version pin; clean template updates apply automatically.");
484
- await apply({});
485
- return;
486
- }
487
- for (const entry of preview.entries) {
488
- console.error(` ${formatKnowledgeEntryLine(entry)}`);
489
- }
490
- for (const entry of preview.templates) {
491
- console.error(` [template] ${entry.alias} (modified — needs consent)`);
492
- }
493
- // ONE namespaced grammar — the flags pass to the server verbatim. `--bind-to`
494
- // is sugar for `knowledge.<alias>={bind_to}`; `--apply-all` fills the
495
- // remaining consent entries with the accept verb (template → revert).
496
- const resolutions = parseResolveFlags(args.resolve);
497
- for (const [alias, id] of Object.entries(args.bind_to)) {
498
- resolutions[`knowledge.${alias}`] = { bind_to: id };
499
- }
500
- if (args.applyAll) {
501
- for (const entry of preview.entries) {
502
- if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined) {
503
- resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
504
- }
505
- }
506
- for (const entry of preview.templates) {
507
- if (resolutions[`template.${entry.alias}`] === undefined) {
508
- resolutions[`template.${entry.alias}`] = "revert";
509
- }
510
- }
511
- }
512
- const unresolvedKnowledge = preview.entries.filter((entry) => knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined);
513
- const unresolvedTemplates = preview.templates.filter((entry) => resolutions[`template.${entry.alias}`] === undefined);
514
- if (unresolvedKnowledge.length > 0 || unresolvedTemplates.length > 0) {
515
- console.error(" These need a resolution before upgrading (or pass --apply-all to accept the package's version for all):");
516
- for (const entry of unresolvedKnowledge)
517
- console.error(formatKnowledgeResolveHint(entry));
518
- for (const entry of unresolvedTemplates)
519
- console.error(formatTemplateResolveHint(entry));
520
- process.exit(1);
521
- }
522
- await apply(resolutions);
523
- }
524
- /** Parse repeated `--bind-to alias=kdc_id` flags into an alias → doc-id consent map. */
525
- export function parseBindToFlags(bindTo) {
526
- const map = {};
527
- for (const entry of bindTo) {
528
- const eq = entry.indexOf("=");
529
- if (eq <= 0 || eq === entry.length - 1) {
530
- throw new Error(`Invalid --bind-to "${entry}" — expected <alias>=<kdc_id>.`);
531
- }
532
- map[entry.slice(0, eq)] = entry.slice(eq + 1);
533
- }
534
- return map;
535
- }
536
- /** Print advisory `knowledge_expects` misses loudly (never blocks the install). */
537
- function warnMissingExpectedDocs(missing) {
538
- if (missing.length === 0)
539
- return;
540
- console.error(` ⚠ ${missing.length} expected knowledge doc(s) the package's agents route to are missing from this workspace:`);
541
- for (const name of missing)
542
- console.error(` - "${name}"`);
543
- console.error(" The package installed, but those agents will degrade until a doc with each name exists.");
544
- }
545
- export async function packageInstall(client, args) {
546
- // Surface the trust badge at the consent point: installing materializes the
547
- // package's workflows/agents (or its doc corpus) under YOUR authority.
548
- const pkg = await client.getPackage(args.package_id);
549
- // The kind hint only earns a mention when it adds information (content —
550
- // no app materializes); "(package)" after "third-party package" is noise.
551
- const kindSuffix = pkg.kind === "content" ? " (content — docs/templates, no app)" : "";
552
- console.error(`Installing ${pkg.name} — ${trustBadge(pkg)}${kindSuffix}...`);
553
- const result = await client.installPackage(args.package_id, {
554
- ...(args.version !== undefined ? { version: args.version } : {}),
555
- ...(args.bind_to && Object.keys(args.bind_to).length > 0 ? { bind_to: args.bind_to } : {}),
556
- ...(args.config && Object.keys(args.config).length > 0 ? { config: args.config } : {}),
557
- });
558
- if (result.kind === "content") {
559
- const { installation, warnings } = result;
560
- const docs = installation.binding.knowledge;
561
- const templates = installation.binding.templates;
562
- console.error(`Installed ${pkg.name} v${installation.package_version} (workspace ${installation.workspace_id}).`);
563
- const docAliases = Object.keys(docs);
564
- if (docAliases.length > 0) {
565
- console.error(` ${docAliases.length} doc(s) live: ${docAliases.map((a) => `${a}→${docs[a]}`).join(", ")}`);
566
- }
567
- const templateAliases = Object.keys(templates);
568
- if (templateAliases.length > 0) {
569
- console.error(` ${templateAliases.length} template(s) live: ${templateAliases.map((a) => `${a}→${templates[a]}`).join(", ")}`);
570
- }
571
- warnMissingExpectedDocs(warnings.missing_expected_docs);
572
- console.error(` Upgrade later: lotics upgrade ${args.package_id}`);
573
- console.error(` Uninstall: lotics uninstall ${args.package_id} [--keep-content]`);
574
- return;
575
- }
576
- const { app, knowledge_warnings } = result;
577
- const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
578
- console.error(`Installed ${app.name} ${versionLabel} → ${app.id} (workspace ${app.workspace_id}).`);
579
- console.error(" The data model, queries, workflows, and agents are live.");
580
- warnMissingExpectedDocs(knowledge_warnings.missing_expected_docs);
581
- // Follow-up ops key on the NEW installation id (app_), not the package id typed
582
- // at `install` — signpost each one with the id inlined (the content branch above
583
- // does the same), so the id switch is never a silent trap.
584
- console.error(` Pull it for local editing: lotics app pull ${app.id}`);
585
- console.error(` Upgrade later: lotics upgrade ${app.id}`);
586
- console.error(` Health / uninstall: lotics package doctor ${app.id} · lotics uninstall ${app.id} [--archive-tables]`);
587
- }
588
- /**
589
- * `lotics uninstall <app_id|package_id>` — ONE top-level command over both
590
- * installation kinds, dispatched by the id form (mirrors `lotics upgrade`):
591
- * - a package id (`apg_`) → THIS workspace's STANDALONE CONTENT installation
592
- * (content installs are addressed by package id, UNIQUE per workspace, so the
593
- * `pci_` resource id never surfaces): deletes the row and (unless
594
- * `--keep-content`) archives its package-bound docs AND templates, listing each
595
- * archived id.
596
- * - anything else (an `app_id`) → an APP installation: archives its workflow
597
- * artifacts and — with `--archive-tables` — the scaffolded entity tables
598
- * (provenance- + reference-gated server-side).
599
- * A flag used on the wrong path is a loud error, never silently ignored.
600
- */
601
- export async function packageUninstall(client, args) {
602
- if (args.id.startsWith("apg_")) {
603
- if (args.archive_tables) {
604
- throw new Error("--archive-tables applies only to an app installation (<app_id>). A content package " +
605
- "has no scaffolded tables — use --keep-content to retain its docs/templates.");
606
- }
607
- const pkg = await client.getPackage(args.id);
608
- // kind is a DERIVED display hint (contractHasAppSurface): 'content' = no app.
609
- if (pkg.kind !== "content") {
610
- throw new Error(`Package ${args.id} is an app package — uninstall an app installation by its app id: lotics uninstall <app_id>.`);
611
- }
612
- const installation = await resolveWorkspaceContentInstallation(client, args.id);
613
- const result = await client.uninstallContentPackage(installation.id, {
614
- keep_content: args.keep_content,
615
- });
616
- if (args.keep_content) {
617
- console.error(`Uninstalled ${pkg.name} (${pkg.id}) — its docs and templates were kept as ordinary workspace content.`);
618
- }
619
- else {
620
- console.error(`Uninstalled ${pkg.name} (${pkg.id}) — archived ` +
621
- `${result.archived_doc_ids.length} doc(s) and ${result.archived_template_ids.length} template(s).`);
622
- for (const docId of result.archived_doc_ids)
623
- console.error(` ${docId}`);
624
- for (const templateId of result.archived_template_ids)
625
- console.error(` ${templateId}`);
626
- }
627
- return;
628
- }
629
- if (args.keep_content) {
630
- throw new Error("--keep-content applies only to a content package (apg_). An app installation " +
631
- "(<app_id>) uses --archive-tables to also archive its scaffolded tables.");
632
- }
633
- const app = await client.getApp(args.id);
634
- if (!app.package_id) {
635
- throw new Error(`App ${args.id} is not a package installation — use the app delete flow for a bespoke app.`);
636
- }
637
- const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
638
- const boundCount = Object.keys(app.workflows ?? {}).length;
639
- const tableCount = Object.keys(app.binding?.entities ?? {}).length;
640
- console.error(`Uninstalling ${app.name} (${app.id}):`);
641
- console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
642
- console.error(args.archive_tables
643
- ? ` Archives ${tableCount} scaffolded table(s) — refused if they were adopted or are still referenced.`
644
- : ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`);
645
- const result = await client.uninstallAppPackage(args.id, {
646
- archive_tables: args.archive_tables,
647
- });
648
- console.error(`Uninstalled ${app.name} (${app.id}).`);
649
- if (result.archived_table_ids.length > 0) {
650
- console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
651
- }
652
- }
653
- /**
654
- * `lotics package list-content` — list the selected workspace's STANDALONE
655
- * content installations (an app-bundled corpus rides its app's
656
- * `binding.knowledge` and shows on the Apps surface instead), each with its
657
- * registry status. The what-is-installed listing: each row leads with the PACKAGE
658
- * id — the address for `lotics upgrade <package_id>` / `lotics uninstall
659
- * <package_id>` (the `pci_` resource id stays hidden).
660
- */
661
- export async function packageListContent(client) {
662
- const workspaceId = client.getWorkspaceId();
663
- if (!workspaceId) {
664
- throw new Error("No workspace selected. Pass --workspace <ws> (or select one) to list its content installations.");
665
- }
666
- const installations = await client.listContentInstallations(workspaceId);
667
- if (installations.length === 0) {
668
- console.error(`No package-managed content installations in workspace ${workspaceId}.`);
669
- return;
670
- }
671
- console.error(`Content installations in workspace ${workspaceId} (${installations.length}):`);
672
- for (const inst of installations) {
673
- const name = inst.package_registry?.name ?? "(unknown package)";
674
- const latest = inst.package_registry?.latest_version;
675
- const updateAvailable = inst.package_registry?.update_available ?? false;
676
- const versionLabel = latest !== undefined && latest !== inst.package_version
677
- ? `v${inst.package_version} → latest v${latest}`
678
- : `v${inst.package_version}`;
679
- console.error(` ${inst.package_id} ${name} ${versionLabel}` +
680
- (updateAvailable ? " → update available" : ""));
681
- }
682
- }
683
- export async function packageEject(client, args) {
684
- const app = await client.ejectPackage(args.app_id);
685
- console.error(`Ejected ${app.name} → ${app.id} (workspace ${app.workspace_id}).`);
686
- console.error(" The package link is severed — it's now a normal bespoke app and can no longer be upgraded.");
687
- console.error(" Its data model, queries, workflows, and templates are unchanged.");
688
- console.error(" Pull the pinned source for local editing: lotics app pull " + app.id);
689
- }
690
- /**
691
- * Parse one `key=value` config assignment. When the knob's type is known (from
692
- * the stored value at `package config --set`) the value is parsed to that type
693
- * loudly; otherwise (`lotics install --config`) it is inferred (true/false →
694
- * boolean, numeric → number, else string) and the server validates it against
695
- * the contract.
696
- */
697
- function parseConfigAssignment(entry, flag, knownType) {
698
- const eq = entry.indexOf("=");
699
- if (eq <= 0 || eq === entry.length - 1) {
700
- throw new Error(`Invalid ${flag} "${entry}" — expected key=value.`);
701
- }
702
- const key = entry.slice(0, eq);
703
- const raw = entry.slice(eq + 1);
704
- if (knownType === "number") {
705
- const n = Number(raw);
706
- if (!Number.isFinite(n))
707
- throw new Error(`Config key "${key}" is a number knob — "${raw}" is not numeric.`);
708
- return { key, value: n };
709
- }
710
- if (knownType === "boolean") {
711
- if (raw !== "true" && raw !== "false") {
712
- throw new Error(`Config key "${key}" is a boolean knob — expected true or false, got "${raw}".`);
713
- }
714
- return { key, value: raw === "true" };
715
- }
716
- if (knownType === "string")
717
- return { key, value: raw };
718
- // Inferred (install override): the server re-validates against the contract.
719
- if (raw === "true" || raw === "false")
720
- return { key, value: raw === "true" };
721
- if (/^-?\d+(\.\d+)?$/.test(raw))
722
- return { key, value: Number(raw) };
723
- return { key, value: raw };
724
- }
725
- /** `--config key=value` (install): inferred types, server-validated. */
726
- export function parseInstallConfigFlags(config) {
727
- const out = {};
728
- for (const entry of config) {
729
- const { key, value } = parseConfigAssignment(entry, "--config");
730
- out[key] = value;
731
- }
732
- return out;
733
- }
734
- /**
735
- * `lotics package config <app_id>` — show the installation's effective config;
736
- * with `--set key=value` (repeatable) partial-merge edits, each value parsed by
737
- * the knob's current type. No `--set` prints the values.
738
- */
739
- export async function packageConfig(client, args) {
740
- const app = await client.getApp(args.app_id);
741
- if (!app.package_id) {
742
- throw new Error(`App ${args.app_id} is not a package installation — it has no config.`);
743
- }
744
- const current = app.config ?? {};
745
- if (args.sets.length === 0) {
746
- const keys = Object.keys(current).sort();
747
- if (keys.length === 0) {
748
- console.error(`${app.name} (${app.id}) — no config knobs.`);
749
- return;
750
- }
751
- console.error(`${app.name} (${app.id}) config:`);
752
- for (const key of keys)
753
- console.log(` ${key} = ${JSON.stringify(current[key])}`);
754
- return;
755
- }
756
- const overrides = {};
757
- for (const entry of args.sets) {
758
- const key = entry.slice(0, Math.max(0, entry.indexOf("=")));
759
- const knownType = typeof current[key];
760
- const { value } = parseConfigAssignment(entry, "--set", knownType === "number" || knownType === "boolean" || knownType === "string" ? knownType : undefined);
761
- overrides[key] = value;
762
- }
763
- const { config } = await client.updateAppPackageConfig(args.app_id, { config: overrides });
764
- console.error(`Updated config for ${app.name} (${app.id}):`);
765
- for (const key of Object.keys(config).sort()) {
766
- const marker = key in overrides ? " *" : "";
767
- console.log(` ${key} = ${JSON.stringify(config[key])}${marker}`);
768
- }
769
- }
770
- /**
771
- * `lotics app unpublish <app_id|package_id> [--undo]` — take a published package
772
- * off the shelf (or `--undo` restore it): new installs refuse it and it hides
773
- * from other orgs, while existing installations keep working and may still
774
- * upgrade. Given an app id (an installation of the package) it resolves the
775
- * package from the app; a package id targets it directly. Owner-org admin-only.
776
- */
777
- export async function appUnpublish(client, args) {
778
- let packageId = args.id;
779
- if (args.id.startsWith("app_")) {
780
- const app = await client.getApp(args.id);
781
- if (!app.package_id) {
782
- throw new Error(`App ${args.id} is not a package installation — it has no package to unpublish. ` +
783
- `Pass the package id directly.`);
784
- }
785
- packageId = app.package_id;
786
- }
787
- const pkg = await client.retirePackage(packageId, { undo: args.undo });
788
- if (pkg.retired_at !== null) {
789
- console.error(`Unpublished ${pkg.name} (${pkg.id}). New installs refuse it and it is hidden from other orgs; ` +
790
- `existing installations keep working and may still upgrade. Undo: lotics app unpublish ${pkg.id} --undo`);
791
- }
792
- else {
793
- console.error(`Re-published ${pkg.name} (${pkg.id}) — installable again.`);
794
- }
795
- }
796
- /**
797
- * `lotics app publish [app_id|.] [--rename old=new ...] [-m <changelog>] [--yes]` —
798
- * FIRST-RELEASE a bespoke app as a package (docs/packages.md § Promotion). Nothing
799
- * starts as a package. Mirrors `app release`'s preview→apply UX: it first shows the
800
- * dry-run preview (GET, no writes) — the package name, the auto-minted RENAMABLE
801
- * aliases (the exact `--rename` keys, so v1's frozen aliases are inspected first,
802
- * never a blind publish), and the extract findings — then APPLIES only with
803
- * `--yes` (else exits 1 with the re-run hint). On apply the server extracts the
804
- * contract, creates the registry package, publishes v1 from the DEPLOYED source +
805
- * dist, and pins the origin as installation #1. `--rename old=new` fixes an
806
- * auto-minted alias before v1 freezes it; an `error` finding blocks the apply. An
807
- * already-linked app releases with `lotics app release` instead. The app id is the
808
- * positional (`.`/omitted → resolved from the local app project manifest).
809
- */
810
- export async function appPublish(client, args) {
811
- const projectDir = path.resolve(args.projectDir ?? process.cwd());
812
- const local = readLocalAppManifest(projectDir);
813
- const explicit = args.app_id !== undefined && args.app_id !== "." ? args.app_id : undefined;
814
- const appId = explicit ?? local?.app_id ?? null;
815
- if (appId === null) {
816
- throw new Error("No app id. Run `lotics app publish` from a pulled app project (lotics app pull <app_id>), " +
817
- "or pass one: lotics app publish <app_id>.");
818
- }
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.
823
- const knowledge = local && local.app_id === appId ? local.knowledge : [];
824
- const renames = parseRenameFlags(args.renames);
825
- // Preview first (GET, no writes) — the same preview→--yes flow as `app release`,
826
- // so the aliases v1 freezes forever are never a blind publish and `--rename`
827
- // targets are inspectable before committing.
828
- const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
829
- const { lines, hasError } = formatExtractReport(preview.findings);
830
- console.error(`Publish preview — ${appId} as new package "${preview.package_name}" (v1):`);
831
- const groups = [
832
- ["entities", preview.renamable_aliases.entities],
833
- ["fields", preview.renamable_aliases.fields],
834
- ["options", preview.renamable_aliases.options],
835
- ["roles", preview.renamable_aliases.roles],
836
- ["templates", preview.renamable_aliases.templates],
837
- ["workflows", preview.renamable_aliases.workflows],
838
- ];
839
- if (groups.some(([, vals]) => vals.length > 0)) {
840
- console.error(" Auto-minted aliases — rename any with --rename <alias>=<new> before v1 freezes them:");
841
- for (const [label, vals] of groups) {
842
- if (vals.length > 0)
843
- console.error(` ${`${label}:`.padEnd(11)} ${vals.join(", ")}`);
844
- }
845
- console.error(" (query / app-workflow / agent runtime aliases are fixed — the shipped source calls them verbatim.)");
846
- }
847
- else {
848
- console.error(" No renamable aliases.");
849
- }
850
- if (lines.length > 0) {
851
- console.error(` Findings (${preview.findings.length}):`);
852
- for (const line of lines)
853
- console.error(line);
854
- }
855
- if (hasError) {
856
- console.error("\nExtract found error findings — the app cannot be published as-is. Fix them in the app, redeploy, and retry.");
857
- process.exitCode = 1;
858
- return;
859
- }
860
- if (!args.yes) {
861
- const idArg = explicit ?? ".";
862
- const renameArgs = args.renames.map((r) => ` --rename ${r}`).join("");
863
- const mArg = args.changelog ? ` -m ${JSON.stringify(args.changelog)}` : "";
864
- console.error(`\nRe-run with --yes to publish v1:`);
865
- console.error(` lotics app publish ${idArg}${renameArgs}${mArg} --yes`);
866
- process.exitCode = 1;
867
- return;
868
- }
869
- const result = await client.publishAppAsPackage(appId, {
870
- renames,
871
- changelog: args.changelog ?? null,
872
- knowledge,
873
- });
874
- console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
875
- console.error(` The app is now installation #1 — develop it in place, then release the next version:`);
876
- console.error(` lotics app pull ${appId} # edit, then lotics app deploy`);
877
- console.error(` lotics app release ${appId} -m "<what changed>"`);
878
- console.error(` Install it elsewhere: lotics install ${result.package_id}`);
879
- }
880
- /**
881
- * `lotics app release [app_id|.] -m <changelog> [--yes]` — snapshot an
882
- * adopted/installed origin app into its next registry version (docs/packages.md
883
- * § Promotion). The origin is the permanent working copy; a release binding-aware-
884
- * extracts it (stable aliases), repackages its DEPLOYED source + dist as the
885
- * bundle, publishes the next version, and re-pins the origin. Prints the preview
886
- * first (next version, new + changed aliases, the bundled-knowledge delta,
887
- * findings); applies only with `--yes`, else exits 1 so a review step can't be
888
- * skipped. An `error` finding blocks the apply.
889
- *
890
- * Run from the pulled app project, the manifest's `lotics.knowledge` (alias →
891
- * doc_id) is the bundle DECLARATION — it re-declares which docs the package owns
892
- * (add/drop/re-snapshot). Forwarded only when non-empty; empty (or a bare id from
893
- * elsewhere) sends nothing, so the current corpus is reconstructed from the pin
894
- * (never silently dropped).
895
- */
896
- export async function appRelease(client, args) {
897
- const projectDir = path.resolve(args.projectDir ?? process.cwd());
898
- const local = readLocalAppManifest(projectDir);
899
- const explicit = args.app_id !== undefined && args.app_id !== "." ? args.app_id : undefined;
900
- const appId = explicit ?? local?.app_id ?? null;
901
- if (appId === null) {
902
- 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
- }
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".
907
- const knowledge = local && local.app_id === appId && local.knowledge.length > 0 ? local.knowledge : undefined;
908
- const preview = await client.previewPackageRelease(appId, { knowledge });
909
- const { lines, hasError } = formatExtractReport(preview.findings);
910
- console.error(`Release preview — ${appId} → ${preview.package_id} v${preview.version}:`);
911
- if (preview.added_aliases.length > 0) {
912
- console.error(` New (${preview.added_aliases.length}): ${preview.added_aliases.join(", ")}`);
913
- }
914
- if (preview.changed_artifacts.length > 0) {
915
- console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
916
- }
917
- // Deploy-skew boundary: a pre-knowledge-declaration server omits the field.
918
- const k = preview.knowledge ?? { added: [], removed: [], changed: [] };
919
- if (k.added.length > 0 || k.removed.length > 0 || k.changed.length > 0) {
920
- const parts = [
921
- k.added.length > 0 ? `+${k.added.join(", ")}` : null,
922
- k.removed.length > 0 ? `dropped ${k.removed.join(", ")}` : null,
923
- k.changed.length > 0 ? `changed ${k.changed.join(", ")}` : null,
924
- ].filter((p) => p !== null);
925
- console.error(` Knowledge: ${parts.join("; ")}`);
926
- }
927
- if (preview.added_aliases.length === 0 &&
928
- preview.changed_artifacts.length === 0 &&
929
- k.added.length === 0 &&
930
- k.removed.length === 0 &&
931
- k.changed.length === 0) {
932
- console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
933
- }
934
- if (lines.length > 0) {
935
- console.error(` Findings (${preview.findings.length}):`);
936
- for (const line of lines)
937
- console.error(line);
938
- }
939
- if (hasError) {
940
- console.error("\nExtract found error findings — the origin cannot be released as-is. Fix them in the app and retry.");
941
- process.exitCode = 1;
942
- return;
943
- }
944
- if (!args.yes) {
945
- console.error(`\nRe-run with --yes to publish v${preview.version}:`);
946
- console.error(` lotics app release ${args.app_id ?? "."} -m ${JSON.stringify(args.changelog)} --yes`);
947
- process.exitCode = 1;
948
- return;
949
- }
950
- const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge });
951
- console.error(`Released ${result.package_id} v${result.version}.`);
952
- console.error(` The origin was re-pinned to v${result.version} — verify: lotics package doctor ${appId}`);
953
- }
954
- /**
955
- * `lotics package yank <package_id> <version> [--undo]` — mark a published
956
- * version uninstallable (or restore it). New installs/upgrades/adopts refuse a
957
- * yanked version and "latest" skips it; installations already pinned keep
958
- * running. Owner-org admin-only.
959
- */
960
- export async function packageYank(client, args) {
961
- const result = await client.yankPackageVersion(args.package_id, args.version, !args.undo);
962
- if (result.yanked_at !== null) {
963
- console.error(`Yanked ${result.package_id} v${result.version} (${result.yanked_at}). ` +
964
- `New installs/upgrades refuse it; pinned installations keep running.`);
965
- }
966
- else {
967
- console.error(`Restored ${result.package_id} v${result.version} — installable again.`);
968
- }
969
- console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
970
- }
971
- /**
972
- * `lotics upgrade <package_id> [--version N]` (fleet path) — bring every
973
- * installation of the package across the caller's org to the target version.
974
- * Hands-off applies only where the preview is clean; skipped/failed
975
- * installations are reported per line and the process exits 1 so a release
976
- * script can gate on "fleet fully current".
977
- */
978
- async function packageFleetUpgrade(client, args) {
979
- const result = await client.fleetUpgradePackage(args.package_id, {
980
- ...(args.version !== undefined ? { version: args.version } : {}),
981
- });
982
- console.error(`Fleet upgrade of ${result.package_id} → v${result.target_version}:`);
983
- if (result.installations.length === 0) {
984
- console.error(" No installations of this package in your organization.");
985
- return;
986
- }
987
- const counts = { upgraded: 0, up_to_date: 0, skipped: 0, failed: 0 };
988
- for (const inst of result.installations) {
989
- counts[inst.outcome]++;
990
- const from = inst.from_version === null ? "?" : `v${inst.from_version}`;
991
- const line = ` [${inst.outcome}] ${inst.workspace_name} — ${inst.app_name} (${from} → v${result.target_version})`;
992
- if (inst.outcome === "skipped" && inst.blockers) {
993
- console.error(`${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified}`);
994
- console.error(` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`);
995
- }
996
- else if (inst.message) {
997
- console.error(`${line}: ${inst.message}`);
998
- }
999
- else {
1000
- console.error(line);
1001
- }
1002
- }
1003
- console.error(`Done: ${counts.upgraded} upgraded, ${counts.up_to_date} already current, ${counts.skipped} skipped, ${counts.failed} failed.`);
1004
- if (counts.skipped > 0 || counts.failed > 0) {
1005
- process.exitCode = 1;
1006
- }
1007
- }