@nexusbloom/cli 0.2.5 → 0.2.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +115 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -101,8 +101,12 @@ program
101
101
  .option("-s, --search <query>", "Search tools by name or description")
102
102
  .action(async (opts, cmd) => {
103
103
  const jsonMode = cmd.parent.opts().json || opts.json;
104
- const tools = await fetchToolsWithFallback();
105
- if (!tools || tools.length === 0) {
104
+ let tools = await fetchToolsWithFallback() || [];
105
+ // Merge in local tools from filesystem
106
+ const localTools = scanLocalTools();
107
+ const localSlugs = new Set(localTools.map(t => t.slug));
108
+ tools = [...localTools, ...tools.filter(t => !localSlugs.has(t.slug))];
109
+ if (tools.length === 0) {
106
110
  if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
107
111
  console.log(chalk.yellow("No tools found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
108
112
  return;
@@ -120,7 +124,7 @@ program
120
124
  if (jsonMode) return console.log(JSON.stringify({ tools: filtered }, null, 2));
121
125
  console.log(chalk.bold(`\n ${filtered.length} tool${filtered.length > 1 ? "s" : ""} available\n`));
122
126
  filtered.forEach((t) => {
123
- const typeBadge = t.type === "community" ? chalk.dim("[community]") : "";
127
+ const typeBadge = t.type === "community" ? chalk.dim("[community]") : t.type === "local" ? chalk.cyan("[local]") : "";
124
128
  console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.description || "").slice(0, 50))} ${typeBadge}`);
125
129
  });
126
130
  console.log();
@@ -286,20 +290,40 @@ async function executeLocal(slug, input, jsonMode) {
286
290
  ensureCacheDir();
287
291
  const cacheFile = path.join(CACHE_DIR, `${slug}.json`);
288
292
 
289
- // Try loading from cache first
293
+ // Try loading from local filesystem first (fastest, most up-to-date)
290
294
  let manifest, coreLogicSource;
291
- if (fs.existsSync(cacheFile)) {
292
- try {
293
- const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
294
- manifest = cached.manifest;
295
- coreLogicSource = cached.coreLogicSource;
296
- if (!jsonMode) console.log(chalk.dim(` Using cached tool: ${slug}`));
297
- } catch {
298
- // Cache corrupted, refetch
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");
299
309
  }
300
310
  }
301
311
 
302
- // Fetch from API if not cached
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
303
327
  if (!manifest || !coreLogicSource) {
304
328
  if (!jsonMode) console.log(chalk.dim(` Fetching tool: ${slug}...`));
305
329
 
@@ -443,6 +467,18 @@ program
443
467
  } catch { /* fallback failed */ }
444
468
  }
445
469
 
470
+ // Fallback: check local filesystem
471
+ if (!manifest) {
472
+ const localPath = findLocalTool(slug);
473
+ if (localPath) {
474
+ const source = fs.readFileSync(localPath, "utf-8");
475
+ const match = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
476
+ if (match) {
477
+ try { manifest = new Function("return " + match[1])(); } catch {}
478
+ }
479
+ }
480
+ }
481
+
446
482
  if (!manifest) {
447
483
  if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found.` }));
448
484
  console.error(chalk.red(`Tool "${slug}" not found.`));
@@ -976,6 +1012,72 @@ async function fetchToolsWithFallback() {
976
1012
 
977
1013
  // ─── Helpers ─────────────────────────────────────────────────────────────────
978
1014
 
1015
+ /**
1016
+ * Search the local filesystem for a tool v2.js file.
1017
+ * Checks common locations relative to CWD.
1018
+ */
1019
+ function findLocalTool(slug) {
1020
+ const cwd = process.cwd();
1021
+ const candidates = [
1022
+ path.join(cwd, "src", "tools", slug, "v2.js"),
1023
+ path.join(cwd, "tools", slug, "v2.js"),
1024
+ path.join(cwd, slug, "v2.js"),
1025
+ path.join(cwd, "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
1026
+ path.join(cwd, "..", "apps", "main-site", "frontend", "src", "tools", slug, "v2.js"),
1027
+ ];
1028
+ try {
1029
+ const config = loadConfig();
1030
+ const toolsDir = config.toolsDir;
1031
+ if (toolsDir && !candidates.some(c => c.includes(toolsDir))) {
1032
+ candidates.unshift(path.join(cwd, toolsDir, slug, "v2.js"));
1033
+ }
1034
+ } catch {}
1035
+ for (const candidate of candidates) {
1036
+ if (fs.existsSync(candidate)) return candidate;
1037
+ }
1038
+ return null;
1039
+ }
1040
+
1041
+ /**
1042
+ * Scan the local filesystem for all tool v2.js files.
1043
+ * Returns array of { slug, name, description, type: "local" }.
1044
+ */
1045
+ function scanLocalTools() {
1046
+ const cwd = process.cwd();
1047
+ const searchDirs = [
1048
+ path.join(cwd, "src", "tools"),
1049
+ path.join(cwd, "tools"),
1050
+ path.join(cwd, "apps", "main-site", "frontend", "src", "tools"),
1051
+ ];
1052
+ const tools = [];
1053
+ for (const dir of searchDirs) {
1054
+ if (!fs.existsSync(dir)) continue;
1055
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1056
+ for (const entry of entries) {
1057
+ if (!entry.isDirectory()) continue;
1058
+ const v2Path = path.join(dir, entry.name, "v2.js");
1059
+ if (!fs.existsSync(v2Path)) continue;
1060
+ const source = fs.readFileSync(v2Path, "utf-8");
1061
+ const match = source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});/);
1062
+ if (!match) continue;
1063
+ try {
1064
+ const m = new Function("return " + match[1])();
1065
+ tools.push({
1066
+ slug: m.slug || entry.name,
1067
+ name: m.name || entry.name,
1068
+ description: m.short_description || "",
1069
+ category: m.category || "uncategorized",
1070
+ runtime: m.runtime || "both",
1071
+ version: m.version || "1.0.0",
1072
+ icon: m.icon || "🔧",
1073
+ type: "local",
1074
+ });
1075
+ } catch {}
1076
+ }
1077
+ }
1078
+ return tools;
1079
+ }
1080
+
979
1081
  function readStdin() {
980
1082
  return new Promise((resolve) => {
981
1083
  const chunks = [];