@lotics/cli 0.76.0 → 0.83.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.
- package/README.md +66 -32
- package/dist/app_commands.d.ts +9 -4
- package/dist/app_commands.js +44 -10
- package/dist/app_commands.test.js +69 -0
- package/dist/args.d.ts +19 -5
- package/dist/args.js +36 -14
- package/dist/args.test.js +25 -17
- package/dist/cli.js +214 -215
- package/dist/cli_dispatch.test.js +77 -25
- package/dist/client.d.ts +240 -170
- package/dist/client.js +122 -111
- package/dist/generate_package_fields.d.ts +39 -38
- package/dist/generate_package_fields.js +113 -60
- package/dist/generate_package_fields.test.js +30 -22
- package/dist/package_commands.d.ts +148 -204
- package/dist/package_commands.js +586 -828
- package/dist/package_commands.test.js +99 -226
- package/dist/src/cli.js +18680 -2572
- package/dist/starter_template.d.ts +0 -19
- package/dist/starter_template.js +0 -389
- package/dist/starter_template.test.js +1 -69
- package/package.json +1 -1
package/dist/package_commands.js
CHANGED
|
@@ -1,158 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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:
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* reset DEV-ONLY, hard-gated: drop the scaffolded tables + re-scaffold clean.
|
|
14
|
-
* install materialize a published version into the current workspace.
|
|
15
|
-
* eject sever an installation's package link (last rung of customization).
|
|
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.)
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* The local installation pin per dev workspace is recorded in the project
|
|
22
|
-
* manifest so a re-sync upgrades in place.
|
|
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.
|
|
23
20
|
*/
|
|
24
21
|
import fs from "node:fs";
|
|
25
22
|
import path from "node:path";
|
|
26
|
-
import { tmpdir } from "node:os";
|
|
27
23
|
import "./client.js";
|
|
28
|
-
import {
|
|
29
|
-
import { generatePackageAppFields, } from "./generate_package_fields.js";
|
|
30
|
-
import { appDirName, fetchLatestNpmVersion, runNpm, runTar, writeAppDts } from "./app_commands.js";
|
|
31
|
-
import { writeFileAtomic } from "./file_command_io.js";
|
|
32
|
-
import { startDevServer, openBrowser } from "./dev/server.js";
|
|
33
|
-
const CONTRACT_FILE = "contract.json";
|
|
34
|
-
/**
|
|
35
|
-
* The origin pin `lotics package extract` writes and `lotics package adopt`
|
|
36
|
-
* reads back: `{ app_id, workspace_id, binding }`. Lives under `.lotics/`, which
|
|
37
|
-
* is in `SOURCE_STAGE_EXCLUDES` — so it never rides along in a published bundle.
|
|
38
|
-
*/
|
|
39
|
-
const ADOPT_BINDING_FILE = "adopt_binding.json";
|
|
24
|
+
import { knowledgeEntryNeedsConsent, validKnowledgeResolutions, } from "@lotics/shared/schemas/packages";
|
|
40
25
|
function packageJsonPath(projectDir) {
|
|
41
26
|
return path.join(projectDir, "package.json");
|
|
42
27
|
}
|
|
43
28
|
function isPlainObject(value) {
|
|
44
29
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
30
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
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) {
|
|
48
41
|
const pkgPath = packageJsonPath(projectDir);
|
|
49
|
-
if (!fs.existsSync(pkgPath))
|
|
50
|
-
|
|
51
|
-
}
|
|
42
|
+
if (!fs.existsSync(pkgPath))
|
|
43
|
+
return null;
|
|
52
44
|
const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
53
45
|
if (!isPlainObject(parsed))
|
|
54
|
-
|
|
55
|
-
const lotics = parsed.lotics;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (const [ws, entry] of Object.entries(pkg.dev)) {
|
|
63
|
-
if (isPlainObject(entry) && typeof entry.app_id === "string" && typeof entry.version === "number") {
|
|
64
|
-
dev[ws] = { app_id: entry.app_id, version: entry.version };
|
|
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 });
|
|
65
54
|
}
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
|
-
|
|
69
|
-
id: typeof pkg.id === "string" ? pkg.id : null,
|
|
70
|
-
name: pkg.name,
|
|
71
|
-
description: typeof pkg.description === "string" ? pkg.description : null,
|
|
72
|
-
version: typeof pkg.version === "number" ? pkg.version : null,
|
|
73
|
-
dev,
|
|
74
|
-
};
|
|
75
|
-
return { pkgJson: parsed, manifest };
|
|
76
|
-
}
|
|
77
|
-
/** Persist an updated manifest back into the project's package.json (atomic write). */
|
|
78
|
-
export function writePackageManifest(projectDir, project) {
|
|
79
|
-
const next = {
|
|
80
|
-
...project.pkgJson,
|
|
81
|
-
lotics: {
|
|
82
|
-
...(isPlainObject(project.pkgJson.lotics) ? project.pkgJson.lotics : {}),
|
|
83
|
-
package: project.manifest,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
// Atomic write: a torn plain write could corrupt package.json and lose the
|
|
87
|
-
// stamped registry `package_id` — which would orphan the published package
|
|
88
|
-
// and create a duplicate on the next publish retry.
|
|
89
|
-
writeFileAtomic(packageJsonPath(projectDir), new TextEncoder().encode(JSON.stringify(next, null, 2) + "\n"));
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The package.json to ship INSIDE `source.tar.gz`: the on-disk manifest with the
|
|
93
|
-
* author-local `lotics.package.dev` map stripped. `dev` is the author's private
|
|
94
|
-
* dev-workspace → installation bookkeeping; it must never reach a consumer (every
|
|
95
|
-
* install carries the source, and `lotics app pull` ejects it). Returns a
|
|
96
|
-
* sanitized copy — the on-disk package.json is left untouched. `id`/`name`/
|
|
97
|
-
* `description`/`version` are the package's stable identity and stay.
|
|
98
|
-
*/
|
|
99
|
-
export function sanitizePackageJsonForSource(pkgJson) {
|
|
100
|
-
const lotics = isPlainObject(pkgJson.lotics) ? pkgJson.lotics : {};
|
|
101
|
-
const pkg = isPlainObject(lotics.package) ? lotics.package : {};
|
|
102
|
-
const { dev: _dev, ...pkgWithoutDev } = pkg;
|
|
103
|
-
return {
|
|
104
|
-
...pkgJson,
|
|
105
|
-
lotics: { ...lotics, package: pkgWithoutDev },
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Transform an app project's package.json (the source archive `lotics app pull`
|
|
110
|
-
* downloads, carrying the `lotics.app_id`/`workspace_id` app manifest) into a
|
|
111
|
-
* package project's `PackageProjectFile`: the app manifest is stripped ENTIRELY
|
|
112
|
-
* and a fresh, unpublished `lotics.package` manifest (id/version null) is
|
|
113
|
-
* grafted. Pure — returns a new value, never mutates the input;
|
|
114
|
-
* `writePackageManifest` writes it (atomic). The bespoke→package promotion's
|
|
115
|
-
* manifest inversion.
|
|
116
|
-
*/
|
|
117
|
-
export function draftPackageProjectFromApp(appPkgJson, args) {
|
|
118
|
-
const { lotics: _appManifest, ...rest } = appPkgJson;
|
|
119
|
-
return {
|
|
120
|
-
pkgJson: rest,
|
|
121
|
-
manifest: { id: null, name: args.name, description: args.description, version: null, dev: {} },
|
|
122
|
-
};
|
|
57
|
+
return { app_id, knowledge };
|
|
123
58
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
typeof raw.workspace_id !== "string" ||
|
|
134
|
-
!isPlainObject(raw.binding)) {
|
|
135
|
-
throw new Error("Malformed .lotics/adopt_binding.json — expected { app_id, workspace_id, binding }.");
|
|
136
|
-
}
|
|
137
|
-
if (raw.app_id !== expectedAppId) {
|
|
138
|
-
throw new Error(`.lotics/adopt_binding.json records app ${raw.app_id}, but you are adopting ${expectedAppId}. ` +
|
|
139
|
-
`A binding maps one app's concrete ids and must never be applied to another — ` +
|
|
140
|
-
`run "lotics package extract ${expectedAppId}" to produce the right pin.`);
|
|
141
|
-
}
|
|
142
|
-
// Boundary adapter: the file is untyped JSON; `isPlainObject` proved the shape
|
|
143
|
-
// and the CLI passes the map straight to the server (the validating authority).
|
|
144
|
-
return {
|
|
145
|
-
app_id: raw.app_id,
|
|
146
|
-
workspace_id: raw.workspace_id,
|
|
147
|
-
binding: raw.binding,
|
|
148
|
-
};
|
|
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
|
+
});
|
|
149
68
|
}
|
|
150
69
|
/**
|
|
151
|
-
* Render
|
|
152
|
-
* info), one ` [<severity>] <area>: <message>` line each, and classify
|
|
153
|
-
* any `error` finding is present.
|
|
154
|
-
*
|
|
155
|
-
* `lines` to stderr and gates on `hasError`.
|
|
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`.
|
|
156
74
|
*/
|
|
157
75
|
export function formatExtractReport(report) {
|
|
158
76
|
const order = ["error", "warning", "info"];
|
|
@@ -161,500 +79,37 @@ export function formatExtractReport(report) {
|
|
|
161
79
|
.map((f) => ` [${f.severity}] ${f.area}: ${f.message}`));
|
|
162
80
|
return { lines, hasError: report.some((f) => f.severity === "error") };
|
|
163
81
|
}
|
|
164
|
-
function dotLoticsDirEnsured(projectDir) {
|
|
165
|
-
const dir = path.join(projectDir, ".lotics");
|
|
166
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
-
return dir;
|
|
168
|
-
}
|
|
169
|
-
/** Numeric "x.y.z" compare — no semver dep (the CLI has zero runtime deps). */
|
|
170
|
-
function cmpVersions(a, b) {
|
|
171
|
-
const pa = a.split(".").map(Number);
|
|
172
|
-
const pb = b.split(".").map(Number);
|
|
173
|
-
for (let i = 0; i < 3; i++) {
|
|
174
|
-
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
175
|
-
if (d !== 0)
|
|
176
|
-
return d;
|
|
177
|
-
}
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The `@lotics/app-sdk` range a package project needs: the live npm latest,
|
|
182
|
-
* clamped to never fall below `STARTER_FALLBACK_SDK_VERSION` — the release
|
|
183
|
-
* that ships `getAppBinding`, which the generated `.lotics/app_fields.ts`
|
|
184
|
-
* imports. A `^0.x` caret range never crosses a minor, so pinning below the
|
|
185
|
-
* floor (offline, or npm not yet carrying the release) would permanently
|
|
186
|
-
* scaffold projects that cannot compile their own generated code.
|
|
187
|
-
*/
|
|
188
|
-
function packageSdkRange(sdkLatest) {
|
|
189
|
-
const version = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0
|
|
190
|
-
? sdkLatest
|
|
191
|
-
: STARTER_FALLBACK_SDK_VERSION;
|
|
192
|
-
return `^${version}`;
|
|
193
|
-
}
|
|
194
|
-
/** Read + JSON-parse the project's contract.json (the alias-keyed declaration). */
|
|
195
|
-
function readContract(projectDir) {
|
|
196
|
-
const contractPath = path.join(projectDir, CONTRACT_FILE);
|
|
197
|
-
if (!fs.existsSync(contractPath)) {
|
|
198
|
-
throw new Error(`No ${CONTRACT_FILE} in ${projectDir}. A package project declares its data model there.`);
|
|
199
|
-
}
|
|
200
|
-
return JSON.parse(fs.readFileSync(contractPath, "utf-8"));
|
|
201
|
-
}
|
|
202
82
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* `generate_package_fields.ts`). Runs on new/extract and on every dev/sync so
|
|
206
|
-
* contract edits keep the aliases addressable. `.lotics` is in
|
|
207
|
-
* SOURCE_STAGE_EXCLUDES: the compiled module ships inside `dist/`, and a
|
|
208
|
-
* post-eject `lotics app pull` regenerates the bespoke (baked) variant.
|
|
83
|
+
* Resolve the installation to operate on: an explicit app id is required (there
|
|
84
|
+
* is no dev-workspace pin to fall back to).
|
|
209
85
|
*/
|
|
210
|
-
function
|
|
211
|
-
const contract = readContract(projectDir);
|
|
212
|
-
const dotLotics = path.join(projectDir, ".lotics");
|
|
213
|
-
fs.mkdirSync(dotLotics, { recursive: true });
|
|
214
|
-
fs.writeFileSync(path.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
|
|
215
|
-
console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
|
|
216
|
-
}
|
|
217
|
-
/** Top-level project entries that never ship in the source archive. */
|
|
218
|
-
const SOURCE_STAGE_EXCLUDES = new Set([
|
|
219
|
-
"node_modules",
|
|
220
|
-
"dist",
|
|
221
|
-
".lotics",
|
|
222
|
-
".git",
|
|
223
|
-
"bundle.tar.gz",
|
|
224
|
-
"package.json",
|
|
225
|
-
]);
|
|
226
|
-
/**
|
|
227
|
-
* Copy the project's source tree into `sourceStage` with explicit TOP-LEVEL
|
|
228
|
-
* excludes, writing a sanitized `package.json` in place of the on-disk one.
|
|
229
|
-
* Deliberately not tar `--exclude` flags: those match at any depth (a nested
|
|
230
|
-
* `templates/dist/` would be silently dropped) and GNU tar vs bsdtar (macOS)
|
|
231
|
-
* disagree on `./`-prefixed patterns, which broke the sanitized-package.json
|
|
232
|
-
* graft on macOS.
|
|
233
|
-
*/
|
|
234
|
-
export function stagePackageSource(projectDir, sourceStage) {
|
|
235
|
-
const project = readPackageProject(projectDir);
|
|
236
|
-
fs.mkdirSync(sourceStage, { recursive: true });
|
|
237
|
-
for (const entry of fs.readdirSync(projectDir)) {
|
|
238
|
-
if (SOURCE_STAGE_EXCLUDES.has(entry) || entry.endsWith(".tsbuildinfo"))
|
|
239
|
-
continue;
|
|
240
|
-
fs.cpSync(path.join(projectDir, entry), path.join(sourceStage, entry), { recursive: true });
|
|
241
|
-
}
|
|
242
|
-
fs.writeFileSync(path.join(sourceStage, "package.json"), JSON.stringify(sanitizePackageJsonForSource(project.pkgJson), null, 2) + "\n");
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Build the publishable bundle: `npm run build` (vite → dist/), then a gzipped
|
|
246
|
-
* tarball carrying `source.tar.gz` (the pullable project tree) + `dist.tar.gz`
|
|
247
|
-
* (the prebuilt assets) as its two top-level members — the publish↔install
|
|
248
|
-
* contract the backend's `extractPackageBundle` reads. Returns the bytes.
|
|
249
|
-
*/
|
|
250
|
-
async function buildPackageBundle(projectDir) {
|
|
251
|
-
console.error("Building...");
|
|
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
|
-
}
|
|
258
|
-
// Stage the two member archives in a temp dir, then tar them into the bundle.
|
|
259
|
-
const stage = fs.mkdtempSync(path.join(tmpdir(), "lotics-pkg-"));
|
|
260
|
-
const bundlePath = path.join(tmpdir(), `lotics-bundle-${Date.now()}.tar.gz`);
|
|
261
|
-
try {
|
|
262
|
-
console.error("Packaging source...");
|
|
263
|
-
// Stage a copy of the source tree with EXPLICIT top-level excludes, then
|
|
264
|
-
// tar the stage with no --exclude flags at all. tar exclude patterns are
|
|
265
|
-
// the wrong tool twice over: they match at ANY depth (a nested
|
|
266
|
-
// `templates/dist/` would be silently dropped from the bundle), and GNU
|
|
267
|
-
// tar vs bsdtar (macOS) disagree on whether `./package.json` and
|
|
268
|
-
// `package.json` are the same pattern — bsdtar strips the leading `./`,
|
|
269
|
-
// which would also exclude the sanitized package.json grafted below.
|
|
270
|
-
//
|
|
271
|
-
// The on-disk package.json carries the author-local `lotics.package.dev`
|
|
272
|
-
// map; the stage gets a sanitized copy in its place, so that bookkeeping
|
|
273
|
-
// never ships to a consumer nor lands in an `lotics app pull` eject.
|
|
274
|
-
const sourceStage = path.join(stage, "source");
|
|
275
|
-
stagePackageSource(projectDir, sourceStage);
|
|
276
|
-
await runTar(["-czf", path.join(stage, "source.tar.gz"), "-C", sourceStage, "."], projectDir);
|
|
277
|
-
console.error("Packaging dist...");
|
|
278
|
-
await runTar(["-czf", path.join(stage, "dist.tar.gz"), "-C", distDir, "."], projectDir);
|
|
279
|
-
console.error("Bundling...");
|
|
280
|
-
await runTar(["-czf", bundlePath, "source.tar.gz", "dist.tar.gz"], stage);
|
|
281
|
-
return fs.readFileSync(bundlePath);
|
|
282
|
-
}
|
|
283
|
-
finally {
|
|
284
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
285
|
-
if (fs.existsSync(bundlePath))
|
|
286
|
-
fs.unlinkSync(bundlePath);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/** The minimal, valid starting contract a `package new` scaffold ships. */
|
|
290
|
-
export function starterContract(name) {
|
|
291
|
-
return {
|
|
292
|
-
entities: [
|
|
293
|
-
{
|
|
294
|
-
alias: "item",
|
|
295
|
-
label: name,
|
|
296
|
-
description: `Records managed by the ${name} package.`,
|
|
297
|
-
fields: [
|
|
298
|
-
{ alias: "name", label: "Name", type: "text", required: true },
|
|
299
|
-
{ alias: "notes", label: "Notes", type: "text" },
|
|
300
|
-
{
|
|
301
|
-
alias: "status",
|
|
302
|
-
label: "Status",
|
|
303
|
-
type: "select",
|
|
304
|
-
options: [
|
|
305
|
-
{ alias: "open", label: "Open", color: "blue" },
|
|
306
|
-
{ alias: "done", label: "Done", color: "green" },
|
|
307
|
-
],
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
roles: [],
|
|
313
|
-
templates: [],
|
|
314
|
-
queries: [
|
|
315
|
-
{
|
|
316
|
-
alias: "items",
|
|
317
|
-
ast: {
|
|
318
|
-
kind: "from_table",
|
|
319
|
-
from_entity: "item",
|
|
320
|
-
sort: [{ field_key: "name", order: "asc" }],
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
workflows: [],
|
|
325
|
-
agents: [],
|
|
326
|
-
config: [
|
|
327
|
-
{ alias: "heading", label: "List heading", type: "text", default: name },
|
|
328
|
-
],
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* `lotics package new <name> [path]` — scaffold a package project. Reuses the
|
|
333
|
-
* app starter (Vite+React+TS) for the code surface, swaps its app manifest for a
|
|
334
|
-
* package manifest, and adds a starter `contract.json`. The project publishes
|
|
335
|
-
* with `lotics package publish` and runs against a dev workspace with
|
|
336
|
-
* `lotics package dev`.
|
|
337
|
-
*/
|
|
338
|
-
export async function packageNew(args) {
|
|
339
|
-
const targetPath = path.resolve(args.targetPath ?? appDirName(args.name));
|
|
340
|
-
if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
|
|
341
|
-
throw new Error(`Target directory ${targetPath} is not empty.`);
|
|
342
|
-
}
|
|
343
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
344
|
-
// The starter bakes an app manifest (app_id/workspace_id) into package.json;
|
|
345
|
-
// a package is workspace-agnostic, so those placeholders are replaced with the
|
|
346
|
-
// package manifest below. The rest of the starter (config, src, tests) is the
|
|
347
|
-
// package's code surface verbatim.
|
|
348
|
-
// Resolve the live @lotics/ui + @lotics/app-sdk versions like `app create`
|
|
349
|
-
// does — the generated .lotics/app_fields.ts imports `getAppBinding`, and a
|
|
350
|
-
// ^0.x caret range never crosses a minor, so a stale starter fallback would
|
|
351
|
-
// permanently pin the project below the API it needs (offline still works
|
|
352
|
-
// off STARTER_FALLBACK_*, kept ≥ that floor).
|
|
353
|
-
const [uiLatest, sdkLatest] = await Promise.all([
|
|
354
|
-
fetchLatestNpmVersion("@lotics/ui"),
|
|
355
|
-
fetchLatestNpmVersion("@lotics/app-sdk"),
|
|
356
|
-
]);
|
|
357
|
-
const files = buildStarterTemplate({
|
|
358
|
-
app_name: args.name,
|
|
359
|
-
app_id: "",
|
|
360
|
-
workspace_id: "",
|
|
361
|
-
ui_version: uiLatest ? `^${uiLatest}` : undefined,
|
|
362
|
-
sdk_version: packageSdkRange(sdkLatest),
|
|
363
|
-
});
|
|
364
|
-
// Package-specific code surface swapped in BY PATH (same mechanism as the
|
|
365
|
-
// package.json manifest swap below): the shared app starter's App.tsx demos
|
|
366
|
-
// in-app routing over static data, wrong for a package. The package starter's
|
|
367
|
-
// App.tsx exercises the binding (F/OPT + useQuery/useConfig), its test mocks the
|
|
368
|
-
// SDK boundary, and its README carries the contract reference.
|
|
369
|
-
const overrides = new Map(buildPackageStarterOverrides({ app_name: args.name }).map((f) => [f.path, f.content]));
|
|
370
|
-
for (const file of files) {
|
|
371
|
-
const fullPath = path.join(targetPath, file.path);
|
|
372
|
-
if (file.path === "package.json") {
|
|
373
|
-
const pkg = JSON.parse(file.content);
|
|
374
|
-
const manifest = {
|
|
375
|
-
id: null,
|
|
376
|
-
name: args.name,
|
|
377
|
-
description: null,
|
|
378
|
-
version: null,
|
|
379
|
-
dev: {},
|
|
380
|
-
};
|
|
381
|
-
pkg.lotics = { package: manifest };
|
|
382
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
383
|
-
fs.writeFileSync(fullPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
387
|
-
fs.writeFileSync(fullPath, overrides.get(file.path) ?? file.content);
|
|
388
|
-
}
|
|
389
|
-
fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(starterContract(args.name), null, 2) + "\n");
|
|
390
|
-
writePackageAppFields(targetPath);
|
|
391
|
-
console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
|
|
392
|
-
console.error("Installing npm dependencies...");
|
|
393
|
-
await runNpm(["install"], targetPath);
|
|
394
|
-
console.error(`\nReady. Next steps:`);
|
|
395
|
-
console.error(` cd ${path.relative(process.cwd(), targetPath) || "."}`);
|
|
396
|
-
console.error(` # edit contract.json + src/App.tsx, then run it against a dev workspace:`);
|
|
397
|
-
console.error(` lotics workspace create "<name> dev" --dev`);
|
|
398
|
-
console.error(` lotics package dev --workspace <dev_ws>`);
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* `lotics package build [path]` — build the publishable bundle and write it to
|
|
402
|
-
* `bundle.tar.gz` in the project. Mostly a local sanity check / CI artifact;
|
|
403
|
-
* `publish` and `dev`/`sync` build the bundle in memory directly.
|
|
404
|
-
*/
|
|
405
|
-
export async function packageBuild(args) {
|
|
406
|
-
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
407
|
-
readPackageProject(projectDir); // assert it's a package project
|
|
408
|
-
const bundle = await buildPackageBundle(projectDir);
|
|
409
|
-
const out = path.join(projectDir, "bundle.tar.gz");
|
|
410
|
-
fs.writeFileSync(out, bundle);
|
|
411
|
-
console.error(`Built ${out} (${(bundle.byteLength / 1024).toFixed(1)} KB)`);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Build + publish a new immutable package version from the project's contract +
|
|
415
|
-
* bundle. Creates the registry package on first publish (stamping its id into
|
|
416
|
-
* the manifest), then publishes the next monotonic version. Returns the new
|
|
417
|
-
* version number and the resolved package id.
|
|
418
|
-
*/
|
|
419
|
-
async function publishVersion(client, projectDir, opts) {
|
|
420
|
-
const project = readPackageProject(projectDir);
|
|
421
|
-
const contract = readContract(projectDir);
|
|
422
|
-
let packageId = project.manifest.id;
|
|
423
|
-
if (packageId === null) {
|
|
424
|
-
const pkg = await client.createAppPackage({
|
|
425
|
-
name: project.manifest.name,
|
|
426
|
-
description: project.manifest.description,
|
|
427
|
-
});
|
|
428
|
-
packageId = pkg.id;
|
|
429
|
-
project.manifest.id = packageId;
|
|
430
|
-
writePackageManifest(projectDir, project);
|
|
431
|
-
console.error(`Created package ${pkg.name} (${pkg.id}).`);
|
|
432
|
-
}
|
|
433
|
-
const bundle = await buildPackageBundle(projectDir);
|
|
434
|
-
console.error("Publishing version...");
|
|
435
|
-
const version = await client.publishAppPackageVersion(packageId, {
|
|
436
|
-
contract,
|
|
437
|
-
bundle,
|
|
438
|
-
changelog: opts.changelog ?? null,
|
|
439
|
-
channel: opts.channel ?? "release",
|
|
440
|
-
});
|
|
441
|
-
project.manifest.version = version.version;
|
|
442
|
-
writePackageManifest(projectDir, project);
|
|
443
|
-
return { package_id: packageId, version: version.version };
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* `lotics package publish [path] [-m <changelog>]` — publish a new version.
|
|
447
|
-
*/
|
|
448
|
-
export async function packagePublish(client, args) {
|
|
449
|
-
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
450
|
-
const { package_id, version } = await publishVersion(client, projectDir, {
|
|
451
|
-
changelog: args.changelog,
|
|
452
|
-
});
|
|
453
|
-
console.error(`Published ${package_id} v${version}.`);
|
|
454
|
-
console.error(` Install it: lotics package install ${package_id} --version ${version}`);
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Fail loud unless `workspace` is a throwaway dev workspace. The dev/sync
|
|
458
|
-
* scaffold path publishes a new version and scaffold-installs package tables into
|
|
459
|
-
* the resolved workspace, so the target MUST be a dev workspace — this mirrors
|
|
460
|
-
* the server-side `package reset` gate so a forgotten `--workspace` (prod
|
|
461
|
-
* selected) can never scaffold package tables into prod. Fails closed: an absent
|
|
462
|
-
* `is_dev` (a server that doesn't yet serialize it) is treated as non-dev.
|
|
463
|
-
*/
|
|
464
|
-
export function assertDevWorkspace(workspace) {
|
|
465
|
-
if (workspace.is_dev === true)
|
|
466
|
-
return;
|
|
467
|
-
throw new Error(`Workspace "${workspace.name}" (${workspace.id}) is not a dev workspace. ` +
|
|
468
|
-
`"lotics package dev"/"sync" scaffold package tables into the target workspace, so it must be a throwaway dev workspace. ` +
|
|
469
|
-
`Create one with "lotics workspace create <name> --dev" and pass --workspace <dev_ws>.`);
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Scaffold-sync the local package into the dev workspace: publish the current
|
|
473
|
-
* contract + bundle as a new version, then install it (first time) or upgrade
|
|
474
|
-
* the recorded installation (subsequent runs) so the dev workspace goes through
|
|
475
|
-
* the exact scaffold + materialize the real install/upgrade run. Records the
|
|
476
|
-
* installation pin per dev workspace in the manifest. Returns the dev
|
|
477
|
-
* installation's app id + name.
|
|
478
|
-
*/
|
|
479
|
-
async function syncToDevWorkspace(client, projectDir) {
|
|
480
|
-
const devWorkspaceId = client.getWorkspaceId();
|
|
481
|
-
if (!devWorkspaceId) {
|
|
482
|
-
throw new Error("No dev workspace selected. Pass --workspace <dev_ws> (a workspace created with --dev).");
|
|
483
|
-
}
|
|
484
|
-
// Guard BEFORE publishing: dev/sync scaffold-installs package tables into the
|
|
485
|
-
// resolved workspace, so it must be a throwaway dev workspace — a developer who
|
|
486
|
-
// forgot --workspace with prod selected would otherwise scaffold into prod.
|
|
487
|
-
// Mirrors the server-side `package reset` gate (which refuses any non-`is_dev`
|
|
488
|
-
// workspace).
|
|
489
|
-
const workspace = await client.getWorkspaceInfo(devWorkspaceId);
|
|
490
|
-
if (!workspace) {
|
|
491
|
-
throw new Error(`Workspace ${devWorkspaceId} is not accessible with these credentials. Pass --workspace <dev_ws> (a workspace created with --dev).`);
|
|
492
|
-
}
|
|
493
|
-
assertDevWorkspace(workspace);
|
|
494
|
-
// Contract may have been edited since the last sync — regenerate the
|
|
495
|
-
// runtime F/OPT/ROLE surface before the build that publishVersion runs.
|
|
496
|
-
writePackageAppFields(projectDir);
|
|
497
|
-
// 'dev' channel: a real immutable version this dev installation pins by
|
|
498
|
-
// number, but never what "install latest" resolves to — a broken
|
|
499
|
-
// mid-iteration contract must not become the org-wide installable default.
|
|
500
|
-
const { package_id, version } = await publishVersion(client, projectDir, {
|
|
501
|
-
changelog: "dev sync",
|
|
502
|
-
channel: "dev",
|
|
503
|
-
});
|
|
504
|
-
// Re-read after publish (publishVersion may have stamped the package id).
|
|
505
|
-
const project = readPackageProject(projectDir);
|
|
506
|
-
const existing = project.manifest.dev[devWorkspaceId];
|
|
507
|
-
let appId;
|
|
508
|
-
if (existing) {
|
|
509
|
-
console.error(`Upgrading dev installation ${existing.app_id} → v${version}...`);
|
|
510
|
-
const app = await client.upgradeAppPackage(existing.app_id, { version });
|
|
511
|
-
appId = app.id;
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
console.error(`Installing ${package_id} v${version} into dev workspace ${devWorkspaceId}...`);
|
|
515
|
-
const app = await client.installAppPackage(package_id, { version });
|
|
516
|
-
appId = app.id;
|
|
517
|
-
}
|
|
518
|
-
project.manifest.dev[devWorkspaceId] = { app_id: appId, version };
|
|
519
|
-
writePackageManifest(projectDir, project);
|
|
520
|
-
const installed = await client.getApp(appId);
|
|
521
|
-
// Regenerate the typed `.lotics/app_{queries,workflows,agents}.d.ts` companions
|
|
522
|
-
// from the live installation — the SAME maps (`apps.queries`/`workflows`/`agents`,
|
|
523
|
-
// keyed by the contract's runtime aliases) `lotics app pull` codegens from — so
|
|
524
|
-
// `useQuery("items")` / `useWorkflow` are typed in the package dev loop, not just
|
|
525
|
-
// in pulled app projects. (`.lotics/app_fields.ts`, the runtime F/OPT surface, was
|
|
526
|
-
// already refreshed from the contract before publish.) `writeAppDts` also heals the
|
|
527
|
-
// tsconfig include/exclude so the generated `.d.ts` actually load.
|
|
528
|
-
writeAppDts(projectDir, {
|
|
529
|
-
workflows: installed.workflows ?? undefined,
|
|
530
|
-
queries: installed.queries ?? undefined,
|
|
531
|
-
agents: installed.agents ?? undefined,
|
|
532
|
-
});
|
|
533
|
-
return { app_id: appId, app_name: installed.name, workspace_id: devWorkspaceId, version };
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* `lotics package sync [path]` — re-run the scaffold-sync into the dev workspace
|
|
537
|
-
* (the continuous loop: edit contract → sync additively migrates + re-materializes).
|
|
538
|
-
*/
|
|
539
|
-
export async function packageSync(client, args) {
|
|
540
|
-
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
541
|
-
const result = await syncToDevWorkspace(client, projectDir);
|
|
542
|
-
console.error(`Synced ${result.app_name} v${result.version} → ${result.app_id} (workspace ${result.workspace_id}).`);
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* `lotics package dev [path] [--workspace <dev_ws>] [--view-as <member>]` —
|
|
546
|
-
* scaffold-sync the package into the dev workspace, then run the existing app
|
|
547
|
-
* dev server against the resulting installation (HMR over the local source, RPC
|
|
548
|
-
* forwarded to the live installation). The inner loop: edit contract → re-run to
|
|
549
|
-
* sync; edit code → hot reload.
|
|
550
|
-
*/
|
|
551
|
-
export async function packageDev(client, args) {
|
|
552
|
-
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
553
|
-
const synced = await syncToDevWorkspace(client, projectDir);
|
|
554
|
-
const handle = await startDevServer({
|
|
555
|
-
projectDir,
|
|
556
|
-
app_id: synced.app_id,
|
|
557
|
-
app_name: synced.app_name,
|
|
558
|
-
workspace_id: synced.workspace_id,
|
|
559
|
-
api_url: client.baseUrl,
|
|
560
|
-
port: args.port,
|
|
561
|
-
vitePort: args.vitePort,
|
|
562
|
-
client,
|
|
563
|
-
});
|
|
564
|
-
await handle.ready;
|
|
565
|
-
const url = `http://localhost:${handle.port}`;
|
|
566
|
-
console.error(`\n lotics package dev`);
|
|
567
|
-
console.error(` package: ${synced.app_name} (dev installation ${synced.app_id} v${synced.version})`);
|
|
568
|
-
console.error(` workspace: ${synced.workspace_id} (dev)`);
|
|
569
|
-
if (client.viewAsMemberId) {
|
|
570
|
-
console.error(` view as: ${client.viewAsMemberId}`);
|
|
571
|
-
}
|
|
572
|
-
console.error(` vite: http://localhost:${handle.vitePort}/`);
|
|
573
|
-
console.error(` open: ${url}`);
|
|
574
|
-
console.error(` rpc: ${client.baseUrl} (via Bearer API key)\n`);
|
|
575
|
-
console.error(` Edit contract.json then re-run "lotics package dev" / "lotics package sync" to migrate.`);
|
|
576
|
-
console.error(` Ctrl-C to stop.\n`);
|
|
577
|
-
openBrowser(url);
|
|
578
|
-
await new Promise((resolve) => {
|
|
579
|
-
const onSig = () => {
|
|
580
|
-
process.off("SIGINT", onSig);
|
|
581
|
-
process.off("SIGTERM", onSig);
|
|
582
|
-
resolve();
|
|
583
|
-
};
|
|
584
|
-
process.on("SIGINT", onSig);
|
|
585
|
-
process.on("SIGTERM", onSig);
|
|
586
|
-
});
|
|
587
|
-
console.error("\nStopping…");
|
|
588
|
-
await handle.stop();
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* `lotics package reset [path]` — DEV-ONLY, hard-gated. Drops the dev
|
|
592
|
-
* installation's package-owned scaffolded tables and re-scaffolds them clean. The
|
|
593
|
-
* backend refuses any workspace not flagged as a dev workspace, so this can never
|
|
594
|
-
* erase a real workspace's data. The dev installation is resolved from the
|
|
595
|
-
* project manifest's pin for the selected workspace.
|
|
596
|
-
*/
|
|
597
|
-
export async function packageReset(client, args) {
|
|
598
|
-
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
599
|
-
const devWorkspaceId = client.getWorkspaceId();
|
|
600
|
-
if (!devWorkspaceId) {
|
|
601
|
-
throw new Error("No dev workspace selected. Pass --workspace <dev_ws> (a workspace created with --dev).");
|
|
602
|
-
}
|
|
603
|
-
const { manifest } = readPackageProject(projectDir);
|
|
604
|
-
const pin = manifest.dev[devWorkspaceId];
|
|
605
|
-
if (!pin) {
|
|
606
|
-
throw new Error(`No dev installation recorded for workspace ${devWorkspaceId}. Run "lotics package dev --workspace ${devWorkspaceId}" first.`);
|
|
607
|
-
}
|
|
608
|
-
console.error(`Resetting dev installation ${pin.app_id} in workspace ${devWorkspaceId}...`);
|
|
609
|
-
const app = await client.resetAppPackage(pin.app_id);
|
|
610
|
-
console.error(`Reset ${app.name} → ${app.id}. The scaffolded tables were dropped and re-created clean.`);
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Resolve the installation to operate on: an explicit app id wins; otherwise
|
|
614
|
-
* fall back to the project manifest's dev pin for the selected workspace (the
|
|
615
|
-
* same resolution `package reset` uses).
|
|
616
|
-
*/
|
|
617
|
-
function resolveInstallationAppId(client, explicit) {
|
|
86
|
+
function resolveInstallationAppId(explicit) {
|
|
618
87
|
if (explicit)
|
|
619
88
|
return explicit;
|
|
620
|
-
|
|
621
|
-
if (!workspaceId) {
|
|
622
|
-
throw new Error("Pass an app id (lotics package doctor <app_id>) or select a workspace.");
|
|
623
|
-
}
|
|
624
|
-
let manifest;
|
|
625
|
-
try {
|
|
626
|
-
({ manifest } = readPackageProject(path.resolve(process.cwd())));
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
// CLI usage boundary: translate "this directory isn't a package project"
|
|
630
|
-
// into the action the caller actually needs.
|
|
631
|
-
throw new Error("No app id given and the current directory is not a package project. " +
|
|
632
|
-
"Pass one explicitly: lotics package doctor <app_id>.");
|
|
633
|
-
}
|
|
634
|
-
const pin = manifest.dev[workspaceId];
|
|
635
|
-
if (!pin) {
|
|
636
|
-
throw new Error(`No app id given and no dev installation recorded for workspace ${workspaceId}. ` +
|
|
637
|
-
"Pass one explicitly: lotics package doctor <app_id>.");
|
|
638
|
-
}
|
|
639
|
-
return pin.app_id;
|
|
89
|
+
throw new Error("Pass an app id — e.g. lotics package doctor <app_id>.");
|
|
640
90
|
}
|
|
91
|
+
const RESOLVE_VERBS = new Set(["recreate", "revert", "keep", "apply", "archive", "unbind"]);
|
|
641
92
|
/**
|
|
642
|
-
* Parse repeated `--resolve <key>=<value>` flags
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
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.
|
|
646
100
|
*/
|
|
647
101
|
export function parseResolveFlags(resolve) {
|
|
648
102
|
const resolutions = {};
|
|
649
103
|
for (const entry of resolve) {
|
|
650
104
|
const eq = entry.indexOf("=");
|
|
651
105
|
if (eq <= 0 || eq === entry.length - 1) {
|
|
652
|
-
throw new Error(`Invalid --resolve "${entry}" — expected <key
|
|
106
|
+
throw new Error(`Invalid --resolve "${entry}" — expected <key>=<verb> (recreate|revert|keep|apply|archive|unbind) or <key>=<existing_id>.`);
|
|
653
107
|
}
|
|
654
108
|
const key = entry.slice(0, eq);
|
|
655
109
|
const value = entry.slice(eq + 1);
|
|
656
|
-
resolutions[key] =
|
|
657
|
-
|
|
110
|
+
resolutions[key] = RESOLVE_VERBS.has(value)
|
|
111
|
+
? value
|
|
112
|
+
: { bind_to: value };
|
|
658
113
|
}
|
|
659
114
|
return resolutions;
|
|
660
115
|
}
|
|
@@ -663,9 +118,10 @@ export function parseResolveFlags(resolve) {
|
|
|
663
118
|
* non-zero when drift is found so scripts can gate on it.
|
|
664
119
|
*/
|
|
665
120
|
export async function packageDoctor(client, args) {
|
|
666
|
-
const app_id = resolveInstallationAppId(
|
|
667
|
-
const health = await client.
|
|
668
|
-
console.error(`${health.package_name} — installation ${health.app_id}`
|
|
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)" : ""));
|
|
669
125
|
console.error(` Installed: v${health.installed_version} Latest: v${health.latest_version}` +
|
|
670
126
|
(health.update_available ? " → update available" : ""));
|
|
671
127
|
if (health.drift.length === 0) {
|
|
@@ -676,33 +132,91 @@ export async function packageDoctor(client, args) {
|
|
|
676
132
|
for (const d of health.drift) {
|
|
677
133
|
console.error(` - ${d.namespace}.${d.alias} → ${d.id} (missing from the workspace)`);
|
|
678
134
|
}
|
|
679
|
-
console.error(` Resolve while upgrading:\n lotics
|
|
135
|
+
console.error(` Resolve while upgrading:\n lotics upgrade ${app_id}` +
|
|
680
136
|
` --resolve <namespace.alias>=recreate (or =<existing_id> to re-point)`);
|
|
681
137
|
process.exitCode = 1;
|
|
682
138
|
}
|
|
683
139
|
if (health.modified.length === 0) {
|
|
684
|
-
console.error(
|
|
140
|
+
console.error(health.is_origin
|
|
141
|
+
? " Package artifacts: no changes since the last release."
|
|
142
|
+
: " Package artifacts: pristine — no local edits an upgrade would revert.");
|
|
143
|
+
}
|
|
144
|
+
else if (health.is_origin) {
|
|
145
|
+
// The origin IS the author's working copy — edits since the last release are
|
|
146
|
+
// the NEXT release's payload, not consumer drift, and never fail the check.
|
|
147
|
+
console.error(` Changed since v${health.installed_version} (${health.modified.length}) — a release will publish these:`);
|
|
148
|
+
for (const m of health.modified) {
|
|
149
|
+
console.error(` - ${m.kind}.${m.alias}`);
|
|
150
|
+
}
|
|
151
|
+
console.error(` Cut the next version: lotics app release ${app_id} -m "<what changed>"`);
|
|
685
152
|
}
|
|
686
153
|
else {
|
|
687
154
|
console.error(` Locally modified package artifacts (${health.modified.length}):`);
|
|
688
155
|
for (const m of health.modified) {
|
|
689
156
|
console.error(` - ${m.kind}.${m.alias} (an upgrade overwrites this unless kept)`);
|
|
690
157
|
}
|
|
691
|
-
console.error(` Consent while upgrading:\n lotics
|
|
158
|
+
console.error(` Consent while upgrading:\n lotics upgrade ${app_id}` +
|
|
692
159
|
` --resolve <kind.alias>=revert (or =keep to retain the edit)`);
|
|
693
160
|
process.exitCode = 1;
|
|
694
161
|
}
|
|
162
|
+
// Package-managed knowledge (bundled with this app install): drift + local edits
|
|
163
|
+
// + unmet expects. All advisory; resolved through the app's package upgrade.
|
|
164
|
+
if (health.knowledge_drift.length > 0) {
|
|
165
|
+
console.error(` Knowledge binding drift (${health.knowledge_drift.length}):`);
|
|
166
|
+
for (const d of health.knowledge_drift) {
|
|
167
|
+
console.error(` - ${d.alias} "${d.name}" → ${d.doc_id ?? "(unbound)"} (missing from the workspace)`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (health.knowledge_modified.length > 0) {
|
|
171
|
+
if (health.is_origin) {
|
|
172
|
+
console.error(` Knowledge changed since v${health.installed_version} (${health.knowledge_modified.length}) — a release will re-snapshot these:`);
|
|
173
|
+
for (const m of health.knowledge_modified) {
|
|
174
|
+
console.error(` - ${m.alias} "${m.name}"`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.error(` Locally edited package knowledge (${health.knowledge_modified.length}):`);
|
|
179
|
+
for (const m of health.knowledge_modified) {
|
|
180
|
+
console.error(` - ${m.alias} "${m.name}" (an upgrade overwrites this unless kept)`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (health.missing_expected_docs.length > 0) {
|
|
185
|
+
console.error(` Missing expected knowledge docs (${health.missing_expected_docs.length}):`);
|
|
186
|
+
for (const name of health.missing_expected_docs) {
|
|
187
|
+
console.error(` - "${name}" (the package's agents route to this name; no matching doc exists)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Drift is genuine breakage on the origin too (a bound id vanished); edited
|
|
191
|
+
// knowledge on the origin is staged release work, not a failure.
|
|
192
|
+
if (health.knowledge_drift.length > 0 || (health.knowledge_modified.length > 0 && !health.is_origin)) {
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
}
|
|
195
|
+
if (health.knowledge_drift.length === 0 &&
|
|
196
|
+
health.knowledge_modified.length === 0 &&
|
|
197
|
+
health.missing_expected_docs.length === 0) {
|
|
198
|
+
console.error(" Knowledge: healthy — bound docs resolve, none locally edited, expects met.");
|
|
199
|
+
}
|
|
695
200
|
if (health.update_available) {
|
|
696
|
-
console.error(` Upgrade: lotics
|
|
201
|
+
console.error(` Upgrade: lotics upgrade ${app_id}`);
|
|
697
202
|
}
|
|
698
203
|
}
|
|
699
204
|
/**
|
|
700
|
-
* Preview-then-apply upgrade. Prints the additive plan +
|
|
701
|
-
* removals; refuses (exit 1, with
|
|
702
|
-
* binding drift
|
|
205
|
+
* Preview-then-apply an app-installation upgrade. Prints the additive plan +
|
|
206
|
+
* informational removals + any bundled-knowledge changes; refuses (exit 1, with
|
|
207
|
+
* the exact --resolve syntax) while any binding drift, modified core artifact, or
|
|
208
|
+
* consent-requiring bundled-knowledge doc lacks a resolution.
|
|
209
|
+
*
|
|
210
|
+
* `--resolve` speaks ONE namespaced grammar, passed to the server verbatim: a
|
|
211
|
+
* drifted binding entry (`<namespace>.<alias>`), a modified artifact
|
|
212
|
+
* (`<kind>.<alias>`), a bundled knowledge doc (`knowledge.<alias>` —
|
|
213
|
+
* apply|keep|archive|recreate|unbind), or a live-role re-point
|
|
214
|
+
* (`roles.<alias>=<grp_id>`). `--bind-to` consents an added-knowledge-doc name
|
|
215
|
+
* collision; `--apply-all` accepts the package's version for every
|
|
216
|
+
* consent-requiring knowledge doc (overwriting local edits).
|
|
703
217
|
*/
|
|
704
218
|
export async function packageUpgrade(client, args) {
|
|
705
|
-
const preview = await client.
|
|
219
|
+
const preview = await client.previewPackageUpgrade(args.app_id, {
|
|
706
220
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
707
221
|
});
|
|
708
222
|
console.error(`Upgrade v${preview.from_version} → v${preview.to_version}` +
|
|
@@ -717,6 +231,12 @@ export async function packageUpgrade(client, args) {
|
|
|
717
231
|
console.error(` Adds: ${added}`);
|
|
718
232
|
if (removed)
|
|
719
233
|
console.error(` Unbinds (workspace data kept): ${removed}`);
|
|
234
|
+
if (preview.knowledge.length > 0) {
|
|
235
|
+
console.error(` Knowledge docs (${preview.knowledge.length}):`);
|
|
236
|
+
for (const entry of preview.knowledge) {
|
|
237
|
+
console.error(` ${formatKnowledgeEntryLine(entry)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
720
240
|
// Breaking contract changes are NOT resolvable via --resolve — scaffold would
|
|
721
241
|
// refuse them on a bound alias, and there is no in-flow remediation. Report
|
|
722
242
|
// every entry and the only two real remedies, then hard-stop.
|
|
@@ -730,9 +250,26 @@ export async function packageUpgrade(client, args) {
|
|
|
730
250
|
` before making the change: lotics package eject ${args.app_id}`);
|
|
731
251
|
process.exit(1);
|
|
732
252
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
253
|
+
// ONE namespaced grammar — the flags pass to the server verbatim. `--bind-to`
|
|
254
|
+
// is sugar for `knowledge.<alias>={bind_to}`; `--apply-all` fills the
|
|
255
|
+
// remaining consent-requiring knowledge entries with the accept verb.
|
|
256
|
+
const resolutions = parseResolveFlags(args.resolve);
|
|
257
|
+
for (const [alias, id] of Object.entries(parseBindToFlags(args.bindTo))) {
|
|
258
|
+
resolutions[`knowledge.${alias}`] = { bind_to: id };
|
|
259
|
+
}
|
|
260
|
+
if (args.applyAll) {
|
|
261
|
+
for (const entry of preview.knowledge) {
|
|
262
|
+
if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined) {
|
|
263
|
+
resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const unresolvedDrift = preview.drift.filter((d) => resolutions[`${d.namespace}.${d.alias}`] === undefined);
|
|
268
|
+
const unresolvedModified = preview.modified.filter((m) => resolutions[`${m.kind}.${m.alias}`] === undefined);
|
|
269
|
+
const unresolvedKnowledge = preview.knowledge.filter((e) => knowledgeEntryNeedsConsent(e) && resolutions[`knowledge.${e.alias}`] === undefined);
|
|
270
|
+
if (unresolvedDrift.length > 0 ||
|
|
271
|
+
unresolvedModified.length > 0 ||
|
|
272
|
+
unresolvedKnowledge.length > 0) {
|
|
736
273
|
if (unresolvedDrift.length > 0) {
|
|
737
274
|
console.error(" Binding drift must be resolved before upgrading:");
|
|
738
275
|
for (const d of unresolvedDrift) {
|
|
@@ -745,14 +282,33 @@ export async function packageUpgrade(client, args) {
|
|
|
745
282
|
console.error(` --resolve ${m.kind}.${m.alias}=revert (overwrite the local edit; or =keep to retain it)`);
|
|
746
283
|
}
|
|
747
284
|
}
|
|
285
|
+
if (unresolvedKnowledge.length > 0) {
|
|
286
|
+
console.error(" Bundled knowledge docs need consent before upgrading (or --apply-all to accept the package's version for all):");
|
|
287
|
+
for (const entry of unresolvedKnowledge) {
|
|
288
|
+
console.error(formatKnowledgeResolveHint(entry));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
748
291
|
process.exit(1);
|
|
749
292
|
}
|
|
750
|
-
const app = await client.
|
|
293
|
+
const app = await client.upgradePackage(args.app_id, {
|
|
751
294
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
752
|
-
...(Object.keys(
|
|
295
|
+
...(Object.keys(resolutions).length > 0 ? { resolutions } : {}),
|
|
753
296
|
});
|
|
754
297
|
console.error(`Upgraded ${app.name} → v${app.package_version} (${app.id}).`);
|
|
755
298
|
}
|
|
299
|
+
/** `--apply-all`'s accept-the-package resolution for a consent-requiring entry. */
|
|
300
|
+
function knowledgeAcceptResolution(change) {
|
|
301
|
+
switch (change) {
|
|
302
|
+
case "changed":
|
|
303
|
+
return "apply";
|
|
304
|
+
case "removed":
|
|
305
|
+
return "archive";
|
|
306
|
+
case "drifted":
|
|
307
|
+
return "recreate";
|
|
308
|
+
case "added":
|
|
309
|
+
return "apply"; // unreachable (added never needs consent), kept total.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
756
312
|
/** The install-consent trust line: official > your own org > third-party. */
|
|
757
313
|
function trustBadge(pkg) {
|
|
758
314
|
if (pkg.is_official)
|
|
@@ -764,11 +320,12 @@ function trustBadge(pkg) {
|
|
|
764
320
|
/**
|
|
765
321
|
* `lotics package show <package_id>` — registry metadata + version history
|
|
766
322
|
* (trust badge, retirement, per-version channel/yank/changelog). The read
|
|
767
|
-
* surface for "what is this package and what shipped when".
|
|
323
|
+
* surface for "what is this package and what shipped when". Kind-agnostic — a
|
|
324
|
+
* content package shows here the same way.
|
|
768
325
|
*/
|
|
769
326
|
export async function packageShow(client, args) {
|
|
770
|
-
const pkg = await client.
|
|
771
|
-
const { versions } = await client.
|
|
327
|
+
const pkg = await client.getPackage(args.package_id);
|
|
328
|
+
const { versions } = await client.listPackageVersions(args.package_id);
|
|
772
329
|
console.error(`${pkg.name} (${pkg.id}) — ${trustBadge(pkg)}`);
|
|
773
330
|
if (pkg.description)
|
|
774
331
|
console.error(` ${pkg.description}`);
|
|
@@ -787,22 +344,260 @@ export async function packageShow(client, args) {
|
|
|
787
344
|
if (versions.length === 0)
|
|
788
345
|
console.error(" (no published versions)");
|
|
789
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* One preview line for a knowledge upgrade entry — `[change] alias "name"` with
|
|
349
|
+
* `modified` / `needs consent` marks. Shared verbatim by the standalone (`pci_`)
|
|
350
|
+
* and the app-install upgrade previews so both speak one vocabulary; the caller
|
|
351
|
+
* owns the leading indent.
|
|
352
|
+
*/
|
|
353
|
+
function formatKnowledgeEntryLine(entry) {
|
|
354
|
+
const marks = [
|
|
355
|
+
entry.modified ? "modified" : null,
|
|
356
|
+
knowledgeEntryNeedsConsent(entry) ? "needs consent" : null,
|
|
357
|
+
].filter((m) => m !== null);
|
|
358
|
+
return `[${entry.change}] ${entry.alias} "${entry.name}"${marks.length ? ` (${marks.join(", ")})` : ""}`;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The exact `--resolve <alias>=<valid options>` remediation line for a
|
|
362
|
+
* consent-requiring knowledge entry (options from the shared
|
|
363
|
+
* `validKnowledgeResolutions`). Shared by both upgrade paths' gate output.
|
|
364
|
+
*/
|
|
365
|
+
function formatKnowledgeResolveHint(entry) {
|
|
366
|
+
const note = entry.change === "changed" || entry.change === "removed"
|
|
367
|
+
? " (a local edit — apply/archive overwrites it; keep retains it)"
|
|
368
|
+
: " (bound doc is gone; recreate from the package, or unbind)";
|
|
369
|
+
return ` --resolve knowledge.${entry.alias}=${validKnowledgeResolutions(entry.change).join("|")}${note}`;
|
|
370
|
+
}
|
|
371
|
+
/** The `--resolve template.<alias>=revert|keep` remediation line for a consent-requiring template. */
|
|
372
|
+
function formatTemplateResolveHint(entry) {
|
|
373
|
+
const note = entry.baseline_unknown
|
|
374
|
+
? " (can't verify the local edit — older package version; revert overwrites, keep retains)"
|
|
375
|
+
: " (a local edit — revert overwrites it with the package's version; keep retains it)";
|
|
376
|
+
return ` --resolve template.${entry.alias}=revert|keep${note}`;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Preview-then-apply a STANDALONE content installation upgrade — knowledge docs
|
|
380
|
+
* AND document templates behind one review gate. Prints the per-alias plan;
|
|
381
|
+
* refuses (exit 1, with the exact `--resolve` syntax) while any consent-requiring
|
|
382
|
+
* entry lacks a resolution — a modified knowledge change/removal or drift, or a
|
|
383
|
+
* locally-edited changed template. `--apply-all` auto-resolves EVERY consent entry
|
|
384
|
+
* by accepting the package's version (knowledge changed→apply, removed→archive,
|
|
385
|
+
* drifted→recreate; template→revert) — an explicit bulk "take upstream" that
|
|
386
|
+
* discards local edits. `--resolve knowledge.<alias>=<value>` /
|
|
387
|
+
* `--resolve template.<alias>=revert|keep` and `--bind-to <alias>=<kdc_id>`
|
|
388
|
+
* resolve entries individually — the ONE namespaced grammar, passed to the
|
|
389
|
+
* server verbatim.
|
|
390
|
+
*/
|
|
391
|
+
export async function packageUpgradeKnowledge(client, args) {
|
|
392
|
+
const preview = await client.previewContentInstallationUpgrade(args.installation_id, {
|
|
393
|
+
...(args.version !== undefined ? { version: args.version } : {}),
|
|
394
|
+
});
|
|
395
|
+
console.error(`Content upgrade v${preview.from_version} → v${preview.to_version}` +
|
|
396
|
+
(preview.changelog ? ` — ${preview.changelog}` : ""));
|
|
397
|
+
const apply = async (resolutions) => {
|
|
398
|
+
const updated = await client.applyContentInstallationUpgrade(args.installation_id, {
|
|
399
|
+
...(args.version !== undefined ? { version: args.version } : {}),
|
|
400
|
+
resolutions,
|
|
401
|
+
});
|
|
402
|
+
console.error(`Upgraded ${args.installation_id} → v${updated.package_version}.`);
|
|
403
|
+
};
|
|
404
|
+
if (preview.entries.length === 0 && preview.templates.length === 0) {
|
|
405
|
+
if (preview.to_version === preview.from_version) {
|
|
406
|
+
console.error(" Already up to date — no content changes.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.error(" No changes needing consent — advancing the version pin; clean template updates apply automatically.");
|
|
410
|
+
await apply({});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
for (const entry of preview.entries) {
|
|
414
|
+
console.error(` ${formatKnowledgeEntryLine(entry)}`);
|
|
415
|
+
}
|
|
416
|
+
for (const entry of preview.templates) {
|
|
417
|
+
console.error(` [template] ${entry.alias} (modified — needs consent)`);
|
|
418
|
+
}
|
|
419
|
+
// ONE namespaced grammar — the flags pass to the server verbatim. `--bind-to`
|
|
420
|
+
// is sugar for `knowledge.<alias>={bind_to}`; `--apply-all` fills the
|
|
421
|
+
// remaining consent entries with the accept verb (template → revert).
|
|
422
|
+
const resolutions = parseResolveFlags(args.resolve);
|
|
423
|
+
for (const [alias, id] of Object.entries(args.bind_to)) {
|
|
424
|
+
resolutions[`knowledge.${alias}`] = { bind_to: id };
|
|
425
|
+
}
|
|
426
|
+
if (args.applyAll) {
|
|
427
|
+
for (const entry of preview.entries) {
|
|
428
|
+
if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined) {
|
|
429
|
+
resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const entry of preview.templates) {
|
|
433
|
+
if (resolutions[`template.${entry.alias}`] === undefined) {
|
|
434
|
+
resolutions[`template.${entry.alias}`] = "revert";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const unresolvedKnowledge = preview.entries.filter((entry) => knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === undefined);
|
|
439
|
+
const unresolvedTemplates = preview.templates.filter((entry) => resolutions[`template.${entry.alias}`] === undefined);
|
|
440
|
+
if (unresolvedKnowledge.length > 0 || unresolvedTemplates.length > 0) {
|
|
441
|
+
console.error(" These need a resolution before upgrading (or pass --apply-all to accept the package's version for all):");
|
|
442
|
+
for (const entry of unresolvedKnowledge)
|
|
443
|
+
console.error(formatKnowledgeResolveHint(entry));
|
|
444
|
+
for (const entry of unresolvedTemplates)
|
|
445
|
+
console.error(formatTemplateResolveHint(entry));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
await apply(resolutions);
|
|
449
|
+
}
|
|
450
|
+
/** Parse repeated `--bind-to alias=kdc_id` flags into an alias → doc-id consent map. */
|
|
451
|
+
export function parseBindToFlags(bindTo) {
|
|
452
|
+
const map = {};
|
|
453
|
+
for (const entry of bindTo) {
|
|
454
|
+
const eq = entry.indexOf("=");
|
|
455
|
+
if (eq <= 0 || eq === entry.length - 1) {
|
|
456
|
+
throw new Error(`Invalid --bind-to "${entry}" — expected <alias>=<kdc_id>.`);
|
|
457
|
+
}
|
|
458
|
+
map[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
459
|
+
}
|
|
460
|
+
return map;
|
|
461
|
+
}
|
|
462
|
+
/** Print advisory `knowledge_expects` misses loudly (never blocks the install). */
|
|
463
|
+
function warnMissingExpectedDocs(missing) {
|
|
464
|
+
if (missing.length === 0)
|
|
465
|
+
return;
|
|
466
|
+
console.error(` ⚠ ${missing.length} expected knowledge doc(s) the package's agents route to are missing from this workspace:`);
|
|
467
|
+
for (const name of missing)
|
|
468
|
+
console.error(` - "${name}"`);
|
|
469
|
+
console.error(" The package installed, but those agents will degrade until a doc with each name exists.");
|
|
470
|
+
}
|
|
790
471
|
export async function packageInstall(client, args) {
|
|
791
472
|
// Surface the trust badge at the consent point: installing materializes the
|
|
792
|
-
// package's workflows/agents
|
|
793
|
-
const pkg = await client.
|
|
794
|
-
|
|
795
|
-
|
|
473
|
+
// package's workflows/agents (or its doc corpus) under YOUR authority.
|
|
474
|
+
const pkg = await client.getPackage(args.package_id);
|
|
475
|
+
const kindLabel = pkg.kind === "content" ? "content package" : "package";
|
|
476
|
+
console.error(`Installing ${pkg.name} — ${trustBadge(pkg)} (${kindLabel})...`);
|
|
477
|
+
const result = await client.installPackage(args.package_id, {
|
|
796
478
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
797
|
-
...(args.
|
|
479
|
+
...(args.bind_to && Object.keys(args.bind_to).length > 0 ? { bind_to: args.bind_to } : {}),
|
|
480
|
+
...(args.config && Object.keys(args.config).length > 0 ? { config: args.config } : {}),
|
|
798
481
|
});
|
|
482
|
+
if (result.kind === "content") {
|
|
483
|
+
const { installation, warnings } = result;
|
|
484
|
+
const docs = installation.binding.knowledge;
|
|
485
|
+
const templates = installation.binding.templates;
|
|
486
|
+
console.error(`Installed ${pkg.name} v${installation.package_version} → content installation ${installation.id} ` +
|
|
487
|
+
`(workspace ${installation.workspace_id}).`);
|
|
488
|
+
const docAliases = Object.keys(docs);
|
|
489
|
+
if (docAliases.length > 0) {
|
|
490
|
+
console.error(` ${docAliases.length} doc(s) live: ${docAliases.map((a) => `${a}→${docs[a]}`).join(", ")}`);
|
|
491
|
+
}
|
|
492
|
+
const templateAliases = Object.keys(templates);
|
|
493
|
+
if (templateAliases.length > 0) {
|
|
494
|
+
console.error(` ${templateAliases.length} template(s) live: ${templateAliases.map((a) => `${a}→${templates[a]}`).join(", ")}`);
|
|
495
|
+
}
|
|
496
|
+
warnMissingExpectedDocs(warnings.missing_expected_docs);
|
|
497
|
+
console.error(` Upgrade later: lotics upgrade ${installation.id}`);
|
|
498
|
+
console.error(` Uninstall: lotics uninstall ${installation.id} [--keep-content]`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const { app, knowledge_warnings } = result;
|
|
799
502
|
const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
|
|
800
503
|
console.error(`Installed ${app.name} ${versionLabel} → ${app.id} (workspace ${app.workspace_id}).`);
|
|
801
504
|
console.error(" The data model, queries, workflows, and agents are live.");
|
|
802
|
-
|
|
505
|
+
warnMissingExpectedDocs(knowledge_warnings.missing_expected_docs);
|
|
506
|
+
// Follow-up ops key on the NEW installation id (app_), not the package id typed
|
|
507
|
+
// at `install` — signpost each one with the id inlined (the content branch above
|
|
508
|
+
// does the same), so the id switch is never a silent trap.
|
|
509
|
+
console.error(` Pull it for local editing: lotics app pull ${app.id}`);
|
|
510
|
+
console.error(` Upgrade later: lotics upgrade ${app.id}`);
|
|
511
|
+
console.error(` Health / uninstall: lotics package doctor ${app.id} · lotics uninstall ${app.id} [--archive-tables]`);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* `lotics uninstall <app_id|pci_id>` — ONE top-level command over both installation
|
|
515
|
+
* kinds, dispatched by the id form (mirrors `lotics upgrade`):
|
|
516
|
+
* - a `pci_` id → a STANDALONE CONTENT installation: deletes the row and (unless
|
|
517
|
+
* `--keep-content`) archives its package-bound docs AND templates, listing each
|
|
518
|
+
* archived id.
|
|
519
|
+
* - anything else (an `app_id`) → an APP installation: archives its workflow
|
|
520
|
+
* artifacts and — with `--archive-tables` — the scaffolded entity tables
|
|
521
|
+
* (provenance- + reference-gated server-side).
|
|
522
|
+
* A flag used on the wrong path is a loud error, never silently ignored.
|
|
523
|
+
*/
|
|
524
|
+
export async function packageUninstall(client, args) {
|
|
525
|
+
if (args.id.startsWith("pci_")) {
|
|
526
|
+
if (args.archive_tables) {
|
|
527
|
+
throw new Error("--archive-tables applies only to an app installation (<app_id>). A content installation " +
|
|
528
|
+
"(pci_) has no scaffolded tables — use --keep-content to retain its docs/templates.");
|
|
529
|
+
}
|
|
530
|
+
const result = await client.uninstallContentPackage(args.id, {
|
|
531
|
+
keep_content: args.keep_content,
|
|
532
|
+
});
|
|
533
|
+
if (args.keep_content) {
|
|
534
|
+
console.error(`Uninstalled content installation ${result.installation_id} — its docs and templates were kept as ordinary workspace content.`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.error(`Uninstalled content installation ${result.installation_id} — archived ` +
|
|
538
|
+
`${result.archived_doc_ids.length} doc(s) and ${result.archived_template_ids.length} template(s).`);
|
|
539
|
+
for (const docId of result.archived_doc_ids)
|
|
540
|
+
console.error(` ${docId}`);
|
|
541
|
+
for (const templateId of result.archived_template_ids)
|
|
542
|
+
console.error(` ${templateId}`);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (args.keep_content) {
|
|
547
|
+
throw new Error("--keep-content applies only to a standalone content installation (pci_). An app installation " +
|
|
548
|
+
"(<app_id>) uses --archive-tables to also archive its scaffolded tables.");
|
|
549
|
+
}
|
|
550
|
+
const app = await client.getApp(args.id);
|
|
551
|
+
if (!app.package_id) {
|
|
552
|
+
throw new Error(`App ${args.id} is not a package installation — use the app delete flow for a bespoke app.`);
|
|
553
|
+
}
|
|
554
|
+
const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
|
|
555
|
+
const boundCount = Object.keys(app.workflows ?? {}).length;
|
|
556
|
+
const tableCount = Object.keys(app.binding?.entities ?? {}).length;
|
|
557
|
+
console.error(`Uninstalling ${app.name} (${app.id}):`);
|
|
558
|
+
console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
|
|
559
|
+
console.error(args.archive_tables
|
|
560
|
+
? ` Archives ${tableCount} scaffolded table(s) — refused if they were adopted or are still referenced.`
|
|
561
|
+
: ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`);
|
|
562
|
+
const result = await client.uninstallAppPackage(args.id, {
|
|
563
|
+
archive_tables: args.archive_tables,
|
|
564
|
+
});
|
|
565
|
+
console.error(`Uninstalled ${app.name} (${app.id}).`);
|
|
566
|
+
if (result.archived_table_ids.length > 0) {
|
|
567
|
+
console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* `lotics package list-content` — list the selected workspace's STANDALONE
|
|
572
|
+
* content installations (an app-bundled corpus rides its app's
|
|
573
|
+
* `binding.knowledge` and shows on the Apps surface instead), each with its
|
|
574
|
+
* registry status. The read surface that surfaces a `pci_` id for
|
|
575
|
+
* `upgrade` / `uninstall` (install prints it once; nothing else did before).
|
|
576
|
+
*/
|
|
577
|
+
export async function packageListContent(client) {
|
|
578
|
+
const workspaceId = client.getWorkspaceId();
|
|
579
|
+
if (!workspaceId) {
|
|
580
|
+
throw new Error("No workspace selected. Pass --workspace <ws> (or select one) to list its content installations.");
|
|
581
|
+
}
|
|
582
|
+
const installations = await client.listContentInstallations(workspaceId);
|
|
583
|
+
if (installations.length === 0) {
|
|
584
|
+
console.error(`No package-managed content installations in workspace ${workspaceId}.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
console.error(`Content installations in workspace ${workspaceId} (${installations.length}):`);
|
|
588
|
+
for (const inst of installations) {
|
|
589
|
+
const name = inst.package_registry?.name ?? "(unknown package)";
|
|
590
|
+
const latest = inst.package_registry?.latest_version;
|
|
591
|
+
const updateAvailable = inst.package_registry?.update_available ?? false;
|
|
592
|
+
const versionLabel = latest !== undefined && latest !== inst.package_version
|
|
593
|
+
? `v${inst.package_version} → latest v${latest}`
|
|
594
|
+
: `v${inst.package_version}`;
|
|
595
|
+
console.error(` ${inst.id} ${name} ${versionLabel}` +
|
|
596
|
+
(updateAvailable ? " → update available" : ""));
|
|
597
|
+
}
|
|
803
598
|
}
|
|
804
599
|
export async function packageEject(client, args) {
|
|
805
|
-
const app = await client.
|
|
600
|
+
const app = await client.ejectPackage(args.app_id);
|
|
806
601
|
console.error(`Ejected ${app.name} → ${app.id} (workspace ${app.workspace_id}).`);
|
|
807
602
|
console.error(" The package link is severed — it's now a normal bespoke app and can no longer be upgraded.");
|
|
808
603
|
console.error(" Its data model, queries, workflows, and templates are unchanged.");
|
|
@@ -811,7 +606,7 @@ export async function packageEject(client, args) {
|
|
|
811
606
|
/**
|
|
812
607
|
* Parse one `key=value` config assignment. When the knob's type is known (from
|
|
813
608
|
* the stored value at `package config --set`) the value is parsed to that type
|
|
814
|
-
* loudly; otherwise (`
|
|
609
|
+
* loudly; otherwise (`lotics install --config`) it is inferred (true/false →
|
|
815
610
|
* boolean, numeric → number, else string) and the server validates it against
|
|
816
611
|
* the contract.
|
|
817
612
|
*/
|
|
@@ -889,218 +684,174 @@ export async function packageConfig(client, args) {
|
|
|
889
684
|
}
|
|
890
685
|
}
|
|
891
686
|
/**
|
|
892
|
-
* `lotics
|
|
893
|
-
*
|
|
894
|
-
*
|
|
687
|
+
* `lotics app unpublish <app_id|package_id> [--undo]` — take a published package
|
|
688
|
+
* off the shelf (or `--undo` restore it): new installs refuse it and it hides
|
|
689
|
+
* from other orgs, while existing installations keep working and may still
|
|
690
|
+
* upgrade. Given an app id (an installation of the package) it resolves the
|
|
691
|
+
* package from the app; a package id targets it directly. Owner-org admin-only.
|
|
895
692
|
*/
|
|
896
|
-
export async function
|
|
897
|
-
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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(", ")}`);
|
|
693
|
+
export async function appUnpublish(client, args) {
|
|
694
|
+
let packageId = args.id;
|
|
695
|
+
if (args.id.startsWith("app_")) {
|
|
696
|
+
const app = await client.getApp(args.id);
|
|
697
|
+
if (!app.package_id) {
|
|
698
|
+
throw new Error(`App ${args.id} is not a package installation — it has no package to unpublish. ` +
|
|
699
|
+
`Pass the package id directly.`);
|
|
700
|
+
}
|
|
701
|
+
packageId = app.package_id;
|
|
915
702
|
}
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* `lotics package retire <package_id> [--undo]` — retire (or un-retire) a
|
|
919
|
-
* registry package. Owner-org admin-only.
|
|
920
|
-
*/
|
|
921
|
-
export async function packageRetire(client, args) {
|
|
922
|
-
const pkg = await client.retireAppPackage(args.package_id, { undo: args.undo });
|
|
703
|
+
const pkg = await client.retirePackage(packageId, { undo: args.undo });
|
|
923
704
|
if (pkg.retired_at !== null) {
|
|
924
|
-
console.error(`
|
|
925
|
-
`existing installations keep working and may still upgrade
|
|
705
|
+
console.error(`Unpublished ${pkg.name} (${pkg.id}). New installs refuse it and it is hidden from other orgs; ` +
|
|
706
|
+
`existing installations keep working and may still upgrade. Undo: lotics app unpublish ${pkg.id} --undo`);
|
|
926
707
|
}
|
|
927
708
|
else {
|
|
928
|
-
console.error(`
|
|
709
|
+
console.error(`Re-published ${pkg.name} (${pkg.id}) — installable again.`);
|
|
929
710
|
}
|
|
930
711
|
}
|
|
931
712
|
/**
|
|
932
|
-
* `lotics
|
|
933
|
-
* package
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
713
|
+
* `lotics app publish [app_id|.] [--rename old=new ...] [-m <changelog>] [--yes]` —
|
|
714
|
+
* FIRST-RELEASE a bespoke app as a package (docs/packages.md § Promotion). Nothing
|
|
715
|
+
* starts as a package. Mirrors `app release`'s preview→apply UX: it first shows the
|
|
716
|
+
* dry-run preview (GET, no writes) — the package name, the auto-minted RENAMABLE
|
|
717
|
+
* aliases (the exact `--rename` keys, so v1's frozen aliases are inspected first,
|
|
718
|
+
* never a blind publish), and the extract findings — then APPLIES only with
|
|
719
|
+
* `--yes` (else exits 1 with the re-run hint). On apply the server extracts the
|
|
720
|
+
* contract, creates the registry package, publishes v1 from the DEPLOYED source +
|
|
721
|
+
* dist, and pins the origin as installation #1. `--rename old=new` fixes an
|
|
722
|
+
* auto-minted alias before v1 freezes it; an `error` finding blocks the apply. An
|
|
723
|
+
* already-linked app releases with `lotics app release` instead. The app id is the
|
|
724
|
+
* positional (`.`/omitted → resolved from the local app project manifest).
|
|
942
725
|
*/
|
|
943
|
-
export async function
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
726
|
+
export async function appPublish(client, args) {
|
|
727
|
+
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
728
|
+
const local = readLocalAppManifest(projectDir);
|
|
729
|
+
const explicit = args.app_id !== undefined && args.app_id !== "." ? args.app_id : undefined;
|
|
730
|
+
const appId = explicit ?? local?.app_id ?? null;
|
|
731
|
+
if (appId === null) {
|
|
732
|
+
throw new Error("No app id. Run `lotics app publish` from a pulled app project (lotics app pull <app_id>), " +
|
|
733
|
+
"or pass one: lotics app publish <app_id>.");
|
|
734
|
+
}
|
|
735
|
+
// Forward the app's package-managed knowledge declaration ONLY when the local
|
|
736
|
+
// manifest is this app's own project (agents reference docs by free text, so the
|
|
737
|
+
// author declares which the package owns). A bare id published from elsewhere
|
|
738
|
+
// ships without a bundled corpus — publish from the app dir to include it.
|
|
739
|
+
const knowledge = local && local.app_id === appId ? local.knowledge : [];
|
|
740
|
+
const renames = parseRenameFlags(args.renames);
|
|
741
|
+
// Preview first (GET, no writes) — the same preview→--yes flow as `app release`,
|
|
742
|
+
// so the aliases v1 freezes forever are never a blind publish and `--rename`
|
|
743
|
+
// targets are inspectable before committing.
|
|
744
|
+
const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
|
|
745
|
+
const { lines, hasError } = formatExtractReport(preview.findings);
|
|
746
|
+
console.error(`Publish preview — ${appId} as new package "${preview.package_name}" (v1):`);
|
|
747
|
+
const groups = [
|
|
748
|
+
["entities", preview.renamable_aliases.entities],
|
|
749
|
+
["fields", preview.renamable_aliases.fields],
|
|
750
|
+
["options", preview.renamable_aliases.options],
|
|
751
|
+
["roles", preview.renamable_aliases.roles],
|
|
752
|
+
["templates", preview.renamable_aliases.templates],
|
|
753
|
+
["workflows", preview.renamable_aliases.workflows],
|
|
754
|
+
];
|
|
755
|
+
if (groups.some(([, vals]) => vals.length > 0)) {
|
|
756
|
+
console.error(" Auto-minted aliases — rename any with --rename <alias>=<new> before v1 freezes them:");
|
|
757
|
+
for (const [label, vals] of groups) {
|
|
758
|
+
if (vals.length > 0)
|
|
759
|
+
console.error(` ${`${label}:`.padEnd(11)} ${vals.join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
console.error(" (query / app-workflow / agent runtime aliases are fixed — the shipped source calls them verbatim.)");
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
console.error(" No renamable aliases.");
|
|
947
765
|
}
|
|
948
|
-
const extracted = await client.extractAppPackage(args.app_id);
|
|
949
|
-
const { lines, hasError } = formatExtractReport(extracted.report);
|
|
950
766
|
if (lines.length > 0) {
|
|
951
|
-
console.error(`
|
|
767
|
+
console.error(` Findings (${preview.findings.length}):`);
|
|
952
768
|
for (const line of lines)
|
|
953
769
|
console.error(line);
|
|
954
770
|
}
|
|
955
|
-
else {
|
|
956
|
-
console.error("Extraction report: no findings.");
|
|
957
|
-
}
|
|
958
|
-
const targetPath = path.resolve(args.targetPath ?? appDirName(app.name));
|
|
959
|
-
if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
|
|
960
|
-
throw new Error(`Target directory ${targetPath} is not empty.`);
|
|
961
|
-
}
|
|
962
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
963
|
-
// Pull the app's current source archive — same mechanics as `lotics app pull`
|
|
964
|
-
// (getAppVersion → presigned source URL → download → untar into the project).
|
|
965
|
-
const version = await client.getAppVersion(app.id, app.current_version_id);
|
|
966
|
-
const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
|
|
967
|
-
const tmpFile = path.join(tmpdir(), `lotics-extract-${app.id}-${Date.now()}.tar.gz`);
|
|
968
|
-
console.error("Downloading source archive...");
|
|
969
|
-
await client.downloadFile(sourceUrl, tmpFile);
|
|
970
|
-
try {
|
|
971
|
-
console.error(`Extracting to ${targetPath}...`);
|
|
972
|
-
await runTar(["-xzf", tmpFile, "-C", targetPath], targetPath);
|
|
973
|
-
}
|
|
974
|
-
finally {
|
|
975
|
-
if (fs.existsSync(tmpFile))
|
|
976
|
-
fs.unlinkSync(tmpFile);
|
|
977
|
-
}
|
|
978
|
-
// Swap the app manifest (lotics.app_id/workspace_id) for a fresh, unpublished
|
|
979
|
-
// package manifest — atomic write via the existing manifest writer.
|
|
980
|
-
const appPkgJson = JSON.parse(fs.readFileSync(packageJsonPath(targetPath), "utf-8"));
|
|
981
|
-
const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
|
|
982
|
-
// The origin app's package.json rides in the source archive with ITS OWN
|
|
983
|
-
// @lotics/app-sdk range — raise it to the getAppBinding floor the generated
|
|
984
|
-
// .lotics/app_fields.ts needs (never lower an already-newer range).
|
|
985
|
-
const deps = isPlainObject(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
|
|
986
|
-
const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
|
|
987
|
-
const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
|
|
988
|
-
if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
|
|
989
|
-
const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
|
|
990
|
-
draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
|
|
991
|
-
console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
|
|
992
|
-
}
|
|
993
|
-
writePackageManifest(targetPath, draft);
|
|
994
|
-
// The alias-keyed draft contract — the reviewable master. Pretty + trailing newline.
|
|
995
|
-
fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(extracted.contract, null, 2) + "\n");
|
|
996
|
-
console.error(`Wrote ${CONTRACT_FILE}`);
|
|
997
|
-
// The origin's baked `.lotics/app_fields.ts` (concrete ids) never travels —
|
|
998
|
-
// regenerate it contract-derived so `F`/`OPT` resolve per-install at runtime
|
|
999
|
-
// and the extracted source compiles unchanged.
|
|
1000
|
-
writePackageAppFields(targetPath);
|
|
1001
|
-
// vite.config.ts is starter-owned machinery — refresh it wholesale so the
|
|
1002
|
-
// package project carries the current starter config (notably
|
|
1003
|
-
// `build.target: "es2022"`, which the generated app_fields' top-level await
|
|
1004
|
-
// requires). Origin-side dev customizations (e.g. `lotics ui link` aliases)
|
|
1005
|
-
// don't belong in a package.
|
|
1006
|
-
const starterViteConfig = buildStarterTemplate({
|
|
1007
|
-
app_name: app.name,
|
|
1008
|
-
app_id: "",
|
|
1009
|
-
workspace_id: "",
|
|
1010
|
-
}).find((f) => f.path === "vite.config.ts");
|
|
1011
|
-
if (starterViteConfig === undefined) {
|
|
1012
|
-
throw new Error("starter template is missing vite.config.ts — cannot refresh the package project");
|
|
1013
|
-
}
|
|
1014
|
-
const viteConfigPath = path.join(targetPath, "vite.config.ts");
|
|
1015
|
-
const originViteConfig = fs.existsSync(viteConfigPath)
|
|
1016
|
-
? fs.readFileSync(viteConfigPath, "utf-8")
|
|
1017
|
-
: null;
|
|
1018
|
-
fs.writeFileSync(viteConfigPath, starterViteConfig.content);
|
|
1019
|
-
if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
|
|
1020
|
-
// The origin may carry legitimate customizations (optimizeDeps entries,
|
|
1021
|
-
// plugins) beyond the dev-links that must not ship — never destroy them
|
|
1022
|
-
// silently: stash the original for manual re-application.
|
|
1023
|
-
const stash = path.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
|
|
1024
|
-
fs.writeFileSync(stash, originViteConfig);
|
|
1025
|
-
console.error("Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config " +
|
|
1026
|
-
"differed — its original was saved to .lotics/vite.config.origin.ts; re-apply any " +
|
|
1027
|
-
"custom optimizeDeps/plugins entries you still need (never dev-link aliases).");
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
|
|
1031
|
-
}
|
|
1032
|
-
// Stage each file-backed template at the `bytes_ref` the contract references.
|
|
1033
|
-
// The download lands in the target dir (fresh, so no name collision) under the
|
|
1034
|
-
// file's own name; rename it to the exact `bytes_ref` basename the contract
|
|
1035
|
-
// points at. Processed sequentially so a prior rename frees the name.
|
|
1036
|
-
for (const tf of extracted.template_files) {
|
|
1037
|
-
const dest = path.join(targetPath, tf.bytes_ref);
|
|
1038
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1039
|
-
const { path: downloaded } = await client.downloadFileById(tf.file_id, path.dirname(dest));
|
|
1040
|
-
if (path.resolve(downloaded) !== path.resolve(dest))
|
|
1041
|
-
fs.renameSync(downloaded, dest);
|
|
1042
|
-
console.error(`Wrote ${tf.bytes_ref}`);
|
|
1043
|
-
}
|
|
1044
|
-
// The origin pin the `adopt` step reads back. `.lotics` is in
|
|
1045
|
-
// SOURCE_STAGE_EXCLUDES, so this never ships in a published bundle.
|
|
1046
|
-
const dotLotics = path.join(targetPath, ".lotics");
|
|
1047
|
-
fs.mkdirSync(dotLotics, { recursive: true });
|
|
1048
|
-
fs.writeFileSync(path.join(dotLotics, ADOPT_BINDING_FILE), JSON.stringify({ app_id: app.id, workspace_id: app.workspace_id, binding: extracted.binding }, null, 2) + "\n");
|
|
1049
|
-
console.error(`Wrote .lotics/${ADOPT_BINDING_FILE}`);
|
|
1050
|
-
console.error("Installing npm dependencies...");
|
|
1051
|
-
await runNpm(["install"], targetPath);
|
|
1052
771
|
if (hasError) {
|
|
1053
|
-
console.error("\
|
|
1054
|
-
console.error("Fix the reported items above, then publish (which re-validates the contract).");
|
|
772
|
+
console.error("\nExtract found error findings — the app cannot be published as-is. Fix them in the app, redeploy, and retry.");
|
|
1055
773
|
process.exitCode = 1;
|
|
1056
774
|
return;
|
|
1057
775
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
776
|
+
if (!args.yes) {
|
|
777
|
+
const idArg = explicit ?? ".";
|
|
778
|
+
const renameArgs = args.renames.map((r) => ` --rename ${r}`).join("");
|
|
779
|
+
const mArg = args.changelog ? ` -m ${JSON.stringify(args.changelog)}` : "";
|
|
780
|
+
console.error(`\nRe-run with --yes to publish v1:`);
|
|
781
|
+
console.error(` lotics app publish ${idArg}${renameArgs}${mArg} --yes`);
|
|
782
|
+
process.exitCode = 1;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const result = await client.publishAppAsPackage(appId, {
|
|
786
|
+
renames,
|
|
787
|
+
changelog: args.changelog ?? null,
|
|
788
|
+
knowledge,
|
|
789
|
+
});
|
|
790
|
+
console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
|
|
791
|
+
console.error(` The app is now installation #1 — develop it in place, then release the next version:`);
|
|
792
|
+
console.error(` lotics app pull ${appId} # edit, then lotics app deploy`);
|
|
793
|
+
console.error(` lotics app release ${appId} -m "<what changed>"`);
|
|
794
|
+
console.error(` Install it elsewhere: lotics install ${result.package_id}`);
|
|
1063
795
|
}
|
|
1064
796
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
* REFUSING a pin recorded for a different app. On success the app becomes
|
|
1070
|
-
* installation #1 and is upgradeable again; a server ConflictError (naming the
|
|
1071
|
-
* unfaithful aliases) surfaces verbatim.
|
|
797
|
+
* Resolve the release/publish target app id: an explicit `<app_id>` wins; `.` (or
|
|
798
|
+
* no positional) resolves the local app project manifest's `lotics.app_id` — the
|
|
799
|
+
* project `lotics app pull` writes — so a release from the pulled app dir needs
|
|
800
|
+
* no id.
|
|
1072
801
|
*/
|
|
1073
|
-
|
|
802
|
+
function resolveOriginAppId(projectDir, explicit) {
|
|
803
|
+
if (explicit !== undefined && explicit !== ".")
|
|
804
|
+
return explicit;
|
|
805
|
+
const local = readLocalAppManifest(projectDir);
|
|
806
|
+
if (local?.app_id)
|
|
807
|
+
return local.app_id;
|
|
808
|
+
throw new Error("No app id. Run this from a pulled app project (lotics app pull <app_id>), or pass an app id explicitly.");
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* `lotics app release [app_id|.] -m <changelog> [--yes]` — snapshot an
|
|
812
|
+
* adopted/installed origin app into its next registry version (docs/packages.md
|
|
813
|
+
* § Promotion). The origin is the permanent working copy; a release binding-aware-
|
|
814
|
+
* extracts it (stable aliases), repackages its DEPLOYED source + dist as the
|
|
815
|
+
* bundle, publishes the next version, and re-pins the origin. Prints the preview
|
|
816
|
+
* first (next version, new + changed aliases, findings); applies only with
|
|
817
|
+
* `--yes`, else exits 1 so a review step can't be skipped. An `error` finding
|
|
818
|
+
* blocks the apply.
|
|
819
|
+
*/
|
|
820
|
+
export async function appRelease(client, args) {
|
|
1074
821
|
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
822
|
+
const appId = resolveOriginAppId(projectDir, args.app_id);
|
|
823
|
+
const preview = await client.previewPackageRelease(appId);
|
|
824
|
+
const { lines, hasError } = formatExtractReport(preview.findings);
|
|
825
|
+
console.error(`Release preview — ${appId} → ${preview.package_id} v${preview.version}:`);
|
|
826
|
+
if (preview.added_aliases.length > 0) {
|
|
827
|
+
console.error(` New (${preview.added_aliases.length}): ${preview.added_aliases.join(", ")}`);
|
|
1078
828
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
throw new Error("No version to adopt — publish this project first (lotics package publish) or pass --version N.");
|
|
829
|
+
if (preview.changed_artifacts.length > 0) {
|
|
830
|
+
console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
|
|
1082
831
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
throw new Error(`No .lotics/${ADOPT_BINDING_FILE} in ${projectDir}. Adopt binds the origin app that ` +
|
|
1086
|
-
`"lotics package extract" recorded — run extract to produce this project.`);
|
|
832
|
+
if (preview.added_aliases.length === 0 && preview.changed_artifacts.length === 0) {
|
|
833
|
+
console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
|
|
1087
834
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
835
|
+
if (lines.length > 0) {
|
|
836
|
+
console.error(` Findings (${preview.findings.length}):`);
|
|
837
|
+
for (const line of lines)
|
|
838
|
+
console.error(line);
|
|
839
|
+
}
|
|
840
|
+
if (hasError) {
|
|
841
|
+
console.error("\nExtract found error findings — the origin cannot be released as-is. Fix them in the app and retry.");
|
|
842
|
+
process.exitCode = 1;
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (!args.yes) {
|
|
846
|
+
console.error(`\nRe-run with --yes to publish v${preview.version}:`);
|
|
847
|
+
console.error(` lotics app release ${args.app_id ?? "."} -m ${JSON.stringify(args.changelog)} --yes`);
|
|
848
|
+
process.exitCode = 1;
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const result = await client.releasePackage(appId, { changelog: args.changelog });
|
|
852
|
+
console.error(`Released ${result.package_id} v${result.version}.`);
|
|
853
|
+
console.error(` The origin was re-pinned to v${result.version} — verify: lotics package doctor ${appId}`);
|
|
1096
854
|
}
|
|
1097
|
-
/**
|
|
1098
|
-
* `lotics package fleet-upgrade <package_id> [--version N]` — bring every
|
|
1099
|
-
* installation of the package across the caller's org to the target version.
|
|
1100
|
-
* Hands-off applies only where the preview is clean; skipped/failed
|
|
1101
|
-
* installations are reported per line and the process exits 1 so a release
|
|
1102
|
-
* script can gate on "fleet fully current".
|
|
1103
|
-
*/
|
|
1104
855
|
/**
|
|
1105
856
|
* `lotics package yank <package_id> <version> [--undo]` — mark a published
|
|
1106
857
|
* version uninstallable (or restore it). New installs/upgrades/adopts refuse a
|
|
@@ -1108,7 +859,7 @@ export async function packageAdopt(client, args) {
|
|
|
1108
859
|
* running. Owner-org admin-only.
|
|
1109
860
|
*/
|
|
1110
861
|
export async function packageYank(client, args) {
|
|
1111
|
-
const result = await client.
|
|
862
|
+
const result = await client.yankPackageVersion(args.package_id, args.version, !args.undo);
|
|
1112
863
|
if (result.yanked_at !== null) {
|
|
1113
864
|
console.error(`Yanked ${result.package_id} v${result.version} (${result.yanked_at}). ` +
|
|
1114
865
|
`New installs/upgrades refuse it; pinned installations keep running.`);
|
|
@@ -1118,8 +869,15 @@ export async function packageYank(client, args) {
|
|
|
1118
869
|
}
|
|
1119
870
|
console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
|
|
1120
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* `lotics upgrade <package_id> [--version N]` (fleet path) — bring every
|
|
874
|
+
* installation of the package across the caller's org to the target version.
|
|
875
|
+
* Hands-off applies only where the preview is clean; skipped/failed
|
|
876
|
+
* installations are reported per line and the process exits 1 so a release
|
|
877
|
+
* script can gate on "fleet fully current".
|
|
878
|
+
*/
|
|
1121
879
|
export async function packageFleetUpgrade(client, args) {
|
|
1122
|
-
const result = await client.
|
|
880
|
+
const result = await client.fleetUpgradePackage(args.package_id, {
|
|
1123
881
|
...(args.version !== undefined ? { version: args.version } : {}),
|
|
1124
882
|
});
|
|
1125
883
|
console.error(`Fleet upgrade of ${result.package_id} → v${result.target_version}:`);
|
|
@@ -1134,7 +892,7 @@ export async function packageFleetUpgrade(client, args) {
|
|
|
1134
892
|
const line = ` [${inst.outcome}] ${inst.workspace_name} — ${inst.app_name} (${from} → v${result.target_version})`;
|
|
1135
893
|
if (inst.outcome === "skipped" && inst.blockers) {
|
|
1136
894
|
console.error(`${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified}`);
|
|
1137
|
-
console.error(` resolve via: lotics
|
|
895
|
+
console.error(` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`);
|
|
1138
896
|
}
|
|
1139
897
|
else if (inst.message) {
|
|
1140
898
|
console.error(`${line}: ${inst.message}`);
|