xlsx-for-ai 2.25.2 → 2.26.1
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/index.js +171 -0
- package/lib/annotations.js +122 -0
- package/package.json +6 -3
package/index.js
CHANGED
|
@@ -151,6 +151,7 @@ async function runClean(opts, absPath) {
|
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
152
|
|
|
153
153
|
const STAMP_SUBCOMMANDS = new Set(['stamp', 'verify-stamp', 'receipt', 'verify-receipt']);
|
|
154
|
+
const HEAL_SUBCOMMANDS = new Set(['heal']);
|
|
154
155
|
|
|
155
156
|
// Strip _meta.file_b64 before writing the meta block to stdout. The
|
|
156
157
|
// stamped/receipted workbook can be megabytes; dumping it to a terminal
|
|
@@ -214,6 +215,172 @@ function loadChecksFile(checksPath) {
|
|
|
214
215
|
return parsed;
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Heal subcommand — exposes the healer-deep HTTP routes from the CLI.
|
|
220
|
+
//
|
|
221
|
+
// xlsx-for-ai heal <path> diagnose-only (default)
|
|
222
|
+
// xlsx-for-ai heal <path> --diagnose-only explicit form of the default
|
|
223
|
+
// xlsx-for-ai heal <path> --operation <op> --params <json> [--mode <m>] [--out <path>]
|
|
224
|
+
// xlsx-for-ai heal <path> --format text|json [other flags]
|
|
225
|
+
//
|
|
226
|
+
// `--intent`/`--from`/`--to` are reserved for v1.1 once the
|
|
227
|
+
// /api/v1/tools/xlsx_healer_intent route ships; the CLI rejects
|
|
228
|
+
// those flags today so callers see a clean "v1.1" message rather
|
|
229
|
+
// than a server 404.
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
async function runHealSubcommand(rest) {
|
|
233
|
+
if (rest.length === 0 || rest[0].startsWith('-')) {
|
|
234
|
+
process.stderr.write(
|
|
235
|
+
'Usage: xlsx-for-ai heal <file.xlsx> [--diagnose-only | --operation <op> --params <json>] [--mode as_copy|in_place] [--out <path>] [--format text|json]\n',
|
|
236
|
+
);
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
const filePath = path.resolve(rest[0]);
|
|
240
|
+
if (!fs.existsSync(filePath)) {
|
|
241
|
+
process.stderr.write(`File not found: ${filePath}\n`);
|
|
242
|
+
process.exit(4);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Parse flags
|
|
246
|
+
let diagnoseOnly = false;
|
|
247
|
+
let operation = null;
|
|
248
|
+
let paramsJson = null;
|
|
249
|
+
let mode = 'as_copy';
|
|
250
|
+
let outPath = null;
|
|
251
|
+
let format = 'text';
|
|
252
|
+
for (let i = 1; i < rest.length; i++) {
|
|
253
|
+
const a = rest[i];
|
|
254
|
+
if (a === '--diagnose-only') diagnoseOnly = true;
|
|
255
|
+
else if (a === '--operation') operation = nextRequiredArg(rest, i++, '--operation');
|
|
256
|
+
else if (a === '--params') paramsJson = nextRequiredArg(rest, i++, '--params');
|
|
257
|
+
else if (a === '--mode') mode = nextRequiredArg(rest, i++, '--mode');
|
|
258
|
+
else if (a === '--out') outPath = nextRequiredArg(rest, i++, '--out');
|
|
259
|
+
else if (a === '--format') format = nextRequiredArg(rest, i++, '--format');
|
|
260
|
+
else if (a === '--intent' || a === '--from' || a === '--to') {
|
|
261
|
+
process.stderr.write(`xlsx-for-ai heal: ${a} is reserved for v1.1 (intent-driven cures via /xlsx_healer_intent); not yet wired\n`);
|
|
262
|
+
process.exit(2);
|
|
263
|
+
} else {
|
|
264
|
+
process.stderr.write(`Unknown flag: ${a}\n`);
|
|
265
|
+
process.exit(2);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Validate mutually-exclusive shapes early — clearer than letting
|
|
270
|
+
// the server emit `conflicting_repair_directives` on its O8 check.
|
|
271
|
+
if (diagnoseOnly && operation) {
|
|
272
|
+
process.stderr.write('xlsx-for-ai heal: --diagnose-only and --operation are mutually exclusive\n');
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
if (!diagnoseOnly && !operation) {
|
|
276
|
+
// Default to diagnose-only — first-touch use of `xlsx-for-ai heal`
|
|
277
|
+
// should show the user what's wrong before they pick a cure.
|
|
278
|
+
diagnoseOnly = true;
|
|
279
|
+
}
|
|
280
|
+
if (mode !== 'as_copy' && mode !== 'in_place') {
|
|
281
|
+
process.stderr.write(`xlsx-for-ai heal: --mode must be 'as_copy' or 'in_place' (got '${mode}')\n`);
|
|
282
|
+
process.exit(2);
|
|
283
|
+
}
|
|
284
|
+
if (format !== 'text' && format !== 'json') {
|
|
285
|
+
process.stderr.write(`xlsx-for-ai heal: --format must be 'text' or 'json' (got '${format}')\n`);
|
|
286
|
+
process.exit(2);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await ensureRegistered();
|
|
290
|
+
const fileB64 = fs.readFileSync(filePath).toString('base64');
|
|
291
|
+
|
|
292
|
+
// ---- diagnose path -----------------------------------------------------
|
|
293
|
+
if (diagnoseOnly) {
|
|
294
|
+
let result;
|
|
295
|
+
try {
|
|
296
|
+
result = await callTool('xlsx_healer_diagnose', { file_b64: fileB64 });
|
|
297
|
+
} catch (err) {
|
|
298
|
+
// friendlyCliError (defined above in this file) maps known
|
|
299
|
+
// API error codes to short canned messages — raw err.message
|
|
300
|
+
// is suppressed unless XFA_DEBUG=1 is set, so internal server
|
|
301
|
+
// detail / paths / stack traces never reach the user's
|
|
302
|
+
// terminal or CI logs by default. Same sanitization shape
|
|
303
|
+
// the other subcommands use for API failures.
|
|
304
|
+
process.stderr.write(friendlyCliError('xlsx-for-ai heal --diagnose-only', err) + '\n');
|
|
305
|
+
process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
|
|
306
|
+
}
|
|
307
|
+
const meta = (result && result._meta) || {};
|
|
308
|
+
if (format === 'json') {
|
|
309
|
+
// metaForStdout strips file_b64 if present — diagnose has none,
|
|
310
|
+
// but the helper is safe to call unconditionally.
|
|
311
|
+
process.stdout.write(JSON.stringify(metaForStdout(meta) || {}, null, 2) + '\n');
|
|
312
|
+
} else {
|
|
313
|
+
const text = (result.content || []).map((c) => c.text).join('\n');
|
|
314
|
+
process.stdout.write(text + '\n');
|
|
315
|
+
}
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- cure path ---------------------------------------------------------
|
|
320
|
+
let cureParams = {};
|
|
321
|
+
if (paramsJson !== null) {
|
|
322
|
+
try {
|
|
323
|
+
cureParams = JSON.parse(paramsJson);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
process.stderr.write(`xlsx-for-ai heal: --params is not valid JSON: ${e.message}\n`);
|
|
326
|
+
process.exit(2);
|
|
327
|
+
}
|
|
328
|
+
if (cureParams === null || typeof cureParams !== 'object' || Array.isArray(cureParams)) {
|
|
329
|
+
process.stderr.write('xlsx-for-ai heal: --params must be a JSON object\n');
|
|
330
|
+
process.exit(2);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const body = { file_b64: fileB64, operation, cure_params: cureParams, mode };
|
|
335
|
+
let result;
|
|
336
|
+
try {
|
|
337
|
+
result = await callTool('xlsx_healer_cure', body);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
// Same sanitization shape as the diagnose path — friendlyCliError
|
|
340
|
+
// (above) maps known codes to canned messages; raw err.message
|
|
341
|
+
// only surfaces with XFA_DEBUG=1 for incident triage.
|
|
342
|
+
process.stderr.write(friendlyCliError(`xlsx-for-ai heal --operation ${operation}`, err) + '\n');
|
|
343
|
+
process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const meta = (result && result._meta) || {};
|
|
347
|
+
|
|
348
|
+
// Write the cured workbook to disk when `as_copy` mode produced
|
|
349
|
+
// bytes. `in_place` mode is server-side conceptual; the client
|
|
350
|
+
// still receives the cured bytes here (we'd overwrite the input
|
|
351
|
+
// file). Refuse to overwrite the source unless the caller passed
|
|
352
|
+
// --out explicitly.
|
|
353
|
+
if (meta.file_b64) {
|
|
354
|
+
let resolvedOut = outPath;
|
|
355
|
+
if (!resolvedOut) {
|
|
356
|
+
const parsed = path.parse(filePath);
|
|
357
|
+
resolvedOut = path.join(parsed.dir, `${parsed.name}-healed${parsed.ext || '.xlsx'}`);
|
|
358
|
+
}
|
|
359
|
+
const sourceIsTarget = path.resolve(resolvedOut) === path.resolve(filePath);
|
|
360
|
+
if (sourceIsTarget && mode !== 'in_place') {
|
|
361
|
+
process.stderr.write(
|
|
362
|
+
'xlsx-for-ai heal: refusing to overwrite source — pass --out <other-path> or --mode in_place\n',
|
|
363
|
+
);
|
|
364
|
+
process.exit(2);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
fs.writeFileSync(resolvedOut, Buffer.from(meta.file_b64, 'base64'));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
process.stderr.write(`xlsx-for-ai heal: failed to write ${resolvedOut}: ${e.message}\n`);
|
|
370
|
+
process.exit(4);
|
|
371
|
+
}
|
|
372
|
+
process.stderr.write(`Wrote ${resolvedOut}\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (format === 'json') {
|
|
376
|
+
process.stdout.write(JSON.stringify(metaForStdout(meta) || {}, null, 2) + '\n');
|
|
377
|
+
} else {
|
|
378
|
+
const text = (result.content || []).map((c) => c.text).join('\n');
|
|
379
|
+
process.stdout.write(text + '\n');
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
217
384
|
async function runStampSubcommand(subcmd, rest) {
|
|
218
385
|
if (rest.length === 0 || rest[0].startsWith('-')) {
|
|
219
386
|
process.stderr.write(`Usage: xlsx-for-ai ${subcmd} <path> [...]\n`);
|
|
@@ -342,6 +509,10 @@ async function main() {
|
|
|
342
509
|
const code = await runStampSubcommand(argv[0], argv.slice(1));
|
|
343
510
|
process.exit(code);
|
|
344
511
|
}
|
|
512
|
+
if (argv.length > 0 && HEAL_SUBCOMMANDS.has(argv[0])) {
|
|
513
|
+
const code = await runHealSubcommand(argv.slice(1));
|
|
514
|
+
process.exit(code);
|
|
515
|
+
}
|
|
345
516
|
|
|
346
517
|
const opts = parseArgs(argv);
|
|
347
518
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP tool annotations — canonical source.
|
|
5
|
+
*
|
|
6
|
+
* Per MCP spec (2025-06-18+) tool annotations describe runtime behavior:
|
|
7
|
+
* - title Human-readable tool name
|
|
8
|
+
* - readOnlyHint Tool does NOT modify its environment
|
|
9
|
+
* - destructiveHint Tool may perform irreversible side-effects
|
|
10
|
+
*
|
|
11
|
+
* The annotations live here rather than inline on each tool definition so:
|
|
12
|
+
* 1. They overlay onto tools regardless of source — static fallback,
|
|
13
|
+
* cached catalog, or freshly-fetched remote. The remote /api/v1/tools/list
|
|
14
|
+
* currently returns minimal entries; this overlay restores the
|
|
15
|
+
* annotations the wire format would otherwise drop.
|
|
16
|
+
* 2. They drive manifest generation downstream (MCPB, M365 declarative
|
|
17
|
+
* agent, future OpenAPI). One annotation change → all manifests
|
|
18
|
+
* regenerate consistently.
|
|
19
|
+
*
|
|
20
|
+
* Classification rules:
|
|
21
|
+
* - readOnlyHint: true → tool only reads; never writes a file or causes
|
|
22
|
+
* an externally observable side-effect.
|
|
23
|
+
* - destructiveHint: true → tool causes an irreversible external action
|
|
24
|
+
* (e.g., posts to a third-party system).
|
|
25
|
+
* Note: tools that write a NEW file (Save-As shape) are NOT destructive
|
|
26
|
+
* even though readOnlyHint is false — the source workbook is preserved.
|
|
27
|
+
* destructiveHint is reserved for actions that cannot be undone by
|
|
28
|
+
* deleting the output, which means external side-effects.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const TOOL_ANNOTATIONS = Object.freeze({
|
|
32
|
+
// ---- Reading / inspection: 35 read-only tools -------------------------
|
|
33
|
+
xlsx_read: { title: 'Read Excel file', readOnlyHint: true, destructiveHint: false },
|
|
34
|
+
xlsx_list_sheets: { title: 'List Excel sheets', readOnlyHint: true, destructiveHint: false },
|
|
35
|
+
xlsx_schema: { title: 'Infer Excel column types', readOnlyHint: true, destructiveHint: false },
|
|
36
|
+
xlsx_diff: { title: 'Diff two Excel workbooks', readOnlyHint: true, destructiveHint: false },
|
|
37
|
+
xlsx_describe: { title: 'Summarize Excel columns', readOnlyHint: true, destructiveHint: false },
|
|
38
|
+
xlsx_filter: { title: 'Filter Excel rows', readOnlyHint: true, destructiveHint: false },
|
|
39
|
+
xlsx_aggregate: { title: 'Group-by aggregate Excel rows', readOnlyHint: true, destructiveHint: false },
|
|
40
|
+
xlsx_named_ranges: { title: 'List Excel named ranges', readOnlyHint: true, destructiveHint: false },
|
|
41
|
+
xlsx_sort: { title: 'Sort Excel rows', readOnlyHint: true, destructiveHint: false },
|
|
42
|
+
xlsx_value_counts: { title: 'Count Excel column values', readOnlyHint: true, destructiveHint: false },
|
|
43
|
+
xlsx_formulas: { title: 'Inspect Excel formulas', readOnlyHint: true, destructiveHint: false },
|
|
44
|
+
xlsx_tables: { title: 'List Excel tables', readOnlyHint: true, destructiveHint: false },
|
|
45
|
+
xlsx_pivot: { title: 'Pivot Excel data', readOnlyHint: true, destructiveHint: false },
|
|
46
|
+
xlsx_eval: { title: 'Evaluate Excel formula', readOnlyHint: true, destructiveHint: false },
|
|
47
|
+
xlsx_validate: { title: 'Cross-engine validate Excel', readOnlyHint: true, destructiveHint: false },
|
|
48
|
+
xlsx_data_validations: { title: 'List Excel data-validation rules', readOnlyHint: true, destructiveHint: false },
|
|
49
|
+
xlsx_hyperlinks: { title: 'List Excel hyperlinks', readOnlyHint: true, destructiveHint: false },
|
|
50
|
+
xlsx_topology: { title: 'Map Excel sheet topology', readOnlyHint: true, destructiveHint: false },
|
|
51
|
+
xlsx_conditional_formats: { title: 'List Excel conditional formats', readOnlyHint: true, destructiveHint: false },
|
|
52
|
+
xlsx_comments: { title: 'List Excel comments', readOnlyHint: true, destructiveHint: false },
|
|
53
|
+
xlsx_doctor: { title: 'Audit Excel workbook health', readOnlyHint: true, destructiveHint: false },
|
|
54
|
+
xlsx_form_controls: { title: 'List Excel form controls', readOnlyHint: true, destructiveHint: false },
|
|
55
|
+
xlsx_macros: { title: 'List Excel VBA macros', readOnlyHint: true, destructiveHint: false },
|
|
56
|
+
xlsx_merged_cells: { title: 'List Excel merged cells', readOnlyHint: true, destructiveHint: false },
|
|
57
|
+
xlsx_workbook_views: { title: 'List Excel workbook views', readOnlyHint: true, destructiveHint: false },
|
|
58
|
+
xlsx_print_settings: { title: 'List Excel print settings', readOnlyHint: true, destructiveHint: false },
|
|
59
|
+
xlsx_properties: { title: 'Read Excel document properties', readOnlyHint: true, destructiveHint: false },
|
|
60
|
+
xlsx_external_links: { title: 'List Excel external links', readOnlyHint: true, destructiveHint: false },
|
|
61
|
+
xlsx_slicers_timelines: { title: 'List Excel slicers and timelines', readOnlyHint: true, destructiveHint: false },
|
|
62
|
+
xlsx_pivot_tables: { title: 'List Excel pivot tables', readOnlyHint: true, destructiveHint: false },
|
|
63
|
+
xlsx_images: { title: 'List Excel embedded images', readOnlyHint: true, destructiveHint: false },
|
|
64
|
+
xlsx_charts: { title: 'List Excel charts', readOnlyHint: true, destructiveHint: false },
|
|
65
|
+
xlsx_protection: { title: 'List Excel protection settings', readOnlyHint: true, destructiveHint: false },
|
|
66
|
+
xlsx_styles: { title: 'List Excel cell styles', readOnlyHint: true, destructiveHint: false },
|
|
67
|
+
xlsx_verify_stamp: { title: 'Verify Excel integrity stamp', readOnlyHint: true, destructiveHint: false },
|
|
68
|
+
|
|
69
|
+
// ---- Writing — non-destructive: 5 Save-As-shape tools -----------------
|
|
70
|
+
// Source workbook is preserved; output goes to a new path or returned bytes.
|
|
71
|
+
xlsx_write: { title: 'Write Excel file', readOnlyHint: false, destructiveHint: false },
|
|
72
|
+
xlsx_redact: { title: 'Redact Excel file', readOnlyHint: false, destructiveHint: false },
|
|
73
|
+
xlsx_convert: { title: 'Convert Excel to other format', readOnlyHint: false, destructiveHint: false },
|
|
74
|
+
xlsx_data_clean: { title: 'Clean Excel data', readOnlyHint: false, destructiveHint: false },
|
|
75
|
+
xlsx_stamp: { title: 'Stamp Excel with integrity verification', readOnlyHint: false, destructiveHint: false },
|
|
76
|
+
|
|
77
|
+
// ---- External side-effects — destructive: 2 tools ---------------------
|
|
78
|
+
// A post can't be undone; the message lands in a third-party system.
|
|
79
|
+
xlsx_post_slack: { title: 'Post Excel summary to Slack', readOnlyHint: false, destructiveHint: true },
|
|
80
|
+
xlsx_post_teams: { title: 'Post Excel summary to Teams', readOnlyHint: false, destructiveHint: true },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Overlay annotations onto an MCP-shaped tool array.
|
|
85
|
+
*
|
|
86
|
+
* Returns a new array with each tool extended with an `annotations` object
|
|
87
|
+
* pulled from TOOL_ANNOTATIONS by name. Tools without a known annotation
|
|
88
|
+
* pass through unchanged — this is intentional so that a dynamically-
|
|
89
|
+
* discovered tool the client doesn't recognize still appears, just without
|
|
90
|
+
* the annotation hints. The annotation map should be updated whenever
|
|
91
|
+
* a new tool is added to the server.
|
|
92
|
+
*/
|
|
93
|
+
// Keys we refuse to copy from upstream annotation objects — guards against
|
|
94
|
+
// prototype-pollution if a remote /api/v1/tools/list ever returns hostile
|
|
95
|
+
// data. Our overlay map is a frozen const so it's safe to spread directly;
|
|
96
|
+
// the danger is only the foreign `existing` object.
|
|
97
|
+
const POLLUTION_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
98
|
+
|
|
99
|
+
function applyAnnotations(tools) {
|
|
100
|
+
if (!Array.isArray(tools)) return tools;
|
|
101
|
+
return tools.map((t) => {
|
|
102
|
+
if (!t || typeof t.name !== 'string') return t;
|
|
103
|
+
const ann = TOOL_ANNOTATIONS[t.name];
|
|
104
|
+
if (!ann) return t;
|
|
105
|
+
// Preserve any annotations the upstream source already carries; ours fill
|
|
106
|
+
// in the gaps without clobbering richer remote data. Reject pollution
|
|
107
|
+
// keys and non-plain-object inputs.
|
|
108
|
+
const merged = { ...ann };
|
|
109
|
+
if (t.annotations && typeof t.annotations === 'object' && !Array.isArray(t.annotations)) {
|
|
110
|
+
for (const [k, v] of Object.entries(t.annotations)) {
|
|
111
|
+
if (POLLUTION_KEYS.has(k)) continue;
|
|
112
|
+
merged[k] = v;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ...t, annotations: merged };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
TOOL_ANNOTATIONS,
|
|
121
|
+
applyAnnotations,
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xlsx-for-ai",
|
|
3
3
|
"mcpName": "io.github.senoff/xlsx-for-ai",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.26.1",
|
|
5
5
|
"description": "The MCP server that makes LLMs reliable on real-world Excel spreadsheets. Thin npm client over a hosted API — read, write, diff, redact, and supervise .xlsx files from any MCP-aware agent.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
14
|
"mcp.js",
|
|
15
|
+
"lib/annotations.js",
|
|
15
16
|
"lib/client.js",
|
|
16
17
|
"lib/config.js",
|
|
17
|
-
"lib/register.js",
|
|
18
|
-
"lib/fallback-read.js",
|
|
19
18
|
"lib/discover.js",
|
|
19
|
+
"lib/fallback-read.js",
|
|
20
|
+
"lib/register.js",
|
|
20
21
|
"README.md",
|
|
21
22
|
"SECURITY.md",
|
|
22
23
|
"LICENSE"
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
"test": "node --test test/v2/*.test.js",
|
|
26
27
|
"build-manifests": "node scripts/build-manifests.js",
|
|
27
28
|
"check-manifests": "node scripts/build-manifests.js --check",
|
|
29
|
+
"check-publish-allowlist": "node scripts/check-publish-allowlist.js",
|
|
30
|
+
"prepublishOnly": "node scripts/check-publish-allowlist.js && node scripts/build-manifests.js --check",
|
|
28
31
|
"prepare": "husky"
|
|
29
32
|
},
|
|
30
33
|
"keywords": [
|