@jackwener/opencli 0.5.2 → 0.6.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 +23 -7
- package/README.zh-CN.md +24 -8
- package/SKILL.md +6 -2
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/main.js +7 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/package.json +1 -1
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/main.ts +8 -0
- package/src/setup.ts +169 -0
- package/src/tui.ts +171 -0
package/src/tui.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tui.ts — Zero-dependency interactive TUI components
|
|
3
|
+
*
|
|
4
|
+
* Uses raw stdin mode + ANSI escape codes for interactive prompts.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export interface CheckboxItem {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
checked: boolean;
|
|
12
|
+
/** Optional status to display after the label */
|
|
13
|
+
status?: string;
|
|
14
|
+
statusColor?: 'green' | 'yellow' | 'red' | 'dim';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Interactive multi-select checkbox prompt.
|
|
19
|
+
*
|
|
20
|
+
* Controls:
|
|
21
|
+
* ↑/↓ or j/k — navigate
|
|
22
|
+
* Space — toggle selection
|
|
23
|
+
* a — toggle all
|
|
24
|
+
* Enter — confirm
|
|
25
|
+
* q/Esc — cancel (returns empty)
|
|
26
|
+
*/
|
|
27
|
+
export async function checkboxPrompt(
|
|
28
|
+
items: CheckboxItem[],
|
|
29
|
+
opts: { title?: string; hint?: string } = {},
|
|
30
|
+
): Promise<string[]> {
|
|
31
|
+
if (items.length === 0) return [];
|
|
32
|
+
|
|
33
|
+
const { stdin, stdout } = process;
|
|
34
|
+
if (!stdin.isTTY) {
|
|
35
|
+
// Non-interactive: return all checked items
|
|
36
|
+
return items.filter(i => i.checked).map(i => i.value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cursor = 0;
|
|
40
|
+
const state = items.map(i => ({ ...i }));
|
|
41
|
+
|
|
42
|
+
function colorStatus(status: string | undefined, color: CheckboxItem['statusColor']): string {
|
|
43
|
+
if (!status) return '';
|
|
44
|
+
switch (color) {
|
|
45
|
+
case 'green': return chalk.green(status);
|
|
46
|
+
case 'yellow': return chalk.yellow(status);
|
|
47
|
+
case 'red': return chalk.red(status);
|
|
48
|
+
case 'dim': return chalk.dim(status);
|
|
49
|
+
default: return chalk.dim(status);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function render() {
|
|
54
|
+
// Move cursor to start and clear
|
|
55
|
+
let out = '';
|
|
56
|
+
|
|
57
|
+
if (opts.title) {
|
|
58
|
+
out += `\n${chalk.bold(opts.title)}\n\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < state.length; i++) {
|
|
62
|
+
const item = state[i];
|
|
63
|
+
const pointer = i === cursor ? chalk.cyan('❯') : ' ';
|
|
64
|
+
const checkbox = item.checked ? chalk.green('◉') : chalk.dim('○');
|
|
65
|
+
const label = i === cursor ? chalk.bold(item.label) : item.label;
|
|
66
|
+
const status = colorStatus(item.status, item.statusColor);
|
|
67
|
+
out += ` ${pointer} ${checkbox} ${label}${status ? ` ${status}` : ''}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
out += `\n ${chalk.dim('↑↓ navigate · Space toggle · a all · Enter confirm · q cancel')}\n`;
|
|
71
|
+
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Promise<string[]>((resolve) => {
|
|
76
|
+
const wasRaw = stdin.isRaw;
|
|
77
|
+
stdin.setRawMode(true);
|
|
78
|
+
stdin.resume();
|
|
79
|
+
stdout.write('\x1b[?25l'); // Hide cursor
|
|
80
|
+
|
|
81
|
+
let firstDraw = true;
|
|
82
|
+
|
|
83
|
+
function draw() {
|
|
84
|
+
// Clear previous render (skip on first draw)
|
|
85
|
+
if (!firstDraw) {
|
|
86
|
+
const lines = render().split('\n').length;
|
|
87
|
+
stdout.write(`\x1b[${lines}A\x1b[J`);
|
|
88
|
+
}
|
|
89
|
+
firstDraw = false;
|
|
90
|
+
stdout.write(render());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cleanup() {
|
|
94
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
95
|
+
stdin.pause();
|
|
96
|
+
stdin.removeListener('data', onData);
|
|
97
|
+
// Clear the TUI and restore cursor
|
|
98
|
+
const lines = render().split('\n').length;
|
|
99
|
+
stdout.write(`\x1b[${lines}A\x1b[J`);
|
|
100
|
+
stdout.write('\x1b[?25h'); // Show cursor
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onData(data: Buffer) {
|
|
104
|
+
const key = data.toString();
|
|
105
|
+
|
|
106
|
+
// Arrow up / k
|
|
107
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
108
|
+
cursor = (cursor - 1 + state.length) % state.length;
|
|
109
|
+
draw();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Arrow down / j
|
|
114
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
115
|
+
cursor = (cursor + 1) % state.length;
|
|
116
|
+
draw();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Space — toggle
|
|
121
|
+
if (key === ' ') {
|
|
122
|
+
state[cursor].checked = !state[cursor].checked;
|
|
123
|
+
draw();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Tab — toggle and move down
|
|
128
|
+
if (key === '\t') {
|
|
129
|
+
state[cursor].checked = !state[cursor].checked;
|
|
130
|
+
cursor = (cursor + 1) % state.length;
|
|
131
|
+
draw();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 'a' — toggle all
|
|
136
|
+
if (key === 'a') {
|
|
137
|
+
const allChecked = state.every(i => i.checked);
|
|
138
|
+
for (const item of state) item.checked = !allChecked;
|
|
139
|
+
draw();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Enter — confirm
|
|
144
|
+
if (key === '\r' || key === '\n') {
|
|
145
|
+
cleanup();
|
|
146
|
+
const selected = state.filter(i => i.checked).map(i => i.value);
|
|
147
|
+
// Show summary
|
|
148
|
+
stdout.write(` ${chalk.green('✓')} ${chalk.bold(`${selected.length} file(s) selected`)}\n\n`);
|
|
149
|
+
resolve(selected);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// q / Esc — cancel
|
|
154
|
+
if (key === 'q' || key === '\x1b') {
|
|
155
|
+
cleanup();
|
|
156
|
+
stdout.write(` ${chalk.yellow('✗')} ${chalk.dim('Cancelled')}\n\n`);
|
|
157
|
+
resolve([]);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ctrl+C — exit process
|
|
162
|
+
if (key === '\x03') {
|
|
163
|
+
cleanup();
|
|
164
|
+
process.exit(130);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
stdin.on('data', onData);
|
|
169
|
+
draw();
|
|
170
|
+
});
|
|
171
|
+
}
|