@vegastack/design 0.1.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/bin/check-updates.mjs +313 -0
- package/bin/vegastack-design.mjs +66 -0
- package/bin/verify-registry-item.mjs +496 -0
- package/css/base.css +2 -0
- package/css/theme.css +4 -0
- package/css/utilities.css +2 -0
- package/dist/chunk-FBU37ITM.js +52 -0
- package/dist/icons/index.d.ts +154 -0
- package/dist/icons/index.js +73 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +10 -0
- package/dist/preset.d.ts +19 -0
- package/dist/preset.js +12 -0
- package/package.json +88 -0
- package/preset.css +39 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// vegastack-design check-updates — show which copied-in VegaStack components have a newer version
|
|
3
|
+
// published in the registry, so you know what to re-pull. shadcn registries are copy-in ("you own
|
|
4
|
+
// the code") with NO automatic updates; this turns that into a single command.
|
|
5
|
+
//
|
|
6
|
+
// HOW IT WORKS (one network request):
|
|
7
|
+
// 1. Resolve the @vegastack registry url + auth headers from components.json (or env).
|
|
8
|
+
// 2. Scan the components dir for files carrying the provenance header the registry stamps:
|
|
9
|
+
// // @vegastack <name>@<version> sha256-<integrity>
|
|
10
|
+
// That header IS the version pin recorded at copy-in time.
|
|
11
|
+
// 3. Fetch the registry INDEX once (…/registry.json) — every item carries meta.version + meta.integrity.
|
|
12
|
+
// 4. Compare BY HASH (not version number): if the published integrity differs from the one in your
|
|
13
|
+
// header, that component changed → an update is available. (Comparing by hash avoids false
|
|
14
|
+
// "update" noise when the global version bumps but a given component's content didn't change.)
|
|
15
|
+
//
|
|
16
|
+
// It does NOT detect local edits to a component's body (the header is the PUBLISHED hash, not a hash
|
|
17
|
+
// of your current file) — that is what `vegastack-design verify --post-write` is for. The footer
|
|
18
|
+
// reminds you to `git diff` before `--overwrite`.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// npx vegastack-design check-updates
|
|
22
|
+
// npx vegastack-design check-updates --filter button,dialog --json
|
|
23
|
+
// npx vegastack-design check-updates --fail-on-update # CI drift gate (exit 1 if stale)
|
|
24
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
25
|
+
import { join, resolve, basename } from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const PROVENANCE_RE = /^\/\/ @vegastack (\S+)@(\S+) sha256-([A-Za-z0-9+/=]+)/;
|
|
29
|
+
const DEFAULT_REGISTRY = 'https://design.vegastack.com/r';
|
|
30
|
+
const SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', '.turbo', 'out']);
|
|
31
|
+
|
|
32
|
+
const USAGE = `vegastack-design check-updates — show which copied-in components have newer registry versions
|
|
33
|
+
|
|
34
|
+
Usage: vegastack-design check-updates [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--dir <path> Components directory (default: components.json aliases.ui, else components/ui)
|
|
38
|
+
--filter <names> Comma-separated component names (supports * globs)
|
|
39
|
+
--json Machine-readable output
|
|
40
|
+
--fail-on-update Exit 1 if any component has an update (for CI)
|
|
41
|
+
--registry <url> Override registry URL template (…/{name}.json)
|
|
42
|
+
--cwd <path> Project root holding components.json (default: cwd)
|
|
43
|
+
--no-color Disable ANSI colors
|
|
44
|
+
-h, --help Show this help
|
|
45
|
+
|
|
46
|
+
Config: reads the @vegastack registry url + headers from components.json; \${ENV} placeholders expand
|
|
47
|
+
from .env.local / .env or the shell; falls back to VEGASTACK_REGISTRY + CF_ACCESS_CLIENT_ID/SECRET.`;
|
|
48
|
+
|
|
49
|
+
// ── arg parsing ────────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const out = { filter: null, json: false, failOnUpdate: false, noColor: false, dir: null, cwd: '.', registry: null, help: false };
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const a = argv[i];
|
|
54
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
55
|
+
else if (a === '--json') out.json = true;
|
|
56
|
+
else if (a === '--fail-on-update') out.failOnUpdate = true;
|
|
57
|
+
else if (a === '--no-color') out.noColor = true;
|
|
58
|
+
else if (a === '--dir') out.dir = argv[++i];
|
|
59
|
+
else if (a === '--cwd') out.cwd = argv[++i];
|
|
60
|
+
else if (a === '--registry') out.registry = argv[++i];
|
|
61
|
+
else if (a === '--filter') out.filter = argv[++i];
|
|
62
|
+
else throw new UsageError(`unknown option: ${a}`);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
class UsageError extends Error {}
|
|
67
|
+
|
|
68
|
+
// ── env expansion + config resolution ────────────────────────────────────────────────────────────
|
|
69
|
+
// shadcn expands ${VAR} in components.json from .env.local/.env too, so we load them (process/shell
|
|
70
|
+
// env still wins) — otherwise the gated prod registry would 403 here while `shadcn add` succeeds.
|
|
71
|
+
let ENV = process.env;
|
|
72
|
+
function loadEnv(cwd) {
|
|
73
|
+
const merged = {};
|
|
74
|
+
for (const f of ['.env', '.env.local']) {
|
|
75
|
+
let txt;
|
|
76
|
+
try {
|
|
77
|
+
txt = readFileSync(join(cwd, f), 'utf8');
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const raw of txt.split('\n')) {
|
|
82
|
+
const line = raw.trim();
|
|
83
|
+
if (!line || line.startsWith('#')) continue;
|
|
84
|
+
const eq = line.indexOf('=');
|
|
85
|
+
if (eq === -1) continue;
|
|
86
|
+
const key = line.slice(0, eq).trim();
|
|
87
|
+
let val = line.slice(eq + 1).trim();
|
|
88
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
|
|
89
|
+
merged[key] = val;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ...merged, ...process.env }; // process/shell env wins (dotenv semantics)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const missingEnv = new Set();
|
|
96
|
+
function expandEnv(value) {
|
|
97
|
+
if (typeof value !== 'string') return value;
|
|
98
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => {
|
|
99
|
+
const v = ENV[name];
|
|
100
|
+
if (v == null || v === '') missingEnv.add(name);
|
|
101
|
+
return v ?? '';
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readJson(path) {
|
|
106
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function envHeaders() {
|
|
110
|
+
const h = {};
|
|
111
|
+
if (ENV.CF_ACCESS_CLIENT_ID) h['CF-Access-Client-Id'] = ENV.CF_ACCESS_CLIENT_ID;
|
|
112
|
+
if (ENV.CF_ACCESS_CLIENT_SECRET) h['CF-Access-Client-Secret'] = ENV.CF_ACCESS_CLIENT_SECRET;
|
|
113
|
+
return h;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Resolve { urlTemplate, headers } in precedence order: --registry > components.json > env.
|
|
117
|
+
function resolveRegistry(opts, componentsJson) {
|
|
118
|
+
if (opts.registry) {
|
|
119
|
+
return { urlTemplate: opts.registry, headers: envHeaders() };
|
|
120
|
+
}
|
|
121
|
+
const reg = componentsJson?.registries?.['@vegastack'];
|
|
122
|
+
if (reg) {
|
|
123
|
+
if (typeof reg === 'string') return { urlTemplate: expandEnv(reg), headers: {} };
|
|
124
|
+
const headers = {};
|
|
125
|
+
for (const [k, v] of Object.entries(reg.headers ?? {})) headers[k] = expandEnv(v);
|
|
126
|
+
return { urlTemplate: expandEnv(reg.url), headers };
|
|
127
|
+
}
|
|
128
|
+
return { urlTemplate: `${(ENV.VEGASTACK_REGISTRY ?? DEFAULT_REGISTRY).replace(/\/$/, '')}/{name}.json`, headers: envHeaders() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Turn an item-url template into the index url: replace {name}→registry, else append /registry.json.
|
|
132
|
+
function indexUrl(urlTemplate) {
|
|
133
|
+
if (urlTemplate.includes('{name}')) return urlTemplate.replace('{name}', 'registry');
|
|
134
|
+
return `${urlTemplate.replace(/\/$/, '')}/registry.json`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── components dir + scan ──────────────────────────────────────────────────────────────────────
|
|
138
|
+
function resolveComponentsDir(cwd, dirFlag, componentsJson) {
|
|
139
|
+
if (dirFlag) return resolve(cwd, dirFlag);
|
|
140
|
+
const ui = componentsJson?.aliases?.ui;
|
|
141
|
+
const rel = ui ? ui.replace(/^@\//, '').replace(/^~\//, '') : 'components/ui';
|
|
142
|
+
const direct = resolve(cwd, rel); // assumes @/ = project root
|
|
143
|
+
if (existsSync(direct)) return direct;
|
|
144
|
+
const srcVariant = resolve(cwd, 'src', rel); // common alias: @/* -> src/*
|
|
145
|
+
if (existsSync(srcVariant)) return srcVariant;
|
|
146
|
+
return direct; // documented default; "none found" message hints --dir
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function walk(dir, out = []) {
|
|
150
|
+
let entries;
|
|
151
|
+
try {
|
|
152
|
+
entries = readdirSync(dir);
|
|
153
|
+
} catch {
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
for (const name of entries) {
|
|
157
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
158
|
+
const p = join(dir, name);
|
|
159
|
+
const s = statSync(p);
|
|
160
|
+
if (s.isDirectory()) walk(p, out);
|
|
161
|
+
else if (/\.tsx?$/.test(name)) out.push(p);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readHeader(file) {
|
|
167
|
+
let firstLine = '';
|
|
168
|
+
try {
|
|
169
|
+
const fd = readFileSync(file, 'utf8');
|
|
170
|
+
firstLine = fd.slice(0, fd.indexOf('\n') === -1 ? fd.length : fd.indexOf('\n'));
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const m = PROVENANCE_RE.exec(firstLine);
|
|
175
|
+
return m ? { name: m[1], version: m[2], hash: `sha256-${m[3]}`, file } : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function globToRe(pattern) {
|
|
179
|
+
return new RegExp('^' + pattern.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── colors ─────────────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
function makeColor(enabled) {
|
|
184
|
+
const wrap = (code) => (s) => (enabled ? `[${code}m${s}[0m` : s);
|
|
185
|
+
return { yellow: wrap(33), green: wrap(32), dim: wrap(2), bold: wrap(1), red: wrap(31) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── main ─────────────────────────────────────────────────────────────────────────────────────────
|
|
189
|
+
export async function main(argv) {
|
|
190
|
+
let opts;
|
|
191
|
+
try {
|
|
192
|
+
opts = parseArgs(argv);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(err.message + '\n');
|
|
195
|
+
console.error(USAGE);
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
if (opts.help) {
|
|
199
|
+
console.log(USAGE);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const cwd = resolve(opts.cwd);
|
|
204
|
+
ENV = loadEnv(cwd); // pick up .env.local / .env (shadcn does the same)
|
|
205
|
+
const color = makeColor(process.stdout.isTTY && !opts.noColor && !process.env.NO_COLOR);
|
|
206
|
+
|
|
207
|
+
// components.json (optional when --registry + --dir are both given)
|
|
208
|
+
let componentsJson = null;
|
|
209
|
+
const cjPath = join(cwd, 'components.json');
|
|
210
|
+
if (existsSync(cjPath)) {
|
|
211
|
+
try {
|
|
212
|
+
componentsJson = readJson(cjPath);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error(`✗ could not parse ${cjPath}: ${err.message}`);
|
|
215
|
+
return 2;
|
|
216
|
+
}
|
|
217
|
+
} else if (!opts.registry || !opts.dir) {
|
|
218
|
+
console.error(`✗ no components.json found at ${cjPath} (need it for the registry config + components dir, or pass --registry and --dir)`);
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { urlTemplate, headers } = resolveRegistry(opts, componentsJson);
|
|
223
|
+
const idxUrl = indexUrl(urlTemplate);
|
|
224
|
+
const dir = resolveComponentsDir(cwd, opts.dir, componentsJson);
|
|
225
|
+
|
|
226
|
+
// scan for installed VegaStack components
|
|
227
|
+
let installed = walk(dir).map(readHeader).filter(Boolean);
|
|
228
|
+
if (opts.filter) {
|
|
229
|
+
const res = opts.filter.split(',').map((s) => globToRe(s.trim()));
|
|
230
|
+
installed = installed.filter((c) => res.some((re) => re.test(c.name)));
|
|
231
|
+
}
|
|
232
|
+
if (installed.length === 0) {
|
|
233
|
+
if (opts.json) console.log(JSON.stringify({ registry: idxUrl, checked: 0, updates: 0, items: [] }, null, 2));
|
|
234
|
+
else console.log(`No VegaStack components found in ${dir}. (Add some with \`shadcn add @vegastack/<name>\`.)`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// fetch the registry index once
|
|
239
|
+
let index;
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(idxUrl, { headers });
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
console.error(`✗ registry index fetch failed: HTTP ${res.status} ${res.statusText} (${idxUrl})`);
|
|
244
|
+
if (missingEnv.size) console.error(` hint: these auth env vars are unset: ${[...missingEnv].join(', ')}`);
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
index = await res.json();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`✗ could not reach the registry at ${idxUrl}: ${err.message}`);
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
const remote = new Map();
|
|
253
|
+
for (const item of index.items ?? []) remote.set(item.name, { version: item.meta?.version, integrity: item.meta?.integrity });
|
|
254
|
+
|
|
255
|
+
// compare by hash
|
|
256
|
+
const rows = installed
|
|
257
|
+
.map((c) => {
|
|
258
|
+
const r = remote.get(c.name);
|
|
259
|
+
let status;
|
|
260
|
+
if (!r) status = 'missing';
|
|
261
|
+
else if (r.integrity && c.hash === r.integrity) status = 'current';
|
|
262
|
+
else status = 'update';
|
|
263
|
+
return { name: c.name, current: c.version, latest: r?.version ?? null, status };
|
|
264
|
+
})
|
|
265
|
+
.sort((a, b) => {
|
|
266
|
+
const rank = { update: 0, current: 1, missing: 2 };
|
|
267
|
+
return rank[a.status] - rank[b.status] || a.name.localeCompare(b.name);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const updates = rows.filter((r) => r.status === 'update');
|
|
271
|
+
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify({ registry: idxUrl, checked: rows.length, updates: updates.length, items: rows }, null, 2));
|
|
274
|
+
return opts.failOnUpdate && updates.length ? 1 : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// human table
|
|
278
|
+
const host = (() => {
|
|
279
|
+
try {
|
|
280
|
+
return new URL(idxUrl).host + new URL(idxUrl).pathname.replace(/\/registry\.json$/, '');
|
|
281
|
+
} catch {
|
|
282
|
+
return idxUrl;
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
console.log(`\nChecking ${rows.length} VegaStack component(s) against ${host} …\n`);
|
|
286
|
+
const nameW = Math.max(...rows.map((r) => r.name.length), 4);
|
|
287
|
+
const GLYPH = { update: color.yellow('⬆'), current: color.green('✓'), missing: color.dim('?') };
|
|
288
|
+
for (const r of rows) {
|
|
289
|
+
const ver = r.status === 'update' ? `${r.current} → ${r.latest ?? '?'}` : r.current;
|
|
290
|
+
const note =
|
|
291
|
+
r.status === 'update' ? color.yellow('update available') :
|
|
292
|
+
r.status === 'current' ? color.dim('up to date') :
|
|
293
|
+
color.dim('not in registry (renamed/removed)');
|
|
294
|
+
console.log(` ${GLYPH[r.status]} ${r.name.padEnd(nameW)} ${ver.padEnd(16)} ${note}`);
|
|
295
|
+
}
|
|
296
|
+
console.log('');
|
|
297
|
+
if (updates.length) {
|
|
298
|
+
const first = updates[0].name;
|
|
299
|
+
console.log(color.bold(`${updates.length} update(s) available.`) + ' Review & apply (repeat per component):');
|
|
300
|
+
console.log(` npx shadcn@latest add @vegastack/${first} --diff`);
|
|
301
|
+
console.log(` npx shadcn@latest add @vegastack/${first} --overwrite`);
|
|
302
|
+
console.log(color.dim('\nNote: --overwrite replaces files; if you customized a component, git diff first.'));
|
|
303
|
+
} else {
|
|
304
|
+
console.log(color.green('Everything is up to date.'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return opts.failOnUpdate && updates.length ? 1 : 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// standalone execution (so `node check-updates.mjs` works; the dispatcher imports main() instead)
|
|
311
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href || basename(process.argv[1] ?? '') === 'check-updates.mjs') {
|
|
312
|
+
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
313
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// vegastack-design — the VegaStack design-system CLI.
|
|
3
|
+
//
|
|
4
|
+
// Subcommands:
|
|
5
|
+
// check-updates Show which copied-in components have newer registry versions (what to re-pull).
|
|
6
|
+
// verify Verify a registry item's integrity before/after `shadcn add` (Sigstore + hash).
|
|
7
|
+
//
|
|
8
|
+
// The bin is named `vegastack-design` (NOT `vegastack`) so it never collides with a platform CLI.
|
|
9
|
+
// `check-updates` is imported in-process; `verify` is spawned (it's the standalone, hash-parity-tested
|
|
10
|
+
// verifier — we run it untouched). Exit codes are forwarded from the subcommand.
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const HERE = new URL('.', import.meta.url);
|
|
16
|
+
|
|
17
|
+
const USAGE = `vegastack-design — VegaStack design-system CLI
|
|
18
|
+
|
|
19
|
+
Usage: vegastack-design <command> [options]
|
|
20
|
+
|
|
21
|
+
Commands:
|
|
22
|
+
check-updates Show which copied-in components have newer registry versions
|
|
23
|
+
verify Verify a registry item's integrity (pre/post \`shadcn add\`)
|
|
24
|
+
|
|
25
|
+
Run \`vegastack-design <command> --help\` for command options.
|
|
26
|
+
-v, --version Print version
|
|
27
|
+
-h, --help Show this help`;
|
|
28
|
+
|
|
29
|
+
function version() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
32
|
+
} catch {
|
|
33
|
+
return '0.0.0';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
38
|
+
|
|
39
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
40
|
+
console.log(version());
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
if (cmd == null || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
44
|
+
console.log(USAGE);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (cmd === 'check-updates') {
|
|
49
|
+
// imported in-process (it's our own code with an exported main())
|
|
50
|
+
const { main } = await import(new URL('./check-updates.mjs', HERE).href);
|
|
51
|
+
process.exit(await main(rest));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (cmd === 'verify') {
|
|
55
|
+
// spawn the standalone verifier untouched; mark the dispatch so it skips its deprecation notice.
|
|
56
|
+
const verifier = fileURLToPath(new URL('./verify-registry-item.mjs', HERE));
|
|
57
|
+
const r = spawnSync(process.execPath, [verifier, ...rest], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
env: { ...process.env, VEGASTACK_DESIGN_DISPATCH: '1' },
|
|
60
|
+
});
|
|
61
|
+
process.exit(r.status ?? 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(`unknown command: ${cmd}\n`);
|
|
65
|
+
console.error(USAGE);
|
|
66
|
+
process.exit(2);
|