@nexusbloom/cli 0.2.12 → 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/README.md +96 -19
- package/package.json +2 -1
- package/src/abbr.js +111 -0
- package/src/index.js +278 -940
- package/src/list.js +107 -0
- package/src/paths.js +42 -0
- package/src/run.js +380 -0
- package/src/tools.js +561 -0
- package/src/ui.js +371 -0
package/src/index.js
CHANGED
|
@@ -4,88 +4,46 @@
|
|
|
4
4
|
* NexusBloom CLI (nxb)
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* nxb list
|
|
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...]
|
|
10
|
-
* nxb run <slug> --input '{}'
|
|
11
|
-
* nxb run <slug> --file input.json
|
|
12
|
-
* nxb run <slug> --local
|
|
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
|
|
15
|
-
* nxb
|
|
16
|
-
* nxb config
|
|
17
|
-
* nxb
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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`
|
|
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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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>`
|
|
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 =
|
|
140
|
-
const tools = await
|
|
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(
|
|
148
|
-
(t
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
106
|
+
// ─── `nxb run [slug] [args...]` ──────────────────────────────────────────────
|
|
167
107
|
program
|
|
168
108
|
.command("run")
|
|
169
|
-
.description("Execute a tool")
|
|
170
|
-
.argument("
|
|
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,356 +116,61 @@ program
|
|
|
176
116
|
.action(async (slug, args, opts, cmd) => {
|
|
177
117
|
const jsonMode = cmd.parent.opts().json;
|
|
178
118
|
|
|
179
|
-
// ──
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
slug
|
|
130
|
+
resolved = abbrMap[slug];
|
|
131
|
+
if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${resolved}`));
|
|
186
132
|
}
|
|
187
133
|
|
|
188
|
-
// ── Build input body
|
|
189
|
-
|
|
134
|
+
// ── Build input body ──
|
|
135
|
+
const body = await buildInputBody(opts, args);
|
|
136
|
+
if (body === null) return;
|
|
190
137
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
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
|
-
}
|
|
243
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
// ───
|
|
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 =
|
|
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
|
-
}
|
|
461
|
-
|
|
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 */ }
|
|
156
|
+
const jsonMode = cmd?.parent?.opts().json || false;
|
|
157
|
+
const resolved = await resolveToolSlug(slug);
|
|
471
158
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
const toolInfo = await fetchToolFromSupabase(slug);
|
|
476
|
-
if (toolInfo) manifest = toolInfo.manifest;
|
|
477
|
-
} catch { /* fallback failed */ }
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Fallback: check local filesystem
|
|
481
|
-
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
|
-
}
|
|
159
|
+
const toolInfo = await fetchToolSource(resolved);
|
|
160
|
+
const manifest = toolInfo?.manifest;
|
|
491
161
|
|
|
492
162
|
if (!manifest) {
|
|
493
|
-
if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${
|
|
494
|
-
console.error(chalk.red(`Tool "${
|
|
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
|
-
|
|
500
|
-
|
|
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
|
-
const def = prop.default !== undefined ? chalk.dim(` [default: ${JSON.stringify(prop.default)}]`) : "";
|
|
512
|
-
console.log(` ${chalk.cyan(name)} (${prop.type})${req}${def}`);
|
|
513
|
-
if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
|
|
514
|
-
if (prop.enum && prop.enum.length > 0) {
|
|
515
|
-
console.log(` ${chalk.dim("Allowed values:")} ${chalk.cyan(prop.enum.map(v => JSON.stringify(v)).join(", "))}`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (manifest.output_schema?.properties) {
|
|
520
|
-
console.log(chalk.bold(`\n Output:`));
|
|
521
|
-
for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
|
|
522
|
-
console.log(` ${chalk.cyan(name)} (${prop.type})`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
console.log();
|
|
169
|
+
|
|
170
|
+
renderToolInfo(manifest, resolved);
|
|
526
171
|
});
|
|
527
172
|
|
|
528
|
-
// ─── `nxb transform <slug>`
|
|
173
|
+
// ─── `nxb transform <slug>` ──────────────────────────────────────────────────
|
|
529
174
|
program
|
|
530
175
|
.command("transform")
|
|
531
176
|
.description("Pipe stdin through a tool and write result to stdout")
|
|
@@ -533,17 +178,15 @@ program
|
|
|
533
178
|
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
534
179
|
.option("--local", "Execute locally (faster for repeated use)")
|
|
535
180
|
.action(async (slug, opts, cmd) => {
|
|
536
|
-
|
|
537
|
-
slug = await resolveToolSlug(slug);
|
|
538
|
-
|
|
181
|
+
const resolved = await resolveToolSlug(slug);
|
|
539
182
|
const jsonMode = cmd.parent.opts().json;
|
|
540
|
-
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
541
183
|
|
|
542
184
|
const input = await readStdin();
|
|
543
185
|
if (!input) {
|
|
544
186
|
if (jsonMode) return console.log(JSON.stringify({ error: "No input received from stdin" }));
|
|
545
187
|
console.error(chalk.red("No input received from stdin. Pipe data into this command."));
|
|
546
|
-
process.
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
547
190
|
}
|
|
548
191
|
|
|
549
192
|
let body;
|
|
@@ -553,46 +196,37 @@ program
|
|
|
553
196
|
body = { input };
|
|
554
197
|
}
|
|
555
198
|
|
|
556
|
-
if (opts.local) {
|
|
557
|
-
return await
|
|
199
|
+
if (opts.local || (!opts.key && !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key"))) {
|
|
200
|
+
return await runLocal(resolved, body, { jsonMode, slugResolved: true });
|
|
558
201
|
}
|
|
559
202
|
|
|
203
|
+
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
560
204
|
if (!apiKey) {
|
|
561
205
|
if (jsonMode) return console.log(JSON.stringify({ error: "API key required" }));
|
|
562
206
|
console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or use --local mode."));
|
|
563
|
-
process.
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return;
|
|
564
209
|
}
|
|
565
210
|
|
|
566
|
-
|
|
567
|
-
const res = await fetch(`${API_BASE}/api/run/${slug}`, {
|
|
568
|
-
method: "POST",
|
|
569
|
-
headers: {
|
|
570
|
-
"Content-Type": "application/json",
|
|
571
|
-
Authorization: `Bearer ${apiKey}`,
|
|
572
|
-
},
|
|
573
|
-
body: JSON.stringify(body),
|
|
574
|
-
});
|
|
575
|
-
if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
|
|
576
|
-
if (!jsonMode) console.log(chalk.dim(` API not deployed, falling back to local execution...`));
|
|
577
|
-
return await executeLocal(slug, body, jsonMode);
|
|
578
|
-
}
|
|
579
|
-
const data = await res.json();
|
|
580
|
-
if (data.success) {
|
|
581
|
-
const output = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
|
|
582
|
-
if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
|
|
583
|
-
process.stdout.write(output + "\n");
|
|
584
|
-
} else {
|
|
585
|
-
if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
|
|
586
|
-
console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
|
|
587
|
-
process.exit(1);
|
|
588
|
-
}
|
|
589
|
-
} catch (err) {
|
|
590
|
-
if (!jsonMode) console.log(chalk.dim(` API unreachable, falling back to local execution...`));
|
|
591
|
-
return await executeLocal(slug, body, jsonMode);
|
|
592
|
-
}
|
|
211
|
+
await runRemote(resolved, body, { apiKey, jsonMode, slugResolved: true });
|
|
593
212
|
});
|
|
594
213
|
|
|
595
|
-
// ─── `nxb
|
|
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` ────────────────────────────────────────────────────────────
|
|
596
230
|
const configCmd = program
|
|
597
231
|
.command("config")
|
|
598
232
|
.description("Manage CLI configuration");
|
|
@@ -652,7 +286,7 @@ configCmd
|
|
|
652
286
|
}
|
|
653
287
|
});
|
|
654
288
|
|
|
655
|
-
// ─── `nxb cache`
|
|
289
|
+
// ─── `nxb cache` ─────────────────────────────────────────────────────────────
|
|
656
290
|
const cacheCmd = program
|
|
657
291
|
.command("cache")
|
|
658
292
|
.description("Manage local tool cache");
|
|
@@ -661,13 +295,20 @@ cacheCmd
|
|
|
661
295
|
.command("clear")
|
|
662
296
|
.description("Clear the local tool cache")
|
|
663
297
|
.action(() => {
|
|
664
|
-
|
|
665
|
-
|
|
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);
|
|
666
303
|
let count = 0;
|
|
667
304
|
for (const file of files) {
|
|
668
|
-
fs.unlinkSync(path.join(
|
|
305
|
+
fs.unlinkSync(path.join(TOOLS_CACHE_DIR, file));
|
|
669
306
|
count++;
|
|
670
307
|
}
|
|
308
|
+
// Also remove tools-list.json
|
|
309
|
+
if (fs.existsSync(TOOLS_LIST_CACHE_PATH)) {
|
|
310
|
+
fs.unlinkSync(TOOLS_LIST_CACHE_PATH);
|
|
311
|
+
}
|
|
671
312
|
console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
|
|
672
313
|
});
|
|
673
314
|
|
|
@@ -675,8 +316,11 @@ cacheCmd
|
|
|
675
316
|
.command("list")
|
|
676
317
|
.description("List cached tools")
|
|
677
318
|
.action(() => {
|
|
678
|
-
|
|
679
|
-
|
|
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);
|
|
680
324
|
if (files.length === 0) {
|
|
681
325
|
console.log(chalk.yellow(" No cached tools."));
|
|
682
326
|
return;
|
|
@@ -684,461 +328,14 @@ cacheCmd
|
|
|
684
328
|
console.log(chalk.bold(`\n ${files.length} cached tool${files.length > 1 ? "s" : ""}:\n`));
|
|
685
329
|
files.forEach((f) => {
|
|
686
330
|
const slug = f.replace(/\.json$/, "");
|
|
687
|
-
const stats = fs.statSync(path.join(
|
|
331
|
+
const stats = fs.statSync(path.join(TOOLS_CACHE_DIR, f));
|
|
688
332
|
const age = Math.round((Date.now() - stats.mtimeMs) / 1000 / 60);
|
|
689
333
|
console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago`)}`);
|
|
690
334
|
});
|
|
691
335
|
console.log();
|
|
692
336
|
});
|
|
693
337
|
|
|
694
|
-
// ───
|
|
695
|
-
|
|
696
|
-
function generateAbbreviations(slugs) {
|
|
697
|
-
const abbrMap = {};
|
|
698
|
-
const used = new Set();
|
|
699
|
-
|
|
700
|
-
for (const slug of slugs) {
|
|
701
|
-
// Try first letter of each word
|
|
702
|
-
const parts = slug.split("-");
|
|
703
|
-
let abbr = parts.map((p) => p[0]).join("").toLowerCase();
|
|
704
|
-
|
|
705
|
-
// If taken, try longer combinations
|
|
706
|
-
if (used.has(abbr) || abbr.length < 2) {
|
|
707
|
-
for (let len = 2; len <= slug.length; len++) {
|
|
708
|
-
abbr = slug.slice(0, len);
|
|
709
|
-
if (!used.has(abbr)) break;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
used.add(abbr);
|
|
714
|
-
abbrMap[slug] = abbr;
|
|
715
|
-
abbrMap[abbr] = slug;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return abbrMap;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// ─── `nxb abbr` — show tool abbreviations ────────────────────────────────────
|
|
722
|
-
|
|
723
|
-
program
|
|
724
|
-
.command("abbr")
|
|
725
|
-
.description("Show tool abbreviations")
|
|
726
|
-
.argument("[query]", "Search/filter abbreviations (e.g. 'ip' shows all IP-related tools)")
|
|
727
|
-
.action(async (query, opts, cmd) => {
|
|
728
|
-
const tools = await fetchToolsWithFallback();
|
|
729
|
-
if (!tools || tools.length === 0) {
|
|
730
|
-
console.log(chalk.yellow(" No tools available. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
const slugs = tools.map((t) => t.slug);
|
|
734
|
-
const abbrMap = generateAbbreviations(slugs);
|
|
735
|
-
|
|
736
|
-
// Filter by query if provided
|
|
737
|
-
let filtered = slugs;
|
|
738
|
-
if (query) {
|
|
739
|
-
const q = query.toLowerCase();
|
|
740
|
-
filtered = slugs.filter(
|
|
741
|
-
(slug) => slug.includes(q) || abbrMap[slug].includes(q) || (tools || []).find(t => t.slug === slug)?.name?.toLowerCase().includes(q)
|
|
742
|
-
);
|
|
743
|
-
if (filtered.length === 0) {
|
|
744
|
-
console.log(chalk.yellow(` No abbreviations matching "${query}"`));
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
console.log(chalk.bold(`\n Tool Abbreviations${query ? ` matching "${query}"` : ""}:\n`));
|
|
750
|
-
filtered.forEach((slug) => {
|
|
751
|
-
const tool = (tools || []).find(t => t.slug === slug);
|
|
752
|
-
const name = tool?.name || slug;
|
|
753
|
-
console.log(` ${chalk.cyan(abbrMap[slug].padEnd(15))} → ${chalk.dim(slug.padEnd(30))} ${chalk.gray(name)}`);
|
|
754
|
-
});
|
|
755
|
-
console.log();
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
// ─── Shared abbreviation resolver ─────────────────────────────────────────────
|
|
759
|
-
// Extracts the abbr resolution logic so all commands that accept slugs can use it.
|
|
760
|
-
|
|
761
|
-
async function resolveToolSlug(slug) {
|
|
762
|
-
const allTools = await fetchToolsWithFallback();
|
|
763
|
-
if (!allTools) return slug;
|
|
764
|
-
const slugs = allTools.map((t) => t.slug);
|
|
765
|
-
const abbrMap = generateAbbreviations(slugs);
|
|
766
|
-
if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
|
|
767
|
-
return abbrMap[slug];
|
|
768
|
-
}
|
|
769
|
-
return slug;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// ─── `nxb json` — create JSON input for tools ───────────────────────────────
|
|
773
|
-
|
|
774
|
-
program
|
|
775
|
-
.command("json")
|
|
776
|
-
.description("Create JSON input for tools (pipe into nxb run)")
|
|
777
|
-
.option("-t, --template <slug>", "Generate JSON template for a specific tool")
|
|
778
|
-
.option("-i, --interactive", "Interactive mode - prompts for each field")
|
|
779
|
-
.option("-s, --set <key=value...>", "Set specific field values (e.g. -s count=10 format=csv)")
|
|
780
|
-
.option("-a, --add <json>", "Merge additional JSON into the output (e.g. -a '{\"count\":20}')")
|
|
781
|
-
.action(async (opts, cmd) => {
|
|
782
|
-
const jsonMode = cmd.parent.opts().json;
|
|
783
|
-
|
|
784
|
-
if (opts.template) {
|
|
785
|
-
// Resolve abbreviation
|
|
786
|
-
opts.template = await resolveToolSlug(opts.template);
|
|
787
|
-
|
|
788
|
-
// Fetch tool schema — try API first, then Supabase
|
|
789
|
-
let manifest = null;
|
|
790
|
-
try {
|
|
791
|
-
const res = await fetch(`${API_BASE}/api/run/${opts.template}`);
|
|
792
|
-
if (res.ok) {
|
|
793
|
-
const data = await res.json();
|
|
794
|
-
manifest = data.data?.manifest || data.manifest || data;
|
|
795
|
-
}
|
|
796
|
-
} catch { /* API failed */ }
|
|
797
|
-
|
|
798
|
-
if (!manifest) {
|
|
799
|
-
const toolInfo = await fetchToolFromSupabase(opts.template);
|
|
800
|
-
if (toolInfo) manifest = toolInfo.manifest;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (!manifest) {
|
|
804
|
-
console.error(chalk.red(`Tool "${opts.template}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API.`));
|
|
805
|
-
process.exit(1);
|
|
806
|
-
}
|
|
807
|
-
const schema = manifest.input_schema || {};
|
|
808
|
-
|
|
809
|
-
let result = {};
|
|
810
|
-
|
|
811
|
-
if (opts.interactive) {
|
|
812
|
-
// Interactive mode
|
|
813
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
814
|
-
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
815
|
-
|
|
816
|
-
if (schema.properties) {
|
|
817
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
818
|
-
const required = schema.required?.includes(key);
|
|
819
|
-
const hint = prop.description ? ` (${prop.description})` : "";
|
|
820
|
-
const def = prop.default !== undefined ? ` [${JSON.stringify(prop.default)}]` : "";
|
|
821
|
-
const enumInfo = prop.enum && prop.enum.length > 0
|
|
822
|
-
? ` {${prop.enum.map(v => JSON.stringify(v)).join("|")}}`
|
|
823
|
-
: "";
|
|
824
|
-
const answer = await ask(chalk.cyan(` ${key}${hint}${enumInfo}${def}: `));
|
|
825
|
-
const val = answer || prop.default;
|
|
826
|
-
if (val !== undefined && val !== "") {
|
|
827
|
-
result[key] = prop.type === "number" || prop.type === "integer" ? Number(val) : val;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
rl.close();
|
|
832
|
-
} else {
|
|
833
|
-
// Generate template with defaults
|
|
834
|
-
if (schema.properties) {
|
|
835
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
836
|
-
if (prop.default !== undefined) {
|
|
837
|
-
result[key] = prop.default;
|
|
838
|
-
} else if (prop.type === "string") {
|
|
839
|
-
result[key] = prop.enum ? prop.enum[0] : "";
|
|
840
|
-
} else if (prop.type === "number" || prop.type === "integer") {
|
|
841
|
-
result[key] = 0;
|
|
842
|
-
} else if (prop.type === "boolean") {
|
|
843
|
-
result[key] = false;
|
|
844
|
-
} else if (prop.type === "array") {
|
|
845
|
-
result[key] = [];
|
|
846
|
-
} else if (prop.type === "object") {
|
|
847
|
-
result[key] = {};
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// ── Apply --set overrides ──────────────────────────────────────
|
|
854
|
-
if (opts.set) {
|
|
855
|
-
const sets = Array.isArray(opts.set) ? opts.set : [opts.set];
|
|
856
|
-
for (const pair of sets) {
|
|
857
|
-
const eqIdx = pair.indexOf("=");
|
|
858
|
-
if (eqIdx > 0) {
|
|
859
|
-
const k = pair.slice(0, eqIdx);
|
|
860
|
-
let v = pair.slice(eqIdx + 1);
|
|
861
|
-
// Try parsing as number or boolean
|
|
862
|
-
if (v === "true") v = true;
|
|
863
|
-
else if (v === "false") v = false;
|
|
864
|
-
else if (!isNaN(v) && v.trim() !== "") v = Number(v);
|
|
865
|
-
result[k] = v;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ── Apply --add merge ──────────────────────────────────────────
|
|
871
|
-
if (opts.add) {
|
|
872
|
-
try {
|
|
873
|
-
const extra = typeof opts.add === "string" ? JSON.parse(opts.add) : opts.add;
|
|
874
|
-
result = { ...result, ...extra };
|
|
875
|
-
} catch {
|
|
876
|
-
console.error(chalk.red("Error: --add must be valid JSON"));
|
|
877
|
-
process.exit(1);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const output = JSON.stringify(result, null, jsonMode ? 2 : 0);
|
|
882
|
-
if (jsonMode) return console.log(output);
|
|
883
|
-
console.log(output);
|
|
884
|
-
} else {
|
|
885
|
-
console.log(chalk.yellow("Usage:"));
|
|
886
|
-
console.log(chalk.dim(" nxb json --template <slug> Generate JSON template with defaults"));
|
|
887
|
-
console.log(chalk.dim(" nxb json --template <slug> -i Interactive mode (prompts for fields)"));
|
|
888
|
-
console.log(chalk.dim(" nxb json --template <slug> -s count=10 Set specific field values"));
|
|
889
|
-
console.log(chalk.dim(" nxb json --template <slug> -a '{\"x\":1}' Merge additional JSON"));
|
|
890
|
-
console.log(chalk.dim(""));
|
|
891
|
-
console.log(chalk.dim(" Examples:"));
|
|
892
|
-
console.log(chalk.dim(" nxb json --template data-generator | nxb run data-generator --input @-"));
|
|
893
|
-
console.log(chalk.dim(" nxb run data-generator --input \"$(nxb json --template dg -s count=5)\""));
|
|
894
|
-
console.log(chalk.dim(" nxb json --template color-palette -s color=#4A5D23 --json"));
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
// ─── Supabase Helpers ─────────────────────────────────────────────────────────
|
|
899
|
-
|
|
900
|
-
const SUPABASE_URL_DEFAULT = "https://dhxaqatmuprehrkszxzs.supabase.co";
|
|
901
|
-
const SUPABASE_ANON_KEY_DEFAULT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoeGFxYXRtdXByZWhya3N6eHpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMxNjIzNTQsImV4cCI6MjA4ODczODM1NH0.pq7DxW66ETQt6QlIAu2WiLO08tfA7JHQVPd2SvvA3sM";
|
|
902
|
-
|
|
903
|
-
function getSupabaseKey() {
|
|
904
|
-
return process.env.NEXUSBLOOM_SUPABASE_ANON_KEY
|
|
905
|
-
|| configGet("supabaseKey")
|
|
906
|
-
|| SUPABASE_ANON_KEY_DEFAULT;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function getSupabaseUrl() {
|
|
910
|
-
return process.env.NEXUSBLOOM_SUPABASE_URL
|
|
911
|
-
|| SUPABASE_URL_DEFAULT;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function supabaseHeaders() {
|
|
915
|
-
const key = getSupabaseKey();
|
|
916
|
-
return { apikey: key, Authorization: `Bearer ${key}` };
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Fetch tool list from Supabase directly (fallback when API is down).
|
|
921
|
-
* Returns array of {slug, name, short_description, category, runtime, version, icon} or null.
|
|
922
|
-
*/
|
|
923
|
-
async function fetchToolsFromSupabase() {
|
|
924
|
-
const headers = supabaseHeaders();
|
|
925
|
-
const supabaseUrl = getSupabaseUrl();
|
|
926
|
-
try {
|
|
927
|
-
const res = await fetch(
|
|
928
|
-
`${supabaseUrl}/rest/v1/tools?select=slug,name,short_description,tool_type,runtime,version,icon&published=eq.true&order=name.asc`,
|
|
929
|
-
{ headers }
|
|
930
|
-
);
|
|
931
|
-
if (!res.ok) return null;
|
|
932
|
-
const tools = await res.json();
|
|
933
|
-
if (!Array.isArray(tools)) return null;
|
|
934
|
-
|
|
935
|
-
// Also fetch community tools
|
|
936
|
-
const communityRes = await fetch(
|
|
937
|
-
`${supabaseUrl}/rest/v1/community_tools?select=slug,name,short_description,category,version,icon,author_id&status=eq.published&order=name.asc`,
|
|
938
|
-
{ headers }
|
|
939
|
-
);
|
|
940
|
-
const communityTools = communityRes.ok ? (await communityRes.json()) : [];
|
|
941
|
-
|
|
942
|
-
const allTools = [
|
|
943
|
-
...tools.map(t => ({
|
|
944
|
-
slug: t.slug,
|
|
945
|
-
name: t.name,
|
|
946
|
-
description: t.short_description || "",
|
|
947
|
-
category: t.tool_type || "uncategorized",
|
|
948
|
-
runtime: t.runtime || "both",
|
|
949
|
-
version: t.version || "1.0.0",
|
|
950
|
-
icon: t.icon || "🔧",
|
|
951
|
-
type: "builtin",
|
|
952
|
-
})),
|
|
953
|
-
...communityTools.map(t => ({
|
|
954
|
-
slug: t.slug,
|
|
955
|
-
name: t.name,
|
|
956
|
-
description: t.short_description || "",
|
|
957
|
-
category: t.category || "uncategorized",
|
|
958
|
-
runtime: "both",
|
|
959
|
-
version: t.version || "1.0.0",
|
|
960
|
-
icon: t.icon || "🛠️",
|
|
961
|
-
type: "community",
|
|
962
|
-
author_id: t.author_id,
|
|
963
|
-
})),
|
|
964
|
-
];
|
|
965
|
-
|
|
966
|
-
return allTools;
|
|
967
|
-
} catch {
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Fetch a single tool's v2_source from Supabase (fallback).
|
|
974
|
-
* Returns {manifest, coreLogicSource, v2Source} or null.
|
|
975
|
-
*/
|
|
976
|
-
async function fetchToolFromSupabase(slug) {
|
|
977
|
-
const headers = supabaseHeaders();
|
|
978
|
-
const supabaseUrl = getSupabaseUrl();
|
|
979
|
-
try {
|
|
980
|
-
// Try builtin tools first
|
|
981
|
-
const toolRes = await fetch(
|
|
982
|
-
`${supabaseUrl}/rest/v1/tools?slug=eq.${slug}&select=id&published=eq.true`,
|
|
983
|
-
{ headers }
|
|
984
|
-
);
|
|
985
|
-
let tools = await toolRes.json();
|
|
986
|
-
let toolId = tools?.[0]?.id;
|
|
987
|
-
let toolType = "builtin";
|
|
988
|
-
|
|
989
|
-
// Fall back to community tools
|
|
990
|
-
if (!toolId) {
|
|
991
|
-
const comRes = await fetch(
|
|
992
|
-
`${supabaseUrl}/rest/v1/community_tools?slug=eq.${slug}&select=id&status=eq.published`,
|
|
993
|
-
{ headers }
|
|
994
|
-
);
|
|
995
|
-
const comTools = await comRes.json();
|
|
996
|
-
toolId = comTools?.[0]?.id;
|
|
997
|
-
toolType = "community";
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
if (!toolId) return null;
|
|
1001
|
-
|
|
1002
|
-
const versionRes = await fetch(
|
|
1003
|
-
`${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`,
|
|
1004
|
-
{ headers }
|
|
1005
|
-
);
|
|
1006
|
-
const versions = await versionRes.json();
|
|
1007
|
-
const v2Source = versions?.[0]?.v2_source;
|
|
1008
|
-
if (!v2Source) return null;
|
|
1009
|
-
|
|
1010
|
-
const manifestMatch = v2Source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});\s*\n/);
|
|
1011
|
-
let manifest = null;
|
|
1012
|
-
if (manifestMatch) {
|
|
1013
|
-
try {
|
|
1014
|
-
manifest = new Function("return " + manifestMatch[1])();
|
|
1015
|
-
} catch {
|
|
1016
|
-
try {
|
|
1017
|
-
manifest = JSON.parse(manifestMatch[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
|
|
1018
|
-
} catch {
|
|
1019
|
-
manifest = eval("(" + manifestMatch[1] + ")");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return { manifest, coreLogicSource: v2Source, v2Source };
|
|
1024
|
-
} catch {
|
|
1025
|
-
return null;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
/**
|
|
1030
|
-
* Try API first, fall back to Supabase. Returns array or null.
|
|
1031
|
-
*/
|
|
1032
|
-
async function fetchToolsWithFallback() {
|
|
1033
|
-
// Try API first
|
|
1034
|
-
try {
|
|
1035
|
-
const url = new URL("/api/tools", API_BASE);
|
|
1036
|
-
const res = await fetch(url);
|
|
1037
|
-
const ct = res.headers.get("content-type") || "";
|
|
1038
|
-
if (ct.includes("json")) {
|
|
1039
|
-
const { tools } = await res.json();
|
|
1040
|
-
if (Array.isArray(tools) && tools.length > 0) return tools;
|
|
1041
|
-
}
|
|
1042
|
-
} catch { /* API failed */ }
|
|
1043
|
-
|
|
1044
|
-
// Fall back to Supabase
|
|
1045
|
-
const supabaseTools = await fetchToolsFromSupabase();
|
|
1046
|
-
if (supabaseTools) return supabaseTools;
|
|
1047
|
-
return null;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
1051
|
-
|
|
1052
|
-
/**
|
|
1053
|
-
* Search the local filesystem for a tool v2.js file.
|
|
1054
|
-
* Checks common locations relative to CWD.
|
|
1055
|
-
*/
|
|
1056
|
-
function findLocalTool(slug) {
|
|
1057
|
-
const cwd = process.cwd();
|
|
1058
|
-
const candidates = [
|
|
1059
|
-
path.join(cwd, "src", "tools", slug, "v2.js"),
|
|
1060
|
-
path.join(cwd, "tools", slug, "v2.js"),
|
|
1061
|
-
path.join(cwd, slug, "v2.js"),
|
|
1062
|
-
path.join(cwd, "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
|
|
1063
|
-
path.join(cwd, "..", "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
|
|
1064
|
-
];
|
|
1065
|
-
try {
|
|
1066
|
-
const config = loadConfig();
|
|
1067
|
-
const toolsDir = config.toolsDir;
|
|
1068
|
-
if (toolsDir && !candidates.some(c => c.includes(toolsDir))) {
|
|
1069
|
-
candidates.unshift(path.join(cwd, toolsDir, slug, "v2.js"));
|
|
1070
|
-
}
|
|
1071
|
-
} catch {}
|
|
1072
|
-
for (const candidate of candidates) {
|
|
1073
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
1074
|
-
}
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Scan the local filesystem for all tool v2.js files.
|
|
1080
|
-
* Returns array of { slug, name, description, type: "local" }.
|
|
1081
|
-
*/
|
|
1082
|
-
function scanLocalTools() {
|
|
1083
|
-
const cwd = process.cwd();
|
|
1084
|
-
const searchDirs = [
|
|
1085
|
-
path.join(cwd, "src", "tools"),
|
|
1086
|
-
path.join(cwd, "tools"),
|
|
1087
|
-
path.join(cwd, "apps", "main-site", "frontend", "src", "tools"),
|
|
1088
|
-
];
|
|
1089
|
-
const tools = [];
|
|
1090
|
-
for (const dir of searchDirs) {
|
|
1091
|
-
if (!fs.existsSync(dir)) continue;
|
|
1092
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1093
|
-
for (const entry of entries) {
|
|
1094
|
-
if (!entry.isDirectory()) continue;
|
|
1095
|
-
const v2Path = path.join(dir, entry.name, "v2.js");
|
|
1096
|
-
if (!fs.existsSync(v2Path)) continue;
|
|
1097
|
-
const source = fs.readFileSync(v2Path, "utf-8");
|
|
1098
|
-
const match = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
|
|
1099
|
-
if (!match) continue;
|
|
1100
|
-
try {
|
|
1101
|
-
const m = new Function("return " + match[1])();
|
|
1102
|
-
tools.push({
|
|
1103
|
-
slug: m.slug || entry.name,
|
|
1104
|
-
name: m.name || entry.name,
|
|
1105
|
-
description: m.short_description || "",
|
|
1106
|
-
category: m.category || "uncategorized",
|
|
1107
|
-
runtime: m.runtime || "both",
|
|
1108
|
-
version: m.version || "1.0.0",
|
|
1109
|
-
icon: m.icon || "🔧",
|
|
1110
|
-
type: "local",
|
|
1111
|
-
});
|
|
1112
|
-
} catch {}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
return tools;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function readStdin() {
|
|
1119
|
-
return new Promise((resolve) => {
|
|
1120
|
-
const chunks = [];
|
|
1121
|
-
process.stdin.setEncoding("utf8");
|
|
1122
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1123
|
-
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
1124
|
-
if (process.stdin.isTTY) resolve("");
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
// ─── QR Code Terminal Renderer ────────────────────────────────────────────
|
|
1129
|
-
|
|
1130
|
-
async function renderQRCode(data, slug) {
|
|
1131
|
-
try {
|
|
1132
|
-
const qr = await QRCode.toString(data, { type: "terminal", small: true });
|
|
1133
|
-
console.log(chalk.cyan("\n QR Code:"));
|
|
1134
|
-
console.log(qr);
|
|
1135
|
-
console.log(chalk.dim(` Data: ${data.slice(0, 80)}${data.length > 80 ? "..." : ""}`));
|
|
1136
|
-
} catch {
|
|
1137
|
-
// Silently skip if QR rendering fails
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// ─── `nxb create` — scaffold a new tool locally ──────────────────────────
|
|
338
|
+
// ─── `nxb create` ────────────────────────────────────────────────────────────
|
|
1142
339
|
const VALID_RUNTIMES = ["browser", "node", "both"];
|
|
1143
340
|
const VALID_CATEGORIES = [
|
|
1144
341
|
"text", "image", "data", "network", "security", "development",
|
|
@@ -1155,13 +352,9 @@ program
|
|
|
1155
352
|
.option("--runtime <runtime>", `Runtime (${VALID_RUNTIMES.join(", ")})`)
|
|
1156
353
|
.option("--dir <dir>", "Target directory (default: ./src/tools/<slug>)")
|
|
1157
354
|
.action(async (options) => {
|
|
1158
|
-
const rl = readline.createInterface({
|
|
1159
|
-
input: process.stdin,
|
|
1160
|
-
output: process.stdout,
|
|
1161
|
-
});
|
|
355
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1162
356
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
1163
357
|
|
|
1164
|
-
// Gather inputs
|
|
1165
358
|
let slug = options.slug;
|
|
1166
359
|
while (!slug || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
1167
360
|
slug = (await ask(chalk.cyan("Tool slug (e.g. my-tool): "))).trim().toLowerCase();
|
|
@@ -1207,7 +400,6 @@ program
|
|
|
1207
400
|
|
|
1208
401
|
rl.close();
|
|
1209
402
|
|
|
1210
|
-
// Generate v2.js source
|
|
1211
403
|
const pascalName = slug
|
|
1212
404
|
.split("-")
|
|
1213
405
|
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
@@ -1288,27 +480,24 @@ export default function ${pascalName}Tool({ onResult }) {
|
|
|
1288
480
|
}
|
|
1289
481
|
`;
|
|
1290
482
|
|
|
1291
|
-
// Determine target directory
|
|
1292
483
|
const targetDir = options.dir || path.join(process.cwd(), "src/tools", slug);
|
|
1293
484
|
const v2Path = path.join(targetDir, "v2.js");
|
|
1294
485
|
const indexPath = path.join(targetDir, "index.js");
|
|
1295
486
|
|
|
1296
|
-
// Check for existing files
|
|
1297
487
|
if (fs.existsSync(v2Path) || fs.existsSync(indexPath)) {
|
|
1298
|
-
const overwrite =
|
|
488
|
+
const overwrite = await new Promise((r) => {
|
|
1299
489
|
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1300
490
|
rl2.question(chalk.yellow(`Tool "${slug}" already exists. Overwrite? (y/N): `), (a) => {
|
|
1301
491
|
rl2.close();
|
|
1302
492
|
r(a.trim().toLowerCase() === "y");
|
|
1303
493
|
});
|
|
1304
|
-
})
|
|
494
|
+
});
|
|
1305
495
|
if (!overwrite) {
|
|
1306
496
|
console.log(chalk.dim("Aborted."));
|
|
1307
497
|
return;
|
|
1308
498
|
}
|
|
1309
499
|
}
|
|
1310
500
|
|
|
1311
|
-
// Write files
|
|
1312
501
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
1313
502
|
fs.writeFileSync(v2Path, v2Source, "utf-8");
|
|
1314
503
|
fs.writeFileSync(indexPath, indexSource, "utf-8");
|
|
@@ -1322,4 +511,153 @@ export default function ${pascalName}Tool({ onResult }) {
|
|
|
1322
511
|
console.log(chalk.dim(" 3. Publish from the VS Code extension or API\n"));
|
|
1323
512
|
});
|
|
1324
513
|
|
|
1325
|
-
|
|
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);
|