@abraca/plugin-cli 2.3.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.
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env node
2
+ import { validatePluginManifest } from "@abraca/schema";
3
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+
7
+ //#region packages/plugin-cli/src/io.ts
8
+ /**
9
+ * Filesystem helpers used across `abra-plugin` commands. Kept tiny and
10
+ * dependency-free so the CLI bundle stays small.
11
+ */
12
+ /**
13
+ * Resolve a CLI-supplied path to an absolute path. Relative paths resolve
14
+ * against `process.cwd()`. Used so commands work no matter where the user
15
+ * invoked them from.
16
+ */
17
+ function resolveCwd(path) {
18
+ return isAbsolute(path) ? path : resolve(process.cwd(), path);
19
+ }
20
+ /** Read a JSON file and return parsed content. Throws with a contextual message on failure. */
21
+ function readJsonFile(path) {
22
+ let raw;
23
+ try {
24
+ raw = readFileSync(path, "utf8");
25
+ } catch (err) {
26
+ if (err.code === "ENOENT") throw new Error(`not found: ${path}`);
27
+ throw new Error(`failed to read ${path}: ${err.message}`);
28
+ }
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch (err) {
32
+ throw new Error(`invalid JSON in ${path}: ${err.message}`);
33
+ }
34
+ }
35
+ /**
36
+ * SHA-256 hash of a file's bytes, formatted as `sha256-<64 hex chars>` to
37
+ * match the manifest's `integrity` field format.
38
+ */
39
+ function sha256OfFile(path) {
40
+ const buf = readFileSync(path);
41
+ return `sha256-${createHash("sha256").update(buf).digest("hex")}`;
42
+ }
43
+ /** Return true if `path` exists and is a regular file. */
44
+ function isFile(path) {
45
+ try {
46
+ return statSync(path).isFile();
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ const useColor = process.stdout.isTTY && process.env.NO_COLOR === void 0;
52
+ const ansi = {
53
+ red: (s) => useColor ? `\x1b[31m${s}\x1b[0m` : s,
54
+ green: (s) => useColor ? `\x1b[32m${s}\x1b[0m` : s,
55
+ yellow: (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s,
56
+ cyan: (s) => useColor ? `\x1b[36m${s}\x1b[0m` : s,
57
+ dim: (s) => useColor ? `\x1b[2m${s}\x1b[0m` : s,
58
+ bold: (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s
59
+ };
60
+
61
+ //#endregion
62
+ //#region packages/plugin-cli/src/commands/validate.ts
63
+ /**
64
+ * `abra-plugin validate [path]` — load a manifest.json and validate it
65
+ * against `@abraca/schema`'s `PluginManifestSchema`. Pretty-prints every
66
+ * issue with its JSON path; exit code 0 on success, 1 on validation
67
+ * failure, 2 on read/parse error.
68
+ *
69
+ * Defaults `path` to `./manifest.json` when omitted.
70
+ *
71
+ * This is the same validator the registry server runs on submission, so a
72
+ * green local check is a strong signal the submission will pass static
73
+ * checks too.
74
+ */
75
+ function validate(opts = {}) {
76
+ const rel = opts.path ?? "manifest.json";
77
+ const abs = resolveCwd(rel);
78
+ if (!isFile(abs)) {
79
+ console.error(ansi.red(`error: ${rel}: file not found`));
80
+ return {
81
+ exitCode: 2,
82
+ issues: []
83
+ };
84
+ }
85
+ let parsed;
86
+ try {
87
+ parsed = readJsonFile(abs);
88
+ } catch (err) {
89
+ console.error(ansi.red(`error: ${err.message}`));
90
+ return {
91
+ exitCode: 2,
92
+ issues: []
93
+ };
94
+ }
95
+ const result = validatePluginManifest(parsed);
96
+ if (result.ok) {
97
+ if (!opts.quiet) console.log(`${ansi.green("✓")} ${rel} ${ansi.dim(`(id=${result.value.id} v${result.value.version})`)}`);
98
+ return {
99
+ exitCode: 0,
100
+ issues: []
101
+ };
102
+ }
103
+ const issues = result.errors.map((e) => ({
104
+ path: e.path.map((p) => String(p)).join("."),
105
+ message: e.message,
106
+ code: e.code
107
+ }));
108
+ console.error(ansi.red(`✗ ${rel}: ${issues.length} issue${issues.length === 1 ? "" : "s"}`));
109
+ for (const issue of issues) {
110
+ const where = issue.path.length > 0 ? issue.path : "<root>";
111
+ console.error(` ${ansi.yellow(where)}: ${issue.message}${issue.code ? ansi.dim(` [${issue.code}]`) : ""}`);
112
+ }
113
+ return {
114
+ exitCode: 1,
115
+ issues
116
+ };
117
+ }
118
+
119
+ //#endregion
120
+ //#region packages/plugin-cli/src/commands/pack.ts
121
+ /**
122
+ * `abra-plugin pack [path]` — recompute the manifest's `integrity` field
123
+ * (SHA-256 of the entry bundle) and write the updated manifest back to disk.
124
+ *
125
+ * Authors run this after every bundle rebuild — the registry server refuses
126
+ * to accept a manifest whose `integrity` doesn't match the artifact, so
127
+ * having it as a one-shot CLI step removes a class of "I forgot to update
128
+ * the hash" submission rejections.
129
+ *
130
+ * The command also validates the manifest after recomputing, exiting
131
+ * non-zero if validation still fails (e.g. unknown capability declared).
132
+ */
133
+ function pack(opts = {}) {
134
+ const rel = opts.path ?? "manifest.json";
135
+ const abs = resolveCwd(rel);
136
+ if (!isFile(abs)) {
137
+ console.error(ansi.red(`error: ${rel}: file not found`));
138
+ return {
139
+ exitCode: 2,
140
+ integrity: null,
141
+ rewrote: false
142
+ };
143
+ }
144
+ let parsed;
145
+ try {
146
+ const value = readJsonFile(abs);
147
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("manifest must be a JSON object");
148
+ parsed = value;
149
+ } catch (err) {
150
+ console.error(ansi.red(`error: ${err.message}`));
151
+ return {
152
+ exitCode: 2,
153
+ integrity: null,
154
+ rewrote: false
155
+ };
156
+ }
157
+ const entry = parsed.entry;
158
+ if (typeof entry !== "string" || entry.length === 0) {
159
+ console.error(ansi.red("error: manifest is missing an 'entry' field (path to bundle)"));
160
+ return {
161
+ exitCode: 2,
162
+ integrity: null,
163
+ rewrote: false
164
+ };
165
+ }
166
+ const entryAbs = join(dirname(abs), entry);
167
+ if (!isFile(entryAbs)) {
168
+ console.error(ansi.red(`error: entry bundle not found: ${relative(process.cwd(), entryAbs)}`));
169
+ return {
170
+ exitCode: 2,
171
+ integrity: null,
172
+ rewrote: false
173
+ };
174
+ }
175
+ const integrity = sha256OfFile(entryAbs);
176
+ const prev = parsed.integrity;
177
+ parsed.integrity = integrity;
178
+ const newJson = `${JSON.stringify(parsed, null, 2)}\n`;
179
+ const oldJson = readFileSync(abs, "utf8");
180
+ if (opts.dryRun) {
181
+ console.log(`${ansi.cyan("dry-run")}: would write integrity=${integrity}`);
182
+ const v = validatePluginManifest(parsed);
183
+ if (!v.ok) {
184
+ console.error(ansi.red(`✗ post-pack validation failed`));
185
+ for (const issue of v.errors) {
186
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
187
+ console.error(` ${ansi.yellow(where)}: ${issue.message}`);
188
+ }
189
+ return {
190
+ exitCode: 1,
191
+ integrity,
192
+ rewrote: false
193
+ };
194
+ }
195
+ return {
196
+ exitCode: 0,
197
+ integrity,
198
+ rewrote: false
199
+ };
200
+ }
201
+ const wrote = newJson !== oldJson;
202
+ if (wrote) writeFileSync(abs, newJson, "utf8");
203
+ const action = wrote ? prev === integrity ? "reformatted" : `updated (${ansi.dim(String(prev ?? "<unset>"))} → ${integrity})` : "unchanged";
204
+ console.log(`${ansi.green("✓")} ${rel}: ${action}`);
205
+ const v = validatePluginManifest(parsed);
206
+ if (!v.ok) {
207
+ console.error(ansi.red(`✗ but manifest fails validation — fix and re-run`));
208
+ for (const issue of v.errors) {
209
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
210
+ console.error(` ${ansi.yellow(where)}: ${issue.message}`);
211
+ }
212
+ return {
213
+ exitCode: 1,
214
+ integrity,
215
+ rewrote: wrote
216
+ };
217
+ }
218
+ return {
219
+ exitCode: 0,
220
+ integrity,
221
+ rewrote: wrote
222
+ };
223
+ }
224
+
225
+ //#endregion
226
+ //#region packages/plugin-cli/src/commands/preview-scan.ts
227
+ /**
228
+ * `abra-plugin preview-scan [path]` — validate the manifest, then run a
229
+ * quick static check on the bundle:
230
+ *
231
+ * - **capability/code mismatch**: if the bundle uses `fetch` / `XHR` /
232
+ * `WebSocket` but the manifest doesn't declare `network[:*]`, flag it.
233
+ * Symmetric: if `network:*` is declared but the bundle has no network
234
+ * calls, warn (declared-but-unused).
235
+ * - **declared `contributes` sanity**: every name in `manifest.contributes`
236
+ * should appear somewhere in the bundle string. Cheap heuristic — a
237
+ * proper AST walk lives on the registry server in Phase H.
238
+ *
239
+ * Designed to mirror what the registry's automated scanner does on
240
+ * submission, so authors catch issues locally.
241
+ *
242
+ * Exit code 0 = clean, 1 = warnings only, 2 = errors. Authors should
243
+ * resolve all errors before submitting; warnings are advisory.
244
+ */
245
+ const NETWORK_API_RE = /\b(fetch|XMLHttpRequest|WebSocket|EventSource)\b/;
246
+ const CLIPBOARD_READ_RE = /navigator\.clipboard\.read(?!Text)?Text?\b/;
247
+ const CLIPBOARD_WRITE_RE = /navigator\.clipboard\.write/;
248
+ function declaresCap(manifest, predicate) {
249
+ return [...manifest.capabilities.required ?? [], ...manifest.capabilities.optional ?? []].some(predicate);
250
+ }
251
+ function previewScan(opts = {}) {
252
+ const rel = opts.path ?? "manifest.json";
253
+ const abs = resolveCwd(rel);
254
+ const findings = [];
255
+ if (!isFile(abs)) {
256
+ console.error(ansi.red(`error: ${rel}: file not found`));
257
+ return {
258
+ exitCode: 2,
259
+ findings
260
+ };
261
+ }
262
+ let parsed;
263
+ try {
264
+ parsed = readJsonFile(abs);
265
+ } catch (err) {
266
+ console.error(ansi.red(`error: ${err.message}`));
267
+ return {
268
+ exitCode: 2,
269
+ findings
270
+ };
271
+ }
272
+ const result = validatePluginManifest(parsed);
273
+ if (!result.ok) {
274
+ console.error(ansi.red(`✗ manifest validation failed`));
275
+ for (const issue of result.errors) {
276
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
277
+ console.error(` ${ansi.yellow(where)}: ${issue.message}`);
278
+ }
279
+ return {
280
+ exitCode: 2,
281
+ findings
282
+ };
283
+ }
284
+ const manifest = result.value;
285
+ const entryAbs = join(dirname(abs), manifest.entry);
286
+ if (!isFile(entryAbs)) {
287
+ findings.push({
288
+ severity: "error",
289
+ rule: "missing-entry",
290
+ message: `entry bundle not found: ${relative(process.cwd(), entryAbs)}`
291
+ });
292
+ printAndExit(findings);
293
+ return {
294
+ exitCode: 2,
295
+ findings
296
+ };
297
+ }
298
+ const source = readFileSync(entryAbs, "utf8");
299
+ const hasNetCalls = NETWORK_API_RE.test(source);
300
+ const hasNetCap = declaresCap(manifest, (c) => c === "network" || c.startsWith("network:"));
301
+ if (hasNetCalls && !hasNetCap) findings.push({
302
+ severity: "error",
303
+ rule: "undeclared-network",
304
+ message: "bundle uses fetch/XHR/WebSocket but no network[:*] capability is declared — add it to capabilities.required or capabilities.optional"
305
+ });
306
+ if (hasNetCap && !hasNetCalls) findings.push({
307
+ severity: "warning",
308
+ rule: "unused-network",
309
+ message: "network[:*] capability is declared but no fetch/XHR/WebSocket usage was found — remove the capability if unused"
310
+ });
311
+ const hasClipRead = CLIPBOARD_READ_RE.test(source);
312
+ const hasClipReadCap = declaresCap(manifest, (c) => c === "clipboard:read");
313
+ if (hasClipRead && !hasClipReadCap) findings.push({
314
+ severity: "error",
315
+ rule: "undeclared-clipboard-read",
316
+ message: "bundle reads the clipboard but clipboard:read is not declared"
317
+ });
318
+ const hasClipWrite = CLIPBOARD_WRITE_RE.test(source);
319
+ const hasClipWriteCap = declaresCap(manifest, (c) => c === "clipboard:write");
320
+ if (hasClipWrite && !hasClipWriteCap) findings.push({
321
+ severity: "error",
322
+ rule: "undeclared-clipboard-write",
323
+ message: "bundle writes the clipboard but clipboard:write is not declared"
324
+ });
325
+ for (const [field, names] of Object.entries(manifest.contributes ?? {})) {
326
+ const list = names ?? [];
327
+ for (const name of list) {
328
+ if (name.length < 3) continue;
329
+ if (!source.includes(name)) findings.push({
330
+ severity: "warning",
331
+ rule: "contributes-not-in-bundle",
332
+ message: `'${name}' declared in contributes.${field} but not referenced in entry bundle — possible drift`
333
+ });
334
+ }
335
+ }
336
+ printAndExit(findings);
337
+ return {
338
+ exitCode: severityExitCode(findings),
339
+ findings
340
+ };
341
+ }
342
+ function severityExitCode(findings) {
343
+ if (findings.some((f) => f.severity === "error")) return 2;
344
+ if (findings.length > 0) return 1;
345
+ return 0;
346
+ }
347
+ function printAndExit(findings) {
348
+ if (findings.length === 0) {
349
+ console.log(`${ansi.green("✓")} clean — manifest + bundle pass preview scan`);
350
+ return;
351
+ }
352
+ for (const f of findings) {
353
+ const badge = f.severity === "error" ? ansi.red("error") : ansi.yellow("warning");
354
+ console.error(`${badge} ${ansi.dim(`[${f.rule}]`)} ${f.message}`);
355
+ }
356
+ }
357
+
358
+ //#endregion
359
+ //#region packages/plugin-cli/src/index.ts
360
+ /**
361
+ * `abra-plugin` — CLI for Abracadabra plugin authors.
362
+ *
363
+ * abra-plugin validate [path/to/manifest.json]
364
+ * abra-plugin pack [path/to/manifest.json] [--dry-run]
365
+ * abra-plugin preview-scan [path/to/manifest.json]
366
+ *
367
+ * Each command defaults the path to `./manifest.json`. Exit codes:
368
+ * 0 success
369
+ * 1 warnings (preview-scan) or manifest-still-fails-validation (pack)
370
+ * 2 read/parse error or hard validation failure
371
+ *
372
+ * No external dependencies — pulls only `@abraca/schema` (Zod validator)
373
+ * + `@abraca/plugin` (manifest types) which are zero-runtime-cost when
374
+ * imported as `import type`.
375
+ */
376
+ function help() {
377
+ const lines = [
378
+ `${ansi.bold("abra-plugin")} — Abracadabra plugin author toolkit`,
379
+ "",
380
+ "Usage:",
381
+ ` ${ansi.cyan("abra-plugin validate")} [path] Validate a manifest against the schema`,
382
+ ` ${ansi.cyan("abra-plugin pack")} [path] [--dry-run] Recompute integrity hash, update manifest`,
383
+ ` ${ansi.cyan("abra-plugin preview-scan")} [path] Validate + static-scan the bundle (registry parity)`,
384
+ "",
385
+ `Path defaults to ${ansi.dim("./manifest.json")} when omitted.`
386
+ ];
387
+ console.log(lines.join("\n"));
388
+ }
389
+ function parseFlag(args, flag) {
390
+ const idx = args.indexOf(flag);
391
+ if (idx >= 0) {
392
+ args.splice(idx, 1);
393
+ return true;
394
+ }
395
+ return false;
396
+ }
397
+ function run(argv) {
398
+ const args = [...argv];
399
+ const command = args.shift();
400
+ switch (command) {
401
+ case void 0:
402
+ case "help":
403
+ case "-h":
404
+ case "--help":
405
+ help();
406
+ return 0;
407
+ case "validate": {
408
+ const quiet = parseFlag(args, "--quiet");
409
+ return validate({
410
+ path: args.shift(),
411
+ quiet
412
+ }).exitCode;
413
+ }
414
+ case "pack": {
415
+ const dryRun = parseFlag(args, "--dry-run");
416
+ return pack({
417
+ path: args.shift(),
418
+ dryRun
419
+ }).exitCode;
420
+ }
421
+ case "preview-scan": return previewScan({ path: args.shift() }).exitCode;
422
+ default:
423
+ console.error(ansi.red(`error: unknown command "${command}"`));
424
+ console.error(`run ${ansi.cyan("abra-plugin help")} for usage`);
425
+ return 2;
426
+ }
427
+ }
428
+ if (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1] ?? "")) process.exit(run(process.argv.slice(2)));
429
+
430
+ //#endregion
431
+ export { pack, previewScan, run, validate };
432
+ //# sourceMappingURL=abracadabra-plugin-cli.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abracadabra-plugin-cli.esm.js","names":[],"sources":["../src/io.ts","../src/commands/validate.ts","../src/commands/pack.ts","../src/commands/preview-scan.ts","../src/index.ts"],"sourcesContent":["/**\n * Filesystem helpers used across `abra-plugin` commands. Kept tiny and\n * dependency-free so the CLI bundle stays small.\n */\n\nimport { readFileSync, statSync } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { isAbsolute, resolve } from \"node:path\";\n\n/**\n * Resolve a CLI-supplied path to an absolute path. Relative paths resolve\n * against `process.cwd()`. Used so commands work no matter where the user\n * invoked them from.\n */\nexport function resolveCwd(path: string): string {\n\treturn isAbsolute(path) ? path : resolve(process.cwd(), path);\n}\n\n/** Read a JSON file and return parsed content. Throws with a contextual message on failure. */\nexport function readJsonFile(path: string): unknown {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, \"utf8\");\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\tif (code === \"ENOENT\") {\n\t\t\tthrow new Error(`not found: ${path}`);\n\t\t}\n\t\tthrow new Error(`failed to read ${path}: ${(err as Error).message}`);\n\t}\n\ttry {\n\t\treturn JSON.parse(raw) as unknown;\n\t} catch (err) {\n\t\tthrow new Error(`invalid JSON in ${path}: ${(err as Error).message}`);\n\t}\n}\n\n/**\n * SHA-256 hash of a file's bytes, formatted as `sha256-<64 hex chars>` to\n * match the manifest's `integrity` field format.\n */\nexport function sha256OfFile(path: string): string {\n\tconst buf = readFileSync(path);\n\treturn `sha256-${createHash(\"sha256\").update(buf).digest(\"hex\")}`;\n}\n\n/** Return true if `path` exists and is a regular file. */\nexport function isFile(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// ── ANSI helpers (no peer dep) ────────────────────────────────────────────────\n\nconst useColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;\n\nexport const ansi = {\n\tred: (s: string) => (useColor ? `\\x1b[31m${s}\\x1b[0m` : s),\n\tgreen: (s: string) => (useColor ? `\\x1b[32m${s}\\x1b[0m` : s),\n\tyellow: (s: string) => (useColor ? `\\x1b[33m${s}\\x1b[0m` : s),\n\tcyan: (s: string) => (useColor ? `\\x1b[36m${s}\\x1b[0m` : s),\n\tdim: (s: string) => (useColor ? `\\x1b[2m${s}\\x1b[0m` : s),\n\tbold: (s: string) => (useColor ? `\\x1b[1m${s}\\x1b[0m` : s),\n};\n","/**\n * `abra-plugin validate [path]` — load a manifest.json and validate it\n * against `@abraca/schema`'s `PluginManifestSchema`. Pretty-prints every\n * issue with its JSON path; exit code 0 on success, 1 on validation\n * failure, 2 on read/parse error.\n *\n * Defaults `path` to `./manifest.json` when omitted.\n *\n * This is the same validator the registry server runs on submission, so a\n * green local check is a strong signal the submission will pass static\n * checks too.\n */\n\nimport { validatePluginManifest } from \"@abraca/schema\";\nimport { ansi, isFile, readJsonFile, resolveCwd } from \"../io.ts\";\n\nexport interface ValidateOptions {\n\t/** Path to the manifest file. Defaults to `./manifest.json`. */\n\tpath?: string;\n\t/** If true, suppress the success summary line. Errors are still printed. */\n\tquiet?: boolean;\n}\n\nexport interface ValidateResult {\n\texitCode: 0 | 1 | 2;\n\t/** Issues found by the schema. Empty on success. */\n\tissues: ReadonlyArray<{ path: string; message: string; code?: string }>;\n}\n\nexport function validate(opts: ValidateOptions = {}): ValidateResult {\n\tconst rel = opts.path ?? \"manifest.json\";\n\tconst abs = resolveCwd(rel);\n\n\tif (!isFile(abs)) {\n\t\tconsole.error(ansi.red(`error: ${rel}: file not found`));\n\t\treturn { exitCode: 2, issues: [] };\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = readJsonFile(abs);\n\t} catch (err) {\n\t\tconsole.error(ansi.red(`error: ${(err as Error).message}`));\n\t\treturn { exitCode: 2, issues: [] };\n\t}\n\n\tconst result = validatePluginManifest(parsed);\n\tif (result.ok) {\n\t\tif (!opts.quiet) {\n\t\t\tconsole.log(\n\t\t\t\t`${ansi.green(\"✓\")} ${rel} ${ansi.dim(`(id=${result.value.id} v${result.value.version})`)}`,\n\t\t\t);\n\t\t}\n\t\treturn { exitCode: 0, issues: [] };\n\t}\n\n\tconst issues = result.errors.map((e) => ({\n\t\tpath: e.path.map((p) => String(p)).join(\".\"),\n\t\tmessage: e.message,\n\t\tcode: e.code,\n\t}));\n\n\tconsole.error(\n\t\tansi.red(`✗ ${rel}: ${issues.length} issue${issues.length === 1 ? \"\" : \"s\"}`),\n\t);\n\tfor (const issue of issues) {\n\t\tconst where = issue.path.length > 0 ? issue.path : \"<root>\";\n\t\tconsole.error(\n\t\t\t` ${ansi.yellow(where)}: ${issue.message}${issue.code ? ansi.dim(` [${issue.code}]`) : \"\"}`,\n\t\t);\n\t}\n\treturn { exitCode: 1, issues };\n}\n","/**\n * `abra-plugin pack [path]` — recompute the manifest's `integrity` field\n * (SHA-256 of the entry bundle) and write the updated manifest back to disk.\n *\n * Authors run this after every bundle rebuild — the registry server refuses\n * to accept a manifest whose `integrity` doesn't match the artifact, so\n * having it as a one-shot CLI step removes a class of \"I forgot to update\n * the hash\" submission rejections.\n *\n * The command also validates the manifest after recomputing, exiting\n * non-zero if validation still fails (e.g. unknown capability declared).\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, relative } from \"node:path\";\nimport { validatePluginManifest } from \"@abraca/schema\";\nimport { ansi, isFile, readJsonFile, resolveCwd, sha256OfFile } from \"../io.ts\";\n\nexport interface PackOptions {\n\t/** Path to the manifest file. Defaults to `./manifest.json`. */\n\tpath?: string;\n\t/** If true, don't write the file — just print the new hash. */\n\tdryRun?: boolean;\n}\n\nexport interface PackResult {\n\texitCode: 0 | 1 | 2;\n\t/** The newly-computed integrity hash, or `null` on early failure. */\n\tintegrity: string | null;\n\t/** Whether the on-disk manifest was rewritten. */\n\trewrote: boolean;\n}\n\nexport function pack(opts: PackOptions = {}): PackResult {\n\tconst rel = opts.path ?? \"manifest.json\";\n\tconst abs = resolveCwd(rel);\n\n\tif (!isFile(abs)) {\n\t\tconsole.error(ansi.red(`error: ${rel}: file not found`));\n\t\treturn { exitCode: 2, integrity: null, rewrote: false };\n\t}\n\n\tlet parsed: Record<string, unknown>;\n\ttry {\n\t\tconst value = readJsonFile(abs);\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\t\tthrow new Error(\"manifest must be a JSON object\");\n\t\t}\n\t\tparsed = value as Record<string, unknown>;\n\t} catch (err) {\n\t\tconsole.error(ansi.red(`error: ${(err as Error).message}`));\n\t\treturn { exitCode: 2, integrity: null, rewrote: false };\n\t}\n\n\tconst entry = parsed.entry;\n\tif (typeof entry !== \"string\" || entry.length === 0) {\n\t\tconsole.error(\n\t\t\tansi.red(\"error: manifest is missing an 'entry' field (path to bundle)\"),\n\t\t);\n\t\treturn { exitCode: 2, integrity: null, rewrote: false };\n\t}\n\n\tconst entryAbs = join(dirname(abs), entry);\n\tif (!isFile(entryAbs)) {\n\t\tconsole.error(\n\t\t\tansi.red(`error: entry bundle not found: ${relative(process.cwd(), entryAbs)}`),\n\t\t);\n\t\treturn { exitCode: 2, integrity: null, rewrote: false };\n\t}\n\n\tconst integrity = sha256OfFile(entryAbs);\n\tconst prev = parsed.integrity;\n\tparsed.integrity = integrity;\n\n\tconst newJson = `${JSON.stringify(parsed, null, 2)}\\n`;\n\tconst oldJson = readFileSync(abs, \"utf8\");\n\n\tif (opts.dryRun) {\n\t\tconsole.log(`${ansi.cyan(\"dry-run\")}: would write integrity=${integrity}`);\n\t\t// Still run validation against the in-memory manifest so the author\n\t\t// sees issues even without writing.\n\t\tconst v = validatePluginManifest(parsed);\n\t\tif (!v.ok) {\n\t\t\tconsole.error(ansi.red(`✗ post-pack validation failed`));\n\t\t\tfor (const issue of v.errors) {\n\t\t\t\tconst where = issue.path.length > 0 ? issue.path.map(String).join(\".\") : \"<root>\";\n\t\t\t\tconsole.error(` ${ansi.yellow(where)}: ${issue.message}`);\n\t\t\t}\n\t\t\treturn { exitCode: 1, integrity, rewrote: false };\n\t\t}\n\t\treturn { exitCode: 0, integrity, rewrote: false };\n\t}\n\n\tconst wrote = newJson !== oldJson;\n\tif (wrote) {\n\t\twriteFileSync(abs, newJson, \"utf8\");\n\t}\n\n\tconst action = wrote\n\t\t? prev === integrity\n\t\t\t? \"reformatted\"\n\t\t\t: `updated (${ansi.dim(String(prev ?? \"<unset>\"))} → ${integrity})`\n\t\t: \"unchanged\";\n\tconsole.log(`${ansi.green(\"✓\")} ${rel}: ${action}`);\n\n\t// Final validation pass — pack succeeds even if the rewrite was clean\n\t// only when the manifest as a whole still validates.\n\tconst v = validatePluginManifest(parsed);\n\tif (!v.ok) {\n\t\tconsole.error(\n\t\t\tansi.red(`✗ but manifest fails validation — fix and re-run`),\n\t\t);\n\t\tfor (const issue of v.errors) {\n\t\t\tconst where = issue.path.length > 0 ? issue.path.map(String).join(\".\") : \"<root>\";\n\t\t\tconsole.error(` ${ansi.yellow(where)}: ${issue.message}`);\n\t\t}\n\t\treturn { exitCode: 1, integrity, rewrote: wrote };\n\t}\n\n\treturn { exitCode: 0, integrity, rewrote: wrote };\n}\n","/**\n * `abra-plugin preview-scan [path]` — validate the manifest, then run a\n * quick static check on the bundle:\n *\n * - **capability/code mismatch**: if the bundle uses `fetch` / `XHR` /\n * `WebSocket` but the manifest doesn't declare `network[:*]`, flag it.\n * Symmetric: if `network:*` is declared but the bundle has no network\n * calls, warn (declared-but-unused).\n * - **declared `contributes` sanity**: every name in `manifest.contributes`\n * should appear somewhere in the bundle string. Cheap heuristic — a\n * proper AST walk lives on the registry server in Phase H.\n *\n * Designed to mirror what the registry's automated scanner does on\n * submission, so authors catch issues locally.\n *\n * Exit code 0 = clean, 1 = warnings only, 2 = errors. Authors should\n * resolve all errors before submitting; warnings are advisory.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join, relative } from \"node:path\";\nimport { validatePluginManifest } from \"@abraca/schema\";\nimport type { PluginManifest } from \"@abraca/plugin\";\nimport { ansi, isFile, readJsonFile, resolveCwd } from \"../io.ts\";\n\nexport interface PreviewScanOptions {\n\tpath?: string;\n}\n\nexport interface PreviewScanFinding {\n\tseverity: \"error\" | \"warning\";\n\trule: string;\n\tmessage: string;\n}\n\nexport interface PreviewScanResult {\n\texitCode: 0 | 1 | 2;\n\tfindings: ReadonlyArray<PreviewScanFinding>;\n}\n\nconst NETWORK_API_RE = /\\b(fetch|XMLHttpRequest|WebSocket|EventSource)\\b/;\nconst CLIPBOARD_READ_RE = /navigator\\.clipboard\\.read(?!Text)?Text?\\b/;\nconst CLIPBOARD_WRITE_RE = /navigator\\.clipboard\\.write/;\n\nfunction declaresCap(\n\tmanifest: PluginManifest,\n\tpredicate: (cap: string) => boolean,\n): boolean {\n\tconst all = [\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t];\n\treturn all.some(predicate);\n}\n\nexport function previewScan(opts: PreviewScanOptions = {}): PreviewScanResult {\n\tconst rel = opts.path ?? \"manifest.json\";\n\tconst abs = resolveCwd(rel);\n\tconst findings: PreviewScanFinding[] = [];\n\n\tif (!isFile(abs)) {\n\t\tconsole.error(ansi.red(`error: ${rel}: file not found`));\n\t\treturn { exitCode: 2, findings };\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = readJsonFile(abs);\n\t} catch (err) {\n\t\tconsole.error(ansi.red(`error: ${(err as Error).message}`));\n\t\treturn { exitCode: 2, findings };\n\t}\n\n\tconst result = validatePluginManifest(parsed);\n\tif (!result.ok) {\n\t\tconsole.error(ansi.red(`✗ manifest validation failed`));\n\t\tfor (const issue of result.errors) {\n\t\t\tconst where = issue.path.length > 0 ? issue.path.map(String).join(\".\") : \"<root>\";\n\t\t\tconsole.error(` ${ansi.yellow(where)}: ${issue.message}`);\n\t\t}\n\t\treturn { exitCode: 2, findings };\n\t}\n\n\tconst manifest = result.value;\n\tconst entryAbs = join(dirname(abs), manifest.entry);\n\n\tif (!isFile(entryAbs)) {\n\t\tfindings.push({\n\t\t\tseverity: \"error\",\n\t\t\trule: \"missing-entry\",\n\t\t\tmessage: `entry bundle not found: ${relative(process.cwd(), entryAbs)}`,\n\t\t});\n\t\tprintAndExit(findings);\n\t\treturn { exitCode: 2, findings };\n\t}\n\n\tconst source = readFileSync(entryAbs, \"utf8\");\n\n\t// Capability/code mismatch checks\n\tconst hasNetCalls = NETWORK_API_RE.test(source);\n\tconst hasNetCap = declaresCap(manifest, (c) => c === \"network\" || c.startsWith(\"network:\"));\n\tif (hasNetCalls && !hasNetCap) {\n\t\tfindings.push({\n\t\t\tseverity: \"error\",\n\t\t\trule: \"undeclared-network\",\n\t\t\tmessage:\n\t\t\t\t\"bundle uses fetch/XHR/WebSocket but no network[:*] capability is declared — add it to capabilities.required or capabilities.optional\",\n\t\t});\n\t}\n\tif (hasNetCap && !hasNetCalls) {\n\t\tfindings.push({\n\t\t\tseverity: \"warning\",\n\t\t\trule: \"unused-network\",\n\t\t\tmessage:\n\t\t\t\t\"network[:*] capability is declared but no fetch/XHR/WebSocket usage was found — remove the capability if unused\",\n\t\t});\n\t}\n\n\tconst hasClipRead = CLIPBOARD_READ_RE.test(source);\n\tconst hasClipReadCap = declaresCap(manifest, (c) => c === \"clipboard:read\");\n\tif (hasClipRead && !hasClipReadCap) {\n\t\tfindings.push({\n\t\t\tseverity: \"error\",\n\t\t\trule: \"undeclared-clipboard-read\",\n\t\t\tmessage: \"bundle reads the clipboard but clipboard:read is not declared\",\n\t\t});\n\t}\n\n\tconst hasClipWrite = CLIPBOARD_WRITE_RE.test(source);\n\tconst hasClipWriteCap = declaresCap(manifest, (c) => c === \"clipboard:write\");\n\tif (hasClipWrite && !hasClipWriteCap) {\n\t\tfindings.push({\n\t\t\tseverity: \"error\",\n\t\t\trule: \"undeclared-clipboard-write\",\n\t\t\tmessage: \"bundle writes the clipboard but clipboard:write is not declared\",\n\t\t});\n\t}\n\n\t// Declared `contributes` sanity — every name should appear in the bundle.\n\tfor (const [field, names] of Object.entries(manifest.contributes ?? {})) {\n\t\tconst list = (names ?? []) as readonly string[];\n\t\tfor (const name of list) {\n\t\t\tif (name.length < 3) continue; // skip short noise\n\t\t\tif (!source.includes(name)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tseverity: \"warning\",\n\t\t\t\t\trule: \"contributes-not-in-bundle\",\n\t\t\t\t\tmessage: `'${name}' declared in contributes.${field} but not referenced in entry bundle — possible drift`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprintAndExit(findings);\n\treturn { exitCode: severityExitCode(findings), findings };\n}\n\nfunction severityExitCode(\n\tfindings: ReadonlyArray<PreviewScanFinding>,\n): 0 | 1 | 2 {\n\tif (findings.some((f) => f.severity === \"error\")) return 2;\n\tif (findings.length > 0) return 1;\n\treturn 0;\n}\n\nfunction printAndExit(findings: ReadonlyArray<PreviewScanFinding>): void {\n\tif (findings.length === 0) {\n\t\tconsole.log(`${ansi.green(\"✓\")} clean — manifest + bundle pass preview scan`);\n\t\treturn;\n\t}\n\tfor (const f of findings) {\n\t\tconst badge =\n\t\t\tf.severity === \"error\" ? ansi.red(\"error\") : ansi.yellow(\"warning\");\n\t\tconsole.error(`${badge} ${ansi.dim(`[${f.rule}]`)} ${f.message}`);\n\t}\n}\n","#!/usr/bin/env node\n/**\n * `abra-plugin` — CLI for Abracadabra plugin authors.\n *\n * abra-plugin validate [path/to/manifest.json]\n * abra-plugin pack [path/to/manifest.json] [--dry-run]\n * abra-plugin preview-scan [path/to/manifest.json]\n *\n * Each command defaults the path to `./manifest.json`. Exit codes:\n * 0 success\n * 1 warnings (preview-scan) or manifest-still-fails-validation (pack)\n * 2 read/parse error or hard validation failure\n *\n * No external dependencies — pulls only `@abraca/schema` (Zod validator)\n * + `@abraca/plugin` (manifest types) which are zero-runtime-cost when\n * imported as `import type`.\n */\n\nimport { validate } from \"./commands/validate.ts\";\nimport { pack } from \"./commands/pack.ts\";\nimport { previewScan } from \"./commands/preview-scan.ts\";\nimport { ansi } from \"./io.ts\";\n\n// Public surface — small, so library consumers can call commands programmatically.\nexport { validate, type ValidateOptions, type ValidateResult } from \"./commands/validate.ts\";\nexport { pack, type PackOptions, type PackResult } from \"./commands/pack.ts\";\nexport {\n\tpreviewScan,\n\ttype PreviewScanOptions,\n\ttype PreviewScanResult,\n\ttype PreviewScanFinding,\n} from \"./commands/preview-scan.ts\";\n\nfunction help(): void {\n\tconst lines = [\n\t\t`${ansi.bold(\"abra-plugin\")} — Abracadabra plugin author toolkit`,\n\t\t\"\",\n\t\t\"Usage:\",\n\t\t` ${ansi.cyan(\"abra-plugin validate\")} [path] Validate a manifest against the schema`,\n\t\t` ${ansi.cyan(\"abra-plugin pack\")} [path] [--dry-run] Recompute integrity hash, update manifest`,\n\t\t` ${ansi.cyan(\"abra-plugin preview-scan\")} [path] Validate + static-scan the bundle (registry parity)`,\n\t\t\"\",\n\t\t`Path defaults to ${ansi.dim(\"./manifest.json\")} when omitted.`,\n\t];\n\tconsole.log(lines.join(\"\\n\"));\n}\n\nfunction parseFlag(args: string[], flag: string): boolean {\n\tconst idx = args.indexOf(flag);\n\tif (idx >= 0) {\n\t\targs.splice(idx, 1);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function run(argv: ReadonlyArray<string>): number {\n\tconst args = [...argv];\n\tconst command = args.shift();\n\n\tswitch (command) {\n\t\tcase undefined:\n\t\tcase \"help\":\n\t\tcase \"-h\":\n\t\tcase \"--help\":\n\t\t\thelp();\n\t\t\treturn 0;\n\n\t\tcase \"validate\": {\n\t\t\tconst quiet = parseFlag(args, \"--quiet\");\n\t\t\tconst path = args.shift();\n\t\t\treturn validate({ path, quiet }).exitCode;\n\t\t}\n\n\t\tcase \"pack\": {\n\t\t\tconst dryRun = parseFlag(args, \"--dry-run\");\n\t\t\tconst path = args.shift();\n\t\t\treturn pack({ path, dryRun }).exitCode;\n\t\t}\n\n\t\tcase \"preview-scan\": {\n\t\t\tconst path = args.shift();\n\t\t\treturn previewScan({ path }).exitCode;\n\t\t}\n\n\t\tdefault:\n\t\t\tconsole.error(ansi.red(`error: unknown command \"${command}\"`));\n\t\t\tconsole.error(`run ${ansi.cyan(\"abra-plugin help\")} for usage`);\n\t\t\treturn 2;\n\t}\n}\n\n// Auto-run when invoked as a CLI (not when imported as a library).\nconst isCliEntry =\n\timport.meta.url === `file://${process.argv[1]}` ||\n\timport.meta.url.endsWith(process.argv[1] ?? \"\");\nif (isCliEntry) {\n\tprocess.exit(run(process.argv.slice(2)));\n}\n"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ //#region packages/plugin-cli/src/commands/validate.d.ts
2
+ /**
3
+ * `abra-plugin validate [path]` — load a manifest.json and validate it
4
+ * against `@abraca/schema`'s `PluginManifestSchema`. Pretty-prints every
5
+ * issue with its JSON path; exit code 0 on success, 1 on validation
6
+ * failure, 2 on read/parse error.
7
+ *
8
+ * Defaults `path` to `./manifest.json` when omitted.
9
+ *
10
+ * This is the same validator the registry server runs on submission, so a
11
+ * green local check is a strong signal the submission will pass static
12
+ * checks too.
13
+ */
14
+ interface ValidateOptions {
15
+ /** Path to the manifest file. Defaults to `./manifest.json`. */
16
+ path?: string;
17
+ /** If true, suppress the success summary line. Errors are still printed. */
18
+ quiet?: boolean;
19
+ }
20
+ interface ValidateResult {
21
+ exitCode: 0 | 1 | 2;
22
+ /** Issues found by the schema. Empty on success. */
23
+ issues: ReadonlyArray<{
24
+ path: string;
25
+ message: string;
26
+ code?: string;
27
+ }>;
28
+ }
29
+ declare function validate(opts?: ValidateOptions): ValidateResult;
30
+ //#endregion
31
+ //#region packages/plugin-cli/src/commands/pack.d.ts
32
+ /**
33
+ * `abra-plugin pack [path]` — recompute the manifest's `integrity` field
34
+ * (SHA-256 of the entry bundle) and write the updated manifest back to disk.
35
+ *
36
+ * Authors run this after every bundle rebuild — the registry server refuses
37
+ * to accept a manifest whose `integrity` doesn't match the artifact, so
38
+ * having it as a one-shot CLI step removes a class of "I forgot to update
39
+ * the hash" submission rejections.
40
+ *
41
+ * The command also validates the manifest after recomputing, exiting
42
+ * non-zero if validation still fails (e.g. unknown capability declared).
43
+ */
44
+ interface PackOptions {
45
+ /** Path to the manifest file. Defaults to `./manifest.json`. */
46
+ path?: string;
47
+ /** If true, don't write the file — just print the new hash. */
48
+ dryRun?: boolean;
49
+ }
50
+ interface PackResult {
51
+ exitCode: 0 | 1 | 2;
52
+ /** The newly-computed integrity hash, or `null` on early failure. */
53
+ integrity: string | null;
54
+ /** Whether the on-disk manifest was rewritten. */
55
+ rewrote: boolean;
56
+ }
57
+ declare function pack(opts?: PackOptions): PackResult;
58
+ //#endregion
59
+ //#region packages/plugin-cli/src/commands/preview-scan.d.ts
60
+ /**
61
+ * `abra-plugin preview-scan [path]` — validate the manifest, then run a
62
+ * quick static check on the bundle:
63
+ *
64
+ * - **capability/code mismatch**: if the bundle uses `fetch` / `XHR` /
65
+ * `WebSocket` but the manifest doesn't declare `network[:*]`, flag it.
66
+ * Symmetric: if `network:*` is declared but the bundle has no network
67
+ * calls, warn (declared-but-unused).
68
+ * - **declared `contributes` sanity**: every name in `manifest.contributes`
69
+ * should appear somewhere in the bundle string. Cheap heuristic — a
70
+ * proper AST walk lives on the registry server in Phase H.
71
+ *
72
+ * Designed to mirror what the registry's automated scanner does on
73
+ * submission, so authors catch issues locally.
74
+ *
75
+ * Exit code 0 = clean, 1 = warnings only, 2 = errors. Authors should
76
+ * resolve all errors before submitting; warnings are advisory.
77
+ */
78
+ interface PreviewScanOptions {
79
+ path?: string;
80
+ }
81
+ interface PreviewScanFinding {
82
+ severity: "error" | "warning";
83
+ rule: string;
84
+ message: string;
85
+ }
86
+ interface PreviewScanResult {
87
+ exitCode: 0 | 1 | 2;
88
+ findings: ReadonlyArray<PreviewScanFinding>;
89
+ }
90
+ declare function previewScan(opts?: PreviewScanOptions): PreviewScanResult;
91
+ //#endregion
92
+ //#region packages/plugin-cli/src/index.d.ts
93
+ declare function run(argv: ReadonlyArray<string>): number;
94
+ //#endregion
95
+ export { type PackOptions, type PackResult, type PreviewScanFinding, type PreviewScanOptions, type PreviewScanResult, type ValidateOptions, type ValidateResult, pack, previewScan, run, validate };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@abraca/plugin-cli",
3
+ "version": "2.3.0",
4
+ "description": "CLI for Abracadabra plugin authors — validate, pack, and preview-scan plugins locally before submission.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/abracadabra-plugin-cli.cjs",
8
+ "module": "dist/abracadabra-plugin-cli.esm.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "abra-plugin": "./dist/abracadabra-plugin-cli.esm.js"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "exports": {
17
+ "source": {
18
+ "import": "./src/index.ts"
19
+ },
20
+ "default": {
21
+ "import": "./dist/abracadabra-plugin-cli.esm.js",
22
+ "require": "./dist/abracadabra-plugin-cli.cjs",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "src",
28
+ "dist"
29
+ ],
30
+ "peerDependencies": {
31
+ "zod": "^4.0.0",
32
+ "@abraca/plugin": "2.3.0",
33
+ "@abraca/schema": "2.3.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "latest",
37
+ "zod": "^4.3.6",
38
+ "@abraca/plugin": "2.3.0",
39
+ "@abraca/schema": "2.3.0"
40
+ },
41
+ "scripts": {
42
+ "test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
43
+ }
44
+ }