@lotics/cli 0.86.0 → 0.86.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -2
- package/dist/client.d.ts +22 -1
- package/dist/client.js +6 -0
- package/dist/dev/file_relay.d.ts +39 -0
- package/dist/dev/file_relay.js +87 -0
- package/dist/dev/file_relay.test.d.ts +1 -0
- package/dist/dev/file_relay.test.js +87 -0
- package/dist/dev/server.d.ts +21 -3
- package/dist/dev/server.js +175 -5
- package/dist/dev/upload_relay.d.ts +36 -0
- package/dist/dev/upload_relay.js +61 -0
- package/dist/dev/upload_relay.test.d.ts +1 -0
- package/dist/dev/upload_relay.test.js +76 -0
- package/dist/dev/wrapper_page.js +7 -1
- package/dist/package_commands.d.ts +15 -8
- package/dist/package_commands.js +111 -22
- package/dist/package_commands.test.js +131 -25
- package/dist/src/cli.js +304 -12
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { parseResolveFlags, parseBindToFlags, parseRenameFlags, readLocalAppManifest, formatExtractReport, appRelease, packageUpgradeByPackageId, } from "./package_commands.js";
|
|
5
|
+
import { parseResolveFlags, parseBindToFlags, parseRenameFlags, readLocalAppManifest, formatExtractReport, appRelease, packageUpgradeByPackageId, packageDoctor, } from "./package_commands.js";
|
|
6
6
|
import { parseArgs } from "./args.js";
|
|
7
7
|
describe("parseRenameFlags", () => {
|
|
8
8
|
it("parses old=new pairs", () => {
|
|
@@ -20,21 +20,42 @@ describe("parseRenameFlags", () => {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
describe("readLocalAppManifest", () => {
|
|
23
|
-
|
|
23
|
+
function withManifest(lotics, fn) {
|
|
24
24
|
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-app-test-"));
|
|
25
25
|
try {
|
|
26
|
-
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({
|
|
27
|
-
|
|
28
|
-
lotics: { app_id: "app_x", knowledge: [{ alias: "sop", doc_id: "kdc_1" }] },
|
|
29
|
-
}));
|
|
30
|
-
expect(readLocalAppManifest(dir)).toEqual({
|
|
31
|
-
app_id: "app_x",
|
|
32
|
-
knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
|
|
33
|
-
});
|
|
26
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "crm", lotics }));
|
|
27
|
+
return fn(dir);
|
|
34
28
|
}
|
|
35
29
|
finally {
|
|
36
30
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
37
31
|
}
|
|
32
|
+
}
|
|
33
|
+
it("reads app_id + knowledge + config from a pulled app project", () => {
|
|
34
|
+
withManifest({
|
|
35
|
+
app_id: "app_x",
|
|
36
|
+
knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
|
|
37
|
+
config: [{ alias: "page_size", label: "Page size", type: "number", default: 25 }],
|
|
38
|
+
}, (dir) => {
|
|
39
|
+
expect(readLocalAppManifest(dir)).toEqual({
|
|
40
|
+
app_id: "app_x",
|
|
41
|
+
knowledge: [{ alias: "sop", doc_id: "kdc_1" }],
|
|
42
|
+
config: [{ alias: "page_size", label: "Page size", type: "number", default: 25 }],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it("defaults knowledge + config to empty arrays when the manifest omits them", () => {
|
|
47
|
+
withManifest({ app_id: "app_x" }, (dir) => {
|
|
48
|
+
expect(readLocalAppManifest(dir)).toEqual({ app_id: "app_x", knowledge: [], config: [] });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it("validates each config knob against the shared schema, throwing NAMING the bad alias", () => {
|
|
52
|
+
withManifest({
|
|
53
|
+
app_id: "app_x",
|
|
54
|
+
// A select knob with no `options` — the shared schema rejects it.
|
|
55
|
+
config: [{ alias: "theme", label: "Theme", type: "select", default: "light" }],
|
|
56
|
+
}, (dir) => {
|
|
57
|
+
expect(() => readLocalAppManifest(dir)).toThrow(/lotics\.config knob "theme"/);
|
|
58
|
+
});
|
|
38
59
|
});
|
|
39
60
|
it("returns null when the dir has no package.json", () => {
|
|
40
61
|
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-empty-"));
|
|
@@ -146,27 +167,30 @@ describe("parseArgs — package list-content", () => {
|
|
|
146
167
|
expect(flags.workspace).toBe("wsp_dev");
|
|
147
168
|
});
|
|
148
169
|
});
|
|
149
|
-
describe("appRelease — knowledge declaration forwarding", () => {
|
|
170
|
+
describe("appRelease — knowledge + config declaration forwarding", () => {
|
|
171
|
+
const pageSizeKnob = { alias: "page_size", label: "Page size", type: "number", default: 25 };
|
|
150
172
|
const previewResult = {
|
|
151
173
|
package_id: "apg_1",
|
|
152
174
|
version: 2,
|
|
153
175
|
added_aliases: [],
|
|
154
176
|
changed_artifacts: [],
|
|
155
177
|
knowledge: { added: ["guide"], removed: [], changed: [] },
|
|
178
|
+
config: { added: ["page_size"], removed: [], changed: [] },
|
|
156
179
|
findings: [],
|
|
157
180
|
};
|
|
158
|
-
/** A mock client capturing the knowledge forwarded to preview + release. */
|
|
159
|
-
function mockClient() {
|
|
181
|
+
/** A mock client capturing the knowledge + config forwarded to preview + release. */
|
|
182
|
+
function mockClient(overridePreview) {
|
|
160
183
|
const preview = [];
|
|
161
184
|
const release = [];
|
|
185
|
+
const result = { ...previewResult, ...overridePreview };
|
|
162
186
|
const client = {
|
|
163
187
|
previewPackageRelease: async (app_id, opts = {}) => {
|
|
164
188
|
preview.push({ app_id, opts });
|
|
165
|
-
return
|
|
189
|
+
return result;
|
|
166
190
|
},
|
|
167
191
|
releasePackage: async (app_id, body) => {
|
|
168
192
|
release.push({ app_id, body });
|
|
169
|
-
return { ...
|
|
193
|
+
return { ...result, findings: undefined };
|
|
170
194
|
},
|
|
171
195
|
};
|
|
172
196
|
return { client, preview, release };
|
|
@@ -176,33 +200,53 @@ describe("appRelease — knowledge declaration forwarding", () => {
|
|
|
176
200
|
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "app", lotics }));
|
|
177
201
|
return fn(dir).finally(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
178
202
|
}
|
|
179
|
-
it("forwards the manifest's knowledge
|
|
203
|
+
it("forwards the manifest's knowledge + config declarations from the app's own project", async () => {
|
|
180
204
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
181
205
|
const { client, preview, release } = mockClient();
|
|
182
|
-
await withManifestDir({ app_id: "app_x", knowledge: [{ alias: "guide", doc_id: "kdc_1" }] }, async (dir) => {
|
|
206
|
+
await withManifestDir({ app_id: "app_x", knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] }, async (dir) => {
|
|
183
207
|
await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
|
|
184
208
|
});
|
|
185
|
-
expect(preview[0]).toEqual({
|
|
186
|
-
|
|
209
|
+
expect(preview[0]).toEqual({
|
|
210
|
+
app_id: "app_x",
|
|
211
|
+
opts: { knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] },
|
|
212
|
+
});
|
|
213
|
+
expect(release[0].body).toEqual({
|
|
214
|
+
changelog: "ship",
|
|
215
|
+
knowledge: [{ alias: "guide", doc_id: "kdc_1" }],
|
|
216
|
+
config: [pageSizeKnob],
|
|
217
|
+
});
|
|
187
218
|
vi.restoreAllMocks();
|
|
188
219
|
});
|
|
189
|
-
it("omits knowledge (reconstruct
|
|
220
|
+
it("omits knowledge + config (reconstruct/carry-forward) when the manifest declares neither", async () => {
|
|
190
221
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
191
222
|
const { client, preview, release } = mockClient();
|
|
192
223
|
await withManifestDir({ app_id: "app_x" }, async (dir) => {
|
|
193
224
|
await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
|
|
194
225
|
});
|
|
195
|
-
expect(preview[0].opts).toEqual({ knowledge: undefined });
|
|
196
|
-
expect(release[0].body).toEqual({ changelog: "ship", knowledge: undefined });
|
|
226
|
+
expect(preview[0].opts).toEqual({ knowledge: undefined, config: undefined });
|
|
227
|
+
expect(release[0].body).toEqual({ changelog: "ship", knowledge: undefined, config: undefined });
|
|
197
228
|
vi.restoreAllMocks();
|
|
198
229
|
});
|
|
199
|
-
it("omits knowledge for a bare app id whose manifest is a DIFFERENT app", async () => {
|
|
230
|
+
it("omits knowledge + config for a bare app id whose manifest is a DIFFERENT app", async () => {
|
|
200
231
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
201
232
|
const { client, preview } = mockClient();
|
|
202
|
-
await withManifestDir({ app_id: "app_other", knowledge: [{ alias: "guide", doc_id: "kdc_1" }] }, async (dir) => {
|
|
233
|
+
await withManifestDir({ app_id: "app_other", knowledge: [{ alias: "guide", doc_id: "kdc_1" }], config: [pageSizeKnob] }, async (dir) => {
|
|
203
234
|
await appRelease(client, { app_id: "app_x", changelog: "ship", yes: true, projectDir: dir });
|
|
204
235
|
});
|
|
205
|
-
expect(preview[0]).toEqual({ app_id: "app_x", opts: { knowledge: undefined } });
|
|
236
|
+
expect(preview[0]).toEqual({ app_id: "app_x", opts: { knowledge: undefined, config: undefined } });
|
|
237
|
+
vi.restoreAllMocks();
|
|
238
|
+
});
|
|
239
|
+
it("warns LOUDLY when a SENT config declaration is ignored by a pre-declaration server (deploy skew)", async () => {
|
|
240
|
+
const errs = [];
|
|
241
|
+
vi.spyOn(console, "error").mockImplementation((msg) => {
|
|
242
|
+
errs.push(String(msg));
|
|
243
|
+
});
|
|
244
|
+
// A pre-declaration server omits `config` from the preview echo.
|
|
245
|
+
const { client } = mockClient({ config: undefined });
|
|
246
|
+
await withManifestDir({ app_id: "app_x", config: [pageSizeKnob] }, async (dir) => {
|
|
247
|
+
await appRelease(client, { app_id: ".", changelog: "ship", yes: true, projectDir: dir });
|
|
248
|
+
});
|
|
249
|
+
expect(errs.some((e) => /server ignored the config declaration/.test(e))).toBe(true);
|
|
206
250
|
vi.restoreAllMocks();
|
|
207
251
|
});
|
|
208
252
|
});
|
|
@@ -307,3 +351,65 @@ describe("formatExtractReport", () => {
|
|
|
307
351
|
expect(formatExtractReport([])).toEqual({ lines: [], hasError: false });
|
|
308
352
|
});
|
|
309
353
|
});
|
|
354
|
+
describe("packageDoctor — content package-id form", () => {
|
|
355
|
+
function mockClient(preview) {
|
|
356
|
+
return {
|
|
357
|
+
getWorkspaceId: () => "wsp_1",
|
|
358
|
+
listContentInstallations: async () => [
|
|
359
|
+
{
|
|
360
|
+
id: "pci_1",
|
|
361
|
+
package_id: "apg_corpus",
|
|
362
|
+
workspace_id: "wsp_1",
|
|
363
|
+
package_version: preview.from_version,
|
|
364
|
+
app_id: null,
|
|
365
|
+
binding: { knowledge: {}, templates: {} },
|
|
366
|
+
installed_by: null,
|
|
367
|
+
created_at: "",
|
|
368
|
+
updated_at: "",
|
|
369
|
+
package_registry: {
|
|
370
|
+
name: "Ops corpus",
|
|
371
|
+
kind: "content",
|
|
372
|
+
is_official: false,
|
|
373
|
+
latest_version: preview.to_version,
|
|
374
|
+
update_available: preview.to_version > preview.from_version,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
previewContentInstallationUpgrade: async () => preview,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
it("reports a healthy, pristine install without failing the check", async () => {
|
|
382
|
+
const lines = [];
|
|
383
|
+
vi.spyOn(console, "error").mockImplementation((msg) => void lines.push(msg));
|
|
384
|
+
const before = process.exitCode;
|
|
385
|
+
await packageDoctor(mockClient({ from_version: 3, to_version: 3, entries: [], templates: [] }), { app_id: "apg_corpus" });
|
|
386
|
+
expect(lines.join("\n")).toContain("Ops corpus — content installation of apg_corpus");
|
|
387
|
+
expect(lines.join("\n")).toContain("Installed: v3 Latest: v3");
|
|
388
|
+
expect(lines.join("\n")).toContain("Binding: healthy");
|
|
389
|
+
expect(lines.join("\n")).toContain("Content: pristine");
|
|
390
|
+
expect(process.exitCode).toBe(before);
|
|
391
|
+
vi.restoreAllMocks();
|
|
392
|
+
});
|
|
393
|
+
it("reports drift (non-zero exit) and local edits with their consent framing", async () => {
|
|
394
|
+
const lines = [];
|
|
395
|
+
vi.spyOn(console, "error").mockImplementation((msg) => void lines.push(msg));
|
|
396
|
+
await packageDoctor(mockClient({
|
|
397
|
+
from_version: 2,
|
|
398
|
+
to_version: 3,
|
|
399
|
+
entries: [
|
|
400
|
+
{ alias: "sop", name: "SOP", change: "drifted", modified: false, doc_id: null },
|
|
401
|
+
{ alias: "pricing", name: "Pricing", change: "changed", modified: true, doc_id: "kdc_2" },
|
|
402
|
+
],
|
|
403
|
+
templates: [{ kind: "template", alias: "bien_ban" }],
|
|
404
|
+
}), { app_id: "apg_corpus" });
|
|
405
|
+
const out = lines.join("\n");
|
|
406
|
+
expect(out).toContain("→ update available");
|
|
407
|
+
expect(out).toContain('knowledge.sop "SOP" (bound doc is gone)');
|
|
408
|
+
expect(out).toContain("--resolve knowledge.<alias>=recreate|unbind");
|
|
409
|
+
expect(out).toContain('knowledge.pricing "Pricing"');
|
|
410
|
+
expect(out).toContain("template.bien_ban");
|
|
411
|
+
expect(process.exitCode).toBe(1);
|
|
412
|
+
process.exitCode = 0;
|
|
413
|
+
vi.restoreAllMocks();
|
|
414
|
+
});
|
|
415
|
+
});
|