@nexusbloom/cli 0.8.0 → 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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/index.js +24 -2
  3. package/src/run.js +35 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,4 +30,4 @@
30
30
  "test:watch": "node --test --watch --import ./test/setup.mjs test/*.test.js",
31
31
  "lint": "node --check src/index.js"
32
32
  }
33
- }
33
+ }
package/src/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  fetchTools, fetchToolSource, resolveToolSlug,
41
41
  generateAbbreviations, normalizeSlug, readStdin,
42
42
  } from "./tools.js";
43
- import { runLocal, runRemote, buildInputBody, runInteractive, reportError } from "./run.js";
43
+ import { runLocal, runRemote, buildInputBody, runInteractive, runInteractiveFor, isExitPromptError, reportError } from "./run.js";
44
44
  import listAction from "./list.js";
45
45
  import { pipeAction, listTransforms, doctorAction } from "./commands.js";
46
46
  import { buildWizard } from "./wizard.js";
@@ -167,6 +167,21 @@ program
167
167
  const body = await buildInputBody(opts, args);
168
168
  if (body === null) return;
169
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
+
170
185
  // ── Local vs remote ──
171
186
  if (opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"))) {
172
187
  return await runLocal(resolved, body, { jsonMode, slugResolved: true });
@@ -710,4 +725,11 @@ function renderToolInfo(manifest, resolved) {
710
725
  console.log();
711
726
  }
712
727
 
713
- program.parse(process.argv);
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
+ });
package/src/run.js CHANGED
@@ -200,6 +200,21 @@ export async function runLocal(slug, input, { jsonMode, slugResolved, quiet } =
200
200
  return { ok: true, result: res.result, durationMs: res.durationMs };
201
201
  }
202
202
 
203
+ /**
204
+ * True when an error is inquirer's "the user pressed Ctrl+C" signal.
205
+ *
206
+ * @inquirer/core rejects with an `ExitPromptError` from inside a readline
207
+ * `SIGINT` handler. Because the CLI parses with `program.parse()` — which does
208
+ * not await the async action handlers — that rejection used to escape as an
209
+ * unhandled rejection and dump an inquirer stack trace over the user's
210
+ * terminal. The class is not exported from `@inquirer/prompts`, and importing
211
+ * `@inquirer/core` directly would tie us to a transitive dependency, so it is
212
+ * matched by name.
213
+ */
214
+ export function isExitPromptError(err) {
215
+ return Boolean(err) && err.name === "ExitPromptError";
216
+ }
217
+
203
218
  /** Print an error on stderr (or as JSON) and set a non-zero exit code. */
204
219
  export function reportError(jsonMode, message) {
205
220
  if (jsonMode) {
@@ -365,15 +380,30 @@ export async function runInteractive(opts = {}) {
365
380
  const selected = await promptSelect("Choose a tool:", choices, { pageSize: 15 });
366
381
  if (!selected) return;
367
382
 
368
- const slug = selected.slug;
369
- const resolved = await resolveToolSlug(slug, tools);
383
+ return await runInteractiveFor(selected.slug, { ...opts, json: jsonMode, tools });
384
+ }
385
+
386
+ /**
387
+ * Interactive configuration for a slug that is already known.
388
+ *
389
+ * Shared by the tool picker above and by `nxb run <slug>` with no input of any
390
+ * kind: a bare `nxb run text-statistics` has nothing to send, so asking for the
391
+ * fields is the useful behaviour rather than failing schema validation with
392
+ * "Missing required fields: text".
393
+ *
394
+ * Returns the executed slug, or undefined when the user backs out or the tool
395
+ * has no schema to configure.
396
+ */
397
+ export async function runInteractiveFor(slug, opts = {}) {
398
+ const { json: jsonMode, tools: knownTools } = opts;
399
+ const resolved = await resolveToolSlug(slug, knownTools);
370
400
 
371
401
  // 2. Fetch manifest / schema
372
402
  const toolInfo = await fetchToolSource(resolved);
373
403
  if (!toolInfo?.manifest) {
374
404
  console.log(chalk.yellow(` No schema available for "${resolved}". Falling back to raw input.`));
375
405
  await runCommand(resolved, {}, { ...opts, json: jsonMode });
376
- return;
406
+ return resolved;
377
407
  }
378
408
 
379
409
  const { manifest } = toolInfo;
@@ -419,10 +449,11 @@ export async function runInteractive(opts = {}) {
419
449
  if (!runIt) {
420
450
  console.log(chalk.dim(" Command copied to clipboard (if available):"));
421
451
  console.log(chalk.gray(` ${fullCommand}`));
422
- return;
452
+ return resolved;
423
453
  }
424
454
 
425
455
  await runCommand(resolved, body, { ...opts, json: jsonMode, resolved });
456
+ return resolved;
426
457
  }
427
458
 
428
459
  /** Prompt for a single schema field based on its type. */