@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
|
@@ -2,139 +2,44 @@ import { describe, it, expect } 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 {
|
|
6
|
-
import {
|
|
7
|
-
describe("
|
|
8
|
-
it("
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(
|
|
5
|
+
import { parseResolveFlags, parseBindToFlags, parseRenameFlags, readLocalAppManifest, formatExtractReport, } from "./package_commands.js";
|
|
6
|
+
import { parseArgs } from "./args.js";
|
|
7
|
+
describe("parseRenameFlags", () => {
|
|
8
|
+
it("parses old=new pairs", () => {
|
|
9
|
+
expect(parseRenameFlags(["item=deal", "don_hang=orders"])).toEqual([
|
|
10
|
+
{ from: "item", to: "deal" },
|
|
11
|
+
{ from: "don_hang", to: "orders" },
|
|
12
|
+
]);
|
|
13
|
+
expect(parseRenameFlags(["e.f=name"])).toEqual([{ from: "e.f", to: "name" }]);
|
|
14
|
+
expect(parseRenameFlags([])).toEqual([]);
|
|
15
15
|
});
|
|
16
|
-
it("
|
|
17
|
-
|
|
18
|
-
expect(
|
|
19
|
-
expect(
|
|
16
|
+
it("rejects a malformed rename", () => {
|
|
17
|
+
expect(() => parseRenameFlags(["nope"])).toThrow(/expected old=new/);
|
|
18
|
+
expect(() => parseRenameFlags(["=x"])).toThrow(/expected old=new/);
|
|
19
|
+
expect(() => parseRenameFlags(["x="])).toThrow(/expected old=new/);
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
-
describe("
|
|
23
|
-
|
|
24
|
-
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-
|
|
25
|
-
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({
|
|
26
|
-
name: "acme-crm",
|
|
27
|
-
version: "0.0.1",
|
|
28
|
-
lotics: {
|
|
29
|
-
package: { id: null, name: "Acme CRM", description: null, version: null, dev: {} },
|
|
30
|
-
},
|
|
31
|
-
}, null, 2) + "\n");
|
|
32
|
-
return dir;
|
|
33
|
-
}
|
|
34
|
-
it("reads the package manifest off package.json#lotics.package", () => {
|
|
35
|
-
const dir = makeProject();
|
|
22
|
+
describe("readLocalAppManifest", () => {
|
|
23
|
+
it("reads app_id + knowledge from a pulled app project", () => {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-app-test-"));
|
|
36
25
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({
|
|
27
|
+
name: "crm",
|
|
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" }],
|
|
44
33
|
});
|
|
45
34
|
}
|
|
46
35
|
finally {
|
|
47
36
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
48
37
|
}
|
|
49
38
|
});
|
|
50
|
-
it("
|
|
51
|
-
const dir =
|
|
52
|
-
try {
|
|
53
|
-
const project = readPackageProject(dir);
|
|
54
|
-
project.manifest.id = "apg_test";
|
|
55
|
-
project.manifest.version = 3;
|
|
56
|
-
project.manifest.dev["wsp_dev"] = { app_id: "app_dev", version: 3 };
|
|
57
|
-
writePackageManifest(dir, project);
|
|
58
|
-
const reread = readPackageProject(dir);
|
|
59
|
-
expect(reread.manifest.id).toBe("apg_test");
|
|
60
|
-
expect(reread.manifest.version).toBe(3);
|
|
61
|
-
expect(reread.manifest.dev).toEqual({ wsp_dev: { app_id: "app_dev", version: 3 } });
|
|
62
|
-
// Non-lotics package.json fields survive the manifest write.
|
|
63
|
-
expect(reread.pkgJson.name).toBe("acme-crm");
|
|
64
|
-
expect(reread.pkgJson.version).toBe("0.0.1");
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
it("writes the manifest atomically, leaving no temp file behind", () => {
|
|
71
|
-
const dir = makeProject();
|
|
72
|
-
try {
|
|
73
|
-
const project = readPackageProject(dir);
|
|
74
|
-
project.manifest.id = "apg_atomic";
|
|
75
|
-
writePackageManifest(dir, project);
|
|
76
|
-
// The atomic write renames a temp file into place; nothing stray remains.
|
|
77
|
-
const leftovers = fs.readdirSync(dir).filter((f) => f.startsWith("package.json."));
|
|
78
|
-
expect(leftovers).toEqual([]);
|
|
79
|
-
expect(readPackageProject(dir).manifest.id).toBe("apg_atomic");
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("sanitizePackageJsonForSource", () => {
|
|
87
|
-
it("strips the author-local lotics.package.dev map", () => {
|
|
88
|
-
const sanitized = sanitizePackageJsonForSource({
|
|
89
|
-
name: "acme-crm",
|
|
90
|
-
version: "0.0.1",
|
|
91
|
-
lotics: {
|
|
92
|
-
package: {
|
|
93
|
-
id: "apg_x",
|
|
94
|
-
name: "Acme CRM",
|
|
95
|
-
description: "d",
|
|
96
|
-
version: 4,
|
|
97
|
-
dev: { wsp_dev: { app_id: "app_dev", version: 4 } },
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
const lotics = sanitized.lotics;
|
|
102
|
-
// The private dev bookkeeping is gone…
|
|
103
|
-
expect("dev" in lotics.package).toBe(false);
|
|
104
|
-
// …while the package identity + other fields survive untouched.
|
|
105
|
-
expect(lotics.package).toEqual({
|
|
106
|
-
id: "apg_x",
|
|
107
|
-
name: "Acme CRM",
|
|
108
|
-
description: "d",
|
|
109
|
-
version: 4,
|
|
110
|
-
});
|
|
111
|
-
expect(sanitized.name).toBe("acme-crm");
|
|
112
|
-
expect(sanitized.version).toBe("0.0.1");
|
|
113
|
-
});
|
|
114
|
-
it("does not mutate the input package.json", () => {
|
|
115
|
-
const input = {
|
|
116
|
-
name: "acme-crm",
|
|
117
|
-
lotics: { package: { id: "apg_x", name: "Acme CRM", dev: { wsp_dev: { app_id: "a", version: 1 } } } },
|
|
118
|
-
};
|
|
119
|
-
sanitizePackageJsonForSource(input);
|
|
120
|
-
expect(input.lotics.package.dev).toEqual({ wsp_dev: { app_id: "a", version: 1 } });
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
describe("assertDevWorkspace", () => {
|
|
124
|
-
it("passes a dev workspace", () => {
|
|
125
|
-
expect(() => assertDevWorkspace({ id: "wsp_d", name: "Dev", is_dev: true })).not.toThrow();
|
|
126
|
-
});
|
|
127
|
-
it("fails loud on a non-dev workspace", () => {
|
|
128
|
-
expect(() => assertDevWorkspace({ id: "wsp_p", name: "Prod", is_dev: false })).toThrow(/not a dev workspace/);
|
|
129
|
-
});
|
|
130
|
-
it("fails closed when is_dev is absent (server doesn't serialize it)", () => {
|
|
131
|
-
expect(() => assertDevWorkspace({ id: "wsp_u", name: "Unknown" })).toThrow(/not a dev workspace/);
|
|
132
|
-
});
|
|
133
|
-
it("rejects a directory that is not a package project", () => {
|
|
134
|
-
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-pkg-test-"));
|
|
39
|
+
it("returns null when the dir has no package.json", () => {
|
|
40
|
+
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-empty-"));
|
|
135
41
|
try {
|
|
136
|
-
|
|
137
|
-
expect(() => readPackageProject(dir)).toThrow(/not a package project/);
|
|
42
|
+
expect(readLocalAppManifest(dir)).toBeNull();
|
|
138
43
|
}
|
|
139
44
|
finally {
|
|
140
45
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -160,81 +65,85 @@ describe("parseResolveFlags", () => {
|
|
|
160
65
|
expect(() => parseResolveFlags(["fields.deal.stage="])).toThrow(/Invalid --resolve/);
|
|
161
66
|
});
|
|
162
67
|
});
|
|
163
|
-
describe("
|
|
164
|
-
it("
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
expect("lotics" in project.pkgJson).toBe(false);
|
|
178
|
-
// …the package manifest is fresh + unpublished…
|
|
179
|
-
expect(project.manifest).toEqual({
|
|
180
|
-
id: null,
|
|
181
|
-
name: "Acme CRM",
|
|
182
|
-
description: null,
|
|
183
|
-
version: null,
|
|
184
|
-
dev: {},
|
|
68
|
+
describe("parseResolveFlags — the one namespaced grammar", () => {
|
|
69
|
+
it("passes knowledge verbs through under knowledge.<alias> keys", () => {
|
|
70
|
+
expect(parseResolveFlags([
|
|
71
|
+
"knowledge.pricing=apply",
|
|
72
|
+
"knowledge.faq=keep",
|
|
73
|
+
"knowledge.old=archive",
|
|
74
|
+
"knowledge.gone=recreate",
|
|
75
|
+
"knowledge.legacy=unbind",
|
|
76
|
+
])).toEqual({
|
|
77
|
+
"knowledge.pricing": "apply",
|
|
78
|
+
"knowledge.faq": "keep",
|
|
79
|
+
"knowledge.old": "archive",
|
|
80
|
+
"knowledge.gone": "recreate",
|
|
81
|
+
"knowledge.legacy": "unbind",
|
|
185
82
|
});
|
|
186
|
-
// …and non-lotics fields survive verbatim.
|
|
187
|
-
expect(project.pkgJson.name).toBe("acme-crm");
|
|
188
|
-
expect(project.pkgJson.version).toBe("0.0.1");
|
|
189
|
-
expect(project.pkgJson.dependencies).toEqual({ "@lotics/app-sdk": "^1.0.0" });
|
|
190
83
|
});
|
|
191
|
-
it("
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const { manifest, pkgJson } = readPackageProject(dir);
|
|
197
|
-
expect(manifest).toEqual({
|
|
198
|
-
id: null,
|
|
199
|
-
name: "Acme CRM",
|
|
200
|
-
description: "A CRM",
|
|
201
|
-
version: null,
|
|
202
|
-
dev: {},
|
|
203
|
-
});
|
|
204
|
-
// The old app manifest keys never made it to disk.
|
|
205
|
-
const lotics = pkgJson.lotics;
|
|
206
|
-
expect("app_id" in lotics).toBe(false);
|
|
207
|
-
expect("workspace_id" in lotics).toBe(false);
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
211
|
-
}
|
|
84
|
+
it("passes template consents through under template.<alias> keys", () => {
|
|
85
|
+
expect(parseResolveFlags(["template.quote=revert", "template.invoice=keep"])).toEqual({
|
|
86
|
+
"template.quote": "revert",
|
|
87
|
+
"template.invoice": "keep",
|
|
88
|
+
});
|
|
212
89
|
});
|
|
213
|
-
it("
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
90
|
+
it("maps a non-verb value to a bind_to id on any key (roles rebind, knowledge adopt)", () => {
|
|
91
|
+
expect(parseResolveFlags(["roles.approver=grp_new", "knowledge.faq=kdc_existing"])).toEqual({
|
|
92
|
+
"roles.approver": { bind_to: "grp_new" },
|
|
93
|
+
"knowledge.faq": { bind_to: "kdc_existing" },
|
|
94
|
+
});
|
|
217
95
|
});
|
|
218
96
|
});
|
|
219
|
-
describe("
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
it("accepts a pin whose app_id matches the app being adopted", () => {
|
|
226
|
-
const pin = parseAdoptBindingFile(validPin, "app_x");
|
|
227
|
-
expect(pin.app_id).toBe("app_x");
|
|
228
|
-
expect(pin.workspace_id).toBe("wsp_y");
|
|
229
|
-
expect(pin.binding.entities).toEqual({ item: "tbl_1" });
|
|
97
|
+
describe("parseBindToFlags", () => {
|
|
98
|
+
it("maps each alias to its consent doc id", () => {
|
|
99
|
+
expect(parseBindToFlags(["playbook=kdc_1", "faq=kdc_2"])).toEqual({
|
|
100
|
+
playbook: "kdc_1",
|
|
101
|
+
faq: "kdc_2",
|
|
102
|
+
});
|
|
230
103
|
});
|
|
231
|
-
it("
|
|
232
|
-
expect(() =>
|
|
104
|
+
it("rejects entries without an alias=id shape", () => {
|
|
105
|
+
expect(() => parseBindToFlags(["playbook"])).toThrow(/Invalid --bind-to/);
|
|
106
|
+
expect(() => parseBindToFlags(["=kdc_1"])).toThrow(/Invalid --bind-to/);
|
|
107
|
+
expect(() => parseBindToFlags(["playbook="])).toThrow(/Invalid --bind-to/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("parseArgs — app publish --rename", () => {
|
|
111
|
+
it("captures repeated --rename flags with the positional app id", () => {
|
|
112
|
+
const { command, subcommand, toolArgs, flags } = parseArgs([
|
|
113
|
+
"app",
|
|
114
|
+
"publish",
|
|
115
|
+
"app_x",
|
|
116
|
+
"--rename",
|
|
117
|
+
"item=deal",
|
|
118
|
+
"--rename",
|
|
119
|
+
"e.f=name",
|
|
120
|
+
]);
|
|
121
|
+
expect(command).toBe("app");
|
|
122
|
+
expect(subcommand).toBe("publish");
|
|
123
|
+
expect(toolArgs).toBe("app_x");
|
|
124
|
+
expect(flags.rename).toEqual(["item=deal", "e.f=name"]);
|
|
233
125
|
});
|
|
234
|
-
it("
|
|
235
|
-
expect(() =>
|
|
236
|
-
expect(() =>
|
|
237
|
-
|
|
126
|
+
it("throws when --rename has no value", () => {
|
|
127
|
+
expect(() => parseArgs(["app", "publish", "--rename"])).toThrow(/--rename requires a value/);
|
|
128
|
+
expect(() => parseArgs(["app", "publish", "--rename", "--yes"])).toThrow(/--rename requires a value/);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("parseArgs — package list-content", () => {
|
|
132
|
+
it("parses the list-content subcommand with no positional", () => {
|
|
133
|
+
const { command, subcommand, toolArgs } = parseArgs(["package", "list-content"]);
|
|
134
|
+
expect(command).toBe("package");
|
|
135
|
+
expect(subcommand).toBe("list-content");
|
|
136
|
+
expect(toolArgs).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
it("captures --workspace for a scoped list-content", () => {
|
|
139
|
+
const { subcommand, flags } = parseArgs([
|
|
140
|
+
"package",
|
|
141
|
+
"list-content",
|
|
142
|
+
"--workspace",
|
|
143
|
+
"wsp_dev",
|
|
144
|
+
]);
|
|
145
|
+
expect(subcommand).toBe("list-content");
|
|
146
|
+
expect(flags.workspace).toBe("wsp_dev");
|
|
238
147
|
});
|
|
239
148
|
});
|
|
240
149
|
describe("formatExtractReport", () => {
|
|
@@ -265,39 +174,3 @@ describe("formatExtractReport", () => {
|
|
|
265
174
|
expect(formatExtractReport([])).toEqual({ lines: [], hasError: false });
|
|
266
175
|
});
|
|
267
176
|
});
|
|
268
|
-
describe("stagePackageSource", () => {
|
|
269
|
-
it("keeps nested dist-named dirs, drops top-level excludes, sanitizes package.json", () => {
|
|
270
|
-
const dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-stage-test-"));
|
|
271
|
-
const stage = fs.mkdtempSync(path.join(tmpdir(), "lotics-stage-out-"));
|
|
272
|
-
try {
|
|
273
|
-
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({
|
|
274
|
-
name: "pkg",
|
|
275
|
-
lotics: { package: { id: "apg_x", name: "pkg", version: 3, dev: { wsp_a: "app_1" } } },
|
|
276
|
-
}));
|
|
277
|
-
fs.mkdirSync(path.join(dir, "src"));
|
|
278
|
-
fs.writeFileSync(path.join(dir, "src", "main.tsx"), "export {}");
|
|
279
|
-
// A nested dir NAMED dist must ship — only the top-level dist is a build output.
|
|
280
|
-
fs.mkdirSync(path.join(dir, "templates", "dist"), { recursive: true });
|
|
281
|
-
fs.writeFileSync(path.join(dir, "templates", "dist", "quote.xlsx"), "bytes");
|
|
282
|
-
fs.mkdirSync(path.join(dir, "dist"));
|
|
283
|
-
fs.writeFileSync(path.join(dir, "dist", "index.js"), "built");
|
|
284
|
-
fs.mkdirSync(path.join(dir, "node_modules", "x"), { recursive: true });
|
|
285
|
-
fs.writeFileSync(path.join(dir, "node_modules", "x", "i.js"), "dep");
|
|
286
|
-
fs.writeFileSync(path.join(dir, "tsconfig.tsbuildinfo"), "{}");
|
|
287
|
-
stagePackageSource(dir, stage);
|
|
288
|
-
expect(fs.existsSync(path.join(stage, "src", "main.tsx"))).toBe(true);
|
|
289
|
-
expect(fs.existsSync(path.join(stage, "templates", "dist", "quote.xlsx"))).toBe(true);
|
|
290
|
-
expect(fs.existsSync(path.join(stage, "dist"))).toBe(false);
|
|
291
|
-
expect(fs.existsSync(path.join(stage, "node_modules"))).toBe(false);
|
|
292
|
-
expect(fs.existsSync(path.join(stage, "tsconfig.tsbuildinfo"))).toBe(false);
|
|
293
|
-
// The staged package.json is the sanitized one — dev pins stripped.
|
|
294
|
-
const staged = JSON.parse(fs.readFileSync(path.join(stage, "package.json"), "utf-8"));
|
|
295
|
-
expect(staged.lotics.package.dev).toBeUndefined();
|
|
296
|
-
expect(staged.lotics.package.id).toBe("apg_x");
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
300
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
});
|