@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/README.md +96 -19
- package/package.json +2 -1
- package/src/abbr.js +111 -0
- package/src/index.js +279 -914
- 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,367 +116,77 @@ 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
|
|
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
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
// ──
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
}
|
|
156
|
+
const jsonMode = cmd?.parent?.opts().json || false;
|
|
157
|
+
const resolved = await resolveToolSlug(slug);
|
|
461
158
|
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
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>`
|
|
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.
|
|
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
|
|
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.
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return;
|
|
557
209
|
}
|
|
558
210
|
|
|
559
|
-
|
|
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
|
|
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`
|
|
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
|
-
|
|
658
|
-
|
|
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(
|
|
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
|
-
|
|
672
|
-
|
|
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(
|
|
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
|
-
// ───
|
|
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 =
|
|
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
|
-
|
|
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);
|