@nexusbloom/cli 0.2.10 → 0.2.12
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/package.json +1 -1
- package/src/index.js +215 -4
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -508,8 +508,12 @@ program
|
|
|
508
508
|
const required = manifest.input_schema.required || [];
|
|
509
509
|
for (const [name, prop] of Object.entries(props)) {
|
|
510
510
|
const req = required.includes(name) ? chalk.red(" *required") : "";
|
|
511
|
-
|
|
511
|
+
const def = prop.default !== undefined ? chalk.dim(` [default: ${JSON.stringify(prop.default)}]`) : "";
|
|
512
|
+
console.log(` ${chalk.cyan(name)} (${prop.type})${req}${def}`);
|
|
512
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
|
+
}
|
|
513
517
|
}
|
|
514
518
|
}
|
|
515
519
|
if (manifest.output_schema?.properties) {
|
|
@@ -525,10 +529,13 @@ program
|
|
|
525
529
|
program
|
|
526
530
|
.command("transform")
|
|
527
531
|
.description("Pipe stdin through a tool and write result to stdout")
|
|
528
|
-
.argument("<slug>", "Tool slug")
|
|
532
|
+
.argument("<slug>", "Tool slug or abbreviation")
|
|
529
533
|
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
530
534
|
.option("--local", "Execute locally (faster for repeated use)")
|
|
531
535
|
.action(async (slug, opts, cmd) => {
|
|
536
|
+
// Resolve abbreviation
|
|
537
|
+
slug = await resolveToolSlug(slug);
|
|
538
|
+
|
|
532
539
|
const jsonMode = cmd.parent.opts().json;
|
|
533
540
|
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
|
|
534
541
|
|
|
@@ -748,6 +755,20 @@ program
|
|
|
748
755
|
console.log();
|
|
749
756
|
});
|
|
750
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
|
+
|
|
751
772
|
// ─── `nxb json` — create JSON input for tools ───────────────────────────────
|
|
752
773
|
|
|
753
774
|
program
|
|
@@ -761,6 +782,9 @@ program
|
|
|
761
782
|
const jsonMode = cmd.parent.opts().json;
|
|
762
783
|
|
|
763
784
|
if (opts.template) {
|
|
785
|
+
// Resolve abbreviation
|
|
786
|
+
opts.template = await resolveToolSlug(opts.template);
|
|
787
|
+
|
|
764
788
|
// Fetch tool schema — try API first, then Supabase
|
|
765
789
|
let manifest = null;
|
|
766
790
|
try {
|
|
@@ -793,8 +817,11 @@ program
|
|
|
793
817
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
794
818
|
const required = schema.required?.includes(key);
|
|
795
819
|
const hint = prop.description ? ` (${prop.description})` : "";
|
|
796
|
-
const def = prop.default ? ` [${prop.default}]` : "";
|
|
797
|
-
const
|
|
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}: `));
|
|
798
825
|
const val = answer || prop.default;
|
|
799
826
|
if (val !== undefined && val !== "") {
|
|
800
827
|
result[key] = prop.type === "number" || prop.type === "integer" ? Number(val) : val;
|
|
@@ -1111,4 +1138,188 @@ async function renderQRCode(data, slug) {
|
|
|
1111
1138
|
}
|
|
1112
1139
|
}
|
|
1113
1140
|
|
|
1141
|
+
// ─── `nxb create` — scaffold a new tool locally ──────────────────────────
|
|
1142
|
+
const VALID_RUNTIMES = ["browser", "node", "both"];
|
|
1143
|
+
const VALID_CATEGORIES = [
|
|
1144
|
+
"text", "image", "data", "network", "security", "development",
|
|
1145
|
+
"education", "business", "utility", "math", "game", "Other",
|
|
1146
|
+
];
|
|
1147
|
+
|
|
1148
|
+
program
|
|
1149
|
+
.command("create")
|
|
1150
|
+
.description("Scaffold a new tool locally")
|
|
1151
|
+
.option("--slug <slug>", "Tool slug (lowercase, hyphenated)")
|
|
1152
|
+
.option("--name <name>", "Display name")
|
|
1153
|
+
.option("--desc <desc>", "Short description")
|
|
1154
|
+
.option("--category <category>", `Category (${VALID_CATEGORIES.join(", ")})`)
|
|
1155
|
+
.option("--runtime <runtime>", `Runtime (${VALID_RUNTIMES.join(", ")})`)
|
|
1156
|
+
.option("--dir <dir>", "Target directory (default: ./src/tools/<slug>)")
|
|
1157
|
+
.action(async (options) => {
|
|
1158
|
+
const rl = readline.createInterface({
|
|
1159
|
+
input: process.stdin,
|
|
1160
|
+
output: process.stdout,
|
|
1161
|
+
});
|
|
1162
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
1163
|
+
|
|
1164
|
+
// Gather inputs
|
|
1165
|
+
let slug = options.slug;
|
|
1166
|
+
while (!slug || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
1167
|
+
slug = (await ask(chalk.cyan("Tool slug (e.g. my-tool): "))).trim().toLowerCase();
|
|
1168
|
+
if (!slug) { console.log(chalk.red("Slug is required.")); continue; }
|
|
1169
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
1170
|
+
console.log(chalk.red("Slug must be lowercase, hyphenated (e.g. my-tool)."));
|
|
1171
|
+
slug = null;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
let name = options.name;
|
|
1176
|
+
while (!name || name.length < 2) {
|
|
1177
|
+
name = (await ask(chalk.cyan("Display name: "))).trim();
|
|
1178
|
+
if (!name) console.log(chalk.red("Name is required."));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
let desc = options.desc;
|
|
1182
|
+
while (!desc) {
|
|
1183
|
+
desc = (await ask(chalk.cyan("Short description: "))).trim();
|
|
1184
|
+
if (!desc) console.log(chalk.red("Description is required."));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
let category = options.category;
|
|
1188
|
+
while (!category || !VALID_CATEGORIES.includes(category)) {
|
|
1189
|
+
const catList = VALID_CATEGORIES.join(", ");
|
|
1190
|
+
category = (await ask(chalk.cyan("Category (" + catList + "): "))).trim().toLowerCase();
|
|
1191
|
+
if (category === "other") category = "Other";
|
|
1192
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
1193
|
+
console.log(chalk.red("Invalid category. Choose from: " + catList));
|
|
1194
|
+
category = null;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
let runtime = options.runtime;
|
|
1199
|
+
while (!runtime || !VALID_RUNTIMES.includes(runtime)) {
|
|
1200
|
+
const rtList = VALID_RUNTIMES.join(", ");
|
|
1201
|
+
runtime = (await ask(chalk.cyan("Runtime (" + rtList + "): "))).trim().toLowerCase();
|
|
1202
|
+
if (!VALID_RUNTIMES.includes(runtime)) {
|
|
1203
|
+
console.log(chalk.red("Invalid runtime. Choose from: " + rtList));
|
|
1204
|
+
runtime = null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
rl.close();
|
|
1209
|
+
|
|
1210
|
+
// Generate v2.js source
|
|
1211
|
+
const pascalName = slug
|
|
1212
|
+
.split("-")
|
|
1213
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
1214
|
+
.join("");
|
|
1215
|
+
|
|
1216
|
+
const v2Source = `/**
|
|
1217
|
+
* ${name} — ${desc}
|
|
1218
|
+
* Auto-generated by nxb create
|
|
1219
|
+
*/
|
|
1220
|
+
|
|
1221
|
+
export const MANIFEST = {
|
|
1222
|
+
slug: "${slug}",
|
|
1223
|
+
name: "${name}",
|
|
1224
|
+
short_description: "${desc}",
|
|
1225
|
+
icon: "🛠️",
|
|
1226
|
+
category: "${category}",
|
|
1227
|
+
tags: [],
|
|
1228
|
+
runtime: "${runtime}",
|
|
1229
|
+
pricing: "free",
|
|
1230
|
+
version: "1.0.0",
|
|
1231
|
+
timeout_ms: 5000,
|
|
1232
|
+
input_schema: {
|
|
1233
|
+
type: "object",
|
|
1234
|
+
properties: {
|
|
1235
|
+
input: {
|
|
1236
|
+
type: "string",
|
|
1237
|
+
description: "Input text to process",
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
required: ["input"],
|
|
1241
|
+
},
|
|
1242
|
+
output_schema: {
|
|
1243
|
+
type: "object",
|
|
1244
|
+
properties: {
|
|
1245
|
+
result: {
|
|
1246
|
+
type: "string",
|
|
1247
|
+
description: "Processed output",
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
export async function coreLogic(input) {
|
|
1254
|
+
// Replace with your tool's logic
|
|
1255
|
+
return { result: \`Processed: \${input.input}\` };
|
|
1256
|
+
}
|
|
1257
|
+
`;
|
|
1258
|
+
|
|
1259
|
+
const indexSource = `import React from "react";
|
|
1260
|
+
import { MANIFEST } from "./v2";
|
|
1261
|
+
|
|
1262
|
+
export default function ${pascalName}Tool({ onResult }) {
|
|
1263
|
+
const [input, setInput] = React.useState("");
|
|
1264
|
+
const [result, setResult] = React.useState(null);
|
|
1265
|
+
|
|
1266
|
+
const handleRun = async () => {
|
|
1267
|
+
try {
|
|
1268
|
+
const { coreLogic } = await import("./v2");
|
|
1269
|
+
const output = await coreLogic({ input });
|
|
1270
|
+
setResult(output);
|
|
1271
|
+
if (onResult) onResult(output);
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
setResult({ error: err.message });
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
return (
|
|
1278
|
+
<div className="nexusbloom-tool">
|
|
1279
|
+
<h2 className="text-xl font-bold mb-4">{MANIFEST.name}</h2>
|
|
1280
|
+
<p className="text-gray-600 dark:text-gray-400 mb-4">{MANIFEST.short_description}</p>
|
|
1281
|
+
<textarea className="w-full p-2 border rounded" rows={4} value={input}
|
|
1282
|
+
onChange={(e) => setInput(e.target.value)} placeholder="Enter input..." />
|
|
1283
|
+
<button className="mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
1284
|
+
onClick={handleRun}>Run</button>
|
|
1285
|
+
{result && <pre className="mt-4 p-3 bg-gray-100 rounded overflow-auto">{JSON.stringify(result, null, 2)}</pre>}
|
|
1286
|
+
</div>
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
`;
|
|
1290
|
+
|
|
1291
|
+
// Determine target directory
|
|
1292
|
+
const targetDir = options.dir || path.join(process.cwd(), "src/tools", slug);
|
|
1293
|
+
const v2Path = path.join(targetDir, "v2.js");
|
|
1294
|
+
const indexPath = path.join(targetDir, "index.js");
|
|
1295
|
+
|
|
1296
|
+
// Check for existing files
|
|
1297
|
+
if (fs.existsSync(v2Path) || fs.existsSync(indexPath)) {
|
|
1298
|
+
const overwrite = (await new Promise((r) => {
|
|
1299
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1300
|
+
rl2.question(chalk.yellow(`Tool "${slug}" already exists. Overwrite? (y/N): `), (a) => {
|
|
1301
|
+
rl2.close();
|
|
1302
|
+
r(a.trim().toLowerCase() === "y");
|
|
1303
|
+
});
|
|
1304
|
+
}));
|
|
1305
|
+
if (!overwrite) {
|
|
1306
|
+
console.log(chalk.dim("Aborted."));
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Write files
|
|
1312
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1313
|
+
fs.writeFileSync(v2Path, v2Source, "utf-8");
|
|
1314
|
+
fs.writeFileSync(indexPath, indexSource, "utf-8");
|
|
1315
|
+
|
|
1316
|
+
console.log(chalk.green(`\n ✓ Tool "${name}" created successfully!\n`));
|
|
1317
|
+
console.log(chalk.dim(` ${v2Path}`));
|
|
1318
|
+
console.log(chalk.dim(` ${indexPath}\n`));
|
|
1319
|
+
console.log(chalk.dim(" Next steps:"));
|
|
1320
|
+
console.log(chalk.dim(" 1. Edit v2.js to implement your tool logic"));
|
|
1321
|
+
console.log(chalk.dim(" 2. Run nxb run --local <slug> to test locally"));
|
|
1322
|
+
console.log(chalk.dim(" 3. Publish from the VS Code extension or API\n"));
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1114
1325
|
program.parse(process.argv);
|