@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/list.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list.js — `nxb list` command.
|
|
3
|
+
*
|
|
4
|
+
* Lists available tools in a paginated, table-style view with icons,
|
|
5
|
+
* categories, and type badges. Supports:
|
|
6
|
+
*
|
|
7
|
+
* nxb list — interactive pagination (10/page, prev/next)
|
|
8
|
+
* nxb list -s ssl — filter + still paginated
|
|
9
|
+
* nxb list --json — raw JSON
|
|
10
|
+
* nxb list --no-interactive— non-interactive single dump
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { fetchTools, resolveToolSlug, generateAbbreviations } from "./tools.js";
|
|
15
|
+
import { renderHeader, renderTable, paginate, paginateInteractive, typeBadge, categoryBadge, PAGE_SIZE, renderPageInfo } from "./ui.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render a single page of tools as a table.
|
|
19
|
+
*/
|
|
20
|
+
function renderToolsPage(result) {
|
|
21
|
+
if (result.total === 0) {
|
|
22
|
+
console.log(chalk.yellow(" No tools found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rows = result.items.map((t, i) => ({
|
|
27
|
+
"#": chalk.gray(result.startIndex + i + 1),
|
|
28
|
+
name: `${t.icon || "🔧"} ${t.name || t.slug}`,
|
|
29
|
+
slug: chalk.cyan(t.slug || ""),
|
|
30
|
+
cat: categoryBadge(t.category || "uncategorized"),
|
|
31
|
+
type: typeBadge(t),
|
|
32
|
+
tags: t.tags && t.tags.length ? chalk.gray(t.tags.slice(0, 3).join(",")) : "",
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const cols = [
|
|
36
|
+
{ key: "#", header: "#", width: 4 },
|
|
37
|
+
{ key: "name", header: "Tool", width: 32 },
|
|
38
|
+
{ key: "slug", header: "Slug", width: 26 },
|
|
39
|
+
{ key: "cat", header: "Category", width: 14, align: "center" },
|
|
40
|
+
{ key: "type", header: "Type", width: 12, align: "center" },
|
|
41
|
+
{ key: "tags", header: "Tags", width: 20 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
renderTable(cols, rows, { headColor: "bold" });
|
|
45
|
+
console.log();
|
|
46
|
+
renderPageInfo(result);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `nxb list` action.
|
|
51
|
+
*/
|
|
52
|
+
export async function listAction(opts = {}) {
|
|
53
|
+
const { search, json: jsonMode, interactive = true, force = false, offline = false } = opts;
|
|
54
|
+
|
|
55
|
+
let tools = await fetchTools({ force, offline });
|
|
56
|
+
let filtered = tools;
|
|
57
|
+
|
|
58
|
+
if (search) {
|
|
59
|
+
const q = search.toLowerCase();
|
|
60
|
+
filtered = tools.filter(
|
|
61
|
+
(t) =>
|
|
62
|
+
(t.slug || "").includes(q) ||
|
|
63
|
+
(t.name || "").toLowerCase().includes(q) ||
|
|
64
|
+
(t.short_description || t.description || "").toLowerCase().includes(q) ||
|
|
65
|
+
(t.category || "").includes(q) ||
|
|
66
|
+
(t.tags || []).some((tg) => tg.toLowerCase().includes(q))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── JSON mode: dump everything, no pagination ──
|
|
71
|
+
if (jsonMode) {
|
|
72
|
+
console.log(JSON.stringify({ tools: filtered }, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Non-interactive: single-page dump ──
|
|
77
|
+
if (!interactive) {
|
|
78
|
+
renderHeader("NexusBloom Tools", { subtitle: `${filtered.length} tool${filtered.length === 1 ? "" : "s"} available` });
|
|
79
|
+
if (filtered.length === 0) return;
|
|
80
|
+
const result = paginate(filtered, filtered.length, 1);
|
|
81
|
+
renderToolsPage(result);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Interactive: paginated with live navigation ──
|
|
86
|
+
const abbrMap = generateAbbreviations(filtered.map((t) => t.slug));
|
|
87
|
+
const abbrCount = Object.keys(abbrMap).filter((k) => !(k in filtered.map((t) => t.slug))).length;
|
|
88
|
+
|
|
89
|
+
renderHeader("NexusBloom Tools", {
|
|
90
|
+
subtitle: `${filtered.length} tool${filtered.length === 1 ? "" : "s"} available · page 1 of ${Math.ceil(filtered.length / PAGE_SIZE) || 1}`,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await paginateInteractive(
|
|
94
|
+
filtered,
|
|
95
|
+
PAGE_SIZE,
|
|
96
|
+
(result) => {
|
|
97
|
+
console.clear();
|
|
98
|
+
renderHeader("NexusBloom Tools", {
|
|
99
|
+
subtitle: `${result.total} tool${result.total === 1 ? "" : "s"} · page ${chalk.cyan(result.page)} of ${chalk.cyan(result.totalPages)}`,
|
|
100
|
+
});
|
|
101
|
+
renderToolsPage(result);
|
|
102
|
+
},
|
|
103
|
+
{}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default listAction;
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.js — Centralised filesystem paths for the nxb CLI.
|
|
3
|
+
*
|
|
4
|
+
* Keeps config + cache locations in one place so every module can share them
|
|
5
|
+
* without duplicating the XDG / home-directory logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
// ─── Config (nxb config) ──────────────────────────────────────────────────────
|
|
16
|
+
export const CONFIG_DIR = path.join(
|
|
17
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
|
|
18
|
+
"nexusbloom"
|
|
19
|
+
);
|
|
20
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
21
|
+
|
|
22
|
+
// ─── Tool list cache (offline tool catalogue) ─────────────────────────────────
|
|
23
|
+
// Stored under ~/.cache/nexusbloom/tools.json — a flat snapshot of every tool
|
|
24
|
+
// so `nxb list` / `nxb abbr` work without a network round-trip.
|
|
25
|
+
export const CACHE_DIR = path.resolve(__dirname, "../.cache");
|
|
26
|
+
export const TOOLS_CACHE_DIR = path.join(CACHE_DIR, "tools");
|
|
27
|
+
export const TOOLS_LIST_CACHE_PATH = path.join(CACHE_DIR, "tools-list.json");
|
|
28
|
+
|
|
29
|
+
// ─── Local tool source cache (for --local execution) ──────────────────────────
|
|
30
|
+
export const TOOL_SOURCE_CACHE_DIR = TOOLS_CACHE_DIR;
|
|
31
|
+
|
|
32
|
+
export function ensureDir(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ensureConfigDir() {
|
|
37
|
+
ensureDir(CONFIG_DIR);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ensureCacheDir() {
|
|
41
|
+
ensureDir(TOOLS_CACHE_DIR);
|
|
42
|
+
}
|
package/src/run.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run.js — `nxb run` command.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Direct: nxb run <slug> [key=value ...] [--input json] [--file path] [--local]
|
|
6
|
+
* 2. Interactive: nxb run (no slug) → select tool → pick inputs → auto-build & execute
|
|
7
|
+
*
|
|
8
|
+
* Imports execution logic from the original monolithic index.js but adds
|
|
9
|
+
* inquirer-driven field prompting for the interactive path.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import QRCode from "qrcode";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
fetchTools,
|
|
20
|
+
fetchToolSource,
|
|
21
|
+
resolveToolSlug,
|
|
22
|
+
configGet,
|
|
23
|
+
API_BASE,
|
|
24
|
+
readStdin,
|
|
25
|
+
} from "./tools.js";
|
|
26
|
+
import {
|
|
27
|
+
renderHeader,
|
|
28
|
+
renderBox,
|
|
29
|
+
typeBadge,
|
|
30
|
+
promptSelect,
|
|
31
|
+
promptInput,
|
|
32
|
+
promptConfirm,
|
|
33
|
+
promptNumber,
|
|
34
|
+
} from "./ui.js";
|
|
35
|
+
|
|
36
|
+
// ─── Remote execution ────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export async function runRemote(slug, body, { apiKey, jsonMode, slugResolved }) {
|
|
39
|
+
const key = apiKey || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
40
|
+
if (!key) {
|
|
41
|
+
if (!jsonMode) console.log(chalk.dim(" No API key — falling back to local execution..."));
|
|
42
|
+
return runLocal(slug, body, { jsonMode, slugResolved });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const spinner = !jsonMode ? ora(chalk.gray(` Running ${slug}...`))`.start()` : null;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${API_BASE}/api/run/${slug}`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
|
|
55
|
+
if (spinner) spinner.succeed(chalk.dim(" API not deployed — falling back to local"));
|
|
56
|
+
return runLocal(slug, body, { jsonMode, slugResolved });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
if (spinner) {
|
|
61
|
+
if (data.success) spinner.succeed(chalk.green(` ${slug} completed`));
|
|
62
|
+
else spinner.fail(chalk.red(` ${slug} failed`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (data.success) {
|
|
66
|
+
if (data.data && typeof data.data === "object" && data.data.svg) {
|
|
67
|
+
await renderQRCode(body.data || body.text || JSON.stringify(body), slug);
|
|
68
|
+
}
|
|
69
|
+
if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
|
|
70
|
+
console.log(chalk.green("\n Result:"));
|
|
71
|
+
console.log(` ${JSON.stringify(data.data, null, 2)}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
|
|
76
|
+
console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (spinner) spinner.fail(chalk.dim(" API unreachable — falling back to local"));
|
|
80
|
+
return runLocal(slug, body, { jsonMode, slugResolved });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Local execution ────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export async function runLocal(slug, input, { jsonMode, slugResolved, source: src } = {}) {
|
|
87
|
+
const toolInfo = await fetchToolSource(slug);
|
|
88
|
+
if (!toolInfo) {
|
|
89
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found.` }));
|
|
90
|
+
console.error(chalk.red(`Tool "${slug}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API.`));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { manifest, coreLogicSource } = toolInfo;
|
|
96
|
+
if (!coreLogicSource) {
|
|
97
|
+
if (jsonMode)
|
|
98
|
+
return console.log(JSON.stringify({ error: "No v2 source available for local execution." }));
|
|
99
|
+
console.error(chalk.yellow("No v2 source available for local execution. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!jsonMode) console.log(chalk.dim(` Using ${slugResolved ? "local" : src || "cached"} tool: ${slug}`));
|
|
105
|
+
|
|
106
|
+
// Strip imports/exports, wrap in IIFE that returns coreLogic
|
|
107
|
+
const cleanSource = coreLogicSource
|
|
108
|
+
.replace(/^import\s+.*?;?\s*$/gm, "")
|
|
109
|
+
.replace(/^export\s+/gm, "");
|
|
110
|
+
const wrappedSource = `(function() {\n${cleanSource}\nreturn coreLogic;\n})()`;
|
|
111
|
+
const coreLogicFn = new Function("return " + wrappedSource)();
|
|
112
|
+
const isAsync = /export\s+async\s+function\s+coreLogic/.test(coreLogicSource);
|
|
113
|
+
|
|
114
|
+
const schema = manifest.input_schema || {};
|
|
115
|
+
const required = schema.required || [];
|
|
116
|
+
const missing = required.filter((k) => input[k] === undefined || input[k] === null);
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Missing required fields: ${missing.join(", ")}` }));
|
|
119
|
+
console.error(chalk.red(`Missing required fields: ${missing.join(", ")}`));
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Apply defaults
|
|
125
|
+
if (schema.properties) {
|
|
126
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
127
|
+
if (input[key] === undefined && prop.default !== undefined) {
|
|
128
|
+
input[key] = prop.default;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const timeoutMs = manifest.timeout_ms || 5000;
|
|
134
|
+
const spinner = !jsonMode ? ora(chalk.gray(` Executing ${slug} (local)`)).start() : null;
|
|
135
|
+
let result;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (isAsync) {
|
|
139
|
+
result = await Promise.race([
|
|
140
|
+
coreLogicFn(input),
|
|
141
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
142
|
+
]);
|
|
143
|
+
} else {
|
|
144
|
+
result = coreLogicFn(input);
|
|
145
|
+
}
|
|
146
|
+
if (spinner) spinner.succeed(chalk.green(` ${slug} completed`));
|
|
147
|
+
|
|
148
|
+
// Fire-and-forget telemetry
|
|
149
|
+
const apiKey = process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
150
|
+
const trackHeaders = { "Content-Type": "application/json" };
|
|
151
|
+
if (apiKey) trackHeaders["Authorization"] = `Bearer ${apiKey}`;
|
|
152
|
+
fetch(`${API_BASE}/api/track-run`, { method: "POST", headers: trackHeaders, body: JSON.stringify({ slug, source: "cli" }) }).catch(() => {});
|
|
153
|
+
|
|
154
|
+
if (jsonMode) return console.log(JSON.stringify({ success: true, data: result }, null, 2));
|
|
155
|
+
console.log(chalk.green("\n Result (local):"));
|
|
156
|
+
console.log(` ${JSON.stringify(result, null, 2)}`);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (spinner) spinner.fail(chalk.red(` Execution error`));
|
|
159
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
160
|
+
console.error(chalk.red(`\n Local execution error: ${err.message}`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Input building ─────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the input body from key=value args, --input, --file, and stdin.
|
|
169
|
+
* Priority: --input > --file > stdin > key=value args.
|
|
170
|
+
*/
|
|
171
|
+
export async function buildInputBody(opts, args) {
|
|
172
|
+
const { input: inputFlag, file } = opts;
|
|
173
|
+
let body = {};
|
|
174
|
+
|
|
175
|
+
// stdin detection (auto or explicit @-)
|
|
176
|
+
const isPiped = inputFlag === "@-" || inputFlag === "@stdin" || (!inputFlag && !file && !process.stdin.isTTY);
|
|
177
|
+
if (isPiped) {
|
|
178
|
+
try {
|
|
179
|
+
const stdin = await readStdin();
|
|
180
|
+
if (stdin && stdin.trim()) body = JSON.parse(stdin);
|
|
181
|
+
} catch {
|
|
182
|
+
console.error(chalk.red("Error: Invalid JSON received from stdin"));
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --input flag (highest priority after stdin)
|
|
189
|
+
if (inputFlag && inputFlag !== "@-" && inputFlag !== "@stdin") {
|
|
190
|
+
try {
|
|
191
|
+
body = JSON.parse(inputFlag);
|
|
192
|
+
} catch {
|
|
193
|
+
console.error(chalk.red("Error: --input must be valid JSON"));
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --file
|
|
200
|
+
if (file) {
|
|
201
|
+
try {
|
|
202
|
+
const fileContent = fs.readFileSync(path.resolve(file), "utf-8");
|
|
203
|
+
body = { ...JSON.parse(fileContent), ...body };
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(chalk.red(`Error reading file: ${err.message}`));
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// key=value args (lowest priority)
|
|
212
|
+
if (!inputFlag && !file) {
|
|
213
|
+
(args || []).forEach((arg) => {
|
|
214
|
+
const [k, ...v] = arg.split("=");
|
|
215
|
+
body[k] = v.join("=");
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return body;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── QR code renderer ────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
async function renderQRCode(data, slug) {
|
|
225
|
+
try {
|
|
226
|
+
const qr = await QRCode.toString(data, { type: "terminal", small: true });
|
|
227
|
+
console.log(chalk.cyan("\n QR Code:"));
|
|
228
|
+
console.log(qr);
|
|
229
|
+
console.log(chalk.dim(` Data: ${data.slice(0, 80)}${data.length > 80 ? "..." : ""}`));
|
|
230
|
+
} catch {
|
|
231
|
+
/* silently skip */
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Interactive runner ────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* `nxb run` with no slug: interactively select a tool and its inputs,
|
|
239
|
+
* then auto-build & execute the exact command.
|
|
240
|
+
*/
|
|
241
|
+
export async function runInteractive(opts = {}) {
|
|
242
|
+
const { json: jsonMode } = opts;
|
|
243
|
+
const tools = await fetchTools({ offline: true });
|
|
244
|
+
|
|
245
|
+
if (!tools || tools.length === 0) {
|
|
246
|
+
console.log(chalk.yellow(" No tools available. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
renderHeader("NexusBloom — Interactive Runner", { subtitle: "Select a tool to run" });
|
|
251
|
+
|
|
252
|
+
// 1. Select a tool
|
|
253
|
+
const choices = tools.map((t) => ({
|
|
254
|
+
name: `${t.icon || "🔧"} ${t.name || t.slug} ${typeBadge(t)}`,
|
|
255
|
+
value: t,
|
|
256
|
+
description: t.short_description || t.description || "",
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
const selected = await promptSelect("Choose a tool:", choices, { pageSize: 15 });
|
|
260
|
+
if (!selected) return;
|
|
261
|
+
|
|
262
|
+
const slug = selected.slug;
|
|
263
|
+
const resolved = await resolveToolSlug(slug, tools);
|
|
264
|
+
|
|
265
|
+
// 2. Fetch manifest / schema
|
|
266
|
+
const toolInfo = await fetchToolSource(resolved);
|
|
267
|
+
if (!toolInfo?.manifest) {
|
|
268
|
+
console.log(chalk.yellow(` No schema available for "${resolved}". Falling back to raw input.`));
|
|
269
|
+
await runCommand(resolved, {}, { ...opts, jsonMode });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { manifest } = toolInfo;
|
|
274
|
+
const schema = manifest.input_schema || { type: "object", properties: {} };
|
|
275
|
+
|
|
276
|
+
renderHeader("Configure Inputs", { subtitle: manifest.short_description || manifest.name });
|
|
277
|
+
|
|
278
|
+
// 3. Prompt for each field
|
|
279
|
+
const body = {};
|
|
280
|
+
const props = schema.properties || {};
|
|
281
|
+
const required = schema.required || [];
|
|
282
|
+
|
|
283
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
284
|
+
const req = required.includes(key);
|
|
285
|
+
const hint = prop.description || "";
|
|
286
|
+
const suffix = req ? chalk.red(" *required") : chalk.dim(" (optional)");
|
|
287
|
+
const label = `${key}${suffix}${hint ? chalk.dim(` — ${hint}`) : ""}`;
|
|
288
|
+
|
|
289
|
+
const val = await promptField(key, prop);
|
|
290
|
+
if (val !== undefined) body[key] = val;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Allow extra fields
|
|
294
|
+
const extra = await promptConfirm("Add a custom field?", { default: false });
|
|
295
|
+
if (extra) {
|
|
296
|
+
const k = await promptInput(" Field name:");
|
|
297
|
+
if (k) {
|
|
298
|
+
const v = await promptInput(` Value for "${k}":`);
|
|
299
|
+
if (v !== undefined) body[k] = v;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 4. Auto-build the command
|
|
304
|
+
const inputJson = JSON.stringify(body);
|
|
305
|
+
const command = `nxb run ${resolved} --input '${inputJson}'`;
|
|
306
|
+
const isLocal = !process.env.NEXUSBLOOM_API_KEY && !configGet("apiKey") && !configGet("key");
|
|
307
|
+
const fullCommand = `${command}${isLocal ? " --local" : ""}`;
|
|
308
|
+
|
|
309
|
+
renderBox("Generated Command", [
|
|
310
|
+
chalk.green(fullCommand),
|
|
311
|
+
"",
|
|
312
|
+
chalk.dim(`Tool: ${manifest.name} (${resolved})`),
|
|
313
|
+
chalk.dim(`Input: ${inputJson}`),
|
|
314
|
+
], { width: 78 });
|
|
315
|
+
|
|
316
|
+
// 5. Confirm & execute
|
|
317
|
+
const runIt = await promptConfirm("Execute now?", { default: true });
|
|
318
|
+
if (!runIt) {
|
|
319
|
+
console.log(chalk.dim(" Command copied to clipboard (if available):"));
|
|
320
|
+
console.log(chalk.gray(` ${fullCommand}`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Execute directly via the same path
|
|
325
|
+
await runCommand(resolved, body, { ...opts, jsonMode, resolved });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Prompt for a single schema field based on its type. */
|
|
329
|
+
async function promptField(key, prop) {
|
|
330
|
+
const { type, enum: enumVals, default: def } = prop;
|
|
331
|
+
|
|
332
|
+
// Enum → select
|
|
333
|
+
if (enumVals && Array.isArray(enumVals) && enumVals.length > 0) {
|
|
334
|
+
const choices = enumVals.map((v) => ({
|
|
335
|
+
name: String(v),
|
|
336
|
+
value: v,
|
|
337
|
+
...(v === def && { checked: true }),
|
|
338
|
+
}));
|
|
339
|
+
if (def !== undefined) choices.unshift({ name: chalk.gray("(default)"), value: def });
|
|
340
|
+
return promptSelect(`${key}:`, choices);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Number / integer
|
|
344
|
+
if (type === "number" || type === "integer") {
|
|
345
|
+
return promptNumber(`${key}:`, { min: prop.minimum, max: prop.maximum, default: def });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Boolean
|
|
349
|
+
if (type === "boolean") {
|
|
350
|
+
return promptConfirm(`${key}:`, { default: def ?? true });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Object / array → JSON string input
|
|
354
|
+
if (type === "object" || type === "array") {
|
|
355
|
+
const defaultStr = def !== undefined ? JSON.stringify(def) : [];
|
|
356
|
+
const raw = await promptInput(`${key}: (JSON)`, { defaultValue: defaultStr });
|
|
357
|
+
if (!raw) return def;
|
|
358
|
+
try {
|
|
359
|
+
return JSON.parse(raw);
|
|
360
|
+
} catch {
|
|
361
|
+
console.log(chalk.yellow(" Invalid JSON — using raw string"));
|
|
362
|
+
return raw;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Default: string input
|
|
367
|
+
return promptInput(`${key}:`, { defaultValue: def });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Execute a resolved slug+body, choosing local vs remote. */
|
|
371
|
+
async function runCommand(slug, body, opts = {}) {
|
|
372
|
+
const { json: jsonMode, resolved, local } = opts;
|
|
373
|
+
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
374
|
+
|
|
375
|
+
if (local || !apiKey) {
|
|
376
|
+
await runLocal(slug, body, { jsonMode, slugResolved: resolved });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await runRemote(slug, body, { apiKey, jsonMode, slugResolved: resolved });
|
|
380
|
+
}
|