@lotics/cli 0.76.0 → 0.76.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/args.d.ts +15 -2
- package/dist/args.js +21 -0
- package/dist/cli.js +82 -31
- package/dist/client.d.ts +171 -30
- package/dist/client.js +77 -32
- package/dist/package_commands.d.ts +194 -32
- package/dist/package_commands.js +846 -93
- package/dist/package_commands.test.js +354 -5
- package/dist/src/cli.js +18856 -1538
- package/package.json +1 -1
package/dist/package_commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `lotics package *` subcommands — the app-package authoring + dev/test/run loop
|
|
3
|
-
* (see docs/
|
|
3
|
+
* (see docs/packages.md). An app *package* is a versioned, workspace-agnostic
|
|
4
4
|
* blueprint Lotics maintains once and installs into many workspaces. This module
|
|
5
5
|
* implements the full harness:
|
|
6
6
|
*
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
27
28
|
import "./client.js";
|
|
29
|
+
import { knowledgeEntryNeedsConsent, validKnowledgeResolutions, } from "@lotics/shared/schemas/packages";
|
|
28
30
|
import { buildStarterTemplate, buildPackageStarterOverrides, STARTER_FALLBACK_SDK_VERSION, } from "./starter_template.js";
|
|
29
31
|
import { generatePackageAppFields, } from "./generate_package_fields.js";
|
|
30
32
|
import { appDirName, fetchLatestNpmVersion, runNpm, runTar, writeAppDts } from "./app_commands.js";
|
|
@@ -37,6 +39,13 @@ const CONTRACT_FILE = "contract.json";
|
|
|
37
39
|
* is in `SOURCE_STAGE_EXCLUDES` — so it never rides along in a published bundle.
|
|
38
40
|
*/
|
|
39
41
|
const ADOPT_BINDING_FILE = "adopt_binding.json";
|
|
42
|
+
const PACKAGE_TEMPLATE_KINDS = new Set([
|
|
43
|
+
"html",
|
|
44
|
+
"email",
|
|
45
|
+
"excel",
|
|
46
|
+
"word",
|
|
47
|
+
"pdf-form",
|
|
48
|
+
]);
|
|
40
49
|
function packageJsonPath(projectDir) {
|
|
41
50
|
return path.join(projectDir, "package.json");
|
|
42
51
|
}
|
|
@@ -65,11 +74,51 @@ export function readPackageProject(projectDir) {
|
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
}
|
|
77
|
+
const knowledge = {};
|
|
78
|
+
if (isPlainObject(pkg.knowledge)) {
|
|
79
|
+
for (const [alias, entry] of Object.entries(pkg.knowledge)) {
|
|
80
|
+
if (!isPlainObject(entry) || typeof entry.name !== "string" || entry.name.length === 0) {
|
|
81
|
+
throw new Error(`${pkgPath}: lotics.package.knowledge.${alias} must be an object with a non-empty "name".`);
|
|
82
|
+
}
|
|
83
|
+
knowledge[alias] = {
|
|
84
|
+
name: entry.name,
|
|
85
|
+
description: typeof entry.description === "string" ? entry.description : null,
|
|
86
|
+
active_by_default: typeof entry.active_by_default === "boolean" ? entry.active_by_default : true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const templates = {};
|
|
91
|
+
if (isPlainObject(pkg.templates)) {
|
|
92
|
+
for (const [alias, entry] of Object.entries(pkg.templates)) {
|
|
93
|
+
if (!isPlainObject(entry) ||
|
|
94
|
+
typeof entry.name !== "string" ||
|
|
95
|
+
entry.name.length === 0 ||
|
|
96
|
+
typeof entry.type !== "string" ||
|
|
97
|
+
!PACKAGE_TEMPLATE_KINDS.has(entry.type) ||
|
|
98
|
+
typeof entry.file !== "string" ||
|
|
99
|
+
entry.file.length === 0) {
|
|
100
|
+
throw new Error(`${pkgPath}: lotics.package.templates.${alias} must be an object with a non-empty "name", ` +
|
|
101
|
+
`a "type" of html|email|excel|word|pdf-form, and a non-empty "file" (a name in templates/).`);
|
|
102
|
+
}
|
|
103
|
+
templates[alias] = {
|
|
104
|
+
name: entry.name,
|
|
105
|
+
type: entry.type,
|
|
106
|
+
file: entry.file,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const knowledge_expects = Array.isArray(pkg.knowledge_expects)
|
|
111
|
+
? pkg.knowledge_expects.filter((v) => typeof v === "string")
|
|
112
|
+
: [];
|
|
68
113
|
const manifest = {
|
|
69
114
|
id: typeof pkg.id === "string" ? pkg.id : null,
|
|
70
115
|
name: pkg.name,
|
|
71
116
|
description: typeof pkg.description === "string" ? pkg.description : null,
|
|
117
|
+
kind: pkg.kind === "content" ? "content" : "app",
|
|
72
118
|
version: typeof pkg.version === "number" ? pkg.version : null,
|
|
119
|
+
knowledge,
|
|
120
|
+
templates,
|
|
121
|
+
knowledge_expects,
|
|
73
122
|
dev,
|
|
74
123
|
};
|
|
75
124
|
return { pkgJson: parsed, manifest };
|
|
@@ -118,7 +167,18 @@ export function draftPackageProjectFromApp(appPkgJson, args) {
|
|
|
118
167
|
const { lotics: _appManifest, ...rest } = appPkgJson;
|
|
119
168
|
return {
|
|
120
169
|
pkgJson: rest,
|
|
121
|
-
manifest: {
|
|
170
|
+
manifest: {
|
|
171
|
+
id: null,
|
|
172
|
+
name: args.name,
|
|
173
|
+
description: args.description,
|
|
174
|
+
// Extraction promotes a bespoke APP to a package — always app kind.
|
|
175
|
+
kind: "app",
|
|
176
|
+
version: null,
|
|
177
|
+
knowledge: {},
|
|
178
|
+
templates: {},
|
|
179
|
+
knowledge_expects: [],
|
|
180
|
+
dev: {},
|
|
181
|
+
},
|
|
122
182
|
};
|
|
123
183
|
}
|
|
124
184
|
/**
|
|
@@ -140,11 +200,21 @@ export function parseAdoptBindingFile(raw, expectedAppId) {
|
|
|
140
200
|
`run "lotics package extract ${expectedAppId}" to produce the right pin.`);
|
|
141
201
|
}
|
|
142
202
|
// Boundary adapter: the file is untyped JSON; `isPlainObject` proved the shape
|
|
143
|
-
// and the CLI passes the
|
|
203
|
+
// and the CLI passes the maps straight to the server (the validating authority).
|
|
204
|
+
// `knowledge_binding` is optional (absent for a pre-knowledge extract / an app
|
|
205
|
+
// with no package-managed docs) → defaults to {}.
|
|
206
|
+
const knowledge_binding = {};
|
|
207
|
+
if (isPlainObject(raw.knowledge_binding)) {
|
|
208
|
+
for (const [alias, id] of Object.entries(raw.knowledge_binding)) {
|
|
209
|
+
if (typeof id === "string")
|
|
210
|
+
knowledge_binding[alias] = id;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
144
213
|
return {
|
|
145
214
|
app_id: raw.app_id,
|
|
146
215
|
workspace_id: raw.workspace_id,
|
|
147
216
|
binding: raw.binding,
|
|
217
|
+
knowledge_binding,
|
|
148
218
|
};
|
|
149
219
|
}
|
|
150
220
|
/**
|
|
@@ -199,6 +269,133 @@ function readContract(projectDir) {
|
|
|
199
269
|
}
|
|
200
270
|
return JSON.parse(fs.readFileSync(contractPath, "utf-8"));
|
|
201
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Fold a package project's knowledge corpus into its contract before publish:
|
|
274
|
+
* per `lotics.package.knowledge` alias, read `knowledge/<alias>.md`, sha256 the
|
|
275
|
+
* bytes, and emit the contract's `knowledge` namespace (alias → {name,
|
|
276
|
+
* description, content_ref, content_sha256, active_by_default}) +
|
|
277
|
+
* `knowledge_expects` (from `lotics.package.knowledge_expects`). This is the ONE
|
|
278
|
+
* place the sha is computed — it can't be hand-authored, which is why knowledge
|
|
279
|
+
* (unlike a template `bytes_ref`) is derived rather than written into
|
|
280
|
+
* contract.json. The content files themselves ride in `source.tar.gz`
|
|
281
|
+
* automatically: `knowledge/` is a top-level dir, so `stagePackageSource` copies
|
|
282
|
+
* it like any other source (the same mechanism that carries template bytes). Pure
|
|
283
|
+
* w.r.t. `contract` — returns a new object; the server re-validates the folded
|
|
284
|
+
* namespace at publish.
|
|
285
|
+
*/
|
|
286
|
+
export function foldKnowledgeIntoContract(projectDir, project, contract) {
|
|
287
|
+
const aliases = Object.keys(project.manifest.knowledge);
|
|
288
|
+
const expects = project.manifest.knowledge_expects;
|
|
289
|
+
if (aliases.length === 0 && expects.length === 0)
|
|
290
|
+
return contract;
|
|
291
|
+
const knowledge = {};
|
|
292
|
+
for (const alias of aliases) {
|
|
293
|
+
const entry = project.manifest.knowledge[alias];
|
|
294
|
+
const contentRef = `knowledge/${alias}.md`;
|
|
295
|
+
const filePath = path.join(projectDir, "knowledge", `${alias}.md`);
|
|
296
|
+
if (!fs.existsSync(filePath)) {
|
|
297
|
+
throw new Error(`Declared knowledge doc "${alias}" (lotics.package.knowledge) has no ${contentRef} in the project. ` +
|
|
298
|
+
`Create the file, or remove the manifest entry.`);
|
|
299
|
+
}
|
|
300
|
+
const bytes = fs.readFileSync(filePath);
|
|
301
|
+
knowledge[alias] = {
|
|
302
|
+
name: entry.name,
|
|
303
|
+
description: entry.description ?? "",
|
|
304
|
+
content_ref: contentRef,
|
|
305
|
+
content_sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
306
|
+
active_by_default: entry.active_by_default,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return { ...contract, knowledge, knowledge_expects: expects };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Fold a CONTENT package's manifest-declared templates into its contract before
|
|
313
|
+
* publish: per `lotics.package.templates` alias, an INLINE kind (html/email) reads
|
|
314
|
+
* `templates/<file>` into the contract template's `content`; a FILE-BACKED kind
|
|
315
|
+
* (excel/word/pdf-form) records `bytes_ref: templates/<file>` (the bytes ride in
|
|
316
|
+
* `source.tar.gz` — `templates/` is a top-level dir `stagePackageSource` copies,
|
|
317
|
+
* the same mechanism that carries knowledge content + app-package template bytes).
|
|
318
|
+
* The per-template `content_sha256` is NOT computed here — `foldTemplateShasIntoContract`
|
|
319
|
+
* runs after and derives it (inline: the utf-8 content; file-backed: the file bytes).
|
|
320
|
+
*
|
|
321
|
+
* Appends to any templates the contract already declares (an app package authors
|
|
322
|
+
* templates in `contract.json` directly; the manifest path is how a content
|
|
323
|
+
* package — with no `contract.json` templates — declares them). An alias in BOTH
|
|
324
|
+
* is a loud error. Pure w.r.t. `contract` — returns a new object.
|
|
325
|
+
*/
|
|
326
|
+
export function foldTemplatesIntoContract(projectDir, project, contract) {
|
|
327
|
+
if (!isPlainObject(contract)) {
|
|
328
|
+
throw new Error(`${CONTRACT_FILE} in ${projectDir} must be a JSON object.`);
|
|
329
|
+
}
|
|
330
|
+
const aliases = Object.keys(project.manifest.templates);
|
|
331
|
+
if (aliases.length === 0)
|
|
332
|
+
return contract;
|
|
333
|
+
const existing = Array.isArray(contract.templates) ? contract.templates : [];
|
|
334
|
+
const existingAliases = new Set(existing
|
|
335
|
+
.filter((t) => isPlainObject(t))
|
|
336
|
+
.map((t) => t.alias)
|
|
337
|
+
.filter((a) => typeof a === "string"));
|
|
338
|
+
const folded = aliases.map((alias) => {
|
|
339
|
+
if (existingAliases.has(alias)) {
|
|
340
|
+
throw new Error(`Template alias "${alias}" is declared in both lotics.package.templates and ${CONTRACT_FILE} — ` +
|
|
341
|
+
`declare each template once.`);
|
|
342
|
+
}
|
|
343
|
+
const entry = project.manifest.templates[alias];
|
|
344
|
+
const filePath = path.join(projectDir, "templates", entry.file);
|
|
345
|
+
if (!fs.existsSync(filePath)) {
|
|
346
|
+
throw new Error(`Declared template "${alias}" (lotics.package.templates) has no templates/${entry.file} in the project. ` +
|
|
347
|
+
`Stage the file, then rebuild.`);
|
|
348
|
+
}
|
|
349
|
+
if (entry.type === "html" || entry.type === "email") {
|
|
350
|
+
return { alias, label: entry.name, type: entry.type, content: fs.readFileSync(filePath, "utf-8") };
|
|
351
|
+
}
|
|
352
|
+
return { alias, label: entry.name, type: entry.type, bytes_ref: `templates/${entry.file}` };
|
|
353
|
+
});
|
|
354
|
+
return { ...contract, templates: [...existing, ...folded] };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Fold a freshly-computed `content_sha256` into every template of a package
|
|
358
|
+
* contract before publish — the modified-detection reference the install/upgrade
|
|
359
|
+
* flow compares live content against. Like knowledge's sha (foldKnowledge…), it
|
|
360
|
+
* can't be hand-authored: an INLINE template's sha covers its utf-8 `content`; a
|
|
361
|
+
* FILE-BACKED template's sha covers the `bytes_ref` file's bytes (the same bytes
|
|
362
|
+
* that ride in `source.tar.gz` and install stores verbatim). Always recomputed
|
|
363
|
+
* (never trusted from contract.json), so the published contract's sha is exact.
|
|
364
|
+
* Pure w.r.t. `contract` — returns a new object; the server re-validates.
|
|
365
|
+
*/
|
|
366
|
+
export function foldTemplateShasIntoContract(projectDir, contract) {
|
|
367
|
+
const templates = contract.templates;
|
|
368
|
+
if (!Array.isArray(templates) || templates.length === 0)
|
|
369
|
+
return contract;
|
|
370
|
+
const withShas = templates.map((template) => {
|
|
371
|
+
if (!isPlainObject(template) ||
|
|
372
|
+
typeof template.type !== "string" ||
|
|
373
|
+
typeof template.alias !== "string") {
|
|
374
|
+
// Malformed — leave as-is; the server's contract validator rejects it.
|
|
375
|
+
return template;
|
|
376
|
+
}
|
|
377
|
+
if (template.type === "html" || template.type === "email") {
|
|
378
|
+
if (typeof template.content !== "string") {
|
|
379
|
+
throw new Error(`Inline template "${template.alias}" must carry a string "content".`);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
...template,
|
|
383
|
+
content_sha256: createHash("sha256").update(Buffer.from(template.content, "utf-8")).digest("hex"),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (typeof template.bytes_ref !== "string") {
|
|
387
|
+
throw new Error(`File-backed template "${template.alias}" must carry a string "bytes_ref".`);
|
|
388
|
+
}
|
|
389
|
+
const filePath = path.join(projectDir, template.bytes_ref);
|
|
390
|
+
if (!fs.existsSync(filePath)) {
|
|
391
|
+
throw new Error(`Template "${template.alias}" references "${template.bytes_ref}", which is not in the project. ` +
|
|
392
|
+
`Stage the file, then rebuild.`);
|
|
393
|
+
}
|
|
394
|
+
const bytes = fs.readFileSync(filePath);
|
|
395
|
+
return { ...template, content_sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
396
|
+
});
|
|
397
|
+
return { ...contract, templates: withShas };
|
|
398
|
+
}
|
|
202
399
|
/**
|
|
203
400
|
* (Re)generate the package project's `.lotics/app_fields.ts` from its
|
|
204
401
|
* contract.json — the runtime-resolving F/OPT/ROLE surface (see
|
|
@@ -242,23 +439,39 @@ export function stagePackageSource(projectDir, sourceStage) {
|
|
|
242
439
|
fs.writeFileSync(path.join(sourceStage, "package.json"), JSON.stringify(sanitizePackageJsonForSource(project.pkgJson), null, 2) + "\n");
|
|
243
440
|
}
|
|
244
441
|
/**
|
|
245
|
-
* Build the publishable bundle:
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
442
|
+
* Build the publishable bundle: a gzipped tarball carrying `source.tar.gz` (the
|
|
443
|
+
* pullable project tree) + `dist.tar.gz` (the prebuilt assets) as its two
|
|
444
|
+
* top-level members — the publish↔install contract the backend's
|
|
445
|
+
* `extractPackageBundle` reads (both members are required). Returns the bytes.
|
|
446
|
+
*
|
|
447
|
+
* An `app` package compiles its frontend (`npm run build` → `dist/`). A
|
|
448
|
+
* `content` package has NO app source to compile, so the build is skipped and a
|
|
449
|
+
* minimal placeholder `dist.tar.gz` is shipped: the bundle format requires the
|
|
450
|
+
* member, but a content install never deploys a frontend (it only reads the
|
|
451
|
+
* `knowledge/*.md` files from `source.tar.gz`), so nothing ever reads it.
|
|
249
452
|
*/
|
|
250
453
|
async function buildPackageBundle(projectDir) {
|
|
251
|
-
|
|
252
|
-
await runNpm(["run", "build"], projectDir);
|
|
253
|
-
const distDir = path.join(projectDir, "dist");
|
|
254
|
-
if (!fs.existsSync(distDir)) {
|
|
255
|
-
throw new Error(`Build did not produce a dist/ directory in ${projectDir}. ` +
|
|
256
|
-
`Check that 'npm run build' is configured correctly.`);
|
|
257
|
-
}
|
|
454
|
+
const { manifest } = readPackageProject(projectDir);
|
|
258
455
|
// Stage the two member archives in a temp dir, then tar them into the bundle.
|
|
259
456
|
const stage = fs.mkdtempSync(path.join(tmpdir(), "lotics-pkg-"));
|
|
260
457
|
const bundlePath = path.join(tmpdir(), `lotics-bundle-${Date.now()}.tar.gz`);
|
|
261
458
|
try {
|
|
459
|
+
let distDir;
|
|
460
|
+
if (manifest.kind === "content") {
|
|
461
|
+
console.error("Content package — skipping app build.");
|
|
462
|
+
distDir = path.join(stage, "empty-dist");
|
|
463
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
464
|
+
fs.writeFileSync(path.join(distDir, ".content-package"), "Content package — no frontend bundle. Content ships in source.tar.gz under knowledge/.\n");
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
console.error("Building...");
|
|
468
|
+
await runNpm(["run", "build"], projectDir);
|
|
469
|
+
distDir = path.join(projectDir, "dist");
|
|
470
|
+
if (!fs.existsSync(distDir)) {
|
|
471
|
+
throw new Error(`Build did not produce a dist/ directory in ${projectDir}. ` +
|
|
472
|
+
`Check that 'npm run build' is configured correctly.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
262
475
|
console.error("Packaging source...");
|
|
263
476
|
// Stage a copy of the source tree with EXPLICIT top-level excludes, then
|
|
264
477
|
// tar the stage with no --exclude flags at all. tar exclude patterns are
|
|
@@ -328,12 +541,79 @@ export function starterContract(name) {
|
|
|
328
541
|
],
|
|
329
542
|
};
|
|
330
543
|
}
|
|
544
|
+
/** An empty contract — all app namespaces empty (knowledge is folded in at build). */
|
|
545
|
+
function emptyContract() {
|
|
546
|
+
return {
|
|
547
|
+
entities: [],
|
|
548
|
+
roles: [],
|
|
549
|
+
templates: [],
|
|
550
|
+
queries: [],
|
|
551
|
+
workflows: [],
|
|
552
|
+
agents: [],
|
|
553
|
+
config: [],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
331
556
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
557
|
+
* Scaffold a CONTENT package project — a standalone doc corpus, NO app source.
|
|
558
|
+
* Just a `lotics.package` manifest (kind: "content"), an empty base
|
|
559
|
+
* `contract.json` (the app namespaces MUST stay empty for a content package;
|
|
560
|
+
* `build` folds the docs into the `knowledge` namespace), and a `knowledge/` dir
|
|
561
|
+
* with one example doc wired into the manifest so the scaffold is immediately
|
|
562
|
+
* publishable (publish refuses a content package with zero docs). No npm
|
|
563
|
+
* install — a content package has no dependencies to compile.
|
|
564
|
+
*/
|
|
565
|
+
function scaffoldContentPackage(name, targetPath) {
|
|
566
|
+
const exampleAlias = "overview";
|
|
567
|
+
const manifest = {
|
|
568
|
+
id: null,
|
|
569
|
+
name,
|
|
570
|
+
description: null,
|
|
571
|
+
kind: "content",
|
|
572
|
+
version: null,
|
|
573
|
+
knowledge: {
|
|
574
|
+
[exampleAlias]: {
|
|
575
|
+
name: `${name} Overview`,
|
|
576
|
+
description: "What this corpus covers — replace with your doc's summary.",
|
|
577
|
+
active_by_default: true,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
// A content package can also ship standalone document templates — declare them
|
|
581
|
+
// under `lotics.package.templates` with the files in a `templates/` dir. See
|
|
582
|
+
// the commented example printed by the scaffold + docs/packages.md § Content.
|
|
583
|
+
templates: {},
|
|
584
|
+
knowledge_expects: [],
|
|
585
|
+
dev: {},
|
|
586
|
+
};
|
|
587
|
+
const pkgJson = {
|
|
588
|
+
name: appDirName(name),
|
|
589
|
+
private: true,
|
|
590
|
+
version: "0.0.0",
|
|
591
|
+
lotics: { package: manifest },
|
|
592
|
+
};
|
|
593
|
+
fs.writeFileSync(packageJsonPath(targetPath), JSON.stringify(pkgJson, null, 2) + "\n");
|
|
594
|
+
fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(emptyContract(), null, 2) + "\n");
|
|
595
|
+
const knowledgeDir = path.join(targetPath, "knowledge");
|
|
596
|
+
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
597
|
+
fs.writeFileSync(path.join(knowledgeDir, `${exampleAlias}.md`), `# ${name}\n\nReplace this file with the corpus content. Structure it under clear\n` +
|
|
598
|
+
`Markdown headers so an agent can outline and read it by section.\n`);
|
|
599
|
+
console.error(`Scaffolded a content package into ${targetPath}`);
|
|
600
|
+
console.error(`\nReady. Next steps:`);
|
|
601
|
+
console.error(` cd ${path.relative(process.cwd(), targetPath) || "."}`);
|
|
602
|
+
console.error(` # add docs: create knowledge/<alias>.md + a lotics.package.knowledge.<alias> entry`);
|
|
603
|
+
console.error(` # (name/description/active_by_default), then publish:`);
|
|
604
|
+
console.error(` # add templates: create templates/<file> + a lotics.package.templates.<alias> entry:`);
|
|
605
|
+
console.error(` # "templates": { "quote": { "name": "Quote", "type": "html", "file": "quote.html" } }`);
|
|
606
|
+
console.error(` # inline kinds html|email read templates/<file> as content; file-backed`);
|
|
607
|
+
console.error(` # excel|word|pdf-form pack the bytes into the bundle (bytes_ref = templates/<file>).`);
|
|
608
|
+
console.error(` lotics package publish -m "v1"`);
|
|
609
|
+
console.error(` lotics package install <package_id>`);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* `lotics package new <name> [path] [--kind content]` — scaffold a package
|
|
613
|
+
* project. An `app` package reuses the app starter (Vite+React+TS) for the code
|
|
614
|
+
* surface, swaps its app manifest for a package manifest, and adds a starter
|
|
615
|
+
* `contract.json`; a `content` package scaffolds a docs-only corpus (no app
|
|
616
|
+
* source). The project publishes with `lotics package publish`.
|
|
337
617
|
*/
|
|
338
618
|
export async function packageNew(args) {
|
|
339
619
|
const targetPath = path.resolve(args.targetPath ?? appDirName(args.name));
|
|
@@ -341,6 +621,10 @@ export async function packageNew(args) {
|
|
|
341
621
|
throw new Error(`Target directory ${targetPath} is not empty.`);
|
|
342
622
|
}
|
|
343
623
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
624
|
+
if ((args.kind ?? "app") === "content") {
|
|
625
|
+
scaffoldContentPackage(args.name, targetPath);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
344
628
|
// The starter bakes an app manifest (app_id/workspace_id) into package.json;
|
|
345
629
|
// a package is workspace-agnostic, so those placeholders are replaced with the
|
|
346
630
|
// package manifest below. The rest of the starter (config, src, tests) is the
|
|
@@ -375,7 +659,11 @@ export async function packageNew(args) {
|
|
|
375
659
|
id: null,
|
|
376
660
|
name: args.name,
|
|
377
661
|
description: null,
|
|
662
|
+
kind: "app",
|
|
378
663
|
version: null,
|
|
664
|
+
knowledge: {},
|
|
665
|
+
templates: {},
|
|
666
|
+
knowledge_expects: [],
|
|
379
667
|
dev: {},
|
|
380
668
|
};
|
|
381
669
|
pkg.lotics = { package: manifest };
|
|
@@ -418,12 +706,25 @@ export async function packageBuild(args) {
|
|
|
418
706
|
*/
|
|
419
707
|
async function publishVersion(client, projectDir, opts) {
|
|
420
708
|
const project = readPackageProject(projectDir);
|
|
421
|
-
const
|
|
709
|
+
const rawContract = readContract(projectDir);
|
|
710
|
+
if (!isPlainObject(rawContract)) {
|
|
711
|
+
throw new Error(`${CONTRACT_FILE} in ${projectDir} must be a JSON object.`);
|
|
712
|
+
}
|
|
713
|
+
// Fold the manifest-declared templates + the knowledge corpus + the per-template
|
|
714
|
+
// content shas into the contract before it is sent. Order: manifest templates
|
|
715
|
+
// FIRST (so their bytes/content land in the contract), then knowledge, then the
|
|
716
|
+
// sha fold LAST (it hashes whatever templates now exist — manifest-folded +
|
|
717
|
+
// contract-authored). All shas are derived (never hand-authored) so the
|
|
718
|
+
// published contract's modified-detection references are exact.
|
|
719
|
+
const contract = foldTemplateShasIntoContract(projectDir, foldKnowledgeIntoContract(projectDir, project, foldTemplatesIntoContract(projectDir, project, rawContract)));
|
|
422
720
|
let packageId = project.manifest.id;
|
|
423
721
|
if (packageId === null) {
|
|
424
|
-
const pkg = await client.
|
|
722
|
+
const pkg = await client.createPackage({
|
|
425
723
|
name: project.manifest.name,
|
|
426
724
|
description: project.manifest.description,
|
|
725
|
+
// Kind is fixed at package creation and immutable after — a knowledge
|
|
726
|
+
// package must be created as such so publish enforces its constraint.
|
|
727
|
+
kind: project.manifest.kind,
|
|
427
728
|
});
|
|
428
729
|
packageId = pkg.id;
|
|
429
730
|
project.manifest.id = packageId;
|
|
@@ -432,7 +733,7 @@ async function publishVersion(client, projectDir, opts) {
|
|
|
432
733
|
}
|
|
433
734
|
const bundle = await buildPackageBundle(projectDir);
|
|
434
735
|
console.error("Publishing version...");
|
|
435
|
-
const version = await client.
|
|
736
|
+
const version = await client.publishPackageVersion(packageId, {
|
|
436
737
|
contract,
|
|
437
738
|
bundle,
|
|
438
739
|
changelog: opts.changelog ?? null,
|
|
@@ -507,13 +808,19 @@ async function syncToDevWorkspace(client, projectDir) {
|
|
|
507
808
|
let appId;
|
|
508
809
|
if (existing) {
|
|
509
810
|
console.error(`Upgrading dev installation ${existing.app_id} → v${version}...`);
|
|
510
|
-
const app = await client.
|
|
811
|
+
const app = await client.upgradePackage(existing.app_id, { version });
|
|
511
812
|
appId = app.id;
|
|
512
813
|
}
|
|
513
814
|
else {
|
|
514
815
|
console.error(`Installing ${package_id} v${version} into dev workspace ${devWorkspaceId}...`);
|
|
515
|
-
const
|
|
516
|
-
|
|
816
|
+
const result = await client.installPackage(package_id, { version });
|
|
817
|
+
// The dev/sync loop scaffolds an APP installation to run the dev server
|
|
818
|
+
// against; a content package has no app to run.
|
|
819
|
+
if (result.kind !== "app") {
|
|
820
|
+
throw new Error(`Package ${package_id} is a content package — "lotics package dev/sync" run an app installation. ` +
|
|
821
|
+
`Install a content package with "lotics package install ${package_id}".`);
|
|
822
|
+
}
|
|
823
|
+
appId = result.app.id;
|
|
517
824
|
}
|
|
518
825
|
project.manifest.dev[devWorkspaceId] = { app_id: appId, version };
|
|
519
826
|
writePackageManifest(projectDir, project);
|
|
@@ -606,7 +913,7 @@ export async function packageReset(client, args) {
|
|
|
606
913
|
throw new Error(`No dev installation recorded for workspace ${devWorkspaceId}. Run "lotics package dev --workspace ${devWorkspaceId}" first.`);
|
|
607
914
|
}
|
|
608
915
|
console.error(`Resetting dev installation ${pin.app_id} in workspace ${devWorkspaceId}...`);
|
|
609
|
-
const app = await client.
|
|
916
|
+
const app = await client.resetPackage(pin.app_id);
|
|
610
917
|
console.error(`Reset ${app.name} → ${app.id}. The scaffolded tables were dropped and re-created clean.`);
|
|
611
918
|
}
|
|
612
919
|
/**
|
|
@@ -664,7 +971,7 @@ export function parseResolveFlags(resolve) {
|
|
|
664
971
|
*/
|
|
665
972
|
export async function packageDoctor(client, args) {
|
|
666
973
|
const app_id = resolveInstallationAppId(client, args.app_id);
|
|
667
|
-
const health = await client.
|
|
974
|
+
const health = await client.getPackageHealth(app_id);
|
|
668
975
|
console.error(`${health.package_name} — installation ${health.app_id}`);
|
|
669
976
|
console.error(` Installed: v${health.installed_version} Latest: v${health.latest_version}` +
|
|
670
977
|
(health.update_available ? " → update available" : ""));
|
|
@@ -692,17 +999,52 @@ export async function packageDoctor(client, args) {
|
|
|
692
999
|
` --resolve <kind.alias>=revert (or =keep to retain the edit)`);
|
|
693
1000
|
process.exitCode = 1;
|
|
694
1001
|
}
|
|
1002
|
+
// Package-managed knowledge (bundled with this app install): drift + local edits
|
|
1003
|
+
// + unmet expects. All advisory; resolved through the app's package upgrade.
|
|
1004
|
+
if (health.knowledge_drift.length > 0) {
|
|
1005
|
+
console.error(` Knowledge binding drift (${health.knowledge_drift.length}):`);
|
|
1006
|
+
for (const d of health.knowledge_drift) {
|
|
1007
|
+
console.error(` - ${d.alias} "${d.name}" → ${d.doc_id ?? "(unbound)"} (missing from the workspace)`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (health.knowledge_modified.length > 0) {
|
|
1011
|
+
console.error(` Locally edited package knowledge (${health.knowledge_modified.length}):`);
|
|
1012
|
+
for (const m of health.knowledge_modified) {
|
|
1013
|
+
console.error(` - ${m.alias} "${m.name}" (an upgrade overwrites this unless kept)`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (health.missing_expected_docs.length > 0) {
|
|
1017
|
+
console.error(` Missing expected knowledge docs (${health.missing_expected_docs.length}):`);
|
|
1018
|
+
for (const name of health.missing_expected_docs) {
|
|
1019
|
+
console.error(` - "${name}" (the package's agents route to this name; no matching doc exists)`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (health.knowledge_drift.length > 0 || health.knowledge_modified.length > 0) {
|
|
1023
|
+
process.exitCode = 1;
|
|
1024
|
+
}
|
|
1025
|
+
if (health.knowledge_drift.length === 0 &&
|
|
1026
|
+
health.knowledge_modified.length === 0 &&
|
|
1027
|
+
health.missing_expected_docs.length === 0) {
|
|
1028
|
+
console.error(" Knowledge: healthy — bound docs resolve, none locally edited, expects met.");
|
|
1029
|
+
}
|
|
695
1030
|
if (health.update_available) {
|
|
696
1031
|
console.error(` Upgrade: lotics package upgrade ${app_id}`);
|
|
697
1032
|
}
|
|
698
1033
|
}
|
|
699
1034
|
/**
|
|
700
|
-
* Preview-then-apply upgrade. Prints the additive plan +
|
|
701
|
-
* removals; refuses (exit 1, with
|
|
702
|
-
* binding drift
|
|
1035
|
+
* Preview-then-apply an app-installation upgrade. Prints the additive plan +
|
|
1036
|
+
* informational removals + any bundled-knowledge changes; refuses (exit 1, with
|
|
1037
|
+
* the exact --resolve syntax) while any binding drift, modified core artifact, or
|
|
1038
|
+
* consent-requiring bundled-knowledge doc lacks a resolution.
|
|
1039
|
+
*
|
|
1040
|
+
* `--resolve` is shared across the finding classes: a key naming a bundled
|
|
1041
|
+
* knowledge doc routes to `knowledge_resolutions` (apply|keep|archive|recreate|
|
|
1042
|
+
* unbind), everything else to core `resolutions`; `--bind-to` consents an
|
|
1043
|
+
* added-knowledge-doc name collision; `--apply-all` accepts the package's version
|
|
1044
|
+
* for every consent-requiring knowledge doc (overwriting local edits).
|
|
703
1045
|
*/
|
|
704
1046
|
export async function packageUpgrade(client, args) {
|
|
705
|
-
const preview = await client.
|
|
1047
|
+
const preview = await client.previewPackageUpgrade(args.app_id, {
|
|
706
1048
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
707
1049
|
});
|
|
708
1050
|
console.error(`Upgrade v${preview.from_version} → v${preview.to_version}` +
|
|
@@ -717,6 +1059,12 @@ export async function packageUpgrade(client, args) {
|
|
|
717
1059
|
console.error(` Adds: ${added}`);
|
|
718
1060
|
if (removed)
|
|
719
1061
|
console.error(` Unbinds (workspace data kept): ${removed}`);
|
|
1062
|
+
if (preview.knowledge.length > 0) {
|
|
1063
|
+
console.error(` Knowledge docs (${preview.knowledge.length}):`);
|
|
1064
|
+
for (const entry of preview.knowledge) {
|
|
1065
|
+
console.error(` ${formatKnowledgeEntryLine(entry)}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
720
1068
|
// Breaking contract changes are NOT resolvable via --resolve — scaffold would
|
|
721
1069
|
// refuse them on a bound alias, and there is no in-flow remediation. Report
|
|
722
1070
|
// every entry and the only two real remedies, then hard-stop.
|
|
@@ -730,9 +1078,31 @@ export async function packageUpgrade(client, args) {
|
|
|
730
1078
|
` before making the change: lotics package eject ${args.app_id}`);
|
|
731
1079
|
process.exit(1);
|
|
732
1080
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1081
|
+
// Route the shared --resolve flags: a knowledge-doc alias resolves the doc, a
|
|
1082
|
+
// core artifact key resolves the artifact; a key that is BOTH throws.
|
|
1083
|
+
const coreKeys = new Set([
|
|
1084
|
+
...preview.drift.map((d) => `${d.namespace}.${d.alias}`),
|
|
1085
|
+
...preview.modified.map((m) => `${m.kind}.${m.alias}`),
|
|
1086
|
+
]);
|
|
1087
|
+
const { coreResolve, knowledgeResolutions } = routeAppUpgradeResolve(args.resolve, preview.knowledge, coreKeys);
|
|
1088
|
+
const resolutions = parseResolveFlags(coreResolve);
|
|
1089
|
+
if (args.applyAll) {
|
|
1090
|
+
for (const entry of preview.knowledge) {
|
|
1091
|
+
if (knowledgeEntryNeedsConsent(entry) && knowledgeResolutions[entry.alias] === undefined) {
|
|
1092
|
+
knowledgeResolutions[entry.alias] = knowledgeAcceptResolution(entry.change);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const knowledge_resolutions = {
|
|
1097
|
+
resolutions: knowledgeResolutions,
|
|
1098
|
+
bind_to: parseBindToFlags(args.bindTo),
|
|
1099
|
+
};
|
|
1100
|
+
const unresolvedDrift = preview.drift.filter((d) => resolutions[`${d.namespace}.${d.alias}`] === undefined);
|
|
1101
|
+
const unresolvedModified = preview.modified.filter((m) => resolutions[`${m.kind}.${m.alias}`] === undefined);
|
|
1102
|
+
const unresolvedKnowledge = preview.knowledge.filter((e) => knowledgeEntryNeedsConsent(e) && knowledgeResolutions[e.alias] === undefined);
|
|
1103
|
+
if (unresolvedDrift.length > 0 ||
|
|
1104
|
+
unresolvedModified.length > 0 ||
|
|
1105
|
+
unresolvedKnowledge.length > 0) {
|
|
736
1106
|
if (unresolvedDrift.length > 0) {
|
|
737
1107
|
console.error(" Binding drift must be resolved before upgrading:");
|
|
738
1108
|
for (const d of unresolvedDrift) {
|
|
@@ -745,14 +1115,36 @@ export async function packageUpgrade(client, args) {
|
|
|
745
1115
|
console.error(` --resolve ${m.kind}.${m.alias}=revert (overwrite the local edit; or =keep to retain it)`);
|
|
746
1116
|
}
|
|
747
1117
|
}
|
|
1118
|
+
if (unresolvedKnowledge.length > 0) {
|
|
1119
|
+
console.error(" Bundled knowledge docs need consent before upgrading (or --apply-all to accept the package's version for all):");
|
|
1120
|
+
for (const entry of unresolvedKnowledge) {
|
|
1121
|
+
console.error(formatKnowledgeResolveHint(entry));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
748
1124
|
process.exit(1);
|
|
749
1125
|
}
|
|
750
|
-
const
|
|
1126
|
+
const hasKnowledgeResolutions = Object.keys(knowledge_resolutions.resolutions).length > 0 ||
|
|
1127
|
+
Object.keys(knowledge_resolutions.bind_to).length > 0;
|
|
1128
|
+
const app = await client.upgradePackage(args.app_id, {
|
|
751
1129
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
752
|
-
...(Object.keys(
|
|
1130
|
+
...(Object.keys(resolutions).length > 0 ? { resolutions } : {}),
|
|
1131
|
+
...(hasKnowledgeResolutions ? { knowledge_resolutions } : {}),
|
|
753
1132
|
});
|
|
754
1133
|
console.error(`Upgraded ${app.name} → v${app.package_version} (${app.id}).`);
|
|
755
1134
|
}
|
|
1135
|
+
/** `--apply-all`'s accept-the-package resolution for a consent-requiring entry. */
|
|
1136
|
+
function knowledgeAcceptResolution(change) {
|
|
1137
|
+
switch (change) {
|
|
1138
|
+
case "changed":
|
|
1139
|
+
return "apply";
|
|
1140
|
+
case "removed":
|
|
1141
|
+
return "archive";
|
|
1142
|
+
case "drifted":
|
|
1143
|
+
return "recreate";
|
|
1144
|
+
case "added":
|
|
1145
|
+
return "apply"; // unreachable (added never needs consent), kept total.
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
756
1148
|
/** The install-consent trust line: official > your own org > third-party. */
|
|
757
1149
|
function trustBadge(pkg) {
|
|
758
1150
|
if (pkg.is_official)
|
|
@@ -764,11 +1156,12 @@ function trustBadge(pkg) {
|
|
|
764
1156
|
/**
|
|
765
1157
|
* `lotics package show <package_id>` — registry metadata + version history
|
|
766
1158
|
* (trust badge, retirement, per-version channel/yank/changelog). The read
|
|
767
|
-
* surface for "what is this package and what shipped when".
|
|
1159
|
+
* surface for "what is this package and what shipped when". Kind-agnostic — a
|
|
1160
|
+
* content package shows here the same way.
|
|
768
1161
|
*/
|
|
769
1162
|
export async function packageShow(client, args) {
|
|
770
|
-
const pkg = await client.
|
|
771
|
-
const { versions } = await client.
|
|
1163
|
+
const pkg = await client.getPackage(args.package_id);
|
|
1164
|
+
const { versions } = await client.listPackageVersions(args.package_id);
|
|
772
1165
|
console.error(`${pkg.name} (${pkg.id}) — ${trustBadge(pkg)}`);
|
|
773
1166
|
if (pkg.description)
|
|
774
1167
|
console.error(` ${pkg.description}`);
|
|
@@ -787,22 +1180,338 @@ export async function packageShow(client, args) {
|
|
|
787
1180
|
if (versions.length === 0)
|
|
788
1181
|
console.error(" (no published versions)");
|
|
789
1182
|
}
|
|
1183
|
+
/**
|
|
1184
|
+
* One preview line for a knowledge upgrade entry — `[change] alias "name"` with
|
|
1185
|
+
* `modified` / `needs consent` marks. Shared verbatim by the standalone (`pci_`)
|
|
1186
|
+
* and the app-install upgrade previews so both speak one vocabulary; the caller
|
|
1187
|
+
* owns the leading indent.
|
|
1188
|
+
*/
|
|
1189
|
+
function formatKnowledgeEntryLine(entry) {
|
|
1190
|
+
const marks = [
|
|
1191
|
+
entry.modified ? "modified" : null,
|
|
1192
|
+
knowledgeEntryNeedsConsent(entry) ? "needs consent" : null,
|
|
1193
|
+
].filter((m) => m !== null);
|
|
1194
|
+
return `[${entry.change}] ${entry.alias} "${entry.name}"${marks.length ? ` (${marks.join(", ")})` : ""}`;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* The exact `--resolve <alias>=<valid options>` remediation line for a
|
|
1198
|
+
* consent-requiring knowledge entry (options from the shared
|
|
1199
|
+
* `validKnowledgeResolutions`). Shared by both upgrade paths' gate output.
|
|
1200
|
+
*/
|
|
1201
|
+
function formatKnowledgeResolveHint(entry) {
|
|
1202
|
+
const note = entry.change === "changed" || entry.change === "removed"
|
|
1203
|
+
? " (a local edit — apply/archive overwrites it; keep retains it)"
|
|
1204
|
+
: " (bound doc is gone; recreate from the package, or unbind)";
|
|
1205
|
+
return ` --resolve ${entry.alias}=${validKnowledgeResolutions(entry.change).join("|")}${note}`;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Route an app-install upgrade's `--resolve` entries into the core-artifact
|
|
1209
|
+
* bucket vs the bundled-knowledge bucket. `--resolve` is shared between the two
|
|
1210
|
+
* finding classes, so each key is matched against the preview: a key that names a
|
|
1211
|
+
* bundled knowledge doc resolves that doc (its value validated against the doc's
|
|
1212
|
+
* change class here — the app path has no separate knowledge-only parser);
|
|
1213
|
+
* anything else stays a core resolution (a `<namespace>.<alias>` drift, a
|
|
1214
|
+
* `<kind>.<alias>` modified core, or a key the server validates). A key that is
|
|
1215
|
+
* BOTH a knowledge alias and a core-artifact key is ambiguous — throw rather than
|
|
1216
|
+
* guess which the operator meant.
|
|
1217
|
+
*/
|
|
1218
|
+
export function routeAppUpgradeResolve(resolve, knowledgeEntries, coreKeys) {
|
|
1219
|
+
const byAlias = new Map(knowledgeEntries.map((e) => [e.alias, e]));
|
|
1220
|
+
const coreResolve = [];
|
|
1221
|
+
const knowledgeResolutions = {};
|
|
1222
|
+
for (const entry of resolve) {
|
|
1223
|
+
const eq = entry.indexOf("=");
|
|
1224
|
+
if (eq <= 0 || eq === entry.length - 1) {
|
|
1225
|
+
throw new Error(`Invalid --resolve "${entry}" — expected <key>=<value>.`);
|
|
1226
|
+
}
|
|
1227
|
+
const key = entry.slice(0, eq);
|
|
1228
|
+
const value = entry.slice(eq + 1);
|
|
1229
|
+
const known = byAlias.get(key);
|
|
1230
|
+
if (known && coreKeys.has(key)) {
|
|
1231
|
+
throw new Error(`--resolve "${key}" is ambiguous — it names both a bundled knowledge doc and a core artifact in this upgrade. ` +
|
|
1232
|
+
`Rename one alias so it is unambiguous; refusing to guess.`);
|
|
1233
|
+
}
|
|
1234
|
+
if (known) {
|
|
1235
|
+
const valid = validKnowledgeResolutions(known.change);
|
|
1236
|
+
const match = valid.find((v) => v === value);
|
|
1237
|
+
if (match === undefined) {
|
|
1238
|
+
throw new Error(`Invalid --resolve value "${value}" for knowledge doc "${key}" (${known.change}) — expected ${valid.join("|")}.`);
|
|
1239
|
+
}
|
|
1240
|
+
knowledgeResolutions[key] = match;
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
coreResolve.push(entry);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return { coreResolve, knowledgeResolutions };
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Route a STANDALONE content upgrade's shared `--resolve <alias>=<value>` flags
|
|
1250
|
+
* into the knowledge vs templates buckets by matching each alias against the two
|
|
1251
|
+
* preview namespaces. A doc alias takes apply|keep|archive|recreate|unbind; a
|
|
1252
|
+
* template alias takes revert|keep. An alias present in BOTH namespaces is
|
|
1253
|
+
* ambiguous → loud error (no guess). An alias in NEITHER → error. The value is
|
|
1254
|
+
* validated against the namespace it routes to. Exported for testing.
|
|
1255
|
+
*/
|
|
1256
|
+
export function routeContentResolveFlags(resolve, knowledgeAliases, templateAliases) {
|
|
1257
|
+
const knowledge = {};
|
|
1258
|
+
const templates = {};
|
|
1259
|
+
const knowledgeValid = new Set(["apply", "keep", "archive", "recreate", "unbind"]);
|
|
1260
|
+
const templateValid = new Set(["revert", "keep"]);
|
|
1261
|
+
for (const entry of resolve) {
|
|
1262
|
+
const eq = entry.indexOf("=");
|
|
1263
|
+
if (eq <= 0 || eq === entry.length - 1) {
|
|
1264
|
+
throw new Error(`Invalid --resolve "${entry}" — expected <alias>=<value>.`);
|
|
1265
|
+
}
|
|
1266
|
+
const key = entry.slice(0, eq);
|
|
1267
|
+
const value = entry.slice(eq + 1);
|
|
1268
|
+
const isKnowledge = knowledgeAliases.has(key);
|
|
1269
|
+
const isTemplate = templateAliases.has(key);
|
|
1270
|
+
if (isKnowledge && isTemplate) {
|
|
1271
|
+
throw new Error(`--resolve "${key}" is ambiguous — it names both a knowledge doc and a template in this upgrade. ` +
|
|
1272
|
+
`Rename one alias in the package so the two content namespaces are disjoint.`);
|
|
1273
|
+
}
|
|
1274
|
+
if (isTemplate) {
|
|
1275
|
+
if (!templateValid.has(value)) {
|
|
1276
|
+
throw new Error(`Invalid --resolve value "${value}" for template "${key}" — expected revert|keep.`);
|
|
1277
|
+
}
|
|
1278
|
+
templates[key] = value;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (isKnowledge) {
|
|
1282
|
+
if (!knowledgeValid.has(value)) {
|
|
1283
|
+
throw new Error(`Invalid --resolve value "${value}" for knowledge alias "${key}" — expected apply|keep|archive|recreate|unbind.`);
|
|
1284
|
+
}
|
|
1285
|
+
knowledge[key] = value;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
throw new Error(`--resolve "${key}" does not name a knowledge doc or template in this upgrade.`);
|
|
1289
|
+
}
|
|
1290
|
+
return { knowledge, templates };
|
|
1291
|
+
}
|
|
1292
|
+
/** The `--resolve <alias>=revert|keep` remediation line for a consent-requiring template. */
|
|
1293
|
+
function formatTemplateResolveHint(entry) {
|
|
1294
|
+
const note = entry.baseline_unknown
|
|
1295
|
+
? " (can't verify the local edit — older package version; revert overwrites, keep retains)"
|
|
1296
|
+
: " (a local edit — revert overwrites it with the package's version; keep retains it)";
|
|
1297
|
+
return ` --resolve ${entry.alias}=revert|keep${note}`;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Preview-then-apply a STANDALONE content installation upgrade — knowledge docs
|
|
1301
|
+
* AND document templates behind one review gate. Prints the per-alias plan;
|
|
1302
|
+
* refuses (exit 1, with the exact `--resolve` syntax) while any consent-requiring
|
|
1303
|
+
* entry lacks a resolution — a modified knowledge change/removal or drift, or a
|
|
1304
|
+
* locally-edited changed template. `--apply-all` auto-resolves EVERY consent entry
|
|
1305
|
+
* by accepting the package's version (knowledge changed→apply, removed→archive,
|
|
1306
|
+
* drifted→recreate; template→revert) — an explicit bulk "take upstream" that
|
|
1307
|
+
* discards local edits. `--resolve <alias>=<value>` and `--bind-to <alias>=<kdc_id>`
|
|
1308
|
+
* resolve entries individually (`--resolve` is routed to the right namespace by
|
|
1309
|
+
* matching the alias against the preview).
|
|
1310
|
+
*/
|
|
1311
|
+
export async function packageUpgradeKnowledge(client, args) {
|
|
1312
|
+
const preview = await client.previewContentInstallationUpgrade(args.installation_id, {
|
|
1313
|
+
...(args.version !== undefined ? { version: args.version } : {}),
|
|
1314
|
+
});
|
|
1315
|
+
console.error(`Content upgrade v${preview.from_version} → v${preview.to_version}` +
|
|
1316
|
+
(preview.changelog ? ` — ${preview.changelog}` : ""));
|
|
1317
|
+
const apply = async (resolutions) => {
|
|
1318
|
+
const updated = await client.applyContentInstallationUpgrade(args.installation_id, {
|
|
1319
|
+
...(args.version !== undefined ? { version: args.version } : {}),
|
|
1320
|
+
resolutions,
|
|
1321
|
+
});
|
|
1322
|
+
console.error(`Upgraded ${args.installation_id} → v${updated.package_version}.`);
|
|
1323
|
+
};
|
|
1324
|
+
if (preview.entries.length === 0 && preview.templates.length === 0) {
|
|
1325
|
+
if (preview.to_version === preview.from_version) {
|
|
1326
|
+
console.error(" Already up to date — no content changes.");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
console.error(" No changes needing consent — advancing the version pin; clean template updates apply automatically.");
|
|
1330
|
+
await apply({ knowledge: { resolutions: {}, bind_to: {} }, templates: {} });
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
for (const entry of preview.entries) {
|
|
1334
|
+
console.error(` ${formatKnowledgeEntryLine(entry)}`);
|
|
1335
|
+
}
|
|
1336
|
+
for (const entry of preview.templates) {
|
|
1337
|
+
console.error(` [template] ${entry.alias} (modified — needs consent)`);
|
|
1338
|
+
}
|
|
1339
|
+
// Route the shared --resolve flags across the two namespaces (loud on an alias
|
|
1340
|
+
// in both), then optionally bulk-accept every remaining consent entry.
|
|
1341
|
+
const routed = routeContentResolveFlags(args.resolve, new Set(preview.entries.map((e) => e.alias)), new Set(preview.templates.map((t) => t.alias)));
|
|
1342
|
+
const resolutions = {
|
|
1343
|
+
knowledge: { resolutions: { ...routed.knowledge }, bind_to: { ...args.bind_to } },
|
|
1344
|
+
templates: { ...routed.templates },
|
|
1345
|
+
};
|
|
1346
|
+
if (args.applyAll) {
|
|
1347
|
+
for (const entry of preview.entries) {
|
|
1348
|
+
if (knowledgeEntryNeedsConsent(entry) && resolutions.knowledge.resolutions[entry.alias] === undefined) {
|
|
1349
|
+
resolutions.knowledge.resolutions[entry.alias] = knowledgeAcceptResolution(entry.change);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
for (const entry of preview.templates) {
|
|
1353
|
+
if (resolutions.templates[entry.alias] === undefined)
|
|
1354
|
+
resolutions.templates[entry.alias] = "revert";
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const unresolvedKnowledge = preview.entries.filter((entry) => knowledgeEntryNeedsConsent(entry) && resolutions.knowledge.resolutions[entry.alias] === undefined);
|
|
1358
|
+
const unresolvedTemplates = preview.templates.filter((entry) => resolutions.templates[entry.alias] === undefined);
|
|
1359
|
+
if (unresolvedKnowledge.length > 0 || unresolvedTemplates.length > 0) {
|
|
1360
|
+
console.error(" These need a resolution before upgrading (or pass --apply-all to accept the package's version for all):");
|
|
1361
|
+
for (const entry of unresolvedKnowledge)
|
|
1362
|
+
console.error(formatKnowledgeResolveHint(entry));
|
|
1363
|
+
for (const entry of unresolvedTemplates)
|
|
1364
|
+
console.error(formatTemplateResolveHint(entry));
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
await apply(resolutions);
|
|
1368
|
+
}
|
|
1369
|
+
/** Parse repeated `--bind-to alias=kdc_id` flags into an alias → doc-id consent map. */
|
|
1370
|
+
export function parseBindToFlags(bindTo) {
|
|
1371
|
+
const map = {};
|
|
1372
|
+
for (const entry of bindTo) {
|
|
1373
|
+
const eq = entry.indexOf("=");
|
|
1374
|
+
if (eq <= 0 || eq === entry.length - 1) {
|
|
1375
|
+
throw new Error(`Invalid --bind-to "${entry}" — expected <alias>=<kdc_id>.`);
|
|
1376
|
+
}
|
|
1377
|
+
map[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
1378
|
+
}
|
|
1379
|
+
return map;
|
|
1380
|
+
}
|
|
1381
|
+
/** Print advisory `knowledge_expects` misses loudly (never blocks the install). */
|
|
1382
|
+
function warnMissingExpectedDocs(missing) {
|
|
1383
|
+
if (missing.length === 0)
|
|
1384
|
+
return;
|
|
1385
|
+
console.error(` ⚠ ${missing.length} expected knowledge doc(s) the package's agents route to are missing from this workspace:`);
|
|
1386
|
+
for (const name of missing)
|
|
1387
|
+
console.error(` - "${name}"`);
|
|
1388
|
+
console.error(" The package installed, but those agents will degrade until a doc with each name exists.");
|
|
1389
|
+
}
|
|
790
1390
|
export async function packageInstall(client, args) {
|
|
791
1391
|
// Surface the trust badge at the consent point: installing materializes the
|
|
792
|
-
// package's workflows/agents
|
|
793
|
-
const pkg = await client.
|
|
794
|
-
|
|
795
|
-
|
|
1392
|
+
// package's workflows/agents (or its doc corpus) under YOUR authority.
|
|
1393
|
+
const pkg = await client.getPackage(args.package_id);
|
|
1394
|
+
const kindLabel = pkg.kind === "content" ? "content package" : "package";
|
|
1395
|
+
console.error(`Installing ${pkg.name} — ${trustBadge(pkg)} (${kindLabel})...`);
|
|
1396
|
+
const result = await client.installPackage(args.package_id, {
|
|
796
1397
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
797
|
-
...(args.
|
|
1398
|
+
...(args.bind_to && Object.keys(args.bind_to).length > 0 ? { bind_to: args.bind_to } : {}),
|
|
1399
|
+
...(args.config && Object.keys(args.config).length > 0 ? { config: args.config } : {}),
|
|
798
1400
|
});
|
|
1401
|
+
if (result.kind === "content") {
|
|
1402
|
+
const { installation, warnings } = result;
|
|
1403
|
+
const docs = installation.binding.knowledge;
|
|
1404
|
+
const templates = installation.binding.templates;
|
|
1405
|
+
console.error(`Installed ${pkg.name} v${installation.package_version} → content installation ${installation.id} ` +
|
|
1406
|
+
`(workspace ${installation.workspace_id}).`);
|
|
1407
|
+
const docAliases = Object.keys(docs);
|
|
1408
|
+
if (docAliases.length > 0) {
|
|
1409
|
+
console.error(` ${docAliases.length} doc(s) live: ${docAliases.map((a) => `${a}→${docs[a]}`).join(", ")}`);
|
|
1410
|
+
}
|
|
1411
|
+
const templateAliases = Object.keys(templates);
|
|
1412
|
+
if (templateAliases.length > 0) {
|
|
1413
|
+
console.error(` ${templateAliases.length} template(s) live: ${templateAliases.map((a) => `${a}→${templates[a]}`).join(", ")}`);
|
|
1414
|
+
}
|
|
1415
|
+
warnMissingExpectedDocs(warnings.missing_expected_docs);
|
|
1416
|
+
console.error(` Upgrade later: lotics package upgrade ${installation.id}`);
|
|
1417
|
+
console.error(` Uninstall: lotics package uninstall ${installation.id} [--keep-content]`);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const { app, knowledge_warnings } = result;
|
|
799
1421
|
const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
|
|
800
1422
|
console.error(`Installed ${app.name} ${versionLabel} → ${app.id} (workspace ${app.workspace_id}).`);
|
|
801
1423
|
console.error(" The data model, queries, workflows, and agents are live.");
|
|
1424
|
+
warnMissingExpectedDocs(knowledge_warnings.missing_expected_docs);
|
|
802
1425
|
console.error(" Pull it for local editing: lotics app pull " + app.id);
|
|
803
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* `lotics package uninstall <app_id|pci_id>` — ONE command over both installation
|
|
1429
|
+
* kinds, dispatched by the id form (mirrors `package upgrade`):
|
|
1430
|
+
* - a `pci_` id → a STANDALONE CONTENT installation: deletes the row and (unless
|
|
1431
|
+
* `--keep-content`) archives its package-bound docs AND templates, listing each
|
|
1432
|
+
* archived id.
|
|
1433
|
+
* - anything else (an `app_id`) → an APP installation: archives its workflow
|
|
1434
|
+
* artifacts and — with `--archive-tables` — the scaffolded entity tables
|
|
1435
|
+
* (provenance- + reference-gated server-side).
|
|
1436
|
+
* A flag used on the wrong path is a loud error, never silently ignored.
|
|
1437
|
+
*/
|
|
1438
|
+
export async function packageUninstall(client, args) {
|
|
1439
|
+
if (args.id.startsWith("pci_")) {
|
|
1440
|
+
if (args.archive_tables) {
|
|
1441
|
+
throw new Error("--archive-tables applies only to an app installation (<app_id>). A content installation " +
|
|
1442
|
+
"(pci_) has no scaffolded tables — use --keep-content to retain its docs/templates.");
|
|
1443
|
+
}
|
|
1444
|
+
const result = await client.uninstallContentPackage(args.id, {
|
|
1445
|
+
keep_content: args.keep_content,
|
|
1446
|
+
});
|
|
1447
|
+
if (args.keep_content) {
|
|
1448
|
+
console.error(`Uninstalled content installation ${result.installation_id} — its docs and templates were kept as ordinary workspace content.`);
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
console.error(`Uninstalled content installation ${result.installation_id} — archived ` +
|
|
1452
|
+
`${result.archived_doc_ids.length} doc(s) and ${result.archived_template_ids.length} template(s).`);
|
|
1453
|
+
for (const docId of result.archived_doc_ids)
|
|
1454
|
+
console.error(` ${docId}`);
|
|
1455
|
+
for (const templateId of result.archived_template_ids)
|
|
1456
|
+
console.error(` ${templateId}`);
|
|
1457
|
+
}
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (args.keep_content) {
|
|
1461
|
+
throw new Error("--keep-content applies only to a standalone content installation (pci_). An app installation " +
|
|
1462
|
+
"(<app_id>) uses --archive-tables to also archive its scaffolded tables.");
|
|
1463
|
+
}
|
|
1464
|
+
const app = await client.getApp(args.id);
|
|
1465
|
+
if (!app.package_id) {
|
|
1466
|
+
throw new Error(`App ${args.id} is not a package installation — use the app delete flow for a bespoke app.`);
|
|
1467
|
+
}
|
|
1468
|
+
const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
|
|
1469
|
+
const boundCount = Object.keys(app.workflows ?? {}).length;
|
|
1470
|
+
const tableCount = Object.keys(app.binding?.entities ?? {}).length;
|
|
1471
|
+
console.error(`Uninstalling ${app.name} (${app.id}):`);
|
|
1472
|
+
console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
|
|
1473
|
+
console.error(args.archive_tables
|
|
1474
|
+
? ` Archives ${tableCount} scaffolded table(s) — refused if they were adopted or are still referenced.`
|
|
1475
|
+
: ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`);
|
|
1476
|
+
const result = await client.uninstallAppPackage(args.id, {
|
|
1477
|
+
archive_tables: args.archive_tables,
|
|
1478
|
+
});
|
|
1479
|
+
console.error(`Uninstalled ${app.name} (${app.id}).`);
|
|
1480
|
+
if (result.archived_table_ids.length > 0) {
|
|
1481
|
+
console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* `lotics package list-content` — list the selected workspace's package-managed
|
|
1486
|
+
* content installations (standalone `pci_` corpora + app-bundled ones), each
|
|
1487
|
+
* with its registry status. The read surface that surfaces a standalone `pci_` id
|
|
1488
|
+
* for `upgrade` / `uninstall` (install prints it once; nothing else did before).
|
|
1489
|
+
*/
|
|
1490
|
+
export async function packageListContent(client) {
|
|
1491
|
+
const workspaceId = client.getWorkspaceId();
|
|
1492
|
+
if (!workspaceId) {
|
|
1493
|
+
throw new Error("No workspace selected. Pass --workspace <ws> (or select one) to list its content installations.");
|
|
1494
|
+
}
|
|
1495
|
+
const installations = await client.listContentInstallations(workspaceId);
|
|
1496
|
+
if (installations.length === 0) {
|
|
1497
|
+
console.error(`No package-managed content installations in workspace ${workspaceId}.`);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
console.error(`Content installations in workspace ${workspaceId} (${installations.length}):`);
|
|
1501
|
+
for (const inst of installations) {
|
|
1502
|
+
const name = inst.package_registry?.name ?? "(unknown package)";
|
|
1503
|
+
const latest = inst.package_registry?.latest_version;
|
|
1504
|
+
const updateAvailable = inst.package_registry?.update_available ?? false;
|
|
1505
|
+
const source = inst.app_id === null ? "standalone" : `app-bundled (${inst.app_id})`;
|
|
1506
|
+
const versionLabel = latest !== undefined && latest !== inst.package_version
|
|
1507
|
+
? `v${inst.package_version} → latest v${latest}`
|
|
1508
|
+
: `v${inst.package_version}`;
|
|
1509
|
+
console.error(` ${inst.id} ${name} ${versionLabel} ${source}` +
|
|
1510
|
+
(updateAvailable ? " → update available" : ""));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
804
1513
|
export async function packageEject(client, args) {
|
|
805
|
-
const app = await client.
|
|
1514
|
+
const app = await client.ejectPackage(args.app_id);
|
|
806
1515
|
console.error(`Ejected ${app.name} → ${app.id} (workspace ${app.workspace_id}).`);
|
|
807
1516
|
console.error(" The package link is severed — it's now a normal bespoke app and can no longer be upgraded.");
|
|
808
1517
|
console.error(" Its data model, queries, workflows, and templates are unchanged.");
|
|
@@ -888,38 +1597,12 @@ export async function packageConfig(client, args) {
|
|
|
888
1597
|
console.log(` ${key} = ${JSON.stringify(config[key])}${marker}`);
|
|
889
1598
|
}
|
|
890
1599
|
}
|
|
891
|
-
/**
|
|
892
|
-
* `lotics package uninstall <app_id> [--archive-tables]` — remove a package
|
|
893
|
-
* installation. Prints what will be archived (workflow artifacts, plus the
|
|
894
|
-
* scaffolded tables when the flag is set), then uninstalls.
|
|
895
|
-
*/
|
|
896
|
-
export async function packageUninstall(client, args) {
|
|
897
|
-
const app = await client.getApp(args.app_id);
|
|
898
|
-
if (!app.package_id) {
|
|
899
|
-
throw new Error(`App ${args.app_id} is not a package installation — use the app delete flow for a bespoke app.`);
|
|
900
|
-
}
|
|
901
|
-
const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
|
|
902
|
-
const boundCount = Object.keys(app.workflows ?? {}).length;
|
|
903
|
-
const tableCount = Object.keys(app.binding?.entities ?? {}).length;
|
|
904
|
-
console.error(`Uninstalling ${app.name} (${app.id}):`);
|
|
905
|
-
console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
|
|
906
|
-
console.error(args.archive_tables
|
|
907
|
-
? ` Archives ${tableCount} scaffolded table(s) — refused if they were adopted or are still referenced.`
|
|
908
|
-
: ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`);
|
|
909
|
-
const result = await client.uninstallAppPackage(args.app_id, {
|
|
910
|
-
archive_tables: args.archive_tables,
|
|
911
|
-
});
|
|
912
|
-
console.error(`Uninstalled ${app.name} (${app.id}).`);
|
|
913
|
-
if (result.archived_table_ids.length > 0) {
|
|
914
|
-
console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
1600
|
/**
|
|
918
1601
|
* `lotics package retire <package_id> [--undo]` — retire (or un-retire) a
|
|
919
1602
|
* registry package. Owner-org admin-only.
|
|
920
1603
|
*/
|
|
921
1604
|
export async function packageRetire(client, args) {
|
|
922
|
-
const pkg = await client.
|
|
1605
|
+
const pkg = await client.retirePackage(args.package_id, { undo: args.undo });
|
|
923
1606
|
if (pkg.retired_at !== null) {
|
|
924
1607
|
console.error(`Retired ${pkg.name} (${pkg.id}). New installs refuse it and it is hidden from other orgs; ` +
|
|
925
1608
|
`existing installations keep working and may still upgrade.`);
|
|
@@ -930,7 +1613,7 @@ export async function packageRetire(client, args) {
|
|
|
930
1613
|
}
|
|
931
1614
|
/**
|
|
932
1615
|
* `lotics package extract <app_id> [path]` — promote a bespoke app to a DRAFT
|
|
933
|
-
* package project (docs/
|
|
1616
|
+
* package project (docs/packages.md § Promotion). Calls the extract read,
|
|
934
1617
|
* prints the findings report grouped by severity, then ALWAYS emits the draft
|
|
935
1618
|
* project (a broken contract is still the reviewable starting point): the app's
|
|
936
1619
|
* current source archive (same mechanics as `lotics app pull`) with the app
|
|
@@ -940,28 +1623,57 @@ export async function packageRetire(client, args) {
|
|
|
940
1623
|
* Exits non-zero when any `error` finding exists — the draft is written, but
|
|
941
1624
|
* publish re-validates and nothing should ship unreviewed.
|
|
942
1625
|
*/
|
|
1626
|
+
/**
|
|
1627
|
+
* Read an APP project's package.json `lotics.knowledge` (`[{ alias, doc_id }]`)
|
|
1628
|
+
* + `lotics.knowledge_expects` (`string[]`) authoring declaration — the docs the
|
|
1629
|
+
* package will own + the doc NAMES its agents route to but do not own. Distinct
|
|
1630
|
+
* from a PACKAGE project's `lotics.package.knowledge` (a metadata record).
|
|
1631
|
+
*/
|
|
1632
|
+
function readAppKnowledgeDeclaration(appPkgJson) {
|
|
1633
|
+
const lotics = isPlainObject(appPkgJson.lotics) ? appPkgJson.lotics : {};
|
|
1634
|
+
const knowledge = [];
|
|
1635
|
+
if (Array.isArray(lotics.knowledge)) {
|
|
1636
|
+
for (const entry of lotics.knowledge) {
|
|
1637
|
+
if (isPlainObject(entry) && typeof entry.alias === "string" && typeof entry.doc_id === "string") {
|
|
1638
|
+
knowledge.push({ alias: entry.alias, doc_id: entry.doc_id });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const knowledge_expects = Array.isArray(lotics.knowledge_expects)
|
|
1643
|
+
? lotics.knowledge_expects.filter((v) => typeof v === "string")
|
|
1644
|
+
: [];
|
|
1645
|
+
return { knowledge, knowledge_expects };
|
|
1646
|
+
}
|
|
1647
|
+
/** Derive the package project's `lotics.package.knowledge` metadata from the extracted contract's knowledge namespace. */
|
|
1648
|
+
function knowledgeManifestFromContract(contract) {
|
|
1649
|
+
const out = {};
|
|
1650
|
+
if (!isPlainObject(contract) || !isPlainObject(contract.knowledge))
|
|
1651
|
+
return out;
|
|
1652
|
+
for (const [alias, entry] of Object.entries(contract.knowledge)) {
|
|
1653
|
+
if (!isPlainObject(entry) || typeof entry.name !== "string")
|
|
1654
|
+
continue;
|
|
1655
|
+
out[alias] = {
|
|
1656
|
+
name: entry.name,
|
|
1657
|
+
description: typeof entry.description === "string" ? entry.description : null,
|
|
1658
|
+
active_by_default: typeof entry.active_by_default === "boolean" ? entry.active_by_default : true,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
943
1663
|
export async function packageExtract(client, args) {
|
|
944
1664
|
const app = await client.getApp(args.app_id);
|
|
945
1665
|
if (!app.current_version_id) {
|
|
946
1666
|
throw new Error(`App ${app.id} has no deployed version — extract stages its source archive, so deploy it first.`);
|
|
947
1667
|
}
|
|
948
|
-
const extracted = await client.extractAppPackage(args.app_id);
|
|
949
|
-
const { lines, hasError } = formatExtractReport(extracted.report);
|
|
950
|
-
if (lines.length > 0) {
|
|
951
|
-
console.error(`Extraction report (${extracted.report.length}):`);
|
|
952
|
-
for (const line of lines)
|
|
953
|
-
console.error(line);
|
|
954
|
-
}
|
|
955
|
-
else {
|
|
956
|
-
console.error("Extraction report: no findings.");
|
|
957
|
-
}
|
|
958
1668
|
const targetPath = path.resolve(args.targetPath ?? appDirName(app.name));
|
|
959
1669
|
if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
|
|
960
1670
|
throw new Error(`Target directory ${targetPath} is not empty.`);
|
|
961
1671
|
}
|
|
962
1672
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
963
|
-
// Pull the app's current source archive — same mechanics as `lotics app
|
|
964
|
-
// (getAppVersion → presigned source URL → download → untar
|
|
1673
|
+
// Pull the app's current source archive FIRST — same mechanics as `lotics app
|
|
1674
|
+
// pull` (getAppVersion → presigned source URL → download → untar). The app's
|
|
1675
|
+
// package.json (in the archive) carries the `lotics.knowledge` declaration
|
|
1676
|
+
// extract needs, so the download precedes the extract call.
|
|
965
1677
|
const version = await client.getAppVersion(app.id, app.current_version_id);
|
|
966
1678
|
const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
|
|
967
1679
|
const tmpFile = path.join(tmpdir(), `lotics-extract-${app.id}-${Date.now()}.tar.gz`);
|
|
@@ -975,10 +1687,31 @@ export async function packageExtract(client, args) {
|
|
|
975
1687
|
if (fs.existsSync(tmpFile))
|
|
976
1688
|
fs.unlinkSync(tmpFile);
|
|
977
1689
|
}
|
|
1690
|
+
const appPkgJson = JSON.parse(fs.readFileSync(packageJsonPath(targetPath), "utf-8"));
|
|
1691
|
+
const appKnowledge = readAppKnowledgeDeclaration(appPkgJson);
|
|
1692
|
+
// Agents reference docs in free text (uninvertible), so the author DECLARED the
|
|
1693
|
+
// package-managed docs in the app manifest; pass them to extract, which loads
|
|
1694
|
+
// each live doc + emits its content bytes to write locally.
|
|
1695
|
+
const extracted = await client.extractPackage(args.app_id, {
|
|
1696
|
+
knowledge: appKnowledge.knowledge,
|
|
1697
|
+
});
|
|
1698
|
+
const { lines, hasError } = formatExtractReport(extracted.report);
|
|
1699
|
+
if (lines.length > 0) {
|
|
1700
|
+
console.error(`Extraction report (${extracted.report.length}):`);
|
|
1701
|
+
for (const line of lines)
|
|
1702
|
+
console.error(line);
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
console.error("Extraction report: no findings.");
|
|
1706
|
+
}
|
|
978
1707
|
// Swap the app manifest (lotics.app_id/workspace_id) for a fresh, unpublished
|
|
979
1708
|
// package manifest — atomic write via the existing manifest writer.
|
|
980
|
-
const appPkgJson = JSON.parse(fs.readFileSync(packageJsonPath(targetPath), "utf-8"));
|
|
981
1709
|
const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
|
|
1710
|
+
// Fold the extracted knowledge metadata + the app's `knowledge_expects` into the
|
|
1711
|
+
// package manifest (the content bytes are written to knowledge/<alias>.md below;
|
|
1712
|
+
// build re-derives the shas from them).
|
|
1713
|
+
draft.manifest.knowledge = knowledgeManifestFromContract(extracted.contract);
|
|
1714
|
+
draft.manifest.knowledge_expects = appKnowledge.knowledge_expects;
|
|
982
1715
|
// The origin app's package.json rides in the source archive with ITS OWN
|
|
983
1716
|
// @lotics/app-sdk range — raise it to the getAppBinding floor the generated
|
|
984
1717
|
// .lotics/app_fields.ts needs (never lower an already-newer range).
|
|
@@ -1041,11 +1774,26 @@ export async function packageExtract(client, args) {
|
|
|
1041
1774
|
fs.renameSync(downloaded, dest);
|
|
1042
1775
|
console.error(`Wrote ${tf.bytes_ref}`);
|
|
1043
1776
|
}
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1777
|
+
// Write each package-managed knowledge doc's content at `knowledge/<alias>.md`
|
|
1778
|
+
// (the path the contract's content_ref names) — build re-derives the sha from
|
|
1779
|
+
// these bytes, so they are the reviewable master alongside contract.json.
|
|
1780
|
+
for (const kf of extracted.knowledge_files) {
|
|
1781
|
+
const dest = path.join(targetPath, kf.content_ref);
|
|
1782
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1783
|
+
fs.writeFileSync(dest, kf.content);
|
|
1784
|
+
console.error(`Wrote ${kf.content_ref}`);
|
|
1785
|
+
}
|
|
1786
|
+
// The origin pin the `adopt` step reads back — incl. the knowledge binding
|
|
1787
|
+
// (alias → origin kdc_ id) so adopt can pin the corpus as installation #1.
|
|
1788
|
+
// `.lotics` is in SOURCE_STAGE_EXCLUDES, so this never ships in a published bundle.
|
|
1046
1789
|
const dotLotics = path.join(targetPath, ".lotics");
|
|
1047
1790
|
fs.mkdirSync(dotLotics, { recursive: true });
|
|
1048
|
-
fs.writeFileSync(path.join(dotLotics, ADOPT_BINDING_FILE), JSON.stringify({
|
|
1791
|
+
fs.writeFileSync(path.join(dotLotics, ADOPT_BINDING_FILE), JSON.stringify({
|
|
1792
|
+
app_id: app.id,
|
|
1793
|
+
workspace_id: app.workspace_id,
|
|
1794
|
+
binding: extracted.binding,
|
|
1795
|
+
knowledge_binding: extracted.knowledge_binding,
|
|
1796
|
+
}, null, 2) + "\n");
|
|
1049
1797
|
console.error(`Wrote .lotics/${ADOPT_BINDING_FILE}`);
|
|
1050
1798
|
console.error("Installing npm dependencies...");
|
|
1051
1799
|
await runNpm(["install"], targetPath);
|
|
@@ -1063,7 +1811,7 @@ export async function packageExtract(client, args) {
|
|
|
1063
1811
|
}
|
|
1064
1812
|
/**
|
|
1065
1813
|
* `lotics package adopt <app_id> [path]` — bind the published package project
|
|
1066
|
-
* onto the origin app (docs/
|
|
1814
|
+
* onto the origin app (docs/packages.md § Promotion). Reads the project
|
|
1067
1815
|
* manifest (must be published — refuses otherwise), resolves the version
|
|
1068
1816
|
* (`--version N` else the manifest's), and reads `.lotics/adopt_binding.json`,
|
|
1069
1817
|
* REFUSING a pin recorded for a different app. On success the app becomes
|
|
@@ -1086,10 +1834,15 @@ export async function packageAdopt(client, args) {
|
|
|
1086
1834
|
`"lotics package extract" recorded — run extract to produce this project.`);
|
|
1087
1835
|
}
|
|
1088
1836
|
const pin = parseAdoptBindingFile(JSON.parse(fs.readFileSync(bindingPath, "utf-8")), args.app_id);
|
|
1089
|
-
const app = await client.
|
|
1837
|
+
const app = await client.adoptPackage(args.app_id, {
|
|
1090
1838
|
package_id: manifest.id,
|
|
1091
1839
|
version,
|
|
1092
1840
|
binding: pin.binding,
|
|
1841
|
+
// The origin knowledge binding (empty when the app owns no package-managed
|
|
1842
|
+
// docs) — the server verifies it maps the contract's knowledge aliases faithfully.
|
|
1843
|
+
...(Object.keys(pin.knowledge_binding).length > 0
|
|
1844
|
+
? { knowledge_binding: pin.knowledge_binding }
|
|
1845
|
+
: {}),
|
|
1093
1846
|
});
|
|
1094
1847
|
console.error(`Adopted ${app.name} → installation of ${app.package_id} v${app.package_version}.`);
|
|
1095
1848
|
console.error(` Verify the installation: lotics package doctor ${app.id}`);
|
|
@@ -1108,7 +1861,7 @@ export async function packageAdopt(client, args) {
|
|
|
1108
1861
|
* running. Owner-org admin-only.
|
|
1109
1862
|
*/
|
|
1110
1863
|
export async function packageYank(client, args) {
|
|
1111
|
-
const result = await client.
|
|
1864
|
+
const result = await client.yankPackageVersion(args.package_id, args.version, !args.undo);
|
|
1112
1865
|
if (result.yanked_at !== null) {
|
|
1113
1866
|
console.error(`Yanked ${result.package_id} v${result.version} (${result.yanked_at}). ` +
|
|
1114
1867
|
`New installs/upgrades refuse it; pinned installations keep running.`);
|
|
@@ -1119,7 +1872,7 @@ export async function packageYank(client, args) {
|
|
|
1119
1872
|
console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
|
|
1120
1873
|
}
|
|
1121
1874
|
export async function packageFleetUpgrade(client, args) {
|
|
1122
|
-
const result = await client.
|
|
1875
|
+
const result = await client.fleetUpgradePackage(args.package_id, {
|
|
1123
1876
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
1124
1877
|
});
|
|
1125
1878
|
console.error(`Fleet upgrade of ${result.package_id} → v${result.target_version}:`);
|