@nexusbloom/cli 0.3.4 → 0.8.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 +10 -3
- 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 +330 -280
- 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 +203 -100
- 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, 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,26 +135,32 @@ 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 ──
|
|
@@ -154,15 +186,15 @@ program
|
|
|
154
186
|
.argument("<slug>", "Tool slug or abbreviation")
|
|
155
187
|
.action(async (slug, opts, cmd) => {
|
|
156
188
|
const jsonMode = cmd?.parent?.opts().json || false;
|
|
157
|
-
const
|
|
189
|
+
const safe = requireSlug(slug, jsonMode);
|
|
190
|
+
if (!safe) return;
|
|
158
191
|
|
|
192
|
+
const resolved = await resolveToolSlug(safe);
|
|
159
193
|
const toolInfo = await fetchToolSource(resolved);
|
|
160
194
|
const manifest = toolInfo?.manifest;
|
|
161
195
|
|
|
162
196
|
if (!manifest) {
|
|
163
|
-
|
|
164
|
-
console.error(chalk.red(`Tool "${resolved}" not found.`));
|
|
165
|
-
return;
|
|
197
|
+
return reportError(jsonMode, `Tool "${resolved}" not found. Try \`nxb search ${safe}\`.`);
|
|
166
198
|
}
|
|
167
199
|
|
|
168
200
|
if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
|
|
@@ -173,20 +205,20 @@ program
|
|
|
173
205
|
// ─── `nxb transform <slug>` ──────────────────────────────────────────────────
|
|
174
206
|
program
|
|
175
207
|
.command("transform")
|
|
176
|
-
.description("Pipe stdin through a tool and write result to stdout")
|
|
208
|
+
.description("Pipe stdin through a tool and write the result to stdout")
|
|
177
209
|
.argument("<slug>", "Tool slug or abbreviation")
|
|
178
210
|
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
179
211
|
.option("--local", "Execute locally (faster for repeated use)")
|
|
180
212
|
.action(async (slug, opts, cmd) => {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
213
|
+
const jsonMode = jsonFlag(cmd);
|
|
214
|
+
const safe = requireSlug(slug, jsonMode);
|
|
215
|
+
if (!safe) return;
|
|
216
|
+
|
|
217
|
+
const resolved = await resolveToolSlug(safe);
|
|
183
218
|
|
|
184
219
|
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;
|
|
220
|
+
if (!input || !input.trim()) {
|
|
221
|
+
return reportError(jsonMode, "No input received from stdin. Pipe data into this command.");
|
|
190
222
|
}
|
|
191
223
|
|
|
192
224
|
let body;
|
|
@@ -196,19 +228,123 @@ program
|
|
|
196
228
|
body = { input };
|
|
197
229
|
}
|
|
198
230
|
|
|
199
|
-
|
|
200
|
-
|
|
231
|
+
// `transform` exists to be piped, so stdout carries the payload and every
|
|
232
|
+
// decoration goes to stderr.
|
|
233
|
+
const useLocal =
|
|
234
|
+
opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"));
|
|
235
|
+
|
|
236
|
+
const res = useLocal
|
|
237
|
+
? await runLocal(resolved, body, { jsonMode: false, slugResolved: true, quiet: true })
|
|
238
|
+
: await runRemote(resolved, body, {
|
|
239
|
+
apiKey: opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key"),
|
|
240
|
+
jsonMode: false,
|
|
241
|
+
slugResolved: true,
|
|
242
|
+
quiet: true,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!res?.ok) {
|
|
246
|
+
return reportError(jsonMode, res?.error || "Transform failed.");
|
|
201
247
|
}
|
|
202
248
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
249
|
+
// A string result is emitted verbatim so it can feed another process;
|
|
250
|
+
// anything else is JSON so it can feed another `nxb transform`.
|
|
251
|
+
console.log(typeof res.result === "string" ? res.result : JSON.stringify(res.result, null, 2));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ─── `nxb pipe` ──────────────────────────────────────────────────────────────
|
|
255
|
+
program
|
|
256
|
+
.command("pipe")
|
|
257
|
+
.description("Run a pipeline: connect one tool's output into another's input")
|
|
258
|
+
.argument("[spec]", 'Pipeline string, e.g. "tool-a x=@in | tool-b count=$n|len"')
|
|
259
|
+
.option("--dry-run", "Check the wiring and type compatibility without running")
|
|
260
|
+
.option("--remote", "Run every step against the API instead of locally")
|
|
261
|
+
.option("--transforms", "List the available connection transforms")
|
|
262
|
+
.option("--build", "Build a pipeline interactively")
|
|
263
|
+
.action(async (spec, opts, cmd) => {
|
|
264
|
+
const jsonMode = jsonFlag(cmd);
|
|
265
|
+
if (opts.transforms) return listTransforms(jsonMode);
|
|
266
|
+
if (opts.build || (!spec && isInteractive())) {
|
|
267
|
+
// Building nothing is a failure, not a quiet success — scripts need to
|
|
268
|
+
// be able to detect that no pipeline came out.
|
|
269
|
+
const built = await buildWizard();
|
|
270
|
+
if (!built?.ok) process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!spec) {
|
|
274
|
+
console.error(chalk.red(" Give a pipeline, or run `nxb pipe --build`."));
|
|
207
275
|
process.exitCode = 1;
|
|
208
276
|
return;
|
|
209
277
|
}
|
|
278
|
+
await pipeAction(spec, { json: jsonMode, dryRun: opts.dryRun, remote: opts.remote });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ─── `nxb pipeline <list|run|delete>` ────────────────────────────────────────
|
|
282
|
+
const savedCmd = program
|
|
283
|
+
.command("pipeline")
|
|
284
|
+
.description("Manage saved pipelines");
|
|
210
285
|
|
|
211
|
-
|
|
286
|
+
savedCmd
|
|
287
|
+
.command("list")
|
|
288
|
+
.description("List saved pipelines")
|
|
289
|
+
.action((opts, cmd) => {
|
|
290
|
+
const jsonMode = jsonFlag(cmd);
|
|
291
|
+
const items = listPipelines();
|
|
292
|
+
if (jsonMode) return console.log(JSON.stringify({ pipelines: items }, null, 2));
|
|
293
|
+
if (!items.length) {
|
|
294
|
+
console.log(chalk.yellow(" No saved pipelines. Create one with `nxb pipe --build`."));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
console.log(chalk.bold(`\n ${items.length} saved pipeline${items.length === 1 ? "" : "s"}\n`));
|
|
298
|
+
for (const it of items) {
|
|
299
|
+
console.log(` ${chalk.cyan(it.name.padEnd(20))} ${chalk.dim(`${it.steps} step${it.steps === 1 ? "" : "s"}`)}`);
|
|
300
|
+
if (it.pipeline) console.log(` ${chalk.gray(it.pipeline.slice(0, 92))}`);
|
|
301
|
+
}
|
|
302
|
+
console.log();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
savedCmd
|
|
306
|
+
.command("run")
|
|
307
|
+
.description("Run a saved pipeline")
|
|
308
|
+
.argument("<name>", "Pipeline name")
|
|
309
|
+
.option("--dry-run", "Check the wiring without running")
|
|
310
|
+
.option("--remote", "Run every step against the API")
|
|
311
|
+
.action(async (name, opts, cmd) => {
|
|
312
|
+
const jsonMode = jsonFlag(cmd);
|
|
313
|
+
const def = loadPipeline(name);
|
|
314
|
+
if (!def?.pipeline) {
|
|
315
|
+
return reportError(jsonMode, `No saved pipeline named "${name}". Try \`nxb pipeline list\`.`);
|
|
316
|
+
}
|
|
317
|
+
await pipeAction(def.pipeline, { json: jsonMode, dryRun: opts.dryRun, remote: opts.remote });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
savedCmd
|
|
321
|
+
.command("delete")
|
|
322
|
+
.description("Delete a saved pipeline")
|
|
323
|
+
.argument("<name>", "Pipeline name")
|
|
324
|
+
.action((name, _opts, cmd) => {
|
|
325
|
+
const jsonMode = jsonFlag(cmd);
|
|
326
|
+
try {
|
|
327
|
+
if (!deletePipeline(name)) return reportError(jsonMode, `No saved pipeline named "${name}".`);
|
|
328
|
+
console.log(chalk.green(` Deleted "${name}".`));
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return reportError(jsonMode, err.message);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ─── `nxb doctor <slug>` ─────────────────────────────────────────────────────
|
|
335
|
+
program
|
|
336
|
+
.command("doctor")
|
|
337
|
+
.description("Compare a tool's real output against its declared output_schema")
|
|
338
|
+
.argument("<slug>", "Tool slug or abbreviation")
|
|
339
|
+
.action(async (slug, opts, cmd) => {
|
|
340
|
+
const jsonMode = jsonFlag(cmd);
|
|
341
|
+
const safe = requireSlug(slug, jsonMode);
|
|
342
|
+
if (!safe) return;
|
|
343
|
+
const resolved = await resolveToolSlug(safe);
|
|
344
|
+
// `nxb doctor x || exit 1` is the whole point of a drift detector; a
|
|
345
|
+
// finding that exits 0 makes the gate silently useless.
|
|
346
|
+
const verdict = await doctorAction(resolved, { json: jsonMode });
|
|
347
|
+
if (verdict && verdict.ok === false) process.exitCode = 1;
|
|
212
348
|
});
|
|
213
349
|
|
|
214
350
|
// ─── `nxb abbr [query]` ──────────────────────────────────────────────────────
|
|
@@ -216,11 +352,13 @@ program
|
|
|
216
352
|
.command("abbr")
|
|
217
353
|
.description("Show tool abbreviations (paginated)")
|
|
218
354
|
.argument("[query]", "Search/filter abbreviations")
|
|
355
|
+
.option("-s, --search <query>", "Search/filter abbreviations")
|
|
219
356
|
.option("--no-interactive", "Disable interactive navigation")
|
|
220
357
|
.action(async (query, opts, cmd) => {
|
|
221
|
-
const jsonMode = cmd
|
|
358
|
+
const jsonMode = jsonFlag(cmd);
|
|
222
359
|
await abbrAction({
|
|
223
360
|
query,
|
|
361
|
+
search: opts.search,
|
|
224
362
|
json: jsonMode,
|
|
225
363
|
interactive: opts.interactive !== false,
|
|
226
364
|
});
|
|
@@ -234,7 +372,7 @@ const configCmd = program
|
|
|
234
372
|
configCmd
|
|
235
373
|
.command("set")
|
|
236
374
|
.description("Set a config value (prompts for value if omitted)")
|
|
237
|
-
.argument("<key>", "Config key (key,
|
|
375
|
+
.argument("<key>", "Config key (key, supabaseKey, telemetry, toolsDir)")
|
|
238
376
|
.argument("[value]", "Config value (prompted if not provided)")
|
|
239
377
|
.action(async (key, value) => {
|
|
240
378
|
if (!value) {
|
|
@@ -245,12 +383,18 @@ configCmd
|
|
|
245
383
|
return;
|
|
246
384
|
}
|
|
247
385
|
configSet(key, value);
|
|
248
|
-
|
|
386
|
+
// Never echo a secret back to the terminal / shell history.
|
|
387
|
+
if (isSecretKey(key)) {
|
|
388
|
+
console.log(chalk.green(` Set ${key} (stored, value hidden)`));
|
|
389
|
+
console.log(chalk.dim(` ${CONFIG_PATH}`));
|
|
390
|
+
} else {
|
|
391
|
+
console.log(chalk.green(` Set ${key} to "${value}"`));
|
|
392
|
+
}
|
|
249
393
|
});
|
|
250
394
|
|
|
251
395
|
configCmd
|
|
252
396
|
.command("get")
|
|
253
|
-
.description("Get a config value")
|
|
397
|
+
.description("Get a config value (secrets redacted)")
|
|
254
398
|
.argument("[key]", "Config key (omit to show all)")
|
|
255
399
|
.action((key) => {
|
|
256
400
|
const store = loadConfig();
|
|
@@ -259,256 +403,147 @@ configCmd
|
|
|
259
403
|
if (value === undefined) {
|
|
260
404
|
console.log(chalk.yellow(` ${key} is not set`));
|
|
261
405
|
} else {
|
|
262
|
-
|
|
406
|
+
const shown = isSecretKey(key) ? maskSecret(value) : value;
|
|
407
|
+
console.log(` ${key}: ${shown}`);
|
|
263
408
|
}
|
|
264
409
|
} 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
|
-
}
|
|
410
|
+
printConfig(store);
|
|
272
411
|
}
|
|
273
412
|
});
|
|
274
413
|
|
|
275
414
|
configCmd
|
|
276
|
-
.command("
|
|
277
|
-
.description("
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
415
|
+
.command("unset")
|
|
416
|
+
.description("Remove a config value (e.g. a stored API key)")
|
|
417
|
+
.argument("<key>", "Config key to remove")
|
|
418
|
+
.action((key, opts, cmd) => {
|
|
419
|
+
const jsonMode = jsonFlag(cmd);
|
|
420
|
+
if (!configUnset(key)) {
|
|
421
|
+
return reportError(jsonMode, `"${key}" is not set.`);
|
|
422
|
+
}
|
|
423
|
+
if (jsonMode) {
|
|
424
|
+
console.log(JSON.stringify({ success: true, data: { unset: key } }, null, 2));
|
|
282
425
|
} else {
|
|
283
|
-
|
|
284
|
-
console.log(` ${k}: ${v}`);
|
|
285
|
-
});
|
|
426
|
+
console.log(chalk.green(` Removed ${key}`));
|
|
286
427
|
}
|
|
287
428
|
});
|
|
288
429
|
|
|
430
|
+
configCmd
|
|
431
|
+
.command("list")
|
|
432
|
+
.description("List all config values (secrets redacted)")
|
|
433
|
+
.action(() => printConfig(loadConfig()));
|
|
434
|
+
|
|
435
|
+
function maskSecret(value) {
|
|
436
|
+
const s = value == null ? "" : String(value);
|
|
437
|
+
return s.length <= 8 ? "********" : `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function printConfig(store) {
|
|
441
|
+
if (!store || Object.keys(store).length === 0) {
|
|
442
|
+
console.log(chalk.yellow(" No config values set."));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const safe = redactedConfig(store);
|
|
446
|
+
for (const [k, v] of Object.entries(safe)) {
|
|
447
|
+
console.log(` ${k}: ${v}`);
|
|
448
|
+
}
|
|
449
|
+
console.log(chalk.dim(`\n Stored at ${CONFIG_PATH}`));
|
|
450
|
+
}
|
|
451
|
+
|
|
289
452
|
// ─── `nxb cache` ─────────────────────────────────────────────────────────────
|
|
290
453
|
const cacheCmd = program
|
|
291
454
|
.command("cache")
|
|
292
455
|
.description("Manage local tool cache");
|
|
293
456
|
|
|
457
|
+
/** Delete everything inside a cache dir (recursively). Returns entries removed. */
|
|
458
|
+
function clearCacheDir(dir) {
|
|
459
|
+
if (!fs.existsSync(dir)) return 0;
|
|
460
|
+
let count = 0;
|
|
461
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
462
|
+
const target = path.join(dir, entry.name);
|
|
463
|
+
try {
|
|
464
|
+
if (entry.isDirectory()) fs.rmSync(target, { recursive: true, force: true });
|
|
465
|
+
else fs.unlinkSync(target);
|
|
466
|
+
count++;
|
|
467
|
+
} catch {
|
|
468
|
+
/* skip anything we cannot remove */
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return count;
|
|
472
|
+
}
|
|
473
|
+
|
|
294
474
|
cacheCmd
|
|
295
475
|
.command("clear")
|
|
296
476
|
.description("Clear the local tool cache")
|
|
297
477
|
.action(() => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
478
|
+
const count = clearCacheDir(TOOLS_CACHE_DIR);
|
|
479
|
+
let removedList = 0;
|
|
480
|
+
if (fs.existsSync(TOOLS_LIST_CACHE_PATH)) {
|
|
481
|
+
try {
|
|
482
|
+
fs.unlinkSync(TOOLS_LIST_CACHE_PATH);
|
|
483
|
+
removedList = 1;
|
|
484
|
+
} catch {
|
|
485
|
+
/* best effort */
|
|
486
|
+
}
|
|
301
487
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
488
|
+
// Older versions cached inside the installed package directory.
|
|
489
|
+
const legacy = clearCacheDir(LEGACY_CACHE_DIR);
|
|
490
|
+
if (legacy) {
|
|
491
|
+
try {
|
|
492
|
+
fs.rmdirSync(LEGACY_CACHE_DIR);
|
|
493
|
+
} catch {
|
|
494
|
+
/* not empty / not ours — leave it */
|
|
495
|
+
}
|
|
307
496
|
}
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
497
|
+
const total = count + removedList;
|
|
498
|
+
if (total === 0 && legacy === 0) {
|
|
499
|
+
console.log(chalk.yellow(" Nothing cached."));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
console.log(chalk.green(` Cleared ${total} cached item${total !== 1 ? "s" : ""}.`));
|
|
503
|
+
if (legacy) {
|
|
504
|
+
console.log(chalk.dim(` Removed ${legacy} legacy cache entr${legacy === 1 ? "y" : "ies"} from the old package directory.`));
|
|
311
505
|
}
|
|
312
|
-
console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
|
|
313
506
|
});
|
|
314
507
|
|
|
315
508
|
cacheCmd
|
|
316
509
|
.command("list")
|
|
317
510
|
.description("List cached tools")
|
|
318
|
-
.
|
|
511
|
+
.option("-s, --search <query>", "Filter cached tools by slug")
|
|
512
|
+
.action((opts) => {
|
|
319
513
|
if (!fs.existsSync(TOOLS_CACHE_DIR)) {
|
|
320
514
|
console.log(chalk.yellow(" No cached tools."));
|
|
321
515
|
return;
|
|
322
516
|
}
|
|
323
|
-
const
|
|
517
|
+
const query = (opts.search || "").toLowerCase();
|
|
518
|
+
const files = fs
|
|
519
|
+
.readdirSync(TOOLS_CACHE_DIR)
|
|
520
|
+
.filter((f) => f.endsWith(".json"))
|
|
521
|
+
.map((f) => f.replace(/\.json$/, ""))
|
|
522
|
+
.filter((slug) => !query || slug.toLowerCase().includes(query));
|
|
523
|
+
|
|
324
524
|
if (files.length === 0) {
|
|
325
|
-
console.log(chalk.yellow(" No cached tools."));
|
|
525
|
+
console.log(chalk.yellow(query ? ` No cached tools matching "${opts.search}".` : " No cached tools."));
|
|
326
526
|
return;
|
|
327
527
|
}
|
|
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`)}`);
|
|
528
|
+
console.log(chalk.bold(`\n ${files.length} cached tool${files.length !== 1 ? "s" : ""}:\n`));
|
|
529
|
+
files.sort().forEach((slug) => {
|
|
530
|
+
const stats = fs.statSync(path.join(TOOLS_CACHE_DIR, `${slug}.json`));
|
|
531
|
+
const age = Math.round((Date.now() - stats.mtimeMs) / 60000);
|
|
532
|
+
const size = (stats.size / 1024).toFixed(1);
|
|
533
|
+
console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago · ${size}kb`)}`);
|
|
334
534
|
});
|
|
335
535
|
console.log();
|
|
336
536
|
});
|
|
337
537
|
|
|
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"));
|
|
538
|
+
cacheCmd
|
|
539
|
+
.command("path")
|
|
540
|
+
.description("Show where config and cache are stored")
|
|
541
|
+
.action(() => {
|
|
542
|
+
console.log(chalk.bold("\n Paths\n"));
|
|
543
|
+
console.log(` ${chalk.gray("config :")} ${CONFIG_PATH}`);
|
|
544
|
+
console.log(` ${chalk.gray("cache :")} ${CACHE_DIR}`);
|
|
545
|
+
console.log(` ${chalk.gray("tools :")} ${TOOLS_CACHE_DIR}`);
|
|
546
|
+
console.log();
|
|
512
547
|
});
|
|
513
548
|
|
|
514
549
|
// ─── `nxb json` ─────────────────────────────────────────────────────────────
|
|
@@ -520,15 +555,18 @@ program
|
|
|
520
555
|
.option("-s, --set <key=value...>", "Set specific field values (e.g. -s count=10 format=csv)")
|
|
521
556
|
.option("-a, --add <json>", "Merge additional JSON into the output (e.g. -a '{\"count\":20}')")
|
|
522
557
|
.action(async (opts, cmd) => {
|
|
523
|
-
const jsonMode = cmd
|
|
558
|
+
const jsonMode = jsonFlag(cmd);
|
|
524
559
|
|
|
525
560
|
if (opts.template) {
|
|
526
|
-
const
|
|
561
|
+
const safe = requireSlug(opts.template, jsonMode);
|
|
562
|
+
if (!safe) return;
|
|
563
|
+
|
|
564
|
+
const resolved = await resolveToolSlug(safe);
|
|
527
565
|
const toolInfo = await fetchToolSource(resolved);
|
|
528
566
|
const manifest = toolInfo?.manifest;
|
|
529
567
|
|
|
530
568
|
if (!manifest) {
|
|
531
|
-
console.error(chalk.red(`Tool "${resolved}" not found
|
|
569
|
+
console.error(chalk.red(`Tool "${resolved}" not found.`));
|
|
532
570
|
process.exitCode = 1;
|
|
533
571
|
return;
|
|
534
572
|
}
|
|
@@ -537,6 +575,12 @@ program
|
|
|
537
575
|
let result = {};
|
|
538
576
|
|
|
539
577
|
if (opts.interactive) {
|
|
578
|
+
if (!isInteractive()) {
|
|
579
|
+
console.error(chalk.red("Not an interactive terminal — cannot prompt for fields."));
|
|
580
|
+
console.error(chalk.dim(" Drop -i, or use `nxb run` bare to pick fields interactively."));
|
|
581
|
+
process.exitCode = 1;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
540
584
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
541
585
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
542
586
|
|
|
@@ -621,6 +665,12 @@ program
|
|
|
621
665
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
622
666
|
|
|
623
667
|
function promptValue(key) {
|
|
668
|
+
if (!isInteractive()) {
|
|
669
|
+
console.error(chalk.red("Not an interactive terminal — pass the value as an argument."));
|
|
670
|
+
console.error(chalk.dim(` nxb config set ${key} <value>`));
|
|
671
|
+
process.exitCode = 1;
|
|
672
|
+
return Promise.resolve("");
|
|
673
|
+
}
|
|
624
674
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
625
675
|
return new Promise((resolve) => {
|
|
626
676
|
rl.question(chalk.cyan(` Enter value for "${key}": `), (answer) => {
|