@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.
- package/package.json +1 -1
- package/src/abbr.js +21 -52
- package/src/list.js +25 -50
- package/src/ui.js +62 -258
package/package.json
CHANGED
package/src/abbr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* abbr.js — `nxb abbr` command.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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,
|
|
14
|
-
import {
|
|
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.
|
|
18
|
+
console.log(chalk.yellow(" No tools available."));
|
|
22
19
|
return;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 +
|
|
7
|
+
* nxb list -s ssl — filter + paginated
|
|
9
8
|
* nxb list --json — raw JSON
|
|
10
|
-
* nxb list --no-interactive—
|
|
9
|
+
* nxb list --no-interactive— single dump
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import chalk from "chalk";
|
|
14
|
-
import { fetchTools
|
|
15
|
-
import {
|
|
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.
|
|
18
|
+
console.log(chalk.yellow(" No tools found."));
|
|
23
19
|
return;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
56
|
-
*
|
|
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 = "
|
|
59
|
-
const
|
|
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.
|
|
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
|
-
*
|
|
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 = [], {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
*
|
|
59
|
+
* Simple text badge, no background colours.
|
|
101
60
|
*
|
|
102
|
-
* renderBadge("
|
|
103
|
-
* renderBadge("
|
|
104
|
-
* renderBadge("custom", "info") → generic info badge
|
|
61
|
+
* renderBadge("builtin") → "[builtin]"
|
|
62
|
+
* renderBadge("text") → "[text]"
|
|
105
63
|
*/
|
|
106
|
-
export function renderBadge(text
|
|
107
|
-
|
|
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
|
-
/**
|
|
68
|
+
/** Short type indicator for a tool. */
|
|
116
69
|
export function typeBadge(tool) {
|
|
117
70
|
const t = tool?.type || "builtin";
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
88
|
+
* Pure pagination: slice a list + return navigation metadata.
|
|
186
89
|
*
|
|
187
|
-
*
|
|
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
|
|
213
|
-
*
|
|
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
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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}
|
|
255
|
-
choices,
|
|
256
|
-
pageSize:
|
|
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 (
|
|
260
|
-
if (
|
|
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
|
-
|
|
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
|
+
}
|