@nexusbloom/cli 0.2.12 → 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/README.md +96 -19
- package/package.json +2 -1
- package/src/abbr.js +80 -0
- package/src/index.js +278 -940
- package/src/list.js +82 -0
- package/src/paths.js +42 -0
- package/src/run.js +380 -0
- package/src/tools.js +561 -0
- package/src/ui.js +175 -0
package/src/ui.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui.js — Terminal UI helpers for the nxb CLI.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight: pagination engine, inquirer prompt wrappers, simple text helpers.
|
|
5
|
+
* No box drawing, no background badges, no heavy tables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import {
|
|
10
|
+
select,
|
|
11
|
+
input,
|
|
12
|
+
confirm,
|
|
13
|
+
number,
|
|
14
|
+
checkbox,
|
|
15
|
+
Separator,
|
|
16
|
+
} from "@inquirer/prompts";
|
|
17
|
+
|
|
18
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const PAGE_SIZE = 10;
|
|
21
|
+
|
|
22
|
+
// ─── Simple helpers ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Print a simple bold header line.
|
|
26
|
+
*
|
|
27
|
+
* renderHeader("Results", { subtitle: "3 items" })
|
|
28
|
+
* // → "Results — 3 items"
|
|
29
|
+
*/
|
|
30
|
+
export function renderHeader(title, { subtitle, color = "cyan" } = {}) {
|
|
31
|
+
const prefix = chalk.bold[color](` ${title}`);
|
|
32
|
+
if (subtitle) {
|
|
33
|
+
console.log(`\n${prefix} ${chalk.gray("—")} ${chalk.dim(subtitle)}\n`);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(`\n${prefix}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wraps an array of content lines in a simple bordered box.
|
|
41
|
+
* Used sparingly (e.g. in the interactive runner's generated command).
|
|
42
|
+
*/
|
|
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);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Simple text badge, no background colours.
|
|
60
|
+
*
|
|
61
|
+
* renderBadge("builtin") → "[builtin]"
|
|
62
|
+
* renderBadge("text") → "[text]"
|
|
63
|
+
*/
|
|
64
|
+
export function renderBadge(text) {
|
|
65
|
+
return chalk.bold(chalk.gray(`[${text}]`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Short type indicator for a tool. */
|
|
69
|
+
export function typeBadge(tool) {
|
|
70
|
+
const t = tool?.type || "builtin";
|
|
71
|
+
const styles = {
|
|
72
|
+
local: chalk.cyan,
|
|
73
|
+
builtin: chalk.green,
|
|
74
|
+
community: chalk.magenta,
|
|
75
|
+
};
|
|
76
|
+
const colorFn = styles[t] || chalk.gray;
|
|
77
|
+
return colorFn(`[${t}]`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Short category indicator. */
|
|
81
|
+
export function categoryBadge(cat) {
|
|
82
|
+
return chalk.gray(`[${(cat || "other").slice(0, 10)}]`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Pagination engine ───────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pure pagination: slice a list + return navigation metadata.
|
|
89
|
+
*
|
|
90
|
+
* paginate(items, 10, 2) → { items, page, totalPages, total, ... }
|
|
91
|
+
*/
|
|
92
|
+
export function paginate(items, pageSize = PAGE_SIZE, currentPage = 1) {
|
|
93
|
+
const list = items || [];
|
|
94
|
+
const total = list.length;
|
|
95
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
96
|
+
const page = Math.max(1, Math.min(currentPage, totalPages));
|
|
97
|
+
const startIndex = (page - 1) * pageSize;
|
|
98
|
+
const endIndex = Math.min(startIndex + pageSize, total);
|
|
99
|
+
return {
|
|
100
|
+
items: list.slice(startIndex, endIndex),
|
|
101
|
+
page,
|
|
102
|
+
totalPages,
|
|
103
|
+
total,
|
|
104
|
+
pageSize,
|
|
105
|
+
startIndex,
|
|
106
|
+
endIndex,
|
|
107
|
+
hasPrev: page > 1,
|
|
108
|
+
hasNext: page < totalPages,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Interactive paginator: renders via `renderPage`, offers prev/next/done/cancel.
|
|
114
|
+
* Returns the last page result or null on cancel.
|
|
115
|
+
*/
|
|
116
|
+
export async function paginateInteractive(items, pageSize, renderPage, options = {}) {
|
|
117
|
+
const all = items || [];
|
|
118
|
+
if (all.length === 0) {
|
|
119
|
+
renderPage(paginate([], pageSize, 1));
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let page = 1;
|
|
124
|
+
let result = paginate(all, pageSize, page);
|
|
125
|
+
|
|
126
|
+
while (true) {
|
|
127
|
+
renderPage(result);
|
|
128
|
+
|
|
129
|
+
if (result.totalPages <= 1) return result;
|
|
130
|
+
|
|
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" });
|
|
136
|
+
|
|
137
|
+
const action = await select({
|
|
138
|
+
message: chalk.gray(`Page ${result.page}/${result.totalPages} · ${result.total} items`),
|
|
139
|
+
choices: [new Separator(), ...nav],
|
|
140
|
+
pageSize: 8,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (action === "cancel") return null;
|
|
144
|
+
if (action === "done") return result;
|
|
145
|
+
if (action === "prev") page = Math.max(1, page - 1);
|
|
146
|
+
if (action === "next") page = Math.min(result.totalPages, page + 1);
|
|
147
|
+
result = paginate(all, pageSize, page);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Prompt wrappers ─────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export async function promptSelect(message, choices, opts = {}) {
|
|
154
|
+
return select({ message: chalk.cyan(message), choices, pageSize: opts.pageSize || 10, ...opts });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function promptInput(message, { defaultValue, validate, ...opts } = {}) {
|
|
158
|
+
return input({ message: chalk.cyan(message), default: defaultValue, ...(validate ? { validate } : {}), ...opts });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function promptConfirm(message, { default: def = true } = {}) {
|
|
162
|
+
return confirm({ message: chalk.cyan(message), default: def });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function promptNumber(message, { min, max, ...opts } = {}) {
|
|
166
|
+
return number({ message: chalk.cyan(message), ...(min != null ? { min } : {}), ...(max != null ? { max } : {}), ...opts });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function promptCheckbox(message, choices, opts = {}) {
|
|
170
|
+
return checkbox({ message: chalk.cyan(message), choices, pageSize: opts.pageSize || 10, ...opts });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function sep(label = "") {
|
|
174
|
+
return new Separator(label ? ` — ${label} — ` : "");
|
|
175
|
+
}
|