preva-cli 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +171 -2
- package/bin/preva.js +10 -0
- package/package.json +40 -3
- package/src/commands/add.js +54 -0
- package/src/commands/commit.js +138 -0
- package/src/commands/push.js +98 -0
- package/src/index.js +83 -0
- package/src/utils/colors.js +27 -0
- package/src/utils/git.js +355 -0
- package/src/utils/scanner.js +142 -0
- package/src/utils/ui.js +352 -0
package/src/utils/ui.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import { colors } from './colors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if the current process is in an interactive terminal.
|
|
6
|
+
*/
|
|
7
|
+
export function isInteractive() {
|
|
8
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interactive Multi-Select Checkbox Prompt using native Node readline & raw mode.
|
|
13
|
+
*/
|
|
14
|
+
export function checkboxList({ message, choices, pageSize = 15 }) {
|
|
15
|
+
if (!isInteractive()) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Interactive prompt unavailable in non-interactive environment (no TTY).\nRun with an interactive terminal to use preva preview.'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const items = choices.map((c) => ({
|
|
23
|
+
name: c.name || c.path,
|
|
24
|
+
value: c.value !== undefined ? c.value : c.path,
|
|
25
|
+
checked: Boolean(c.checked),
|
|
26
|
+
flagged: Boolean(c.flagged),
|
|
27
|
+
reason: c.reason || '',
|
|
28
|
+
status: c.status || '',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
let cursor = 0;
|
|
32
|
+
let pageOffset = 0;
|
|
33
|
+
let renderedLineCount = 0;
|
|
34
|
+
|
|
35
|
+
// Save previous raw mode state
|
|
36
|
+
const wasRaw = process.stdin.isRaw;
|
|
37
|
+
readline.emitKeypressEvents(process.stdin);
|
|
38
|
+
if (process.stdin.setRawMode) {
|
|
39
|
+
process.stdin.setRawMode(true);
|
|
40
|
+
}
|
|
41
|
+
process.stdin.resume();
|
|
42
|
+
|
|
43
|
+
// Hide cursor
|
|
44
|
+
process.stdout.write('\x1b[?25l');
|
|
45
|
+
|
|
46
|
+
function cleanup() {
|
|
47
|
+
// Show cursor
|
|
48
|
+
process.stdout.write('\x1b[?25h');
|
|
49
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
50
|
+
if (process.stdin.setRawMode) {
|
|
51
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
52
|
+
}
|
|
53
|
+
process.stdin.pause();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function render(firstTime = false) {
|
|
57
|
+
// Clear previous lines
|
|
58
|
+
if (!firstTime && renderedLineCount > 0) {
|
|
59
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const outputLines = [];
|
|
63
|
+
|
|
64
|
+
// Header
|
|
65
|
+
outputLines.push(
|
|
66
|
+
`${colors.green('?')} ${colors.bold(message)} ${colors.dim(
|
|
67
|
+
'(<space> toggle, <a> toggle all, <enter> confirm)'
|
|
68
|
+
)}`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Adjust pagination window
|
|
72
|
+
if (cursor < pageOffset) {
|
|
73
|
+
pageOffset = cursor;
|
|
74
|
+
} else if (cursor >= pageOffset + pageSize) {
|
|
75
|
+
pageOffset = cursor - pageSize + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const visibleItems = items.slice(pageOffset, pageOffset + pageSize);
|
|
79
|
+
|
|
80
|
+
visibleItems.forEach((item, index) => {
|
|
81
|
+
const actualIndex = pageOffset + index;
|
|
82
|
+
const isCurrent = actualIndex === cursor;
|
|
83
|
+
const pointer = isCurrent ? colors.cyan('❯ ') : ' ';
|
|
84
|
+
const checkbox = item.checked ? colors.green('[x] ') : colors.gray('[ ] ');
|
|
85
|
+
|
|
86
|
+
let statusText = '';
|
|
87
|
+
if (item.status === 'new') statusText = colors.green(' (new)');
|
|
88
|
+
else if (item.status === 'deleted') statusText = colors.red(' (deleted)');
|
|
89
|
+
else if (item.status === 'renamed') statusText = colors.yellow(' (renamed)');
|
|
90
|
+
else if (item.status) statusText = colors.cyan(` (${item.status})`);
|
|
91
|
+
|
|
92
|
+
let flagNotice = '';
|
|
93
|
+
if (item.flagged) {
|
|
94
|
+
flagNotice = colors.yellow(` [flagged: ${item.reason || 'sensitive'}]`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nameText = isCurrent ? colors.bold(item.name) : item.name;
|
|
98
|
+
outputLines.push(`${pointer}${checkbox}${nameText}${statusText}${flagNotice}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Pagination indicators
|
|
102
|
+
const remainingBelow = items.length - (pageOffset + pageSize);
|
|
103
|
+
const remainingAbove = pageOffset;
|
|
104
|
+
if (items.length > pageSize) {
|
|
105
|
+
const hints = [];
|
|
106
|
+
if (remainingAbove > 0) hints.push(`▲ ${remainingAbove} above`);
|
|
107
|
+
if (remainingBelow > 0) hints.push(`▼ ${remainingBelow} below`);
|
|
108
|
+
outputLines.push(colors.dim(` (${hints.join(' | ')})`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderedLineCount = outputLines.length;
|
|
112
|
+
process.stdout.write(outputLines.map((line) => `\x1b[2K\r${line}`).join('\n') + '\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function onKeypress(str, key) {
|
|
116
|
+
if (key.ctrl && key.name === 'c') {
|
|
117
|
+
cleanup();
|
|
118
|
+
process.stdout.write('\n');
|
|
119
|
+
process.exit(130);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
123
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
124
|
+
render();
|
|
125
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
126
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
127
|
+
render();
|
|
128
|
+
} else if (key.name === 'space') {
|
|
129
|
+
items[cursor].checked = !items[cursor].checked;
|
|
130
|
+
render();
|
|
131
|
+
} else if (str === 'a' || str === 'A') {
|
|
132
|
+
const hasUnchecked = items.some((i) => !i.checked);
|
|
133
|
+
items.forEach((i) => {
|
|
134
|
+
i.checked = hasUnchecked;
|
|
135
|
+
});
|
|
136
|
+
render();
|
|
137
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
138
|
+
cleanup();
|
|
139
|
+
const selected = items.filter((i) => i.checked).map((i) => i.value);
|
|
140
|
+
resolve(selected);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
process.stdin.on('keypress', onKeypress);
|
|
145
|
+
render(true);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Interactive Yes/No Confirmation Prompt.
|
|
151
|
+
*/
|
|
152
|
+
export function confirmPrompt({ message, defaultValue = false }) {
|
|
153
|
+
if (!isInteractive()) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'Interactive prompt unavailable in non-interactive environment (no TTY).\nRun with an interactive terminal to use preva preview.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const wasRaw = process.stdin.isRaw;
|
|
161
|
+
readline.emitKeypressEvents(process.stdin);
|
|
162
|
+
if (process.stdin.setRawMode) {
|
|
163
|
+
process.stdin.setRawMode(true);
|
|
164
|
+
}
|
|
165
|
+
process.stdin.resume();
|
|
166
|
+
|
|
167
|
+
const hint = defaultValue ? '[Y/n]' : '[y/N]';
|
|
168
|
+
process.stdout.write(`${colors.green('?')} ${colors.bold(message)} ${colors.cyan(hint)}: `);
|
|
169
|
+
|
|
170
|
+
function cleanup() {
|
|
171
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
172
|
+
if (process.stdin.setRawMode) {
|
|
173
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
174
|
+
}
|
|
175
|
+
process.stdin.pause();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onKeypress(str, key) {
|
|
179
|
+
if (key.ctrl && key.name === 'c') {
|
|
180
|
+
cleanup();
|
|
181
|
+
process.stdout.write('\n');
|
|
182
|
+
process.exit(130);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let answer = defaultValue;
|
|
186
|
+
let char = '';
|
|
187
|
+
|
|
188
|
+
if (key.name === 'y' || str === 'y' || str === 'Y') {
|
|
189
|
+
answer = true;
|
|
190
|
+
char = 'y';
|
|
191
|
+
} else if (key.name === 'n' || str === 'n' || str === 'N') {
|
|
192
|
+
answer = false;
|
|
193
|
+
char = 'n';
|
|
194
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
195
|
+
answer = defaultValue;
|
|
196
|
+
char = defaultValue ? 'y' : 'n';
|
|
197
|
+
} else {
|
|
198
|
+
return; // Ignore other keys
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
cleanup();
|
|
202
|
+
process.stdout.write(`${char}\n`);
|
|
203
|
+
resolve(answer);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
process.stdin.on('keypress', onKeypress);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Interactive Single-Select Prompt using native Node readline & raw mode.
|
|
212
|
+
*/
|
|
213
|
+
export function selectPrompt({ message, choices, pageSize = 12 }) {
|
|
214
|
+
if (!isInteractive()) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
'Interactive prompt unavailable in non-interactive environment (no TTY).\nRun with an interactive terminal to use preva preview.'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const items = choices.map((c) => {
|
|
222
|
+
if (typeof c === 'string') {
|
|
223
|
+
return { name: c, value: c, description: '' };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
name: c.name || c.value,
|
|
227
|
+
value: c.value,
|
|
228
|
+
description: c.description || '',
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let cursor = 0;
|
|
233
|
+
let pageOffset = 0;
|
|
234
|
+
let renderedLineCount = 0;
|
|
235
|
+
|
|
236
|
+
const wasRaw = process.stdin.isRaw;
|
|
237
|
+
readline.emitKeypressEvents(process.stdin);
|
|
238
|
+
if (process.stdin.setRawMode) {
|
|
239
|
+
process.stdin.setRawMode(true);
|
|
240
|
+
}
|
|
241
|
+
process.stdin.resume();
|
|
242
|
+
|
|
243
|
+
// Hide cursor
|
|
244
|
+
process.stdout.write('\x1b[?25l');
|
|
245
|
+
|
|
246
|
+
function cleanup() {
|
|
247
|
+
process.stdout.write('\x1b[?25h');
|
|
248
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
249
|
+
if (process.stdin.setRawMode) {
|
|
250
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
251
|
+
}
|
|
252
|
+
process.stdin.pause();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function render(firstTime = false) {
|
|
256
|
+
if (!firstTime && renderedLineCount > 0) {
|
|
257
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const outputLines = [];
|
|
261
|
+
outputLines.push(
|
|
262
|
+
`${colors.green('?')} ${colors.bold(message)} ${colors.dim('(Use arrow keys, <enter> to select)')}`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (cursor < pageOffset) {
|
|
266
|
+
pageOffset = cursor;
|
|
267
|
+
} else if (cursor >= pageOffset + pageSize) {
|
|
268
|
+
pageOffset = cursor - pageSize + 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const visibleItems = items.slice(pageOffset, pageOffset + pageSize);
|
|
272
|
+
|
|
273
|
+
visibleItems.forEach((item, index) => {
|
|
274
|
+
const actualIndex = pageOffset + index;
|
|
275
|
+
const isCurrent = actualIndex === cursor;
|
|
276
|
+
const pointer = isCurrent ? colors.cyan('❯ ') : ' ';
|
|
277
|
+
const nameText = isCurrent ? colors.bold(colors.cyan(item.name)) : item.name;
|
|
278
|
+
const descText = item.description ? ` ${colors.dim(`- ${item.description}`)}` : '';
|
|
279
|
+
outputLines.push(`${pointer}${nameText}${descText}`);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const remainingBelow = items.length - (pageOffset + pageSize);
|
|
283
|
+
const remainingAbove = pageOffset;
|
|
284
|
+
if (items.length > pageSize) {
|
|
285
|
+
const hints = [];
|
|
286
|
+
if (remainingAbove > 0) hints.push(`▲ ${remainingAbove} above`);
|
|
287
|
+
if (remainingBelow > 0) hints.push(`▼ ${remainingBelow} below`);
|
|
288
|
+
outputLines.push(colors.dim(` (${hints.join(' | ')})`));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
renderedLineCount = outputLines.length;
|
|
292
|
+
process.stdout.write(outputLines.map((line) => `\x1b[2K\r${line}`).join('\n') + '\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function onKeypress(str, key) {
|
|
296
|
+
if (key.ctrl && key.name === 'c') {
|
|
297
|
+
cleanup();
|
|
298
|
+
process.stdout.write('\n');
|
|
299
|
+
process.exit(130);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
303
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
304
|
+
render();
|
|
305
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
306
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
307
|
+
render();
|
|
308
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
309
|
+
cleanup();
|
|
310
|
+
resolve(items[cursor].value);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
process.stdin.on('keypress', onKeypress);
|
|
315
|
+
render(true);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Text Input Prompt using native Node readline interface.
|
|
321
|
+
*/
|
|
322
|
+
export function inputPrompt({ message, defaultValue = '', placeholder = '', allowEmpty = true }) {
|
|
323
|
+
if (!isInteractive()) {
|
|
324
|
+
throw new Error('Interactive prompt unavailable in non-interactive environment (no TTY).');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
const rl = readline.createInterface({
|
|
329
|
+
input: process.stdin,
|
|
330
|
+
output: process.stdout,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const hint = placeholder ? ` ${colors.dim(`(${placeholder})`)}` : '';
|
|
334
|
+
const defaultHint = defaultValue ? ` ${colors.dim(`[${defaultValue}]`)}` : '';
|
|
335
|
+
const promptText = `${colors.green('?')} ${colors.bold(message)}${hint}${defaultHint}: `;
|
|
336
|
+
|
|
337
|
+
function ask() {
|
|
338
|
+
rl.question(promptText, (answer) => {
|
|
339
|
+
const val = answer.trim() || defaultValue;
|
|
340
|
+
if (!allowEmpty && !val) {
|
|
341
|
+
console.log(colors.yellow(' This field is required. Please enter a value.'));
|
|
342
|
+
ask();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
rl.close();
|
|
346
|
+
resolve(val);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
ask();
|
|
351
|
+
});
|
|
352
|
+
}
|