@nexusbloom/cli 0.3.0 → 0.3.1

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 (4) hide show
  1. package/package.json +1 -1
  2. package/src/abbr.js +21 -52
  3. package/src/list.js +25 -50
  4. package/src/ui.js +62 -258
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/abbr.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * abbr.js — `nxb abbr` command.
3
3
  *
4
- * Displays the tool abbreviation map in a paginated, table-style view.
4
+ * Simple paginated abbreviation view.
5
5
  *
6
6
  * nxb abbr — interactive pagination (10/page)
7
7
  * nxb abbr ip — filter by query (matches slug, abbr, or name)
@@ -10,48 +10,30 @@
10
10
  */
11
11
 
12
12
  import chalk from "chalk";
13
- import { fetchTools, resolveToolSlug, generateAbbreviations } from "./tools.js";
14
- import { renderHeader, renderTable, paginate, paginateInteractive, typeBadge, PAGE_SIZE, renderPageInfo } from "./ui.js";
13
+ import { fetchTools, generateAbbreviations } from "./tools.js";
14
+ import { paginate, paginateInteractive, PAGE_SIZE } from "./ui.js";
15
15
 
16
- /**
17
- * Render a single page of abbreviations as a table.
18
- */
19
16
  function renderAbbrPage(result, abbrMap) {
20
17
  if (result.total === 0) {
21
- console.log(chalk.yellow(" No tools available. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
18
+ console.log(chalk.yellow(" No tools available."));
22
19
  return;
23
20
  }
24
21
 
25
- const rows = result.items.map((t, i) => ({
26
- "#": chalk.gray(result.startIndex + i + 1),
27
- abbr: chalk.green.bold(abbrMap[t.slug] || ""),
28
- slug: chalk.cyan(t.slug),
29
- name: t.name || "",
30
- type: typeBadge(t),
31
- }));
32
-
33
- const cols = [
34
- { key: "#", header: "#", width: 4 },
35
- { key: "abbr", header: "Abbr", width: 10, align: "center" },
36
- { key: "slug", header: "Slug", width: 28 },
37
- { key: "name", header: "Tool", width: 30 },
38
- { key: "type", header: "Type", width: 10, align: "center" },
39
- ];
40
-
41
- renderTable(cols, rows, { headColor: "bold" });
42
- console.log();
43
- renderPageInfo(result);
22
+ for (let i = 0; i < result.items.length; i++) {
23
+ const t = result.items[i];
24
+ const idx = chalk.gray(String(result.startIndex + i + 1).padStart(2));
25
+ const abbr = chalk.green.bold((abbrMap[t.slug] || "").padEnd(10));
26
+ const slug = chalk.cyan(t.slug || "");
27
+ console.log(` ${idx} ${abbr} ${slug}`);
28
+ }
44
29
  }
45
30
 
46
31
  export async function abbrAction(opts = {}) {
47
32
  const { query, json: jsonMode, interactive = true, force = false, offline = false } = opts;
48
33
 
49
34
  const tools = await fetchTools({ force, offline });
50
-
51
- // Build abbreviation map
52
35
  const abbrMap = generateAbbreviations(tools.map((t) => t.slug));
53
36
 
54
- // Filter
55
37
  let filtered = tools;
56
38
  if (query) {
57
39
  const q = query.toLowerCase();
@@ -60,52 +42,39 @@ export async function abbrAction(opts = {}) {
60
42
  return (
61
43
  abbr?.toLowerCase().includes(q) ||
62
44
  t.slug.toLowerCase().includes(q) ||
63
- (t.name || "").toLowerCase().includes(q) ||
64
- (t.short_description || "").toLowerCase().includes(q)
45
+ (t.name || "").toLowerCase().includes(q)
65
46
  );
66
47
  });
67
48
  }
68
49
 
69
- if (filtered.length === 0 && !jsonMode) {
70
- console.log(chalk.yellow(` No abbreviations${query ? ` matching "${query}"` : ``}.`));
71
- return;
72
- }
73
-
74
- // JSON mode
75
50
  if (jsonMode) {
76
- const output = filtered.reduce((acc, t) => {
77
- acc[t.slug] = abbrMap[t.slug];
78
- return acc;
79
- }, {});
51
+ const output = filtered.reduce((acc, t) => { acc[t.slug] = abbrMap[t.slug]; return acc; }, {});
80
52
  console.log(JSON.stringify({ abbreviations: output, count: filtered.length }, null, 2));
81
53
  return;
82
54
  }
83
55
 
84
- // Non-interactive single dump
85
56
  if (!interactive) {
86
- renderHeader("Tool Abbreviations", { subtitle: `${filtered.length} tool${filtered.length === 1 ? "" : "s"} available` });
57
+ if (filtered.length === 0) {
58
+ console.log(chalk.yellow(` No abbreviations${query ? ` matching "${query}"` : ""}.`));
59
+ return;
60
+ }
87
61
  const result = paginate(filtered, filtered.length, 1);
62
+ console.log(chalk.bold(`\n Tool Abbreviations (${filtered.length} total)\n`));
88
63
  renderAbbrPage(result, abbrMap);
64
+ console.log();
89
65
  return;
90
66
  }
91
67
 
92
- // Interactive paginated view
93
- renderHeader("Tool Abbreviations", {
94
- subtitle: `${filtered.length} abbreviation${filtered.length === 1 ? "" : "s"} available`,
95
- });
96
-
97
68
  await paginateInteractive(
98
69
  filtered,
99
70
  PAGE_SIZE,
100
71
  (result) => {
101
72
  console.clear();
102
- renderHeader("Tool Abbreviations", {
103
- subtitle: `${result.total} abbreviation${result.total === 1 ? "" : "s"} · page ${chalk.cyan(result.page)} of ${chalk.cyan(result.totalPages)}`,
104
- });
73
+ console.log(chalk.bold(`\n Tool Abbreviations (${result.total} · page ${result.page}/${result.totalPages})\n`));
105
74
  renderAbbrPage(result, abbrMap);
106
75
  },
107
76
  {}
108
77
  );
109
78
  }
110
79
 
111
- export default abbrAction;
80
+ export default abbrAction;
package/src/list.js CHANGED
@@ -1,54 +1,37 @@
1
1
  /**
2
2
  * list.js — `nxb list` command.
3
3
  *
4
- * Lists available tools in a paginated, table-style view with icons,
5
- * categories, and type badges. Supports:
4
+ * Simple paginated tool listing. Clean lines, no fancy backgrounds, no tables.
6
5
  *
7
6
  * nxb list — interactive pagination (10/page, prev/next)
8
- * nxb list -s ssl — filter + still paginated
7
+ * nxb list -s ssl — filter + paginated
9
8
  * nxb list --json — raw JSON
10
- * nxb list --no-interactive— non-interactive single dump
9
+ * nxb list --no-interactive— single dump
11
10
  */
12
11
 
13
12
  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";
13
+ import { fetchTools } from "./tools.js";
14
+ import { paginate, paginateInteractive, PAGE_SIZE } from "./ui.js";
16
15
 
17
- /**
18
- * Render a single page of tools as a table.
19
- */
20
16
  function renderToolsPage(result) {
21
17
  if (result.total === 0) {
22
- console.log(chalk.yellow(" No tools found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
18
+ console.log(chalk.yellow(" No tools found."));
23
19
  return;
24
20
  }
25
21
 
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);
22
+ for (let i = 0; i < result.items.length; i++) {
23
+ const t = result.items[i];
24
+ const idx = chalk.gray(String(result.startIndex + i + 1).padStart(2));
25
+ const icon = t.icon || "🔧";
26
+ const slug = chalk.cyan(t.slug || "");
27
+ const desc = (t.short_description || t.description || "").slice(0, 55);
28
+ console.log(` ${idx} ${icon} ${slug.padEnd(28)} ${chalk.dim(desc)}`);
29
+ if (t.tags && t.tags.length) {
30
+ console.log(` ${chalk.gray(t.tags.map(tg => `#${tg}`).join(" "))}`);
31
+ }
32
+ }
47
33
  }
48
34
 
49
- /**
50
- * The `nxb list` action.
51
- */
52
35
  export async function listAction(opts = {}) {
53
36
  const { search, json: jsonMode, interactive = true, force = false, offline = false } = opts;
54
37
 
@@ -67,41 +50,33 @@ export async function listAction(opts = {}) {
67
50
  );
68
51
  }
69
52
 
70
- // ── JSON mode: dump everything, no pagination ──
71
53
  if (jsonMode) {
72
54
  console.log(JSON.stringify({ tools: filtered }, null, 2));
73
55
  return;
74
56
  }
75
57
 
76
- // ── Non-interactive: single-page dump ──
77
58
  if (!interactive) {
78
- renderHeader("NexusBloom Tools", { subtitle: `${filtered.length} tool${filtered.length === 1 ? "" : "s"} available` });
79
- if (filtered.length === 0) return;
59
+ if (filtered.length === 0) {
60
+ console.log(chalk.yellow(" No tools found."));
61
+ return;
62
+ }
80
63
  const result = paginate(filtered, filtered.length, 1);
64
+ console.log(chalk.bold(`\n NexusBloom Tools (${filtered.length} available)\n`));
81
65
  renderToolsPage(result);
66
+ console.log();
82
67
  return;
83
68
  }
84
69
 
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
70
  await paginateInteractive(
94
71
  filtered,
95
72
  PAGE_SIZE,
96
73
  (result) => {
97
74
  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
- });
75
+ console.log(chalk.bold(`\n NexusBloom Tools (${result.total} · page ${result.page}/${result.totalPages})\n`));
101
76
  renderToolsPage(result);
102
77
  },
103
78
  {}
104
79
  );
105
80
  }
106
81
 
107
- export default listAction;
82
+ export default listAction;
package/src/ui.js CHANGED
@@ -1,15 +1,8 @@
1
1
  /**
2
2
  * ui.js — Terminal UI helpers for the nxb CLI.
3
3
  *
4
- * Everything rendering / prompting related lives here so command modules
5
- * stay lean and the look-and-feel is consistent across `list`, `abbr`, `run`, etc.
6
- *
7
- * Exports:
8
- * - Visual primitives : renderHeader, renderBox, renderBadge, divider
9
- * - Table printer : renderTable
10
- * - Pagination engine : paginate (pure) + paginateInteractive (live nav)
11
- * - Prompt wrappers : promptSelect, promptInput, promptConfirm,
12
- * promptNumber, promptCheckbox, promptMultiSelect
4
+ * Lightweight: pagination engine, inquirer prompt wrappers, simple text helpers.
5
+ * No box drawing, no background badges, no heavy tables.
13
6
  */
14
7
 
15
8
  import chalk from "chalk";
@@ -26,167 +19,75 @@ import {
26
19
 
27
20
  export const PAGE_SIZE = 10;
28
21
 
29
- // Box-drawing palette
30
- const BOX = {
31
- topLeft: "┌",
32
- topRight: "┐",
33
- bottomLeft: "└",
34
- bottomRight: "┘",
35
- h: "─",
36
- v: "│",
37
- };
38
-
39
- // Badge style map per tool type / category
40
- const BADGE_STYLES = {
41
- local: { label: chalk.bgBlueBright.white(" local "), border: "blueBright" },
42
- builtin: { label: chalk.bgGreen.white(" builtin "), border: "green" },
43
- community: { label: chalk.bgMagenta.white("community"), border: "magenta" },
44
- uncategorized: { label: chalk.bgGray.white(" uncat "), border: "gray" },
45
- };
46
-
47
22
  // ─── Simple helpers ──────────────────────────────────────────────────────────
48
23
 
49
- /** Print a thin horizontal rule across the terminal. */
50
- export function divider(width = 60) {
51
- return console.log(chalk.gray("─".repeat(width)));
52
- }
53
-
54
24
  /**
55
- * Render a centered header with a title and optional subtitle.
56
- * Clears any previous inquirer prompt state.
25
+ * Print a simple bold header line.
26
+ *
27
+ * renderHeader("Results", { subtitle: "3 items" })
28
+ * // → "Results — 3 items"
57
29
  */
58
- export function renderHeader(title, { subtitle, color = "cyanBright" } = {}) {
59
- const bar = "═".repeat(Math.max(title.length + 4, 30));
60
- const line1 = chalk[color].bold(` ${bar}`);
61
- const line2 = chalk[color].bold(` ║ ${title.padEnd(title.length)} ║`);
62
- const line3 = chalk[color].bold(` ${bar}`);
63
- console.log("\n" + line1);
64
- console.log(line2);
65
- console.log(line3);
30
+ export function renderHeader(title, { subtitle, color = "cyan" } = {}) {
31
+ const prefix = chalk.bold[color](` ${title}`);
66
32
  if (subtitle) {
67
- console.log(chalk.dim(` ${subtitle}\n`));
33
+ console.log(`\n${prefix} ${chalk.gray("—")} ${chalk.dim(subtitle)}\n`);
68
34
  } else {
69
- console.log();
35
+ console.log(`\n${prefix}\n`);
70
36
  }
71
37
  }
72
38
 
73
39
  /**
74
- * Render a bordered box around arbitrary content lines.
75
- *
76
- * renderBox("Results", ["line one", "line two"], { footer: "done." })
40
+ * Wraps an array of content lines in a simple bordered box.
41
+ * Used sparingly (e.g. in the interactive runner's generated command).
77
42
  */
78
- export function renderBox(title, lines = [], { footer, width, borderColor = "gray" } = {}) {
79
- const border = chalk[borderColor];
80
- const innerWidth = (width || Math.min(72, Math.max(40, Math.max(...(lines || []).map(l => (l || "").length), title.length)))) - 4;
81
- const top = border(`${BOX.topLeft}${BOX.h.repeat(innerWidth + 2)}${BOX.topRight}`);
82
-
83
- const titleLine = border(BOX.v) + " " + chalk.bold(title) + " " + border(BOX.v);
84
-
85
- const body = (lines || []).map((line) => {
86
- const cleaned = typeof line === "string" ? line : String(line);
87
- return border(BOX.v) + " " + cleaned.padEnd(innerWidth) + " " + border(BOX.v);
88
- });
89
-
90
- const bottom = border(`${BOX.bottomLeft}${BOX.h.repeat(innerWidth + 2)}${BOX.bottomRight}`);
91
-
92
- console.log("\n" + top);
93
- console.log(titleLine);
94
- if (body.length) console.log(body.join("\n"));
95
- if (footer) console.log(" " + chalk.dim(footer));
96
- console.log(bottom + "\n");
43
+ export function renderBox(title, lines = [], { width = 60 } = {}) {
44
+ const inner = Math.max(width - 4, 30);
45
+ const top = chalk.gray(`┌─ ${chalk.bold(title)} ${"─".repeat(Math.max(0, inner - title.length - 2))}┐`);
46
+ console.log(`\n${top}`);
47
+ for (const line of lines) {
48
+ if (line === "") {
49
+ console.log(chalk.gray(`│ ${" ".repeat(inner)} │`));
50
+ } else {
51
+ console.log(`${chalk.gray("│ ")}${String(line).padEnd(inner)}${chalk.gray(" │")}`);
52
+ }
53
+ }
54
+ const bottom = chalk.gray(`└${"─".repeat(inner + 2)}┘\n`);
55
+ console.log(bottom);
97
56
  }
98
57
 
99
58
  /**
100
- * Render a small colored badge.
59
+ * Simple text badge, no background colours.
101
60
  *
102
- * renderBadge("local") → blue background
103
- * renderBadge("image") → category badge
104
- * renderBadge("custom", "info") → generic info badge
61
+ * renderBadge("builtin") → "[builtin]"
62
+ * renderBadge("text") → "[text]"
105
63
  */
106
- export function renderBadge(text, type = "info") {
107
- if (type === "tool") {
108
- const style = BADGE_STYLES[text] || BADGE_STYLES.uncategorized;
109
- return style.label;
110
- }
111
- // Generic info badge
112
- return chalk.gray(`[${text}]`);
64
+ export function renderBadge(text) {
65
+ return chalk.bold(chalk.gray(`[${text}]`));
113
66
  }
114
67
 
115
- /** Pre-built type badge for tool objects. */
68
+ /** Short type indicator for a tool. */
116
69
  export function typeBadge(tool) {
117
70
  const t = tool?.type || "builtin";
118
- return renderBadge(t, "tool");
119
- }
120
-
121
- /** Pre-built category badge — shows the category name in a muted block. */
122
- export function categoryBadge(cat) {
123
- const c = (cat || "uncategorized").toLowerCase();
124
- // Known categories get a coloured block
125
- const known = {
126
- text: chalk.bgWhite.black(" text "),
127
- image: chalk.bgCyan.black(" image "),
128
- data: chalk.bgYellow.black(" data "),
129
- network: chalk.bgBlue.white("network"),
130
- security: chalk.bgRed.white("secure"),
131
- development: chalk.bgGreen.white(" dev "),
132
- utility: chalk.bgGray.white("utils "),
133
- math: chalk.bgGray.white(" math "),
134
- business: chalk.bgGray.white("biz "),
135
- education: chalk.bgGray.white(" learn "),
71
+ const styles = {
72
+ local: chalk.cyan,
73
+ builtin: chalk.green,
74
+ community: chalk.magenta,
136
75
  };
137
- if (known[c]) return known[c];
138
- // Show first 6 chars for unknown categories
139
- const label = c === "uncategorized" ? " any " : c.slice(0, 6).padEnd(6);
140
- return chalk.bgGray.white(` ${label} `);
76
+ const colorFn = styles[t] || chalk.gray;
77
+ return colorFn(`[${t}]`);
141
78
  }
142
79
 
143
- // ─── Table printer ───────────────────────────────────────────────────────────
144
-
145
- /**
146
- * Render an aligned text table.
147
- *
148
- * cols: [{ key, header, width?, align?, color? }]
149
- * rows: array of objects keyed by `key`
150
- */
151
- export function renderTable(cols, rows, { indent = " ", headColor = "bold" } = {}) {
152
- const computed = cols.map((c) => ({
153
- ...c,
154
- width: c.width || Math.max(c.header.length, ...rows.map((r) => stringWidth(r[c.key]))),
155
- }));
156
-
157
- const fmt = (val, col) => {
158
- const str = val == null ? "" : String(val);
159
- const trimmed = str.length > col.width ? str.slice(0, col.width - 1) + "…" : str;
160
- let cell = trimmed.padEnd(col.width);
161
- if (col.align === "right") cell = trimmed.padStart(col.width);
162
- if (col.color) return col.color(cell);
163
- return cell;
164
- };
165
-
166
- // Header
167
- const headLine = indent + computed.map((c) => chalk[headColor](c.header.padEnd(c.width))).join(" ");
168
- const sepLine = indent + computed.map((c) => chalk.gray("─".repeat(c.width))).join(" ");
169
-
170
- console.log(headLine);
171
- console.log(sepLine);
172
- for (const row of rows) {
173
- console.log(indent + computed.map((c) => fmt(row[c.key], c)).join(" "));
174
- }
175
- }
176
-
177
- /** Byte-safe string width (strips ANSI, handles emoji roughly). */
178
- function stringWidth(str) {
179
- return (str || "").replace(/\x1b\[[0-9;]*m/g, "").length;
80
+ /** Short category indicator. */
81
+ export function categoryBadge(cat) {
82
+ return chalk.gray(`[${(cat || "other").slice(0, 10)}]`);
180
83
  }
181
84
 
182
85
  // ─── Pagination engine ───────────────────────────────────────────────────────
183
86
 
184
87
  /**
185
- * Pure pagination. Given a flat list, return a slice plus navigation metadata.
88
+ * Pure pagination: slice a list + return navigation metadata.
186
89
  *
187
- * const page = paginate(items, 10, 2);
188
- * // { items, page, totalPages, total, pageSize, startIndex, endIndex,
189
- * hasPrev, hasNext }
90
+ * paginate(items, 10, 2) → { items, page, totalPages, total, ... }
190
91
  */
191
92
  export function paginate(items, pageSize = PAGE_SIZE, currentPage = 1) {
192
93
  const list = items || [];
@@ -209,14 +110,8 @@ export function paginate(items, pageSize = PAGE_SIZE, currentPage = 1) {
209
110
  }
210
111
 
211
112
  /**
212
- * Interactive paginator: renders the current page via `renderPage`, then offers
213
- * prev / next / cancel navigation until the user settles.
214
- *
215
- * renderPage(pageResult) → returns a value or Promise; its return is used as
216
- * the final resolved value when the user picks "Done".
217
- *
218
- * Returns the last rendered page object (with `selected` set when applicable)
219
- * or `null` on cancel.
113
+ * Interactive paginator: renders via `renderPage`, offers prev/next/done/cancel.
114
+ * Returns the last page result or null on cancel.
220
115
  */
221
116
  export async function paginateInteractive(items, pageSize, renderPage, options = {}) {
222
117
  const all = items || [];
@@ -229,143 +124,52 @@ export async function paginateInteractive(items, pageSize, renderPage, options =
229
124
  let result = paginate(all, pageSize, page);
230
125
 
231
126
  while (true) {
232
- // Clear and re-render the page
233
- if (process.stdout.isTTY) process.stdout.write("\x1b[?25l"); // hide cursor
234
127
  renderPage(result);
235
128
 
236
- if (result.totalPages === 1) {
237
- if (process.stdout.isTTY) process.stdout.write("\x1b[?25h");
238
- return result;
239
- }
240
-
241
- // Build navigator choices
242
- const footer = [];
243
- if (result.hasPrev) footer.push({ name: `${chalk.cyan("◀ Prev page")}`, value: "prev", description: "Show previous page" });
244
- footer.push({ name: `${chalk.cyan("→ Done")}`, value: "done", description: "Keep current view" });
245
- if (result.hasNext) footer.push({ name: `${chalk.cyan("▶ Next page")}`, value: "next", description: "Show next page" });
246
- footer.push({ name: `${chalk.red("✕ Cancel")}`, value: "cancel", description: "Abort" });
129
+ if (result.totalPages <= 1) return result;
247
130
 
248
- const choices = [
249
- new Separator(),
250
- ...footer,
251
- ];
131
+ const nav = [];
132
+ if (result.hasPrev) nav.push({ name: "◀ Prev", value: "prev" });
133
+ nav.push({ name: "→ Done", value: "done" });
134
+ if (result.hasNext) nav.push({ name: "Next ▶", value: "next" });
135
+ nav.push({ name: "✕ Cancel", value: "cancel" });
252
136
 
253
137
  const action = await select({
254
- message: chalk.gray(`Page ${result.page} of ${result.totalPages} · ${result.total} items`),
255
- choices,
256
- pageSize: options.pageSize || 8,
138
+ message: chalk.gray(`Page ${result.page}/${result.totalPages} · ${result.total} items`),
139
+ choices: [new Separator(), ...nav],
140
+ pageSize: 8,
257
141
  });
258
142
 
259
- if (process.stdout.isTTY) process.stdout.write("\x1b[?25l"); // hide again before re-render
260
- if (options.beforeRender) await options.beforeRender();
261
- if (action === "cancel") {
262
- if (process.stdout.isTTY) process.stdout.write("\x1b[?25h");
263
- return null;
264
- }
265
- if (action === "done") {
266
- if (process.stdout.isTTY) process.stdout.write("\x1b[?25h");
267
- return result;
268
- }
143
+ if (action === "cancel") return null;
144
+ if (action === "done") return result;
269
145
  if (action === "prev") page = Math.max(1, page - 1);
270
146
  if (action === "next") page = Math.min(result.totalPages, page + 1);
271
147
  result = paginate(all, pageSize, page);
272
148
  }
273
149
  }
274
150
 
275
- /** Pagination footer: `Page 1 of 5 · 42 items` in a subtle style. */
276
- export function renderPageInfo(result) {
277
- console.log(
278
- chalk.dim(
279
- ` Page ${chalk.cyan(result.page)} of ${chalk.cyan(result.totalPages)} ` +
280
- chalk.gray(`· ${result.total} items`)
281
- )
282
- );
283
- }
284
-
285
- // ─── Prompt wrappers (consistent styling + sensible defaults) ─────────────────
151
+ // ─── Prompt wrappers ─────────────────────────────────────────────────────────
286
152
 
287
- /**
288
- * Present a list of choices and return the selected value.
289
- *
290
- * choices: [{ name, value, description? }] or plain strings
291
- * Use a Separator anywhere in the array.
292
- */
293
153
  export async function promptSelect(message, choices, opts = {}) {
294
- return select({
295
- message: chalk.cyan(message),
296
- choices,
297
- pageSize: opts.pageSize || 10,
298
- ...opts,
299
- });
154
+ return select({ message: chalk.cyan(message), choices, pageSize: opts.pageSize || 10, ...opts });
300
155
  }
301
156
 
302
- /** Single-line text input. */
303
157
  export async function promptInput(message, { defaultValue, validate, ...opts } = {}) {
304
- return input({
305
- message: chalk.cyan(message),
306
- default: defaultValue,
307
- ...(validate ? { validate } : {}),
308
- ...opts,
309
- });
158
+ return input({ message: chalk.cyan(message), default: defaultValue, ...(validate ? { validate } : {}), ...opts });
310
159
  }
311
160
 
312
- /** Yes/no confirmation. */
313
161
  export async function promptConfirm(message, { default: def = true } = {}) {
314
- return confirm({
315
- message: chalk.cyan(message),
316
- default: def,
317
- });
162
+ return confirm({ message: chalk.cyan(message), default: def });
318
163
  }
319
164
 
320
- /** Numeric input. */
321
165
  export async function promptNumber(message, { min, max, ...opts } = {}) {
322
- return number({
323
- message: chalk.cyan(message),
324
- ...(min != null ? { min } : {}),
325
- ...(max != null ? { max } : {}),
326
- ...opts,
327
- });
166
+ return number({ message: chalk.cyan(message), ...(min != null ? { min } : {}), ...(max != null ? { max } : {}), ...opts });
328
167
  }
329
168
 
330
- /**
331
- * Multi-select (checkbox) — returns array of selected values.
332
- * Pass `isCheckedAll` to offer a "toggle all" separator-less convenience.
333
- */
334
169
  export async function promptCheckbox(message, choices, opts = {}) {
335
- return checkbox({
336
- message: chalk.cyan(message),
337
- choices,
338
- pageSize: opts.pageSize || 10,
339
- ...opts,
340
- });
170
+ return checkbox({ message: chalk.cyan(message), choices, pageSize: opts.pageSize || 10, ...opts });
341
171
  }
342
172
 
343
- /** Convenience: separator for checkbox/choice lists. */
344
173
  export function sep(label = "") {
345
174
  return new Separator(label ? ` — ${label} — ` : "");
346
- }
347
-
348
- // ─── Tool-specific renderers ─────────────────────────────────────────────────
349
-
350
- /** Render a single tool row for `nxb list` / `nxb search`. */
351
- export function renderToolCard(tool, { compact = false } = {}) {
352
- const icon = tool.icon || "🔧";
353
- const name = chalk.bold.cyan(tool.name || tool.slug);
354
- const slug = chalk.dim(tool.slug);
355
- const badge = typeBadge(tool);
356
- const desc = chalk.dim(tool.short_description || tool.description || "");
357
-
358
- if (compact) {
359
- console.log(
360
- ` ${icon} ${chalk.cyan(tool.slug.padEnd(28))} ${desc.slice(0, 55)}${badge ? " " + badge : ""}`
361
- );
362
- return;
363
- }
364
-
365
- console.log(` ${icon} ${name} ${badge}`);
366
- console.log(` ${slug}`);
367
- if (desc) console.log(` ${desc}`);
368
- const tags = tool.tags && tool.tags.length ? chalk.gray(tool.tags.map((t) => `#${t}`).join(" ")) : "";
369
- if (tags) console.log(` ${tags}`);
370
- console.log();
371
- }
175
+ }