@nexusbloom/cli 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.
Files changed (3) hide show
  1. package/README.md +71 -0
  2. package/package.json +26 -0
  3. package/src/index.js +559 -0
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @nexusbloom/cli
2
+
3
+ CLI tool for NexusBloom — run tools and workflows from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @nexusbloom/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # List available tools
15
+ nxb list
16
+
17
+ # Search tools
18
+ nxb search "ssl"
19
+
20
+ # Run a tool (remote API, requires API key)
21
+ nxb run ssl-certificate-checker domain=example.com --key YOUR_API_KEY
22
+
23
+ # Run with inline JSON input
24
+ nxb run css-gradient-generator --input '{"colors":["#ff6b6b","#4ecdc4"],"angle":135}'
25
+
26
+ # Run with file input
27
+ nxb run css-gradient-generator --file ./gradient-input.json
28
+
29
+ # Run a tool locally (fetches + caches tool source, executes coreLogic on your machine)
30
+ nxb run ssl-certificate-checker domain=example.com --local
31
+
32
+ # Pipe stdin through a tool
33
+ echo '{"domain":"example.com"}' | nxb transform ssl-certificate-checker
34
+
35
+ # Describe what you want
36
+ nxb chat "check if my website example.com is secure"
37
+
38
+ # Show tool schema
39
+ nxb info ssl-certificate-checker
40
+
41
+ # Manage local tool cache
42
+ nxb cache list
43
+ nxb cache clear
44
+ ```
45
+
46
+ ## Options
47
+
48
+ | Flag | Description |
49
+ |------|-------------|
50
+ | `--json` | Output results in JSON format |
51
+ | `--key <key>` | API key (can also set via `NEXUSBLOOM_API_KEY` env var) |
52
+ | `--input <json>` | Inline JSON input (e.g. `'{"domain":"example.com"}'`) |
53
+ | `--file <path>` | Read input from a JSON file |
54
+ | `--local` | Execute coreLogic locally (fetches + caches tool source) |
55
+
56
+ ## Environment Variables
57
+
58
+ - `NEXUSBLOOM_API_KEY` — Your API key (can also be passed with `--key`)
59
+ - `NEXUSBLOOM_API_URL` — API base URL (default: https://nexusbloom.dev)
60
+
61
+ ## Local Execution
62
+
63
+ The `--local` flag fetches the tool's v2 source code from the NexusBloom API,
64
+ caches it locally, and executes the `coreLogic()` function directly on your
65
+ machine. This is useful for:
66
+
67
+ - Offline use (after initial fetch)
68
+ - Faster repeated execution (cached)
69
+ - Privacy-sensitive inputs (data stays on your machine)
70
+
71
+ Use `nxb cache clear` to clear the local cache.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@nexusbloom/cli",
3
+ "version": "0.1.0",
4
+ "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "nxb": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "package.json",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "dependencies": {
21
+ "commander": "^12.0.0",
22
+ "chalk": "^5.3.0",
23
+ "ora": "^8.0.0",
24
+ "conf": "^12.0.0"
25
+ }
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,559 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * NexusBloom CLI (nxb)
5
+ *
6
+ * Usage:
7
+ * nxb list — List available tools
8
+ * nxb search <query> — Search tools
9
+ * nxb run <slug> [args...] — Execute a tool (remote API)
10
+ * nxb run <slug> --input '{}' — Execute with inline JSON input
11
+ * nxb run <slug> --file input.json — Execute with file input
12
+ * nxb run <slug> --local — Execute coreLogic locally (cached)
13
+ * nxb chat <message> — Natural language routing
14
+ * nxb info <slug> — Show tool schema
15
+ * nxb transform <slug> — Pipe stdin through a tool
16
+ * nxb config set <key> <value> — Set config
17
+ * nxb config get [key] — Get config
18
+ * nxb cache clear — Clear local tool cache
19
+ */
20
+
21
+ import { program } from "commander";
22
+ import chalk from "chalk";
23
+ import { createRequire } from "module";
24
+ import Conf from "conf";
25
+ import fs from "fs";
26
+ import path from "path";
27
+ import { fileURLToPath } from "url";
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const { version } = require("../package.json");
31
+
32
+ const API_BASE = process.env.NEXUSBLOOM_API_URL || "https://nexusbloom.dev";
33
+ const config = new Conf({ projectName: "nexusbloom" });
34
+
35
+ // ─── Cache directory for local tool execution ────────────────────────────────
36
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
37
+ const CACHE_DIR = path.resolve(__dirname, "../.cache/tools");
38
+
39
+ function ensureCacheDir() {
40
+ if (!fs.existsSync(CACHE_DIR)) {
41
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
42
+ }
43
+ }
44
+
45
+ // ─── Program ─────────────────────────────────────────────────────────────────
46
+ program
47
+ .name("nxb")
48
+ .description("NexusBloom CLI — run tools and workflows from your terminal")
49
+ .version(version)
50
+ .option("--json", "Output in JSON format");
51
+
52
+ // ─── `nxb list` — list available tools ───────────────────────────────────────
53
+ program
54
+ .command("list")
55
+ .description("List available tools")
56
+ .option("-s, --search <query>", "Search tools by name or description")
57
+ .action(async (opts, cmd) => {
58
+ const jsonMode = cmd.parent.opts().json || opts.json;
59
+ try {
60
+ const url = new URL("/api/v1/tools", API_BASE);
61
+ if (opts.search) url.searchParams.set("search", opts.search);
62
+ const res = await fetch(url);
63
+ const { tools } = await res.json();
64
+ if (!tools || tools.length === 0) {
65
+ if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
66
+ console.log(chalk.yellow("No tools found."));
67
+ return;
68
+ }
69
+ if (jsonMode) return console.log(JSON.stringify({ tools }, null, 2));
70
+ console.log(chalk.bold(`\n ${tools.length} tool${tools.length > 1 ? "s" : ""} available\n`));
71
+ tools.forEach((t) => {
72
+ console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
73
+ });
74
+ console.log();
75
+ } catch (err) {
76
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
77
+ console.error(chalk.red("Failed to fetch tools:"), err.message);
78
+ }
79
+ });
80
+
81
+ // ─── `nxb search <query>` — search tools ─────────────────────────────────────
82
+ program
83
+ .command("search")
84
+ .description("Search for tools")
85
+ .argument("<query>", "Search query")
86
+ .action(async (query, cmd) => {
87
+ const jsonMode = cmd.parent.opts().json;
88
+ try {
89
+ const url = new URL("/api/v1/tools", API_BASE);
90
+ url.searchParams.set("search", query);
91
+ const res = await fetch(url);
92
+ const { tools } = await res.json();
93
+ if (!tools || tools.length === 0) {
94
+ if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
95
+ console.log(chalk.yellow(`No tools matching "${query}".`));
96
+ return;
97
+ }
98
+ if (jsonMode) return console.log(JSON.stringify({ tools, query }, null, 2));
99
+ console.log(chalk.bold(`\n ${tools.length} result${tools.length > 1 ? "s" : ""} for "${query}"\n`));
100
+ tools.forEach((t) => {
101
+ console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
102
+ });
103
+ console.log();
104
+ } catch (err) {
105
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
106
+ console.error(chalk.red("Search failed:"), err.message);
107
+ }
108
+ });
109
+
110
+ // ─── `nxb run <slug>` — execute a tool ───────────────────────────────────────
111
+ program
112
+ .command("run")
113
+ .description("Execute a tool")
114
+ .argument("<slug>", "Tool slug")
115
+ .argument("[args...]", "Key=value arguments (e.g. domain=example.com)")
116
+ .option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
117
+ .option("-i, --input <json>", "Inline JSON input (e.g. '{\"domain\":\"example.com\"}')")
118
+ .option("-f, --file <path>", "Read input from a JSON file")
119
+ .option("--local", "Execute coreLogic locally (fetches tool source, caches it)")
120
+ .action(async (slug, args, opts, cmd) => {
121
+ const jsonMode = cmd.parent.opts().json;
122
+
123
+ // ── Build input body ────────────────────────────────────────────────
124
+ let body = {};
125
+
126
+ // 1. --input flag takes highest priority
127
+ if (opts.input) {
128
+ try {
129
+ body = JSON.parse(opts.input);
130
+ } catch {
131
+ if (jsonMode) return console.log(JSON.stringify({ error: "Invalid JSON in --input flag" }));
132
+ console.error(chalk.red("Error: --input must be valid JSON"));
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ // 2. --file flag reads from a file
138
+ if (opts.file) {
139
+ try {
140
+ const fileContent = fs.readFileSync(path.resolve(opts.file), "utf-8");
141
+ const fileData = JSON.parse(fileContent);
142
+ // Merge file data, with --input taking precedence
143
+ body = { ...fileData, ...body };
144
+ } catch (err) {
145
+ if (jsonMode) return console.log(JSON.stringify({ error: `Failed to read --file: ${err.message}` }));
146
+ console.error(chalk.red(`Error reading file: ${err.message}`));
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ // 3. Key=value args (lowest priority, merged in)
152
+ if (!opts.input && !opts.file) {
153
+ (args || []).forEach((arg) => {
154
+ const [k, ...v] = arg.split("=");
155
+ body[k] = v.join("=");
156
+ });
157
+ }
158
+
159
+ // ── Local execution mode ────────────────────────────────────────────
160
+ if (opts.local) {
161
+ return await executeLocal(slug, body, jsonMode);
162
+ }
163
+
164
+ // ── Remote API execution ────────────────────────────────────────────
165
+ const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || config.get("apiKey");
166
+ if (!apiKey) {
167
+ if (jsonMode) return console.log(JSON.stringify({ error: "API key required. Set NEXUSBLOOM_API_KEY, pass --key, or run `nxb config set key <your-key>`." }));
168
+ console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or run `nxb config set key <your-key>`."));
169
+ process.exit(1);
170
+ }
171
+
172
+ try {
173
+ const res = await fetch(`${API_BASE}/api/run/${slug}`, {
174
+ method: "POST",
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ Authorization: `Bearer ${apiKey}`,
178
+ },
179
+ body: JSON.stringify(body),
180
+ });
181
+ const data = await res.json();
182
+ if (data.success) {
183
+ if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
184
+ console.log(chalk.green("\n Result:"));
185
+ console.log(` ${JSON.stringify(data.data, null, 2)}`);
186
+ } else {
187
+ if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
188
+ console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
189
+ process.exit(1);
190
+ }
191
+ } catch (err) {
192
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
193
+ console.error(chalk.red("Execution failed:"), err.message);
194
+ process.exit(1);
195
+ }
196
+ });
197
+
198
+ // ─── Local execution helper ──────────────────────────────────────────────────
199
+ async function executeLocal(slug, input, jsonMode) {
200
+ ensureCacheDir();
201
+ const cacheFile = path.join(CACHE_DIR, `${slug}.json`);
202
+
203
+ // Try loading from cache first
204
+ let manifest, coreLogicSource;
205
+ if (fs.existsSync(cacheFile)) {
206
+ try {
207
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
208
+ manifest = cached.manifest;
209
+ coreLogicSource = cached.coreLogicSource;
210
+ if (!jsonMode) console.log(chalk.dim(` Using cached tool: ${slug}`));
211
+ } catch {
212
+ // Cache corrupted, refetch
213
+ }
214
+ }
215
+
216
+ // Fetch from API if not cached
217
+ if (!manifest || !coreLogicSource) {
218
+ if (!jsonMode) console.log(chalk.dim(` Fetching tool: ${slug}...`));
219
+ try {
220
+ const res = await fetch(`${API_BASE}/api/run/${slug}`);
221
+ if (!res.ok) {
222
+ if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found` }));
223
+ console.error(chalk.red(`Tool "${slug}" not found.`));
224
+ process.exit(1);
225
+ }
226
+ const data = await res.json();
227
+ manifest = data.manifest || data;
228
+
229
+ // Fetch the v2 source separately
230
+ const sourceRes = await fetch(`${API_BASE}/api/run/${slug}?source=true`);
231
+ if (sourceRes.ok) {
232
+ const sourceData = await sourceRes.json();
233
+ coreLogicSource = sourceData.data?.v2_source;
234
+ } else {
235
+ coreLogicSource = null;
236
+ }
237
+
238
+ // Cache it
239
+ if (manifest) {
240
+ fs.writeFileSync(cacheFile, JSON.stringify({ manifest, coreLogicSource, cachedAt: new Date().toISOString() }), "utf-8");
241
+ }
242
+ } catch (err) {
243
+ if (jsonMode) return console.log(JSON.stringify({ error: `Failed to fetch tool: ${err.message}` }));
244
+ console.error(chalk.red(`Failed to fetch tool: ${err.message}`));
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ if (!coreLogicSource) {
250
+ // Fallback: try to extract coreLogic from the manifest itself
251
+ if (jsonMode) return console.log(JSON.stringify({ error: "No v2 source available for local execution. Try without --local to use remote API." }));
252
+ console.error(chalk.yellow("No v2 source available for local execution. Try without --local to use remote API."));
253
+ process.exit(1);
254
+ }
255
+
256
+ // Execute coreLogic locally
257
+ try {
258
+ // Extract the coreLogic function from the source
259
+ const fnMatch = coreLogicSource.match(/export\s+(async\s+)?function\s+coreLogic[\s\S]*/);
260
+ if (!fnMatch) {
261
+ if (jsonMode) return console.log(JSON.stringify({ error: "Could not extract coreLogic from source" }));
262
+ console.error(chalk.red("Could not extract coreLogic from source"));
263
+ process.exit(1);
264
+ }
265
+ const fnSource = fnMatch[0].replace(/export\s+/, "");
266
+ const isAsync = fnMatch[0].startsWith("export async");
267
+
268
+ // Validate input against manifest
269
+ const schema = manifest.input_schema || {};
270
+ const required = schema.required || [];
271
+ const missing = required.filter((k) => input[k] === undefined || input[k] === null);
272
+ if (missing.length > 0) {
273
+ if (jsonMode) return console.log(JSON.stringify({ error: `Missing required fields: ${missing.join(", ")}` }));
274
+ console.error(chalk.red(`Missing required fields: ${missing.join(", ")}`));
275
+ process.exit(1);
276
+ }
277
+
278
+ // Apply defaults
279
+ if (schema.properties) {
280
+ for (const [key, prop] of Object.entries(schema.properties)) {
281
+ if (input[key] === undefined && prop.default !== undefined) {
282
+ input[key] = prop.default;
283
+ }
284
+ }
285
+ }
286
+
287
+ // Create and execute the function
288
+ const coreLogic = new Function("return " + fnSource)();
289
+ const timeoutMs = manifest.timeout_ms || 5000;
290
+ let result;
291
+
292
+ if (isAsync) {
293
+ result = await Promise.race([
294
+ coreLogic(input),
295
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
296
+ ]);
297
+ } else {
298
+ result = coreLogic(input);
299
+ }
300
+
301
+ if (jsonMode) return console.log(JSON.stringify({ success: true, data: result }, null, 2));
302
+ console.log(chalk.green("\n Result (local):"));
303
+ console.log(` ${JSON.stringify(result, null, 2)}`);
304
+ } catch (err) {
305
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
306
+ console.error(chalk.red(`\n Local execution error: ${err.message}`));
307
+ process.exit(1);
308
+ }
309
+ }
310
+
311
+ // ─── `nxb chat <message>` — describe what you want ───────────────────────────
312
+ program
313
+ .command("chat")
314
+ .description("Describe what you want and let NexusBloom route to the right tools")
315
+ .argument("<message>", "What you want to do")
316
+ .action(async (message, cmd) => {
317
+ const jsonMode = cmd.parent.opts().json;
318
+ try {
319
+ if (!jsonMode) console.log(chalk.dim(" Routing your request..."));
320
+ const res = await fetch(`${API_BASE}/api/v1/route`, {
321
+ method: "POST",
322
+ headers: { "Content-Type": "application/json" },
323
+ body: JSON.stringify({ task: message }),
324
+ });
325
+ const data = await res.json();
326
+ if (jsonMode) return console.log(JSON.stringify(data, null, 2));
327
+ if (data.matched_tools?.length > 0) {
328
+ console.log(chalk.bold(`\n Matched ${data.matched_tools.length} tool${data.matched_tools.length > 1 ? "s" : ""}:\n`));
329
+ data.matched_tools.forEach((t) => {
330
+ console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim(`${Math.round(t.confidence * 100)}% match`)}`);
331
+ });
332
+ console.log();
333
+ if (data.results) {
334
+ console.log(chalk.bold(" Results:\n"));
335
+ data.results.forEach((r) => {
336
+ console.log(` ${chalk.cyan(r.tool)}:`);
337
+ console.log(` ${JSON.stringify(r.data, null, 4)}`);
338
+ });
339
+ }
340
+ } else {
341
+ console.log(chalk.yellow(" No matching tools found."));
342
+ }
343
+ } catch (err) {
344
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
345
+ console.error(chalk.red("Chat failed:"), err.message);
346
+ }
347
+ });
348
+
349
+ // ─── `nxb info <slug>` — show tool schema ────────────────────────────────────
350
+ program
351
+ .command("info")
352
+ .description("Show tool schema and details")
353
+ .argument("<slug>", "Tool slug")
354
+ .action(async (slug, cmd) => {
355
+ const jsonMode = cmd.parent.opts().json;
356
+ try {
357
+ const res = await fetch(`${API_BASE}/api/run/${slug}`);
358
+ if (!res.ok) {
359
+ if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found` }));
360
+ console.error(chalk.red(`Tool "${slug}" not found.`));
361
+ return;
362
+ }
363
+ const data = await res.json();
364
+ const manifest = data.manifest || data;
365
+ if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
366
+ console.log(chalk.bold(`\n ${manifest.name}`));
367
+ console.log(` ${chalk.dim(manifest.short_description || "")}`);
368
+ console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
369
+ console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
370
+ console.log(` Pricing: ${chalk.cyan(manifest.pricing || "free")}`);
371
+ if (manifest.input_schema?.properties) {
372
+ console.log(chalk.bold(`\n Input Parameters:`));
373
+ const props = manifest.input_schema.properties;
374
+ const required = manifest.input_schema.required || [];
375
+ for (const [name, prop] of Object.entries(props)) {
376
+ const req = required.includes(name) ? chalk.red(" *required") : "";
377
+ console.log(` ${chalk.cyan(name)} (${prop.type})${req}`);
378
+ if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
379
+ }
380
+ }
381
+ if (manifest.output_schema?.properties) {
382
+ console.log(chalk.bold(`\n Output:`));
383
+ for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
384
+ console.log(` ${chalk.cyan(name)} (${prop.type})`);
385
+ }
386
+ }
387
+ console.log();
388
+ } catch (err) {
389
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
390
+ console.error(chalk.red("Failed to fetch tool info:"), err.message);
391
+ }
392
+ });
393
+
394
+ // ─── `nxb transform <slug>` — pipe stdin through a tool ──────────────────────
395
+ program
396
+ .command("transform")
397
+ .description("Pipe stdin through a tool and write result to stdout")
398
+ .argument("<slug>", "Tool slug")
399
+ .option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
400
+ .option("--local", "Execute locally (faster for repeated use)")
401
+ .action(async (slug, opts, cmd) => {
402
+ const jsonMode = cmd.parent.opts().json;
403
+ const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || config.get("apiKey");
404
+
405
+ const input = await readStdin();
406
+ if (!input) {
407
+ if (jsonMode) return console.log(JSON.stringify({ error: "No input received from stdin" }));
408
+ console.error(chalk.red("No input received from stdin. Pipe data into this command."));
409
+ process.exit(1);
410
+ }
411
+
412
+ let body;
413
+ try {
414
+ body = JSON.parse(input);
415
+ } catch {
416
+ body = { input };
417
+ }
418
+
419
+ if (opts.local) {
420
+ return await executeLocal(slug, body, jsonMode);
421
+ }
422
+
423
+ if (!apiKey) {
424
+ if (jsonMode) return console.log(JSON.stringify({ error: "API key required" }));
425
+ console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or use --local mode."));
426
+ process.exit(1);
427
+ }
428
+
429
+ try {
430
+ const res = await fetch(`${API_BASE}/api/run/${slug}`, {
431
+ method: "POST",
432
+ headers: {
433
+ "Content-Type": "application/json",
434
+ Authorization: `Bearer ${apiKey}`,
435
+ },
436
+ body: JSON.stringify(body),
437
+ });
438
+ const data = await res.json();
439
+ if (data.success) {
440
+ const output = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
441
+ if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
442
+ process.stdout.write(output + "\n");
443
+ } else {
444
+ if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
445
+ console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
446
+ process.exit(1);
447
+ }
448
+ } catch (err) {
449
+ if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
450
+ console.error(chalk.red("Transform failed:"), err.message);
451
+ process.exit(1);
452
+ }
453
+ });
454
+
455
+ // ─── `nxb config` — manage configuration ─────────────────────────────────────
456
+ const configCmd = program
457
+ .command("config")
458
+ .description("Manage CLI configuration");
459
+
460
+ configCmd
461
+ .command("set")
462
+ .description("Set a config value")
463
+ .argument("<key>", "Config key (key, default-tool)")
464
+ .argument("<value>", "Config value")
465
+ .action((key, value) => {
466
+ config.set(key, value);
467
+ console.log(chalk.green(` Set ${key} to "${value}"`));
468
+ });
469
+
470
+ configCmd
471
+ .command("get")
472
+ .description("Get a config value")
473
+ .argument("[key]", "Config key (omit to show all)")
474
+ .action((key) => {
475
+ if (key) {
476
+ const value = config.get(key);
477
+ if (value === undefined) {
478
+ console.log(chalk.yellow(` ${key} is not set`));
479
+ } else {
480
+ console.log(` ${key}: ${value}`);
481
+ }
482
+ } else {
483
+ const store = config.store;
484
+ if (Object.keys(store).length === 0) {
485
+ console.log(chalk.yellow(" No config values set."));
486
+ } else {
487
+ Object.entries(store).forEach(([k, v]) => {
488
+ console.log(` ${k}: ${v}`);
489
+ });
490
+ }
491
+ }
492
+ });
493
+
494
+ configCmd
495
+ .command("list")
496
+ .description("List all config values (alias for config get)")
497
+ .action(() => {
498
+ const store = config.store;
499
+ if (Object.keys(store).length === 0) {
500
+ console.log(chalk.yellow(" No config values set."));
501
+ } else {
502
+ Object.entries(store).forEach(([k, v]) => {
503
+ console.log(` ${k}: ${v}`);
504
+ });
505
+ }
506
+ });
507
+
508
+ // ─── `nxb cache` — manage local tool cache ─────────────────────────────────
509
+ const cacheCmd = program
510
+ .command("cache")
511
+ .description("Manage local tool cache");
512
+
513
+ cacheCmd
514
+ .command("clear")
515
+ .description("Clear the local tool cache")
516
+ .action(() => {
517
+ ensureCacheDir();
518
+ const files = fs.readdirSync(CACHE_DIR);
519
+ let count = 0;
520
+ for (const file of files) {
521
+ fs.unlinkSync(path.join(CACHE_DIR, file));
522
+ count++;
523
+ }
524
+ console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
525
+ });
526
+
527
+ cacheCmd
528
+ .command("list")
529
+ .description("List cached tools")
530
+ .action(() => {
531
+ ensureCacheDir();
532
+ const files = fs.readdirSync(CACHE_DIR);
533
+ if (files.length === 0) {
534
+ console.log(chalk.yellow(" No cached tools."));
535
+ return;
536
+ }
537
+ console.log(chalk.bold(`\n ${files.length} cached tool${files.length > 1 ? "s" : ""}:\n`));
538
+ files.forEach((f) => {
539
+ const slug = f.replace(/\.json$/, "");
540
+ const stats = fs.statSync(path.join(CACHE_DIR, f));
541
+ const age = Math.round((Date.now() - stats.mtimeMs) / 1000 / 60);
542
+ console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago`)}`);
543
+ });
544
+ console.log();
545
+ });
546
+
547
+ program.parse(process.argv);
548
+
549
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
550
+
551
+ function readStdin() {
552
+ return new Promise((resolve) => {
553
+ const chunks = [];
554
+ process.stdin.setEncoding("utf8");
555
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
556
+ process.stdin.on("end", () => resolve(chunks.join("")));
557
+ if (process.stdin.isTTY) resolve("");
558
+ });
559
+ }