@nexusbloom/cli 0.8.0 → 0.9.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/package.json +2 -2
- package/src/index.js +24 -2
- package/src/pipe.js +1 -1
- package/src/run.js +35 -4
- package/src/wizard.js +45 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexusbloom/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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
|
-
|
|
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/pipe.js
CHANGED
|
@@ -135,7 +135,7 @@ function tokenize(spec) {
|
|
|
135
135
|
* @returns {{kind:"stdin"|"connection"|"literal", headPath?: string,
|
|
136
136
|
* accessors?: Array, chain?: Array, literal?: any}}
|
|
137
137
|
*/
|
|
138
|
-
function parseValueExpr(expr) {
|
|
138
|
+
export function parseValueExpr(expr) {
|
|
139
139
|
const raw = String(expr ?? "");
|
|
140
140
|
|
|
141
141
|
if (raw === "@in" || raw === "@stdin") return { kind: "stdin" };
|
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
|
-
|
|
369
|
-
|
|
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. */
|
package/src/wizard.js
CHANGED
|
@@ -33,7 +33,8 @@ import { execInSandbox } from "./sandbox.js";
|
|
|
33
33
|
import { configGet } from "./tools.js";
|
|
34
34
|
import { runRemote } from "./run.js";
|
|
35
35
|
import { inferType, schemaTypes, typeLabel, compatibility } from "./types.js";
|
|
36
|
-
import { formatPath, parsePath, preview, transformNames } from "./values.js";
|
|
36
|
+
import { formatPath, parsePath, preview, transformNames, MISSING } from "./values.js";
|
|
37
|
+
import { parseValueExpr, resolveAccessors } from "./pipe.js";
|
|
37
38
|
import { isInteractive } from "./ui.js";
|
|
38
39
|
import { renderHeader } from "./ui.js";
|
|
39
40
|
|
|
@@ -283,7 +284,10 @@ export async function buildWizard(opts = {}) {
|
|
|
283
284
|
|
|
284
285
|
console.log(chalk.gray(`\n Running ${spec}\n`));
|
|
285
286
|
const started = Date.now();
|
|
286
|
-
const result = await execute(
|
|
287
|
+
const result = await execute(
|
|
288
|
+
tool.slug,
|
|
289
|
+
literalInput(assignments, outputs.length ? outputs[outputs.length - 1].value : undefined)
|
|
290
|
+
);
|
|
287
291
|
|
|
288
292
|
if (!result.ok) {
|
|
289
293
|
console.log(chalk.red(` ✗ ${result.error}`));
|
|
@@ -328,15 +332,51 @@ export async function buildWizard(opts = {}) {
|
|
|
328
332
|
return { ok: true, spec: pipeline };
|
|
329
333
|
}
|
|
330
334
|
|
|
331
|
-
/**
|
|
332
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Build the input object for a step's trial run.
|
|
337
|
+
*
|
|
338
|
+
* Wired fields (`$path`, `$path|transform`) are resolved against the previous
|
|
339
|
+
* step's real output, using the *same* parser and resolver the pipeline engine
|
|
340
|
+
* uses at run time. That matters twice over:
|
|
341
|
+
*
|
|
342
|
+
* - correctness: a step whose required input is wired previously ran with that
|
|
343
|
+
* input simply absent, so `password-generator → base64-tool` failed with
|
|
344
|
+
* "text must be a non-empty string" no matter what the user chose;
|
|
345
|
+
* - consistency: the trial the wizard shows is the run the replay command
|
|
346
|
+
* will later reproduce, because both go through `parseValueExpr` +
|
|
347
|
+
* `resolveAccessors` rather than a second, drifting implementation.
|
|
348
|
+
*
|
|
349
|
+
* A reference that resolves to nothing is omitted rather than passed as
|
|
350
|
+
* `undefined`, so schema validation reports the field by name.
|
|
351
|
+
*/
|
|
352
|
+
function literalInput(assignments, upstream) {
|
|
333
353
|
const input = {};
|
|
334
354
|
for (const a of assignments) {
|
|
335
355
|
const eq = a.text.indexOf("=");
|
|
336
356
|
if (eq === -1) continue;
|
|
337
357
|
const key = a.text.slice(0, eq);
|
|
338
358
|
const raw = a.text.slice(eq + 1);
|
|
339
|
-
|
|
359
|
+
|
|
360
|
+
if (raw.startsWith("$")) {
|
|
361
|
+
if (upstream === undefined) continue;
|
|
362
|
+
let expr;
|
|
363
|
+
try {
|
|
364
|
+
expr = parseValueExpr(raw);
|
|
365
|
+
} catch {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (expr.kind !== "connection" || !expr.accessors) continue;
|
|
369
|
+
let value;
|
|
370
|
+
try {
|
|
371
|
+
value = resolveAccessors(upstream, expr.accessors);
|
|
372
|
+
} catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (value === MISSING || value === undefined) continue;
|
|
376
|
+
input[key] = value;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
340
380
|
try {
|
|
341
381
|
input[key] = JSON.parse(raw);
|
|
342
382
|
} catch {
|