@nexusbloom/cli 0.3.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -34
- package/TESTING.md +125 -0
- package/package.json +9 -2
- package/src/abbr.js +27 -20
- package/src/commands.js +253 -0
- package/src/compile.js +69 -0
- package/src/diagnostics.js +41 -0
- package/src/index.js +353 -281
- package/src/jsonish.js +213 -0
- package/src/list.js +29 -23
- package/src/paths.js +42 -16
- package/src/pipe.js +709 -0
- package/src/pipelines.js +76 -0
- package/src/run.js +238 -104
- package/src/runner.js +89 -0
- package/src/sandbox.js +167 -0
- package/src/tools.js +177 -26
- package/src/types.js +178 -0
- package/src/ui.js +20 -0
- package/src/values.js +260 -0
- package/src/wizard.js +349 -0
package/src/index.js
CHANGED
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
* nxb run <slug> --file input.json — Execute with file input
|
|
13
13
|
* nxb run <slug> --local — Execute coreLogic locally (cached)
|
|
14
14
|
* nxb run — Interactive: select tool → pick inputs → auto-build command
|
|
15
|
+
* nxb run -s <query> — Filter the tool picker (non-TTY: print the list)
|
|
15
16
|
* nxb info <slug> — Show tool schema
|
|
16
|
-
* nxb abbr
|
|
17
|
+
* nxb abbr [-s <query>] — Show tool abbreviations (paginated)
|
|
17
18
|
* nxb transform <slug> — Pipe stdin through a tool
|
|
18
19
|
* nxb config set <key> [value] — Set config (interactive if value omitted)
|
|
19
|
-
* nxb config get [key] — Get config
|
|
20
|
+
* nxb config get [key] — Get config (secrets redacted)
|
|
20
21
|
* nxb cache clear — Clear local tool cache
|
|
21
22
|
* nxb cache list — List cached tools
|
|
22
|
-
* nxb
|
|
23
|
+
* nxb cache path — Print where config/cache live
|
|
23
24
|
* nxb json --template <slug> — Create JSON input for tools
|
|
24
25
|
*/
|
|
25
26
|
|
|
@@ -34,16 +35,58 @@ const require = createRequire(import.meta.url);
|
|
|
34
35
|
const { version } = require("../package.json");
|
|
35
36
|
|
|
36
37
|
// ─── Local modules ──────────────────────────────────────────────────────────
|
|
37
|
-
import { API_BASE } from "./tools.js";
|
|
38
38
|
import {
|
|
39
|
-
loadConfig,
|
|
39
|
+
loadConfig, configGet, configSet, configUnset, redactedConfig, isSecretKey,
|
|
40
40
|
fetchTools, fetchToolSource, resolveToolSlug,
|
|
41
|
-
generateAbbreviations, readStdin,
|
|
41
|
+
generateAbbreviations, normalizeSlug, readStdin,
|
|
42
42
|
} from "./tools.js";
|
|
43
|
-
import { runLocal, runRemote, buildInputBody, runInteractive } from "./run.js";
|
|
43
|
+
import { runLocal, runRemote, buildInputBody, runInteractive, runInteractiveFor, isExitPromptError, reportError } from "./run.js";
|
|
44
44
|
import listAction from "./list.js";
|
|
45
|
+
import { pipeAction, listTransforms, doctorAction } from "./commands.js";
|
|
46
|
+
import { buildWizard } from "./wizard.js";
|
|
47
|
+
import { listPipelines, loadPipeline, deletePipeline } from "./pipelines.js";
|
|
45
48
|
import abbrAction from "./abbr.js";
|
|
46
|
-
import {
|
|
49
|
+
import { isInteractive } from "./ui.js";
|
|
50
|
+
import {
|
|
51
|
+
CACHE_DIR,
|
|
52
|
+
CONFIG_PATH,
|
|
53
|
+
LEGACY_CACHE_DIR,
|
|
54
|
+
TOOLS_CACHE_DIR,
|
|
55
|
+
TOOLS_LIST_CACHE_PATH,
|
|
56
|
+
} from "./paths.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Was `--json` requested?
|
|
60
|
+
*
|
|
61
|
+
* A subcommand's immediate parent is not always the program: for
|
|
62
|
+
* `nxb pipeline list` it is the `pipeline` command, which has no `json` option
|
|
63
|
+
* of its own. Walking to the root is what makes the global flag mean the same
|
|
64
|
+
* thing at every depth.
|
|
65
|
+
*/
|
|
66
|
+
function jsonFlag(cmd) {
|
|
67
|
+
let c = cmd;
|
|
68
|
+
while (c) {
|
|
69
|
+
if (typeof c.opts === "function") {
|
|
70
|
+
const v = c.opts().json;
|
|
71
|
+
if (v !== undefined) return v;
|
|
72
|
+
}
|
|
73
|
+
c = c.parent;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Guard: slugs become cache filenames and query strings, so reject oddities. */
|
|
79
|
+
function requireSlug(slug, jsonMode) {
|
|
80
|
+
const safe = normalizeSlug(slug);
|
|
81
|
+
if (safe) return safe;
|
|
82
|
+
const msg = `Invalid tool slug: "${slug}". Use letters, digits, dots, dashes and underscores.`;
|
|
83
|
+
// Same envelope as every other failure, so a script keying off `success`
|
|
84
|
+
// does not see `undefined` here and nowhere else.
|
|
85
|
+
if (jsonMode) console.log(JSON.stringify({ success: false, error: msg }));
|
|
86
|
+
else console.error(chalk.red(msg));
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
47
90
|
|
|
48
91
|
// ─── Program ─────────────────────────────────────────────────────────────────
|
|
49
92
|
program
|
|
@@ -58,12 +101,16 @@ program
|
|
|
58
101
|
.description("List available tools (interactive pagination)")
|
|
59
102
|
.option("-s, --search <query>", "Search tools by name, slug, category, or tags")
|
|
60
103
|
.option("--no-interactive", "Disable interactive navigation")
|
|
104
|
+
.option("-r, --refresh", "Bypass the local cache and re-fetch the catalogue")
|
|
105
|
+
.option("--offline", "Use only the local cache (no network)")
|
|
61
106
|
.action(async (opts, cmd) => {
|
|
62
|
-
const jsonMode = cmd
|
|
107
|
+
const jsonMode = jsonFlag(cmd);
|
|
63
108
|
await listAction({
|
|
64
109
|
search: opts.search,
|
|
65
110
|
json: jsonMode,
|
|
66
111
|
interactive: opts.interactive !== false,
|
|
112
|
+
force: Boolean(opts.refresh),
|
|
113
|
+
offline: Boolean(opts.offline),
|
|
67
114
|
});
|
|
68
115
|
});
|
|
69
116
|
|
|
@@ -72,35 +119,14 @@ program
|
|
|
72
119
|
.command("search")
|
|
73
120
|
.description("Search for tools")
|
|
74
121
|
.argument("<query>", "Search query")
|
|
122
|
+
.option("--no-interactive", "Disable interactive navigation")
|
|
75
123
|
.action(async (query, opts, cmd) => {
|
|
76
124
|
const jsonMode = cmd?.parent?.opts().json || false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const q = query.toLowerCase();
|
|
84
|
-
const filtered = tools.filter(
|
|
85
|
-
(t) =>
|
|
86
|
-
(t.slug || "").includes(q) ||
|
|
87
|
-
(t.name || "").toLowerCase().includes(q) ||
|
|
88
|
-
(t.short_description || t.description || "").toLowerCase().includes(q) ||
|
|
89
|
-
(t.category || "").includes(q) ||
|
|
90
|
-
(t.tags || []).some((tg) => tg.toLowerCase().includes(q))
|
|
91
|
-
);
|
|
92
|
-
if (filtered.length === 0) {
|
|
93
|
-
if (jsonMode) return console.log(JSON.stringify({ tools: [], query }));
|
|
94
|
-
console.log(chalk.yellow(`No tools matching "${query}".`));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (jsonMode) return console.log(JSON.stringify({ tools: filtered, query }, null, 2));
|
|
98
|
-
|
|
99
|
-
console.log(chalk.bold(`\n ${filtered.length} result${filtered.length > 1 ? "s" : ""} for "${query}"\n`));
|
|
100
|
-
filtered.forEach((t) => {
|
|
101
|
-
console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.short_description || t.description || "").slice(0, 60))}`);
|
|
125
|
+
await listAction({
|
|
126
|
+
search: query,
|
|
127
|
+
json: jsonMode,
|
|
128
|
+
interactive: opts.interactive !== false,
|
|
102
129
|
});
|
|
103
|
-
console.log();
|
|
104
130
|
});
|
|
105
131
|
|
|
106
132
|
// ─── `nxb run [slug] [args...]` ──────────────────────────────────────────────
|
|
@@ -109,32 +135,53 @@ program
|
|
|
109
135
|
.description("Execute a tool (interactive if no slug is given)")
|
|
110
136
|
.argument("[slug]", "Tool slug or abbreviation")
|
|
111
137
|
.argument("[args...]", "Key=value arguments (e.g. domain=example.com)")
|
|
138
|
+
.option("-s, --search <query>", "Filter tools by name, slug, category, or tags")
|
|
112
139
|
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
113
140
|
.option("-i, --input <json>", "Inline JSON input or @- for stdin (e.g. '{\"domain\":\"example.com\"}' or @-)")
|
|
114
141
|
.option("-f, --file <path>", "Read input from a JSON file")
|
|
115
142
|
.option("--local", "Execute coreLogic locally (fetches tool source, caches it)")
|
|
116
143
|
.action(async (slug, args, opts, cmd) => {
|
|
117
|
-
const jsonMode = cmd
|
|
144
|
+
const jsonMode = jsonFlag(cmd);
|
|
118
145
|
|
|
119
|
-
// ── Interactive mode: no slug provided ──
|
|
146
|
+
// ── Interactive / list mode: no slug provided ──
|
|
120
147
|
if (!slug) {
|
|
121
|
-
return await runInteractive({ json: jsonMode });
|
|
148
|
+
return await runInteractive({ json: jsonMode, search: opts.search });
|
|
122
149
|
}
|
|
123
150
|
|
|
151
|
+
const safeSlug = requireSlug(slug, jsonMode);
|
|
152
|
+
if (!safeSlug) return;
|
|
153
|
+
|
|
124
154
|
// ── Resolve abbreviation ──
|
|
125
155
|
const allTools = await fetchTools();
|
|
126
156
|
const slugs = (allTools || []).map((t) => t.slug);
|
|
127
157
|
const abbrMap = generateAbbreviations(slugs);
|
|
128
|
-
let resolved =
|
|
129
|
-
if (abbrMap[
|
|
130
|
-
resolved = abbrMap[
|
|
131
|
-
|
|
158
|
+
let resolved = safeSlug;
|
|
159
|
+
if (abbrMap[safeSlug] && slugs.includes(abbrMap[safeSlug])) {
|
|
160
|
+
resolved = abbrMap[safeSlug];
|
|
161
|
+
// The result itself goes to stderr, so the notice does too — otherwise
|
|
162
|
+
// `nxb run <abbr> ... > out.json` writes prose into the JSON.
|
|
163
|
+
if (!jsonMode) console.error(chalk.dim(` Resolved: ${safeSlug} → ${resolved}`));
|
|
132
164
|
}
|
|
133
165
|
|
|
134
166
|
// ── Build input body ──
|
|
135
167
|
const body = await buildInputBody(opts, args);
|
|
136
168
|
if (body === null) return;
|
|
137
169
|
|
|
170
|
+
// ── No input at all: configure the tool interactively ──
|
|
171
|
+
// `nxb run text-statistics` with nothing to send used to fail schema
|
|
172
|
+
// validation ("Missing required fields: text"), which is a dead end for a
|
|
173
|
+
// human at a terminal. Ask for the fields instead. Only when the body is
|
|
174
|
+
// genuinely empty: any explicit input (--input/--file, key=value, piped
|
|
175
|
+
// stdin) means the caller is scripting, and a prompt there would hang CI.
|
|
176
|
+
if (Object.keys(body).length === 0 && isInteractive()) {
|
|
177
|
+
return await runInteractiveFor(resolved, {
|
|
178
|
+
json: jsonMode,
|
|
179
|
+
tools: allTools,
|
|
180
|
+
local: Boolean(opts.local),
|
|
181
|
+
key: opts.key,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
138
185
|
// ── Local vs remote ──
|
|
139
186
|
if (opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"))) {
|
|
140
187
|
return await runLocal(resolved, body, { jsonMode, slugResolved: true });
|
|
@@ -154,15 +201,15 @@ program
|
|
|
154
201
|
.argument("<slug>", "Tool slug or abbreviation")
|
|
155
202
|
.action(async (slug, opts, cmd) => {
|
|
156
203
|
const jsonMode = cmd?.parent?.opts().json || false;
|
|
157
|
-
const
|
|
204
|
+
const safe = requireSlug(slug, jsonMode);
|
|
205
|
+
if (!safe) return;
|
|
158
206
|
|
|
207
|
+
const resolved = await resolveToolSlug(safe);
|
|
159
208
|
const toolInfo = await fetchToolSource(resolved);
|
|
160
209
|
const manifest = toolInfo?.manifest;
|
|
161
210
|
|
|
162
211
|
if (!manifest) {
|
|
163
|
-
|
|
164
|
-
console.error(chalk.red(`Tool "${resolved}" not found.`));
|
|
165
|
-
return;
|
|
212
|
+
return reportError(jsonMode, `Tool "${resolved}" not found. Try \`nxb search ${safe}\`.`);
|
|
166
213
|
}
|
|
167
214
|
|
|
168
215
|
if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
|
|
@@ -173,20 +220,20 @@ program
|
|
|
173
220
|
// ─── `nxb transform <slug>` ──────────────────────────────────────────────────
|
|
174
221
|
program
|
|
175
222
|
.command("transform")
|
|
176
|
-
.description("Pipe stdin through a tool and write result to stdout")
|
|
223
|
+
.description("Pipe stdin through a tool and write the result to stdout")
|
|
177
224
|
.argument("<slug>", "Tool slug or abbreviation")
|
|
178
225
|
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
179
226
|
.option("--local", "Execute locally (faster for repeated use)")
|
|
180
227
|
.action(async (slug, opts, cmd) => {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
228
|
+
const jsonMode = jsonFlag(cmd);
|
|
229
|
+
const safe = requireSlug(slug, jsonMode);
|
|
230
|
+
if (!safe) return;
|
|
231
|
+
|
|
232
|
+
const resolved = await resolveToolSlug(safe);
|
|
183
233
|
|
|
184
234
|
const input = await readStdin();
|
|
185
|
-
if (!input) {
|
|
186
|
-
|
|
187
|
-
console.error(chalk.red("No input received from stdin. Pipe data into this command."));
|
|
188
|
-
process.exitCode = 1;
|
|
189
|
-
return;
|
|
235
|
+
if (!input || !input.trim()) {
|
|
236
|
+
return reportError(jsonMode, "No input received from stdin. Pipe data into this command.");
|
|
190
237
|
}
|
|
191
238
|
|
|
192
239
|
let body;
|
|
@@ -196,19 +243,123 @@ program
|
|
|
196
243
|
body = { input };
|
|
197
244
|
}
|
|
198
245
|
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
// `transform` exists to be piped, so stdout carries the payload and every
|
|
247
|
+
// decoration goes to stderr.
|
|
248
|
+
const useLocal =
|
|
249
|
+
opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"));
|
|
250
|
+
|
|
251
|
+
const res = useLocal
|
|
252
|
+
? await runLocal(resolved, body, { jsonMode: false, slugResolved: true, quiet: true })
|
|
253
|
+
: await runRemote(resolved, body, {
|
|
254
|
+
apiKey: opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key"),
|
|
255
|
+
jsonMode: false,
|
|
256
|
+
slugResolved: true,
|
|
257
|
+
quiet: true,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!res?.ok) {
|
|
261
|
+
return reportError(jsonMode, res?.error || "Transform failed.");
|
|
201
262
|
}
|
|
202
263
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
264
|
+
// A string result is emitted verbatim so it can feed another process;
|
|
265
|
+
// anything else is JSON so it can feed another `nxb transform`.
|
|
266
|
+
console.log(typeof res.result === "string" ? res.result : JSON.stringify(res.result, null, 2));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ─── `nxb pipe` ──────────────────────────────────────────────────────────────
|
|
270
|
+
program
|
|
271
|
+
.command("pipe")
|
|
272
|
+
.description("Run a pipeline: connect one tool's output into another's input")
|
|
273
|
+
.argument("[spec]", 'Pipeline string, e.g. "tool-a x=@in | tool-b count=$n|len"')
|
|
274
|
+
.option("--dry-run", "Check the wiring and type compatibility without running")
|
|
275
|
+
.option("--remote", "Run every step against the API instead of locally")
|
|
276
|
+
.option("--transforms", "List the available connection transforms")
|
|
277
|
+
.option("--build", "Build a pipeline interactively")
|
|
278
|
+
.action(async (spec, opts, cmd) => {
|
|
279
|
+
const jsonMode = jsonFlag(cmd);
|
|
280
|
+
if (opts.transforms) return listTransforms(jsonMode);
|
|
281
|
+
if (opts.build || (!spec && isInteractive())) {
|
|
282
|
+
// Building nothing is a failure, not a quiet success — scripts need to
|
|
283
|
+
// be able to detect that no pipeline came out.
|
|
284
|
+
const built = await buildWizard();
|
|
285
|
+
if (!built?.ok) process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (!spec) {
|
|
289
|
+
console.error(chalk.red(" Give a pipeline, or run `nxb pipe --build`."));
|
|
207
290
|
process.exitCode = 1;
|
|
208
291
|
return;
|
|
209
292
|
}
|
|
293
|
+
await pipeAction(spec, { json: jsonMode, dryRun: opts.dryRun, remote: opts.remote });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ─── `nxb pipeline <list|run|delete>` ────────────────────────────────────────
|
|
297
|
+
const savedCmd = program
|
|
298
|
+
.command("pipeline")
|
|
299
|
+
.description("Manage saved pipelines");
|
|
300
|
+
|
|
301
|
+
savedCmd
|
|
302
|
+
.command("list")
|
|
303
|
+
.description("List saved pipelines")
|
|
304
|
+
.action((opts, cmd) => {
|
|
305
|
+
const jsonMode = jsonFlag(cmd);
|
|
306
|
+
const items = listPipelines();
|
|
307
|
+
if (jsonMode) return console.log(JSON.stringify({ pipelines: items }, null, 2));
|
|
308
|
+
if (!items.length) {
|
|
309
|
+
console.log(chalk.yellow(" No saved pipelines. Create one with `nxb pipe --build`."));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.log(chalk.bold(`\n ${items.length} saved pipeline${items.length === 1 ? "" : "s"}\n`));
|
|
313
|
+
for (const it of items) {
|
|
314
|
+
console.log(` ${chalk.cyan(it.name.padEnd(20))} ${chalk.dim(`${it.steps} step${it.steps === 1 ? "" : "s"}`)}`);
|
|
315
|
+
if (it.pipeline) console.log(` ${chalk.gray(it.pipeline.slice(0, 92))}`);
|
|
316
|
+
}
|
|
317
|
+
console.log();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
savedCmd
|
|
321
|
+
.command("run")
|
|
322
|
+
.description("Run a saved pipeline")
|
|
323
|
+
.argument("<name>", "Pipeline name")
|
|
324
|
+
.option("--dry-run", "Check the wiring without running")
|
|
325
|
+
.option("--remote", "Run every step against the API")
|
|
326
|
+
.action(async (name, opts, cmd) => {
|
|
327
|
+
const jsonMode = jsonFlag(cmd);
|
|
328
|
+
const def = loadPipeline(name);
|
|
329
|
+
if (!def?.pipeline) {
|
|
330
|
+
return reportError(jsonMode, `No saved pipeline named "${name}". Try \`nxb pipeline list\`.`);
|
|
331
|
+
}
|
|
332
|
+
await pipeAction(def.pipeline, { json: jsonMode, dryRun: opts.dryRun, remote: opts.remote });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
savedCmd
|
|
336
|
+
.command("delete")
|
|
337
|
+
.description("Delete a saved pipeline")
|
|
338
|
+
.argument("<name>", "Pipeline name")
|
|
339
|
+
.action((name, _opts, cmd) => {
|
|
340
|
+
const jsonMode = jsonFlag(cmd);
|
|
341
|
+
try {
|
|
342
|
+
if (!deletePipeline(name)) return reportError(jsonMode, `No saved pipeline named "${name}".`);
|
|
343
|
+
console.log(chalk.green(` Deleted "${name}".`));
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return reportError(jsonMode, err.message);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
210
348
|
|
|
211
|
-
|
|
349
|
+
// ─── `nxb doctor <slug>` ─────────────────────────────────────────────────────
|
|
350
|
+
program
|
|
351
|
+
.command("doctor")
|
|
352
|
+
.description("Compare a tool's real output against its declared output_schema")
|
|
353
|
+
.argument("<slug>", "Tool slug or abbreviation")
|
|
354
|
+
.action(async (slug, opts, cmd) => {
|
|
355
|
+
const jsonMode = jsonFlag(cmd);
|
|
356
|
+
const safe = requireSlug(slug, jsonMode);
|
|
357
|
+
if (!safe) return;
|
|
358
|
+
const resolved = await resolveToolSlug(safe);
|
|
359
|
+
// `nxb doctor x || exit 1` is the whole point of a drift detector; a
|
|
360
|
+
// finding that exits 0 makes the gate silently useless.
|
|
361
|
+
const verdict = await doctorAction(resolved, { json: jsonMode });
|
|
362
|
+
if (verdict && verdict.ok === false) process.exitCode = 1;
|
|
212
363
|
});
|
|
213
364
|
|
|
214
365
|
// ─── `nxb abbr [query]` ──────────────────────────────────────────────────────
|
|
@@ -216,11 +367,13 @@ program
|
|
|
216
367
|
.command("abbr")
|
|
217
368
|
.description("Show tool abbreviations (paginated)")
|
|
218
369
|
.argument("[query]", "Search/filter abbreviations")
|
|
370
|
+
.option("-s, --search <query>", "Search/filter abbreviations")
|
|
219
371
|
.option("--no-interactive", "Disable interactive navigation")
|
|
220
372
|
.action(async (query, opts, cmd) => {
|
|
221
|
-
const jsonMode = cmd
|
|
373
|
+
const jsonMode = jsonFlag(cmd);
|
|
222
374
|
await abbrAction({
|
|
223
375
|
query,
|
|
376
|
+
search: opts.search,
|
|
224
377
|
json: jsonMode,
|
|
225
378
|
interactive: opts.interactive !== false,
|
|
226
379
|
});
|
|
@@ -234,7 +387,7 @@ const configCmd = program
|
|
|
234
387
|
configCmd
|
|
235
388
|
.command("set")
|
|
236
389
|
.description("Set a config value (prompts for value if omitted)")
|
|
237
|
-
.argument("<key>", "Config key (key,
|
|
390
|
+
.argument("<key>", "Config key (key, supabaseKey, telemetry, toolsDir)")
|
|
238
391
|
.argument("[value]", "Config value (prompted if not provided)")
|
|
239
392
|
.action(async (key, value) => {
|
|
240
393
|
if (!value) {
|
|
@@ -245,12 +398,18 @@ configCmd
|
|
|
245
398
|
return;
|
|
246
399
|
}
|
|
247
400
|
configSet(key, value);
|
|
248
|
-
|
|
401
|
+
// Never echo a secret back to the terminal / shell history.
|
|
402
|
+
if (isSecretKey(key)) {
|
|
403
|
+
console.log(chalk.green(` Set ${key} (stored, value hidden)`));
|
|
404
|
+
console.log(chalk.dim(` ${CONFIG_PATH}`));
|
|
405
|
+
} else {
|
|
406
|
+
console.log(chalk.green(` Set ${key} to "${value}"`));
|
|
407
|
+
}
|
|
249
408
|
});
|
|
250
409
|
|
|
251
410
|
configCmd
|
|
252
411
|
.command("get")
|
|
253
|
-
.description("Get a config value")
|
|
412
|
+
.description("Get a config value (secrets redacted)")
|
|
254
413
|
.argument("[key]", "Config key (omit to show all)")
|
|
255
414
|
.action((key) => {
|
|
256
415
|
const store = loadConfig();
|
|
@@ -259,256 +418,147 @@ configCmd
|
|
|
259
418
|
if (value === undefined) {
|
|
260
419
|
console.log(chalk.yellow(` ${key} is not set`));
|
|
261
420
|
} else {
|
|
262
|
-
|
|
421
|
+
const shown = isSecretKey(key) ? maskSecret(value) : value;
|
|
422
|
+
console.log(` ${key}: ${shown}`);
|
|
263
423
|
}
|
|
264
424
|
} else {
|
|
265
|
-
|
|
266
|
-
console.log(chalk.yellow(" No config values set."));
|
|
267
|
-
} else {
|
|
268
|
-
Object.entries(store).forEach(([k, v]) => {
|
|
269
|
-
console.log(` ${k}: ${v}`);
|
|
270
|
-
});
|
|
271
|
-
}
|
|
425
|
+
printConfig(store);
|
|
272
426
|
}
|
|
273
427
|
});
|
|
274
428
|
|
|
275
429
|
configCmd
|
|
276
|
-
.command("
|
|
277
|
-
.description("
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
430
|
+
.command("unset")
|
|
431
|
+
.description("Remove a config value (e.g. a stored API key)")
|
|
432
|
+
.argument("<key>", "Config key to remove")
|
|
433
|
+
.action((key, opts, cmd) => {
|
|
434
|
+
const jsonMode = jsonFlag(cmd);
|
|
435
|
+
if (!configUnset(key)) {
|
|
436
|
+
return reportError(jsonMode, `"${key}" is not set.`);
|
|
437
|
+
}
|
|
438
|
+
if (jsonMode) {
|
|
439
|
+
console.log(JSON.stringify({ success: true, data: { unset: key } }, null, 2));
|
|
282
440
|
} else {
|
|
283
|
-
|
|
284
|
-
console.log(` ${k}: ${v}`);
|
|
285
|
-
});
|
|
441
|
+
console.log(chalk.green(` Removed ${key}`));
|
|
286
442
|
}
|
|
287
443
|
});
|
|
288
444
|
|
|
445
|
+
configCmd
|
|
446
|
+
.command("list")
|
|
447
|
+
.description("List all config values (secrets redacted)")
|
|
448
|
+
.action(() => printConfig(loadConfig()));
|
|
449
|
+
|
|
450
|
+
function maskSecret(value) {
|
|
451
|
+
const s = value == null ? "" : String(value);
|
|
452
|
+
return s.length <= 8 ? "********" : `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function printConfig(store) {
|
|
456
|
+
if (!store || Object.keys(store).length === 0) {
|
|
457
|
+
console.log(chalk.yellow(" No config values set."));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const safe = redactedConfig(store);
|
|
461
|
+
for (const [k, v] of Object.entries(safe)) {
|
|
462
|
+
console.log(` ${k}: ${v}`);
|
|
463
|
+
}
|
|
464
|
+
console.log(chalk.dim(`\n Stored at ${CONFIG_PATH}`));
|
|
465
|
+
}
|
|
466
|
+
|
|
289
467
|
// ─── `nxb cache` ─────────────────────────────────────────────────────────────
|
|
290
468
|
const cacheCmd = program
|
|
291
469
|
.command("cache")
|
|
292
470
|
.description("Manage local tool cache");
|
|
293
471
|
|
|
472
|
+
/** Delete everything inside a cache dir (recursively). Returns entries removed. */
|
|
473
|
+
function clearCacheDir(dir) {
|
|
474
|
+
if (!fs.existsSync(dir)) return 0;
|
|
475
|
+
let count = 0;
|
|
476
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
477
|
+
const target = path.join(dir, entry.name);
|
|
478
|
+
try {
|
|
479
|
+
if (entry.isDirectory()) fs.rmSync(target, { recursive: true, force: true });
|
|
480
|
+
else fs.unlinkSync(target);
|
|
481
|
+
count++;
|
|
482
|
+
} catch {
|
|
483
|
+
/* skip anything we cannot remove */
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return count;
|
|
487
|
+
}
|
|
488
|
+
|
|
294
489
|
cacheCmd
|
|
295
490
|
.command("clear")
|
|
296
491
|
.description("Clear the local tool cache")
|
|
297
492
|
.action(() => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
493
|
+
const count = clearCacheDir(TOOLS_CACHE_DIR);
|
|
494
|
+
let removedList = 0;
|
|
495
|
+
if (fs.existsSync(TOOLS_LIST_CACHE_PATH)) {
|
|
496
|
+
try {
|
|
497
|
+
fs.unlinkSync(TOOLS_LIST_CACHE_PATH);
|
|
498
|
+
removedList = 1;
|
|
499
|
+
} catch {
|
|
500
|
+
/* best effort */
|
|
501
|
+
}
|
|
301
502
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
503
|
+
// Older versions cached inside the installed package directory.
|
|
504
|
+
const legacy = clearCacheDir(LEGACY_CACHE_DIR);
|
|
505
|
+
if (legacy) {
|
|
506
|
+
try {
|
|
507
|
+
fs.rmdirSync(LEGACY_CACHE_DIR);
|
|
508
|
+
} catch {
|
|
509
|
+
/* not empty / not ours — leave it */
|
|
510
|
+
}
|
|
307
511
|
}
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
512
|
+
const total = count + removedList;
|
|
513
|
+
if (total === 0 && legacy === 0) {
|
|
514
|
+
console.log(chalk.yellow(" Nothing cached."));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
console.log(chalk.green(` Cleared ${total} cached item${total !== 1 ? "s" : ""}.`));
|
|
518
|
+
if (legacy) {
|
|
519
|
+
console.log(chalk.dim(` Removed ${legacy} legacy cache entr${legacy === 1 ? "y" : "ies"} from the old package directory.`));
|
|
311
520
|
}
|
|
312
|
-
console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
|
|
313
521
|
});
|
|
314
522
|
|
|
315
523
|
cacheCmd
|
|
316
524
|
.command("list")
|
|
317
525
|
.description("List cached tools")
|
|
318
|
-
.
|
|
526
|
+
.option("-s, --search <query>", "Filter cached tools by slug")
|
|
527
|
+
.action((opts) => {
|
|
319
528
|
if (!fs.existsSync(TOOLS_CACHE_DIR)) {
|
|
320
529
|
console.log(chalk.yellow(" No cached tools."));
|
|
321
530
|
return;
|
|
322
531
|
}
|
|
323
|
-
const
|
|
532
|
+
const query = (opts.search || "").toLowerCase();
|
|
533
|
+
const files = fs
|
|
534
|
+
.readdirSync(TOOLS_CACHE_DIR)
|
|
535
|
+
.filter((f) => f.endsWith(".json"))
|
|
536
|
+
.map((f) => f.replace(/\.json$/, ""))
|
|
537
|
+
.filter((slug) => !query || slug.toLowerCase().includes(query));
|
|
538
|
+
|
|
324
539
|
if (files.length === 0) {
|
|
325
|
-
console.log(chalk.yellow(" No cached tools."));
|
|
540
|
+
console.log(chalk.yellow(query ? ` No cached tools matching "${opts.search}".` : " No cached tools."));
|
|
326
541
|
return;
|
|
327
542
|
}
|
|
328
|
-
console.log(chalk.bold(`\n ${files.length} cached tool${files.length
|
|
329
|
-
files.forEach((
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago`)}`);
|
|
543
|
+
console.log(chalk.bold(`\n ${files.length} cached tool${files.length !== 1 ? "s" : ""}:\n`));
|
|
544
|
+
files.sort().forEach((slug) => {
|
|
545
|
+
const stats = fs.statSync(path.join(TOOLS_CACHE_DIR, `${slug}.json`));
|
|
546
|
+
const age = Math.round((Date.now() - stats.mtimeMs) / 60000);
|
|
547
|
+
const size = (stats.size / 1024).toFixed(1);
|
|
548
|
+
console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago · ${size}kb`)}`);
|
|
334
549
|
});
|
|
335
550
|
console.log();
|
|
336
551
|
});
|
|
337
552
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
.description("Scaffold a new tool locally")
|
|
348
|
-
.option("--slug <slug>", "Tool slug (lowercase, hyphenated)")
|
|
349
|
-
.option("--name <name>", "Display name")
|
|
350
|
-
.option("--desc <desc>", "Short description")
|
|
351
|
-
.option("--category <category>", `Category (${VALID_CATEGORIES.join(", ")})`)
|
|
352
|
-
.option("--runtime <runtime>", `Runtime (${VALID_RUNTIMES.join(", ")})`)
|
|
353
|
-
.option("--dir <dir>", "Target directory (default: ./src/tools/<slug>)")
|
|
354
|
-
.action(async (options) => {
|
|
355
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
356
|
-
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
357
|
-
|
|
358
|
-
let slug = options.slug;
|
|
359
|
-
while (!slug || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
360
|
-
slug = (await ask(chalk.cyan("Tool slug (e.g. my-tool): "))).trim().toLowerCase();
|
|
361
|
-
if (!slug) { console.log(chalk.red("Slug is required.")); continue; }
|
|
362
|
-
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
363
|
-
console.log(chalk.red("Slug must be lowercase, hyphenated (e.g. my-tool)."));
|
|
364
|
-
slug = null;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
let name = options.name;
|
|
369
|
-
while (!name || name.length < 2) {
|
|
370
|
-
name = (await ask(chalk.cyan("Display name: "))).trim();
|
|
371
|
-
if (!name) console.log(chalk.red("Name is required."));
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
let desc = options.desc;
|
|
375
|
-
while (!desc) {
|
|
376
|
-
desc = (await ask(chalk.cyan("Short description: "))).trim();
|
|
377
|
-
if (!desc) console.log(chalk.red("Description is required."));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
let category = options.category;
|
|
381
|
-
while (!category || !VALID_CATEGORIES.includes(category)) {
|
|
382
|
-
const catList = VALID_CATEGORIES.join(", ");
|
|
383
|
-
category = (await ask(chalk.cyan("Category (" + catList + "): "))).trim().toLowerCase();
|
|
384
|
-
if (category === "other") category = "Other";
|
|
385
|
-
if (!VALID_CATEGORIES.includes(category)) {
|
|
386
|
-
console.log(chalk.red("Invalid category. Choose from: " + catList));
|
|
387
|
-
category = null;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
let runtime = options.runtime;
|
|
392
|
-
while (!runtime || !VALID_RUNTIMES.includes(runtime)) {
|
|
393
|
-
const rtList = VALID_RUNTIMES.join(", ");
|
|
394
|
-
runtime = (await ask(chalk.cyan("Runtime (" + rtList + "): "))).trim().toLowerCase();
|
|
395
|
-
if (!VALID_RUNTIMES.includes(runtime)) {
|
|
396
|
-
console.log(chalk.red("Invalid runtime. Choose from: " + rtList));
|
|
397
|
-
runtime = null;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
rl.close();
|
|
402
|
-
|
|
403
|
-
const pascalName = slug
|
|
404
|
-
.split("-")
|
|
405
|
-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
406
|
-
.join("");
|
|
407
|
-
|
|
408
|
-
const v2Source = `/**
|
|
409
|
-
* ${name} — ${desc}
|
|
410
|
-
* Auto-generated by nxb create
|
|
411
|
-
*/
|
|
412
|
-
|
|
413
|
-
export const MANIFEST = {
|
|
414
|
-
slug: "${slug}",
|
|
415
|
-
name: "${name}",
|
|
416
|
-
short_description: "${desc}",
|
|
417
|
-
icon: "🛠️",
|
|
418
|
-
category: "${category}",
|
|
419
|
-
tags: [],
|
|
420
|
-
runtime: "${runtime}",
|
|
421
|
-
pricing: "free",
|
|
422
|
-
version: "1.0.0",
|
|
423
|
-
timeout_ms: 5000,
|
|
424
|
-
input_schema: {
|
|
425
|
-
type: "object",
|
|
426
|
-
properties: {
|
|
427
|
-
input: {
|
|
428
|
-
type: "string",
|
|
429
|
-
description: "Input text to process",
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
required: ["input"],
|
|
433
|
-
},
|
|
434
|
-
output_schema: {
|
|
435
|
-
type: "object",
|
|
436
|
-
properties: {
|
|
437
|
-
result: {
|
|
438
|
-
type: "string",
|
|
439
|
-
description: "Processed output",
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
export async function coreLogic(input) {
|
|
446
|
-
// Replace with your tool's logic
|
|
447
|
-
return { result: \`Processed: \${input.input}\` };
|
|
448
|
-
}
|
|
449
|
-
`;
|
|
450
|
-
|
|
451
|
-
const indexSource = `import React from "react";
|
|
452
|
-
import { MANIFEST } from "./v2";
|
|
453
|
-
|
|
454
|
-
export default function ${pascalName}Tool({ onResult }) {
|
|
455
|
-
const [input, setInput] = React.useState("");
|
|
456
|
-
const [result, setResult] = React.useState(null);
|
|
457
|
-
|
|
458
|
-
const handleRun = async () => {
|
|
459
|
-
try {
|
|
460
|
-
const { coreLogic } = await import("./v2");
|
|
461
|
-
const output = await coreLogic({ input });
|
|
462
|
-
setResult(output);
|
|
463
|
-
if (onResult) onResult(output);
|
|
464
|
-
} catch (err) {
|
|
465
|
-
setResult({ error: err.message });
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<div className="nexusbloom-tool">
|
|
471
|
-
<h2 className="text-xl font-bold mb-4">{MANIFEST.name}</h2>
|
|
472
|
-
<p className="text-gray-600 dark:text-gray-400 mb-4">{MANIFEST.short_description}</p>
|
|
473
|
-
<textarea className="w-full p-2 border rounded" rows={4} value={input}
|
|
474
|
-
onChange={(e) => setInput(e.target.value)} placeholder="Enter input..." />
|
|
475
|
-
<button className="mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
476
|
-
onClick={handleRun}>Run</button>
|
|
477
|
-
{result && <pre className="mt-4 p-3 bg-gray-100 rounded overflow-auto">{JSON.stringify(result, null, 2)}</pre>}
|
|
478
|
-
</div>
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
`;
|
|
482
|
-
|
|
483
|
-
const targetDir = options.dir || path.join(process.cwd(), "src/tools", slug);
|
|
484
|
-
const v2Path = path.join(targetDir, "v2.js");
|
|
485
|
-
const indexPath = path.join(targetDir, "index.js");
|
|
486
|
-
|
|
487
|
-
if (fs.existsSync(v2Path) || fs.existsSync(indexPath)) {
|
|
488
|
-
const overwrite = await new Promise((r) => {
|
|
489
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
490
|
-
rl2.question(chalk.yellow(`Tool "${slug}" already exists. Overwrite? (y/N): `), (a) => {
|
|
491
|
-
rl2.close();
|
|
492
|
-
r(a.trim().toLowerCase() === "y");
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
if (!overwrite) {
|
|
496
|
-
console.log(chalk.dim("Aborted."));
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
502
|
-
fs.writeFileSync(v2Path, v2Source, "utf-8");
|
|
503
|
-
fs.writeFileSync(indexPath, indexSource, "utf-8");
|
|
504
|
-
|
|
505
|
-
console.log(chalk.green(`\n ✓ Tool "${name}" created successfully!\n`));
|
|
506
|
-
console.log(chalk.dim(` ${v2Path}`));
|
|
507
|
-
console.log(chalk.dim(` ${indexPath}\n`));
|
|
508
|
-
console.log(chalk.dim(" Next steps:"));
|
|
509
|
-
console.log(chalk.dim(" 1. Edit v2.js to implement your tool logic"));
|
|
510
|
-
console.log(chalk.dim(" 2. Run nxb run --local <slug> to test locally"));
|
|
511
|
-
console.log(chalk.dim(" 3. Publish from the VS Code extension or API\n"));
|
|
553
|
+
cacheCmd
|
|
554
|
+
.command("path")
|
|
555
|
+
.description("Show where config and cache are stored")
|
|
556
|
+
.action(() => {
|
|
557
|
+
console.log(chalk.bold("\n Paths\n"));
|
|
558
|
+
console.log(` ${chalk.gray("config :")} ${CONFIG_PATH}`);
|
|
559
|
+
console.log(` ${chalk.gray("cache :")} ${CACHE_DIR}`);
|
|
560
|
+
console.log(` ${chalk.gray("tools :")} ${TOOLS_CACHE_DIR}`);
|
|
561
|
+
console.log();
|
|
512
562
|
});
|
|
513
563
|
|
|
514
564
|
// ─── `nxb json` ─────────────────────────────────────────────────────────────
|
|
@@ -520,15 +570,18 @@ program
|
|
|
520
570
|
.option("-s, --set <key=value...>", "Set specific field values (e.g. -s count=10 format=csv)")
|
|
521
571
|
.option("-a, --add <json>", "Merge additional JSON into the output (e.g. -a '{\"count\":20}')")
|
|
522
572
|
.action(async (opts, cmd) => {
|
|
523
|
-
const jsonMode = cmd
|
|
573
|
+
const jsonMode = jsonFlag(cmd);
|
|
524
574
|
|
|
525
575
|
if (opts.template) {
|
|
526
|
-
const
|
|
576
|
+
const safe = requireSlug(opts.template, jsonMode);
|
|
577
|
+
if (!safe) return;
|
|
578
|
+
|
|
579
|
+
const resolved = await resolveToolSlug(safe);
|
|
527
580
|
const toolInfo = await fetchToolSource(resolved);
|
|
528
581
|
const manifest = toolInfo?.manifest;
|
|
529
582
|
|
|
530
583
|
if (!manifest) {
|
|
531
|
-
console.error(chalk.red(`Tool "${resolved}" not found
|
|
584
|
+
console.error(chalk.red(`Tool "${resolved}" not found.`));
|
|
532
585
|
process.exitCode = 1;
|
|
533
586
|
return;
|
|
534
587
|
}
|
|
@@ -537,6 +590,12 @@ program
|
|
|
537
590
|
let result = {};
|
|
538
591
|
|
|
539
592
|
if (opts.interactive) {
|
|
593
|
+
if (!isInteractive()) {
|
|
594
|
+
console.error(chalk.red("Not an interactive terminal — cannot prompt for fields."));
|
|
595
|
+
console.error(chalk.dim(" Drop -i, or use `nxb run` bare to pick fields interactively."));
|
|
596
|
+
process.exitCode = 1;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
540
599
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
541
600
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
542
601
|
|
|
@@ -621,6 +680,12 @@ program
|
|
|
621
680
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
622
681
|
|
|
623
682
|
function promptValue(key) {
|
|
683
|
+
if (!isInteractive()) {
|
|
684
|
+
console.error(chalk.red("Not an interactive terminal — pass the value as an argument."));
|
|
685
|
+
console.error(chalk.dim(` nxb config set ${key} <value>`));
|
|
686
|
+
process.exitCode = 1;
|
|
687
|
+
return Promise.resolve("");
|
|
688
|
+
}
|
|
624
689
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
625
690
|
return new Promise((resolve) => {
|
|
626
691
|
rl.question(chalk.cyan(` Enter value for "${key}": `), (answer) => {
|
|
@@ -660,4 +725,11 @@ function renderToolInfo(manifest, resolved) {
|
|
|
660
725
|
console.log();
|
|
661
726
|
}
|
|
662
727
|
|
|
663
|
-
|
|
728
|
+
// `parse` does not await the async action handlers, so a Ctrl+C inside an
|
|
729
|
+
// inquirer prompt surfaces here as a rejected promise rather than a thrown
|
|
730
|
+
// error. Catching it turns a stack trace into a clean, conventional exit.
|
|
731
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
732
|
+
if (!isExitPromptError(err)) throw err;
|
|
733
|
+
process.stderr.write("\n Cancelled.\n");
|
|
734
|
+
process.exit(130);
|
|
735
|
+
});
|