@nexusbloom/cli 0.2.11 → 0.3.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/src/index.js CHANGED
@@ -4,88 +4,46 @@
4
4
  * NexusBloom CLI (nxb)
5
5
  *
6
6
  * Usage:
7
- * nxb list — List available tools
7
+ * nxb list — List available tools (interactive pagination)
8
+ * nxb list -s <query> — Filter + paginate
8
9
  * 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)
10
+ * nxb run <slug> [args...] — Execute a tool (remote API)
11
+ * nxb run <slug> --input '{}' — Execute with inline JSON input
12
+ * nxb run <slug> --file input.json — Execute with file input
13
+ * nxb run <slug> --local — Execute coreLogic locally (cached)
14
+ * nxb run — Interactive: select tool → pick inputs → auto-build command
13
15
  * nxb info <slug> — Show tool schema
14
- * nxb transform <slug> — Pipe stdin through a tool
15
- * nxb config set <key> [value] — Set config (interactive if value omitted)
16
- * nxb config get [key] — Get config
17
- * nxb cache clear — Clear local tool cache
16
+ * nxb abbr — Show tool abbreviations (paginated)
17
+ * nxb transform <slug> — Pipe stdin through a tool
18
+ * nxb config set <key> [value] — Set config (interactive if value omitted)
19
+ * nxb config get [key] — Get config
20
+ * nxb cache clear — Clear local tool cache
21
+ * nxb cache list — List cached tools
22
+ * nxb create — Scaffold a new tool locally
23
+ * nxb json --template <slug> — Create JSON input for tools
18
24
  */
19
25
 
20
26
  import { program } from "commander";
21
27
  import chalk from "chalk";
22
- import { createRequire } from "module";
23
28
  import fs from "fs";
24
29
  import path from "path";
25
- import { fileURLToPath } from "url";
26
30
  import readline from "readline";
27
- import QRCode from "qrcode";
31
+ import { createRequire } from "module";
28
32
 
29
33
  const require = createRequire(import.meta.url);
30
34
  const { version } = require("../package.json");
31
35
 
32
- const API_BASE = process.env.NEXUSBLOOM_API_URL || "https://www.nexusbloom.dev";
33
-
34
- // ─── Simple file-based config (avoids `conf` dependency issues) ───────────
35
- const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(require("os").homedir(), ".config"), "nexusbloom");
36
- const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
37
-
38
- function ensureConfigDir() {
39
- if (!fs.existsSync(CONFIG_DIR)) {
40
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
41
- }
42
- }
43
-
44
- function loadConfig() {
45
- try {
46
- if (fs.existsSync(CONFIG_PATH)) {
47
- return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
48
- }
49
- } catch { /* ignore corrupt config */ }
50
- return {};
51
- }
52
-
53
- function saveConfig(store) {
54
- ensureConfigDir();
55
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(store, null, 2), "utf-8");
56
- }
57
-
58
- function configGet(key) {
59
- const store = loadConfig();
60
- if (key) return store[key];
61
- return store;
62
- }
63
-
64
- function configSet(key, value) {
65
- const store = loadConfig();
66
- store[key] = value;
67
- saveConfig(store);
68
- }
69
-
70
- function promptValue(key) {
71
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
72
- return new Promise((resolve) => {
73
- rl.question(chalk.cyan(` Enter value for "${key}": `), (answer) => {
74
- rl.close();
75
- resolve(answer.trim());
76
- });
77
- });
78
- }
79
-
80
- // ─── Cache directory for local tool execution ────────────────────────────────
81
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
82
- const CACHE_DIR = path.resolve(__dirname, "../.cache/tools");
83
-
84
- function ensureCacheDir() {
85
- if (!fs.existsSync(CACHE_DIR)) {
86
- fs.mkdirSync(CACHE_DIR, { recursive: true });
87
- }
88
- }
36
+ // ─── Local modules ──────────────────────────────────────────────────────────
37
+ import { API_BASE } from "./tools.js";
38
+ import {
39
+ loadConfig, saveConfig, configGet, configSet,
40
+ fetchTools, fetchToolSource, resolveToolSlug,
41
+ generateAbbreviations, readStdin,
42
+ } from "./tools.js";
43
+ import { runLocal, runRemote, buildInputBody, runInteractive } from "./run.js";
44
+ import listAction from "./list.js";
45
+ import abbrAction from "./abbr.js";
46
+ import { TOOLS_CACHE_DIR, TOOLS_LIST_CACHE_PATH } from "./paths.js";
89
47
 
90
48
  // ─── Program ─────────────────────────────────────────────────────────────────
91
49
  program
@@ -94,61 +52,42 @@ program
94
52
  .version(version)
95
53
  .option("--json", "Output in JSON format");
96
54
 
97
- // ─── `nxb list` — list available tools ───────────────────────────────────────
55
+ // ─── `nxb list` ──────────────────────────────────────────────────────────────
98
56
  program
99
57
  .command("list")
100
- .description("List available tools")
101
- .option("-s, --search <query>", "Search tools by name or description")
58
+ .description("List available tools (interactive pagination)")
59
+ .option("-s, --search <query>", "Search tools by name, slug, category, or tags")
60
+ .option("--no-interactive", "Disable interactive navigation")
102
61
  .action(async (opts, cmd) => {
103
- const jsonMode = cmd.parent.opts().json || opts.json;
104
- let tools = await fetchToolsWithFallback() || [];
105
- // Merge in local tools from filesystem
106
- const localTools = scanLocalTools();
107
- const localSlugs = new Set(localTools.map(t => t.slug));
108
- tools = [...localTools, ...tools.filter(t => !localSlugs.has(t.slug))];
109
- if (tools.length === 0) {
110
- if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
111
- console.log(chalk.yellow("No tools found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
112
- return;
113
- }
114
- let filtered = tools;
115
- if (opts.search) {
116
- const q = opts.search.toLowerCase();
117
- filtered = tools.filter(t =>
118
- (t.slug || "").includes(q) ||
119
- (t.name || "").toLowerCase().includes(q) ||
120
- (t.description || "").toLowerCase().includes(q) ||
121
- (t.category || "").includes(q)
122
- );
123
- }
124
- if (jsonMode) return console.log(JSON.stringify({ tools: filtered }, null, 2));
125
- console.log(chalk.bold(`\n ${filtered.length} tool${filtered.length > 1 ? "s" : ""} available\n`));
126
- filtered.forEach((t) => {
127
- const typeBadge = t.type === "community" ? chalk.dim("[community]") : t.type === "local" ? chalk.cyan("[local]") : "";
128
- console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.description || "").slice(0, 50))} ${typeBadge}`);
62
+ const jsonMode = cmd.parent.opts().json;
63
+ await listAction({
64
+ search: opts.search,
65
+ json: jsonMode,
66
+ interactive: opts.interactive !== false,
129
67
  });
130
- console.log();
131
68
  });
132
69
 
133
- // ─── `nxb search <query>` — search tools ─────────────────────────────────────
70
+ // ─── `nxb search <query>` ────────────────────────────────────────────────────
134
71
  program
135
72
  .command("search")
136
73
  .description("Search for tools")
137
74
  .argument("<query>", "Search query")
138
75
  .action(async (query, opts, cmd) => {
139
- const jsonMode = (cmd?.parent?.opts().json) || false;
140
- const tools = await fetchToolsWithFallback();
76
+ const jsonMode = cmd?.parent?.opts().json || false;
77
+ const tools = await fetchTools();
141
78
  if (!tools || tools.length === 0) {
142
79
  if (jsonMode) return console.log(JSON.stringify({ tools: [], query }));
143
80
  console.log(chalk.yellow(`No tools found matching "${query}".`));
144
81
  return;
145
82
  }
146
83
  const q = query.toLowerCase();
147
- const filtered = tools.filter(t =>
148
- (t.slug || "").includes(q) ||
149
- (t.name || "").toLowerCase().includes(q) ||
150
- (t.description || "").toLowerCase().includes(q) ||
151
- (t.category || "").includes(q)
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))
152
91
  );
153
92
  if (filtered.length === 0) {
154
93
  if (jsonMode) return console.log(JSON.stringify({ tools: [], query }));
@@ -156,18 +95,19 @@ program
156
95
  return;
157
96
  }
158
97
  if (jsonMode) return console.log(JSON.stringify({ tools: filtered, query }, null, 2));
98
+
159
99
  console.log(chalk.bold(`\n ${filtered.length} result${filtered.length > 1 ? "s" : ""} for "${query}"\n`));
160
100
  filtered.forEach((t) => {
161
- console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
101
+ console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.short_description || t.description || "").slice(0, 60))}`);
162
102
  });
163
103
  console.log();
164
104
  });
165
105
 
166
- // ─── `nxb run <slug>` — execute a tool ───────────────────────────────────────
106
+ // ─── `nxb run [slug] [args...]` ──────────────────────────────────────────────
167
107
  program
168
108
  .command("run")
169
- .description("Execute a tool")
170
- .argument("<slug>", "Tool slug")
109
+ .description("Execute a tool (interactive if no slug is given)")
110
+ .argument("[slug]", "Tool slug or abbreviation")
171
111
  .argument("[args...]", "Key=value arguments (e.g. domain=example.com)")
172
112
  .option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
173
113
  .option("-i, --input <json>", "Inline JSON input or @- for stdin (e.g. '{\"domain\":\"example.com\"}' or @-)")
@@ -176,367 +116,77 @@ program
176
116
  .action(async (slug, args, opts, cmd) => {
177
117
  const jsonMode = cmd.parent.opts().json;
178
118
 
179
- // ── Resolve abbreviation ────────────────────────────────────────────
180
- const allTools = await fetchToolsWithFallback();
119
+ // ── Interactive mode: no slug provided ──
120
+ if (!slug) {
121
+ return await runInteractive({ json: jsonMode });
122
+ }
123
+
124
+ // ── Resolve abbreviation ──
125
+ const allTools = await fetchTools();
181
126
  const slugs = (allTools || []).map((t) => t.slug);
182
127
  const abbrMap = generateAbbreviations(slugs);
128
+ let resolved = slug;
183
129
  if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
184
- if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
185
- slug = abbrMap[slug];
186
- }
187
-
188
- // ── Build input body ────────────────────────────────────────────────
189
- let body = {};
190
-
191
- // 0. Detect stdin pipe (auto-detect or explicit @-)
192
- const isPiped = opts.input === "@-" || opts.input === "@stdin" || (!opts.input && !opts.file && !process.stdin.isTTY);
193
- if (isPiped) {
194
- try {
195
- const stdin = await readStdin();
196
- if (stdin && stdin.trim()) {
197
- body = JSON.parse(stdin);
198
- }
199
- } catch {
200
- if (jsonMode) return console.log(JSON.stringify({ error: "Invalid JSON from stdin" }));
201
- console.error(chalk.red("Error: Invalid JSON received from stdin"));
202
- process.exit(1);
203
- }
204
- }
205
-
206
- // 1. --input flag takes highest priority (skip if @- was used)
207
- if (opts.input && opts.input !== "@-" && opts.input !== "@stdin") {
208
- try {
209
- body = JSON.parse(opts.input);
210
- } catch {
211
- if (jsonMode) return console.log(JSON.stringify({ error: "Invalid JSON in --input flag" }));
212
- console.error(chalk.red("Error: --input must be valid JSON"));
213
- process.exit(1);
214
- }
130
+ resolved = abbrMap[slug];
131
+ if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${resolved}`));
215
132
  }
216
133
 
217
- // 2. --file flag reads from a file
218
- if (opts.file) {
219
- try {
220
- const fileContent = fs.readFileSync(path.resolve(opts.file), "utf-8");
221
- const fileData = JSON.parse(fileContent);
222
- // Merge file data, with --input taking precedence
223
- body = { ...fileData, ...body };
224
- } catch (err) {
225
- if (jsonMode) return console.log(JSON.stringify({ error: `Failed to read --file: ${err.message}` }));
226
- console.error(chalk.red(`Error reading file: ${err.message}`));
227
- process.exit(1);
228
- }
229
- }
230
-
231
- // 3. Key=value args (lowest priority, merged in)
232
- if (!opts.input && !opts.file) {
233
- (args || []).forEach((arg) => {
234
- const [k, ...v] = arg.split("=");
235
- body[k] = v.join("=");
236
- });
237
- }
238
-
239
- // ── Local execution mode ────────────────────────────────────────────
240
- if (opts.local) {
241
- return await executeLocal(slug, body, jsonMode);
242
- }
134
+ // ── Build input body ──
135
+ const body = await buildInputBody(opts, args);
136
+ if (body === null) return;
243
137
 
244
- // ── Remote API execution ────────────────────────────────────────────
245
- const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
246
- if (!apiKey) {
247
- // No API key — try local mode automatically
248
- return await executeLocal(slug, body, jsonMode);
138
+ // ── Local vs remote ──
139
+ if (opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"))) {
140
+ return await runLocal(resolved, body, { jsonMode, slugResolved: true });
249
141
  }
250
142
 
251
- try {
252
- const res = await fetch(`${API_BASE}/api/run/${slug}`, {
253
- method: "POST",
254
- headers: {
255
- "Content-Type": "application/json",
256
- Authorization: `Bearer ${apiKey}`,
257
- },
258
- body: JSON.stringify(body),
259
- });
260
-
261
- if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
262
- // API returned non-JSON (likely not deployed) — fall back to local
263
- if (!jsonMode) console.log(chalk.dim(` API not deployed, falling back to local execution...`));
264
- return await executeLocal(slug, body, jsonMode);
265
- }
266
-
267
- const data = await res.json();
268
- if (data.success) {
269
- if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
270
- console.log(chalk.green("\n Result:"));
271
- // Try to render QR code if the output is SVG-based
272
- if (data.data && typeof data.data === "object" && data.data.svg) {
273
- await renderQRCode(body.data || body.text || JSON.stringify(body), slug);
274
- }
275
- console.log(` ${JSON.stringify(data.data, null, 2)}`);
276
- } else {
277
- if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
278
- console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
279
- process.exit(1);
280
- }
281
- } catch (err) {
282
- // Network error — fall back to local
283
- if (!jsonMode) console.log(chalk.dim(` API unreachable, falling back to local execution...`));
284
- return await executeLocal(slug, body, jsonMode);
285
- }
143
+ return await runRemote(resolved, body, {
144
+ apiKey: opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key"),
145
+ jsonMode,
146
+ slugResolved: true,
147
+ });
286
148
  });
287
149
 
288
- // ─── Local execution helper ──────────────────────────────────────────────────
289
- async function executeLocal(slug, input, jsonMode) {
290
- ensureCacheDir();
291
- const cacheFile = path.join(CACHE_DIR, `${slug}.json`);
292
-
293
- // Try loading from local filesystem first (fastest, most up-to-date)
294
- let manifest, coreLogicSource;
295
- const localToolPath = findLocalTool(slug);
296
- if (localToolPath) {
297
- if (!jsonMode) console.log(chalk.dim(` Using local tool: ${slug}`));
298
- const source = fs.readFileSync(localToolPath, "utf-8");
299
- const manifestMatch = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
300
- if (manifestMatch) {
301
- try {
302
- manifest = new Function("return " + manifestMatch[1])();
303
- } catch { /* will fall through to cache/API */ }
304
- }
305
- coreLogicSource = source;
306
- // Save to cache for next time
307
- if (manifest) {
308
- fs.writeFileSync(cacheFile, JSON.stringify({ manifest, coreLogicSource, cachedAt: new Date().toISOString() }), "utf-8");
309
- }
310
- }
311
-
312
- // Try loading from cache second
313
- if (!manifest || !coreLogicSource) {
314
- if (fs.existsSync(cacheFile)) {
315
- try {
316
- const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
317
- manifest = cached.manifest;
318
- coreLogicSource = cached.coreLogicSource;
319
- if (!jsonMode) console.log(chalk.dim(` Using cached tool: ${slug}`));
320
- } catch {
321
- // Cache corrupted
322
- }
323
- }
324
- }
325
-
326
- // Fetch from API or Supabase if still not found
327
- if (!manifest || !coreLogicSource) {
328
- if (!jsonMode) console.log(chalk.dim(` Fetching tool: ${slug}...`));
329
-
330
- // Try API first
331
- let apiSuccess = false;
332
- try {
333
- const res = await fetch(`${API_BASE}/api/run/${slug}`);
334
- if (res.ok) {
335
- const data = await res.json();
336
- manifest = data.data?.manifest || data.manifest || data;
337
-
338
- const sourceRes = await fetch(`${API_BASE}/api/run/${slug}?source=true`);
339
- if (sourceRes.ok) {
340
- const sourceData = await sourceRes.json();
341
- coreLogicSource = sourceData.data?.v2_source;
342
- }
343
- apiSuccess = true;
344
- }
345
- } catch {
346
- // API failed, try Supabase fallback
347
- }
348
-
349
- // Fallback: query Supabase directly via shared helper
350
- if (!apiSuccess) {
351
- try {
352
- const toolInfo = await fetchToolFromSupabase(slug);
353
- if (toolInfo) {
354
- manifest = toolInfo.manifest;
355
- coreLogicSource = toolInfo.coreLogicSource;
356
- apiSuccess = true;
357
- }
358
- } catch {
359
- // Supabase fallback also failed
360
- }
361
- }
362
-
363
- if (!apiSuccess) {
364
- if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run \`nxb config set supabaseKey <your-anon-key>\` for offline mode.` }));
365
- console.error(chalk.red(`Tool "${slug}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run \`nxb config set supabaseKey <your-anon-key>\` for offline mode.`));
366
- process.exit(1);
367
- }
368
-
369
- // Cache it
370
- if (manifest) {
371
- fs.writeFileSync(cacheFile, JSON.stringify({ manifest, coreLogicSource, cachedAt: new Date().toISOString() }), "utf-8");
372
- }
373
- }
374
-
375
- if (!coreLogicSource) {
376
- // Fallback: try to extract coreLogic from the manifest itself
377
- if (jsonMode) return console.log(JSON.stringify({ error: "No v2 source available for local execution. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run `nxb config set supabaseKey <your-key>`." }));
378
- console.error(chalk.yellow("No v2 source available for local execution. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run `nxb config set supabaseKey <your-key>`."));
379
- process.exit(1);
380
- }
381
-
382
- // Execute coreLogic locally
383
- try {
384
- // Strip imports and exports, then wrap the full module scope in an IIFE
385
- // that returns coreLogic — this preserves module-scoped helpers (e.g. generateField)
386
- const cleanSource = coreLogicSource
387
- .replace(/^import\s+.*?;?\s*$/gm, "") // Remove import statements
388
- .replace(/^export\s+/gm, ""); // Remove export keywords
389
- const wrappedSource = `(function() {\n${cleanSource}\nreturn coreLogic;\n})()`;
390
- const coreLogicFn = new Function("return " + wrappedSource)();
391
- const isAsync = /export\s+async\s+function\s+coreLogic/.test(coreLogicSource);
392
-
393
- // Validate input against manifest
394
- const schema = manifest.input_schema || {};
395
- const required = schema.required || [];
396
- const missing = required.filter((k) => input[k] === undefined || input[k] === null);
397
- if (missing.length > 0) {
398
- if (jsonMode) return console.log(JSON.stringify({ error: `Missing required fields: ${missing.join(", ")}` }));
399
- console.error(chalk.red(`Missing required fields: ${missing.join(", ")}`));
400
- process.exit(1);
401
- }
402
-
403
- // Apply defaults
404
- if (schema.properties) {
405
- for (const [key, prop] of Object.entries(schema.properties)) {
406
- if (input[key] === undefined && prop.default !== undefined) {
407
- input[key] = prop.default;
408
- }
409
- }
410
- }
411
-
412
- // Create and execute the function
413
- const timeoutMs = manifest.timeout_ms || 5000;
414
- let result;
415
-
416
- if (isAsync) {
417
- result = await Promise.race([
418
- coreLogicFn(input),
419
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
420
- ]);
421
- } else {
422
- result = coreLogicFn(input);
423
- }
424
-
425
- // Fire-and-forget: track the run for total runs counting and api_usage
426
- const apiKey = process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
427
- const trackHeaders = { "Content-Type": "application/json" };
428
- if (apiKey) trackHeaders["Authorization"] = `Bearer ${apiKey}`;
429
- fetch(`${API_BASE}/api/track-run`, {
430
- method: "POST",
431
- headers: trackHeaders,
432
- body: JSON.stringify({ slug, source: "cli" }),
433
- }).catch(() => {});
434
-
435
- if (jsonMode) return console.log(JSON.stringify({ success: true, data: result }, null, 2));
436
- console.log(chalk.green("\n Result (local):"));
437
- console.log(` ${JSON.stringify(result, null, 2)}`);
438
- } catch (err) {
439
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
440
- console.error(chalk.red(`\n Local execution error: ${err.message}`));
441
- process.exit(1);
442
- }
443
- }
444
-
445
- // ─── `nxb info <slug>` — show tool schema ────────────────────────────────────
150
+ // ─── `nxb info <slug>` ───────────────────────────────────────────────────────
446
151
  program
447
152
  .command("info")
448
153
  .description("Show tool schema and details")
449
154
  .argument("<slug>", "Tool slug or abbreviation")
450
155
  .action(async (slug, opts, cmd) => {
451
- const jsonMode = (cmd?.parent?.opts().json) || false;
452
-
453
- // Resolve abbreviation
454
- const allTools = await fetchToolsWithFallback();
455
- const slugs = (allTools || []).map((t) => t.slug);
456
- const abbrMap = generateAbbreviations(slugs);
457
- if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
458
- if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
459
- slug = abbrMap[slug];
460
- }
156
+ const jsonMode = cmd?.parent?.opts().json || false;
157
+ const resolved = await resolveToolSlug(slug);
461
158
 
462
- // Try to get manifest — API first, then Supabase fallback
463
- let manifest = null;
464
- try {
465
- const res = await fetch(`${API_BASE}/api/run/${slug}`);
466
- if (res.ok) {
467
- const data = await res.json();
468
- manifest = data.data?.manifest || data.manifest || data;
469
- }
470
- } catch { /* API failed */ }
471
-
472
- if (!manifest) {
473
- // Try Supabase fallback
474
- try {
475
- const toolInfo = await fetchToolFromSupabase(slug);
476
- if (toolInfo) manifest = toolInfo.manifest;
477
- } catch { /* fallback failed */ }
478
- }
159
+ const toolInfo = await fetchToolSource(resolved);
160
+ const manifest = toolInfo?.manifest;
479
161
 
480
- // Fallback: check local filesystem
481
162
  if (!manifest) {
482
- const localPath = findLocalTool(slug);
483
- if (localPath) {
484
- const source = fs.readFileSync(localPath, "utf-8");
485
- const match = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
486
- if (match) {
487
- try { manifest = new Function("return " + match[1])(); } catch {}
488
- }
489
- }
490
- }
491
-
492
- if (!manifest) {
493
- if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found.` }));
494
- console.error(chalk.red(`Tool "${slug}" not found.`));
163
+ if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${resolved}" not found.` }));
164
+ console.error(chalk.red(`Tool "${resolved}" not found.`));
495
165
  return;
496
166
  }
497
167
 
498
168
  if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
499
- const toolName = manifest.name || manifest.slug || slug;
500
- console.log(chalk.bold(`\n ${toolName}`));
501
- console.log(` ${chalk.dim(manifest.short_description || "")}`);
502
- console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
503
- console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
504
- console.log(` Pricing: ${chalk.green("free")}`);
505
- if (manifest.input_schema?.properties) {
506
- console.log(chalk.bold(`\n Input Parameters:`));
507
- const props = manifest.input_schema.properties;
508
- const required = manifest.input_schema.required || [];
509
- for (const [name, prop] of Object.entries(props)) {
510
- const req = required.includes(name) ? chalk.red(" *required") : "";
511
- console.log(` ${chalk.cyan(name)} (${prop.type})${req}`);
512
- if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
513
- }
514
- }
515
- if (manifest.output_schema?.properties) {
516
- console.log(chalk.bold(`\n Output:`));
517
- for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
518
- console.log(` ${chalk.cyan(name)} (${prop.type})`);
519
- }
520
- }
521
- console.log();
169
+
170
+ renderToolInfo(manifest, resolved);
522
171
  });
523
172
 
524
- // ─── `nxb transform <slug>` — pipe stdin through a tool ──────────────────────
173
+ // ─── `nxb transform <slug>` ──────────────────────────────────────────────────
525
174
  program
526
175
  .command("transform")
527
176
  .description("Pipe stdin through a tool and write result to stdout")
528
- .argument("<slug>", "Tool slug")
177
+ .argument("<slug>", "Tool slug or abbreviation")
529
178
  .option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
530
179
  .option("--local", "Execute locally (faster for repeated use)")
531
180
  .action(async (slug, opts, cmd) => {
181
+ const resolved = await resolveToolSlug(slug);
532
182
  const jsonMode = cmd.parent.opts().json;
533
- const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
534
183
 
535
184
  const input = await readStdin();
536
185
  if (!input) {
537
186
  if (jsonMode) return console.log(JSON.stringify({ error: "No input received from stdin" }));
538
187
  console.error(chalk.red("No input received from stdin. Pipe data into this command."));
539
- process.exit(1);
188
+ process.exitCode = 1;
189
+ return;
540
190
  }
541
191
 
542
192
  let body;
@@ -546,46 +196,37 @@ program
546
196
  body = { input };
547
197
  }
548
198
 
549
- if (opts.local) {
550
- return await executeLocal(slug, body, jsonMode);
199
+ if (opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"))) {
200
+ return await runLocal(resolved, body, { jsonMode, slugResolved: true });
551
201
  }
552
202
 
203
+ const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
553
204
  if (!apiKey) {
554
205
  if (jsonMode) return console.log(JSON.stringify({ error: "API key required" }));
555
206
  console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or use --local mode."));
556
- process.exit(1);
207
+ process.exitCode = 1;
208
+ return;
557
209
  }
558
210
 
559
- try {
560
- const res = await fetch(`${API_BASE}/api/run/${slug}`, {
561
- method: "POST",
562
- headers: {
563
- "Content-Type": "application/json",
564
- Authorization: `Bearer ${apiKey}`,
565
- },
566
- body: JSON.stringify(body),
567
- });
568
- if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
569
- if (!jsonMode) console.log(chalk.dim(` API not deployed, falling back to local execution...`));
570
- return await executeLocal(slug, body, jsonMode);
571
- }
572
- const data = await res.json();
573
- if (data.success) {
574
- const output = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
575
- if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
576
- process.stdout.write(output + "\n");
577
- } else {
578
- if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
579
- console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
580
- process.exit(1);
581
- }
582
- } catch (err) {
583
- if (!jsonMode) console.log(chalk.dim(` API unreachable, falling back to local execution...`));
584
- return await executeLocal(slug, body, jsonMode);
585
- }
211
+ await runRemote(resolved, body, { apiKey, jsonMode, slugResolved: true });
586
212
  });
587
213
 
588
- // ─── `nxb config` — manage configuration ─────────────────────────────────────
214
+ // ─── `nxb abbr [query]` ──────────────────────────────────────────────────────
215
+ program
216
+ .command("abbr")
217
+ .description("Show tool abbreviations (paginated)")
218
+ .argument("[query]", "Search/filter abbreviations")
219
+ .option("--no-interactive", "Disable interactive navigation")
220
+ .action(async (query, opts, cmd) => {
221
+ const jsonMode = cmd.parent.opts().json;
222
+ await abbrAction({
223
+ query,
224
+ json: jsonMode,
225
+ interactive: opts.interactive !== false,
226
+ });
227
+ });
228
+
229
+ // ─── `nxb config` ────────────────────────────────────────────────────────────
589
230
  const configCmd = program
590
231
  .command("config")
591
232
  .description("Manage CLI configuration");
@@ -645,7 +286,7 @@ configCmd
645
286
  }
646
287
  });
647
288
 
648
- // ─── `nxb cache` — manage local tool cache ─────────────────────────────────
289
+ // ─── `nxb cache` ─────────────────────────────────────────────────────────────
649
290
  const cacheCmd = program
650
291
  .command("cache")
651
292
  .description("Manage local tool cache");
@@ -654,13 +295,20 @@ cacheCmd
654
295
  .command("clear")
655
296
  .description("Clear the local tool cache")
656
297
  .action(() => {
657
- ensureCacheDir();
658
- const files = fs.readdirSync(CACHE_DIR);
298
+ if (!fs.existsSync(TOOLS_CACHE_DIR)) {
299
+ console.log(chalk.yellow(" No cache directory found."));
300
+ return;
301
+ }
302
+ const files = fs.readdirSync(TOOLS_CACHE_DIR);
659
303
  let count = 0;
660
304
  for (const file of files) {
661
- fs.unlinkSync(path.join(CACHE_DIR, file));
305
+ fs.unlinkSync(path.join(TOOLS_CACHE_DIR, file));
662
306
  count++;
663
307
  }
308
+ // Also remove tools-list.json
309
+ if (fs.existsSync(TOOLS_LIST_CACHE_PATH)) {
310
+ fs.unlinkSync(TOOLS_LIST_CACHE_PATH);
311
+ }
664
312
  console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
665
313
  });
666
314
 
@@ -668,8 +316,11 @@ cacheCmd
668
316
  .command("list")
669
317
  .description("List cached tools")
670
318
  .action(() => {
671
- ensureCacheDir();
672
- const files = fs.readdirSync(CACHE_DIR);
319
+ if (!fs.existsSync(TOOLS_CACHE_DIR)) {
320
+ console.log(chalk.yellow(" No cached tools."));
321
+ return;
322
+ }
323
+ const files = fs.readdirSync(TOOLS_CACHE_DIR);
673
324
  if (files.length === 0) {
674
325
  console.log(chalk.yellow(" No cached tools."));
675
326
  return;
@@ -677,441 +328,14 @@ cacheCmd
677
328
  console.log(chalk.bold(`\n ${files.length} cached tool${files.length > 1 ? "s" : ""}:\n`));
678
329
  files.forEach((f) => {
679
330
  const slug = f.replace(/\.json$/, "");
680
- const stats = fs.statSync(path.join(CACHE_DIR, f));
331
+ const stats = fs.statSync(path.join(TOOLS_CACHE_DIR, f));
681
332
  const age = Math.round((Date.now() - stats.mtimeMs) / 1000 / 60);
682
333
  console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago`)}`);
683
334
  });
684
335
  console.log();
685
336
  });
686
337
 
687
- // ─── Abbreviation generator ──────────────────────────────────────────────────
688
-
689
- function generateAbbreviations(slugs) {
690
- const abbrMap = {};
691
- const used = new Set();
692
-
693
- for (const slug of slugs) {
694
- // Try first letter of each word
695
- const parts = slug.split("-");
696
- let abbr = parts.map((p) => p[0]).join("").toLowerCase();
697
-
698
- // If taken, try longer combinations
699
- if (used.has(abbr) || abbr.length < 2) {
700
- for (let len = 2; len <= slug.length; len++) {
701
- abbr = slug.slice(0, len);
702
- if (!used.has(abbr)) break;
703
- }
704
- }
705
-
706
- used.add(abbr);
707
- abbrMap[slug] = abbr;
708
- abbrMap[abbr] = slug;
709
- }
710
-
711
- return abbrMap;
712
- }
713
-
714
- // ─── `nxb abbr` — show tool abbreviations ────────────────────────────────────
715
-
716
- program
717
- .command("abbr")
718
- .description("Show tool abbreviations")
719
- .argument("[query]", "Search/filter abbreviations (e.g. 'ip' shows all IP-related tools)")
720
- .action(async (query, opts, cmd) => {
721
- const tools = await fetchToolsWithFallback();
722
- if (!tools || tools.length === 0) {
723
- console.log(chalk.yellow(" No tools available. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
724
- return;
725
- }
726
- const slugs = tools.map((t) => t.slug);
727
- const abbrMap = generateAbbreviations(slugs);
728
-
729
- // Filter by query if provided
730
- let filtered = slugs;
731
- if (query) {
732
- const q = query.toLowerCase();
733
- filtered = slugs.filter(
734
- (slug) => slug.includes(q) || abbrMap[slug].includes(q) || (tools || []).find(t => t.slug === slug)?.name?.toLowerCase().includes(q)
735
- );
736
- if (filtered.length === 0) {
737
- console.log(chalk.yellow(` No abbreviations matching "${query}"`));
738
- return;
739
- }
740
- }
741
-
742
- console.log(chalk.bold(`\n Tool Abbreviations${query ? ` matching "${query}"` : ""}:\n`));
743
- filtered.forEach((slug) => {
744
- const tool = (tools || []).find(t => t.slug === slug);
745
- const name = tool?.name || slug;
746
- console.log(` ${chalk.cyan(abbrMap[slug].padEnd(15))} → ${chalk.dim(slug.padEnd(30))} ${chalk.gray(name)}`);
747
- });
748
- console.log();
749
- });
750
-
751
- // ─── `nxb json` — create JSON input for tools ───────────────────────────────
752
-
753
- program
754
- .command("json")
755
- .description("Create JSON input for tools (pipe into nxb run)")
756
- .option("-t, --template <slug>", "Generate JSON template for a specific tool")
757
- .option("-i, --interactive", "Interactive mode - prompts for each field")
758
- .option("-s, --set <key=value...>", "Set specific field values (e.g. -s count=10 format=csv)")
759
- .option("-a, --add <json>", "Merge additional JSON into the output (e.g. -a '{\"count\":20}')")
760
- .action(async (opts, cmd) => {
761
- const jsonMode = cmd.parent.opts().json;
762
-
763
- if (opts.template) {
764
- // Fetch tool schema — try API first, then Supabase
765
- let manifest = null;
766
- try {
767
- const res = await fetch(`${API_BASE}/api/run/${opts.template}`);
768
- if (res.ok) {
769
- const data = await res.json();
770
- manifest = data.data?.manifest || data.manifest || data;
771
- }
772
- } catch { /* API failed */ }
773
-
774
- if (!manifest) {
775
- const toolInfo = await fetchToolFromSupabase(opts.template);
776
- if (toolInfo) manifest = toolInfo.manifest;
777
- }
778
-
779
- if (!manifest) {
780
- console.error(chalk.red(`Tool "${opts.template}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API.`));
781
- process.exit(1);
782
- }
783
- const schema = manifest.input_schema || {};
784
-
785
- let result = {};
786
-
787
- if (opts.interactive) {
788
- // Interactive mode
789
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
790
- const ask = (q) => new Promise((r) => rl.question(q, r));
791
-
792
- if (schema.properties) {
793
- for (const [key, prop] of Object.entries(schema.properties)) {
794
- const required = schema.required?.includes(key);
795
- const hint = prop.description ? ` (${prop.description})` : "";
796
- const def = prop.default ? ` [${prop.default}]` : "";
797
- const answer = await ask(chalk.cyan(` ${key}${hint}${def}: `));
798
- const val = answer || prop.default;
799
- if (val !== undefined && val !== "") {
800
- result[key] = prop.type === "number" || prop.type === "integer" ? Number(val) : val;
801
- }
802
- }
803
- }
804
- rl.close();
805
- } else {
806
- // Generate template with defaults
807
- if (schema.properties) {
808
- for (const [key, prop] of Object.entries(schema.properties)) {
809
- if (prop.default !== undefined) {
810
- result[key] = prop.default;
811
- } else if (prop.type === "string") {
812
- result[key] = prop.enum ? prop.enum[0] : "";
813
- } else if (prop.type === "number" || prop.type === "integer") {
814
- result[key] = 0;
815
- } else if (prop.type === "boolean") {
816
- result[key] = false;
817
- } else if (prop.type === "array") {
818
- result[key] = [];
819
- } else if (prop.type === "object") {
820
- result[key] = {};
821
- }
822
- }
823
- }
824
- }
825
-
826
- // ── Apply --set overrides ──────────────────────────────────────
827
- if (opts.set) {
828
- const sets = Array.isArray(opts.set) ? opts.set : [opts.set];
829
- for (const pair of sets) {
830
- const eqIdx = pair.indexOf("=");
831
- if (eqIdx > 0) {
832
- const k = pair.slice(0, eqIdx);
833
- let v = pair.slice(eqIdx + 1);
834
- // Try parsing as number or boolean
835
- if (v === "true") v = true;
836
- else if (v === "false") v = false;
837
- else if (!isNaN(v) && v.trim() !== "") v = Number(v);
838
- result[k] = v;
839
- }
840
- }
841
- }
842
-
843
- // ── Apply --add merge ──────────────────────────────────────────
844
- if (opts.add) {
845
- try {
846
- const extra = typeof opts.add === "string" ? JSON.parse(opts.add) : opts.add;
847
- result = { ...result, ...extra };
848
- } catch {
849
- console.error(chalk.red("Error: --add must be valid JSON"));
850
- process.exit(1);
851
- }
852
- }
853
-
854
- const output = JSON.stringify(result, null, jsonMode ? 2 : 0);
855
- if (jsonMode) return console.log(output);
856
- console.log(output);
857
- } else {
858
- console.log(chalk.yellow("Usage:"));
859
- console.log(chalk.dim(" nxb json --template <slug> Generate JSON template with defaults"));
860
- console.log(chalk.dim(" nxb json --template <slug> -i Interactive mode (prompts for fields)"));
861
- console.log(chalk.dim(" nxb json --template <slug> -s count=10 Set specific field values"));
862
- console.log(chalk.dim(" nxb json --template <slug> -a '{\"x\":1}' Merge additional JSON"));
863
- console.log(chalk.dim(""));
864
- console.log(chalk.dim(" Examples:"));
865
- console.log(chalk.dim(" nxb json --template data-generator | nxb run data-generator --input @-"));
866
- console.log(chalk.dim(" nxb run data-generator --input \"$(nxb json --template dg -s count=5)\""));
867
- console.log(chalk.dim(" nxb json --template color-palette -s color=#4A5D23 --json"));
868
- }
869
- });
870
-
871
- // ─── Supabase Helpers ─────────────────────────────────────────────────────────
872
-
873
- const SUPABASE_URL_DEFAULT = "https://dhxaqatmuprehrkszxzs.supabase.co";
874
- const SUPABASE_ANON_KEY_DEFAULT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoeGFxYXRtdXByZWhya3N6eHpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMxNjIzNTQsImV4cCI6MjA4ODczODM1NH0.pq7DxW66ETQt6QlIAu2WiLO08tfA7JHQVPd2SvvA3sM";
875
-
876
- function getSupabaseKey() {
877
- return process.env.NEXUSBLOOM_SUPABASE_ANON_KEY
878
- || configGet("supabaseKey")
879
- || SUPABASE_ANON_KEY_DEFAULT;
880
- }
881
-
882
- function getSupabaseUrl() {
883
- return process.env.NEXUSBLOOM_SUPABASE_URL
884
- || SUPABASE_URL_DEFAULT;
885
- }
886
-
887
- function supabaseHeaders() {
888
- const key = getSupabaseKey();
889
- return { apikey: key, Authorization: `Bearer ${key}` };
890
- }
891
-
892
- /**
893
- * Fetch tool list from Supabase directly (fallback when API is down).
894
- * Returns array of {slug, name, short_description, category, runtime, version, icon} or null.
895
- */
896
- async function fetchToolsFromSupabase() {
897
- const headers = supabaseHeaders();
898
- const supabaseUrl = getSupabaseUrl();
899
- try {
900
- const res = await fetch(
901
- `${supabaseUrl}/rest/v1/tools?select=slug,name,short_description,tool_type,runtime,version,icon&published=eq.true&order=name.asc`,
902
- { headers }
903
- );
904
- if (!res.ok) return null;
905
- const tools = await res.json();
906
- if (!Array.isArray(tools)) return null;
907
-
908
- // Also fetch community tools
909
- const communityRes = await fetch(
910
- `${supabaseUrl}/rest/v1/community_tools?select=slug,name,short_description,category,version,icon,author_id&status=eq.published&order=name.asc`,
911
- { headers }
912
- );
913
- const communityTools = communityRes.ok ? (await communityRes.json()) : [];
914
-
915
- const allTools = [
916
- ...tools.map(t => ({
917
- slug: t.slug,
918
- name: t.name,
919
- description: t.short_description || "",
920
- category: t.tool_type || "uncategorized",
921
- runtime: t.runtime || "both",
922
- version: t.version || "1.0.0",
923
- icon: t.icon || "🔧",
924
- type: "builtin",
925
- })),
926
- ...communityTools.map(t => ({
927
- slug: t.slug,
928
- name: t.name,
929
- description: t.short_description || "",
930
- category: t.category || "uncategorized",
931
- runtime: "both",
932
- version: t.version || "1.0.0",
933
- icon: t.icon || "🛠️",
934
- type: "community",
935
- author_id: t.author_id,
936
- })),
937
- ];
938
-
939
- return allTools;
940
- } catch {
941
- return null;
942
- }
943
- }
944
-
945
- /**
946
- * Fetch a single tool's v2_source from Supabase (fallback).
947
- * Returns {manifest, coreLogicSource, v2Source} or null.
948
- */
949
- async function fetchToolFromSupabase(slug) {
950
- const headers = supabaseHeaders();
951
- const supabaseUrl = getSupabaseUrl();
952
- try {
953
- // Try builtin tools first
954
- const toolRes = await fetch(
955
- `${supabaseUrl}/rest/v1/tools?slug=eq.${slug}&select=id&published=eq.true`,
956
- { headers }
957
- );
958
- let tools = await toolRes.json();
959
- let toolId = tools?.[0]?.id;
960
- let toolType = "builtin";
961
-
962
- // Fall back to community tools
963
- if (!toolId) {
964
- const comRes = await fetch(
965
- `${supabaseUrl}/rest/v1/community_tools?slug=eq.${slug}&select=id&status=eq.published`,
966
- { headers }
967
- );
968
- const comTools = await comRes.json();
969
- toolId = comTools?.[0]?.id;
970
- toolType = "community";
971
- }
972
-
973
- if (!toolId) return null;
974
-
975
- const versionRes = await fetch(
976
- `${supabaseUrl}/rest/v1/tool_versions?tool_id=eq.${toolId}&tool_type=eq.${toolType}&status=eq.published&select=v2_source&order=created_at.desc&limit=1`,
977
- { headers }
978
- );
979
- const versions = await versionRes.json();
980
- const v2Source = versions?.[0]?.v2_source;
981
- if (!v2Source) return null;
982
-
983
- const manifestMatch = v2Source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});\s*\n/);
984
- let manifest = null;
985
- if (manifestMatch) {
986
- try {
987
- manifest = new Function("return " + manifestMatch[1])();
988
- } catch {
989
- try {
990
- manifest = JSON.parse(manifestMatch[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
991
- } catch {
992
- manifest = eval("(" + manifestMatch[1] + ")");
993
- }
994
- }
995
- }
996
- return { manifest, coreLogicSource: v2Source, v2Source };
997
- } catch {
998
- return null;
999
- }
1000
- }
1001
-
1002
- /**
1003
- * Try API first, fall back to Supabase. Returns array or null.
1004
- */
1005
- async function fetchToolsWithFallback() {
1006
- // Try API first
1007
- try {
1008
- const url = new URL("/api/tools", API_BASE);
1009
- const res = await fetch(url);
1010
- const ct = res.headers.get("content-type") || "";
1011
- if (ct.includes("json")) {
1012
- const { tools } = await res.json();
1013
- if (Array.isArray(tools) && tools.length > 0) return tools;
1014
- }
1015
- } catch { /* API failed */ }
1016
-
1017
- // Fall back to Supabase
1018
- const supabaseTools = await fetchToolsFromSupabase();
1019
- if (supabaseTools) return supabaseTools;
1020
- return null;
1021
- }
1022
-
1023
- // ─── Helpers ─────────────────────────────────────────────────────────────────
1024
-
1025
- /**
1026
- * Search the local filesystem for a tool v2.js file.
1027
- * Checks common locations relative to CWD.
1028
- */
1029
- function findLocalTool(slug) {
1030
- const cwd = process.cwd();
1031
- const candidates = [
1032
- path.join(cwd, "src", "tools", slug, "v2.js"),
1033
- path.join(cwd, "tools", slug, "v2.js"),
1034
- path.join(cwd, slug, "v2.js"),
1035
- path.join(cwd, "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
1036
- path.join(cwd, "..", "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
1037
- ];
1038
- try {
1039
- const config = loadConfig();
1040
- const toolsDir = config.toolsDir;
1041
- if (toolsDir && !candidates.some(c => c.includes(toolsDir))) {
1042
- candidates.unshift(path.join(cwd, toolsDir, slug, "v2.js"));
1043
- }
1044
- } catch {}
1045
- for (const candidate of candidates) {
1046
- if (fs.existsSync(candidate)) return candidate;
1047
- }
1048
- return null;
1049
- }
1050
-
1051
- /**
1052
- * Scan the local filesystem for all tool v2.js files.
1053
- * Returns array of { slug, name, description, type: "local" }.
1054
- */
1055
- function scanLocalTools() {
1056
- const cwd = process.cwd();
1057
- const searchDirs = [
1058
- path.join(cwd, "src", "tools"),
1059
- path.join(cwd, "tools"),
1060
- path.join(cwd, "apps", "main-site", "frontend", "src", "tools"),
1061
- ];
1062
- const tools = [];
1063
- for (const dir of searchDirs) {
1064
- if (!fs.existsSync(dir)) continue;
1065
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1066
- for (const entry of entries) {
1067
- if (!entry.isDirectory()) continue;
1068
- const v2Path = path.join(dir, entry.name, "v2.js");
1069
- if (!fs.existsSync(v2Path)) continue;
1070
- const source = fs.readFileSync(v2Path, "utf-8");
1071
- const match = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
1072
- if (!match) continue;
1073
- try {
1074
- const m = new Function("return " + match[1])();
1075
- tools.push({
1076
- slug: m.slug || entry.name,
1077
- name: m.name || entry.name,
1078
- description: m.short_description || "",
1079
- category: m.category || "uncategorized",
1080
- runtime: m.runtime || "both",
1081
- version: m.version || "1.0.0",
1082
- icon: m.icon || "🔧",
1083
- type: "local",
1084
- });
1085
- } catch {}
1086
- }
1087
- }
1088
- return tools;
1089
- }
1090
-
1091
- function readStdin() {
1092
- return new Promise((resolve) => {
1093
- const chunks = [];
1094
- process.stdin.setEncoding("utf8");
1095
- process.stdin.on("data", (chunk) => chunks.push(chunk));
1096
- process.stdin.on("end", () => resolve(chunks.join("")));
1097
- if (process.stdin.isTTY) resolve("");
1098
- });
1099
- }
1100
-
1101
- // ─── QR Code Terminal Renderer ────────────────────────────────────────────
1102
-
1103
- async function renderQRCode(data, slug) {
1104
- try {
1105
- const qr = await QRCode.toString(data, { type: "terminal", small: true });
1106
- console.log(chalk.cyan("\n QR Code:"));
1107
- console.log(qr);
1108
- console.log(chalk.dim(` Data: ${data.slice(0, 80)}${data.length > 80 ? "..." : ""}`));
1109
- } catch {
1110
- // Silently skip if QR rendering fails
1111
- }
1112
- }
1113
-
1114
- // ─── `nxb create` — scaffold a new tool locally ──────────────────────────
338
+ // ─── `nxb create` ────────────────────────────────────────────────────────────
1115
339
  const VALID_RUNTIMES = ["browser", "node", "both"];
1116
340
  const VALID_CATEGORIES = [
1117
341
  "text", "image", "data", "network", "security", "development",
@@ -1128,13 +352,9 @@ program
1128
352
  .option("--runtime <runtime>", `Runtime (${VALID_RUNTIMES.join(", ")})`)
1129
353
  .option("--dir <dir>", "Target directory (default: ./src/tools/<slug>)")
1130
354
  .action(async (options) => {
1131
- const rl = readline.createInterface({
1132
- input: process.stdin,
1133
- output: process.stdout,
1134
- });
355
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1135
356
  const ask = (q) => new Promise((r) => rl.question(q, r));
1136
357
 
1137
- // Gather inputs
1138
358
  let slug = options.slug;
1139
359
  while (!slug || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
1140
360
  slug = (await ask(chalk.cyan("Tool slug (e.g. my-tool): "))).trim().toLowerCase();
@@ -1180,7 +400,6 @@ program
1180
400
 
1181
401
  rl.close();
1182
402
 
1183
- // Generate v2.js source
1184
403
  const pascalName = slug
1185
404
  .split("-")
1186
405
  .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
@@ -1261,27 +480,24 @@ export default function ${pascalName}Tool({ onResult }) {
1261
480
  }
1262
481
  `;
1263
482
 
1264
- // Determine target directory
1265
483
  const targetDir = options.dir || path.join(process.cwd(), "src/tools", slug);
1266
484
  const v2Path = path.join(targetDir, "v2.js");
1267
485
  const indexPath = path.join(targetDir, "index.js");
1268
486
 
1269
- // Check for existing files
1270
487
  if (fs.existsSync(v2Path) || fs.existsSync(indexPath)) {
1271
- const overwrite = (await new Promise((r) => {
488
+ const overwrite = await new Promise((r) => {
1272
489
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
1273
490
  rl2.question(chalk.yellow(`Tool "${slug}" already exists. Overwrite? (y/N): `), (a) => {
1274
491
  rl2.close();
1275
492
  r(a.trim().toLowerCase() === "y");
1276
493
  });
1277
- }));
494
+ });
1278
495
  if (!overwrite) {
1279
496
  console.log(chalk.dim("Aborted."));
1280
497
  return;
1281
498
  }
1282
499
  }
1283
500
 
1284
- // Write files
1285
501
  fs.mkdirSync(targetDir, { recursive: true });
1286
502
  fs.writeFileSync(v2Path, v2Source, "utf-8");
1287
503
  fs.writeFileSync(indexPath, indexSource, "utf-8");
@@ -1295,4 +511,153 @@ export default function ${pascalName}Tool({ onResult }) {
1295
511
  console.log(chalk.dim(" 3. Publish from the VS Code extension or API\n"));
1296
512
  });
1297
513
 
1298
- program.parse(process.argv);
514
+ // ─── `nxb json` ─────────────────────────────────────────────────────────────
515
+ program
516
+ .command("json")
517
+ .description("Create JSON input for tools (pipe into nxb run)")
518
+ .option("-t, --template <slug>", "Generate JSON template for a specific tool")
519
+ .option("-i, --interactive", "Interactive mode - prompts for each field")
520
+ .option("-s, --set <key=value...>", "Set specific field values (e.g. -s count=10 format=csv)")
521
+ .option("-a, --add <json>", "Merge additional JSON into the output (e.g. -a '{\"count\":20}')")
522
+ .action(async (opts, cmd) => {
523
+ const jsonMode = cmd.parent.opts().json;
524
+
525
+ if (opts.template) {
526
+ const resolved = await resolveToolSlug(opts.template);
527
+ const toolInfo = await fetchToolSource(resolved);
528
+ const manifest = toolInfo?.manifest;
529
+
530
+ if (!manifest) {
531
+ console.error(chalk.red(`Tool "${resolved}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API.`));
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+
536
+ const schema = manifest.input_schema || {};
537
+ let result = {};
538
+
539
+ if (opts.interactive) {
540
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
541
+ const ask = (q) => new Promise((r) => rl.question(q, r));
542
+
543
+ if (schema.properties) {
544
+ for (const [key, prop] of Object.entries(schema.properties)) {
545
+ const required = schema.required?.includes(key);
546
+ const hint = prop.description ? ` (${prop.description})` : "";
547
+ const def = prop.default !== undefined ? ` [${JSON.stringify(prop.default)}]` : "";
548
+ const enumInfo = prop.enum && prop.enum.length > 0
549
+ ? ` {${prop.enum.map((v) => JSON.stringify(v)).join("|")}}`
550
+ : "";
551
+ const answer = await ask(chalk.cyan(` ${key}${hint}${enumInfo}${def}: `));
552
+ const val = answer || prop.default;
553
+ if (val !== undefined && val !== "") {
554
+ result[key] = prop.type === "number" || prop.type === "integer" ? Number(val) : val;
555
+ }
556
+ }
557
+ }
558
+ rl.close();
559
+ } else {
560
+ if (schema.properties) {
561
+ for (const [key, prop] of Object.entries(schema.properties)) {
562
+ if (prop.default !== undefined) {
563
+ result[key] = prop.default;
564
+ } else if (prop.type === "string") {
565
+ result[key] = prop.enum ? prop.enum[0] : "";
566
+ } else if (prop.type === "number" || prop.type === "integer") {
567
+ result[key] = 0;
568
+ } else if (prop.type === "boolean") {
569
+ result[key] = false;
570
+ } else if (prop.type === "array") {
571
+ result[key] = [];
572
+ } else if (prop.type === "object") {
573
+ result[key] = {};
574
+ }
575
+ }
576
+ }
577
+ }
578
+
579
+ if (opts.set) {
580
+ const sets = Array.isArray(opts.set) ? opts.set : [opts.set];
581
+ for (const pair of sets) {
582
+ const eqIdx = pair.indexOf("=");
583
+ if (eqIdx > 0) {
584
+ const k = pair.slice(0, eqIdx);
585
+ let v = pair.slice(eqIdx + 1);
586
+ if (v === "true") v = true;
587
+ else if (v === "false") v = false;
588
+ else if (!isNaN(v) && v.trim() !== "") v = Number(v);
589
+ result[k] = v;
590
+ }
591
+ }
592
+ }
593
+
594
+ if (opts.add) {
595
+ try {
596
+ const extra = typeof opts.add === "string" ? JSON.parse(opts.add) : opts.add;
597
+ result = { ...result, ...extra };
598
+ } catch {
599
+ console.error(chalk.red("Error: --add must be valid JSON"));
600
+ process.exitCode = 1;
601
+ }
602
+ }
603
+
604
+ const output = JSON.stringify(result, null, jsonMode ? 2 : 0);
605
+ if (jsonMode) return console.log(output);
606
+ console.log(output);
607
+ } else {
608
+ console.log(chalk.yellow("Usage:"));
609
+ console.log(chalk.dim(" nxb json --template <slug> Generate JSON template with defaults"));
610
+ console.log(chalk.dim(" nxb json --template <slug> -i Interactive mode (prompts for fields)"));
611
+ console.log(chalk.dim(" nxb json --template <slug> -s count=10 Set specific field values"));
612
+ console.log(chalk.dim(" nxb json --template <slug> -a '{\"x\":1}' Merge additional JSON"));
613
+ console.log(chalk.dim(""));
614
+ console.log(chalk.dim(" Examples:"));
615
+ console.log(chalk.dim(" nxb json --template data-generator | nxb run data-generator --input @-"));
616
+ console.log(chalk.dim(" nxb run data-generator --input \"$(nxb json --template dg -s count=5)\""));
617
+ console.log(chalk.dim(" nxb json --template color-palette -s color=#4A5D23 --json"));
618
+ }
619
+ });
620
+
621
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
622
+
623
+ function promptValue(key) {
624
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
625
+ return new Promise((resolve) => {
626
+ rl.question(chalk.cyan(` Enter value for "${key}": `), (answer) => {
627
+ rl.close();
628
+ resolve(answer.trim());
629
+ });
630
+ });
631
+ }
632
+
633
+ function renderToolInfo(manifest, resolved) {
634
+ const toolName = manifest.name || manifest.slug || resolved;
635
+ console.log(chalk.bold(`\n ${toolName}`));
636
+ console.log(` ${chalk.dim(manifest.short_description || "")}`);
637
+ console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
638
+ console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
639
+ console.log(` Pricing: ${chalk.green("free")}`);
640
+ if (manifest.input_schema?.properties) {
641
+ console.log(chalk.bold(`\n Input Parameters:`));
642
+ const props = manifest.input_schema.properties;
643
+ const required = manifest.input_schema.required || [];
644
+ for (const [name, prop] of Object.entries(props)) {
645
+ const req = required.includes(name) ? chalk.red(" *required") : "";
646
+ const def = prop.default !== undefined ? chalk.dim(` [default: ${JSON.stringify(prop.default)}]`) : "";
647
+ console.log(` ${chalk.cyan(name)} (${prop.type})${req}${def}`);
648
+ if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
649
+ if (prop.enum && prop.enum.length > 0) {
650
+ console.log(` ${chalk.dim("Allowed values:")} ${chalk.cyan(prop.enum.map((v) => JSON.stringify(v)).join(", "))}`);
651
+ }
652
+ }
653
+ }
654
+ if (manifest.output_schema?.properties) {
655
+ console.log(chalk.bold(`\n Output:`));
656
+ for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
657
+ console.log(` ${chalk.cyan(name)} (${prop.type})`);
658
+ }
659
+ }
660
+ console.log();
661
+ }
662
+
663
+ program.parse(process.argv);