@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.
- package/dist/abracadabra-plugin-cli.cjs +436 -0
- package/dist/abracadabra-plugin-cli.cjs.map +1 -0
- package/dist/abracadabra-plugin-cli.esm.js +432 -0
- package/dist/abracadabra-plugin-cli.esm.js.map +1 -0
- package/dist/index.d.ts +95 -0
- package/package.json +44 -0
- package/src/commands/pack.ts +121 -0
- package/src/commands/preview-scan.ts +176 -0
- package/src/commands/validate.ts +73 -0
- package/src/index.ts +99 -0
- package/src/io.ts +67 -0
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|