@tsuzuku/arise 1.1.1 → 1.2.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/README.md +28 -4
- package/index.js +47 -9
- package/lib/cli.js +39 -2
- package/lib/config/init.js +862 -0
- package/lib/config.js +24 -8
- package/lib/git.js +29 -0
- package/lib/herdr.js +47 -6
- package/lib/interactive.js +644 -0
- package/lib/layout.js +21 -7
- package/lib/lifecycle/create.js +28 -4
- package/lib/lifecycle/nuke.js +44 -2
- package/lib/logger.js +85 -0
- package/lib/skill.js +6 -0
- package/lib/tui/ansi.js +294 -0
- package/lib/tui/prompt.js +629 -0
- package/package.json +1 -1
- package/skills/arise/SKILL.md +6 -0
- package/types.d.ts +46 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { ANSI, truncate } = require('./ansi');
|
|
6
|
+
|
|
7
|
+
function createPathCompleter(options = {}) {
|
|
8
|
+
return function pathCompleter(line) {
|
|
9
|
+
const raw = (line || '').trim();
|
|
10
|
+
|
|
11
|
+
if (raw === '~') {
|
|
12
|
+
return [['~/'], '~'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let expanded = raw;
|
|
16
|
+
if (raw.startsWith('~' + path.sep) || raw.startsWith('~/')) {
|
|
17
|
+
expanded = path.join(os.homedir(), raw.slice(2));
|
|
18
|
+
} else if (raw === '~') {
|
|
19
|
+
expanded = os.homedir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let searchDir;
|
|
23
|
+
let partial;
|
|
24
|
+
|
|
25
|
+
if (raw.endsWith('/') || raw.endsWith(path.sep)) {
|
|
26
|
+
searchDir = expanded;
|
|
27
|
+
partial = '';
|
|
28
|
+
} else {
|
|
29
|
+
const lastSlashIndex = Math.max(raw.lastIndexOf('/'), raw.lastIndexOf('\\'));
|
|
30
|
+
if (lastSlashIndex >= 0) {
|
|
31
|
+
const dirPart = expanded.slice(0, expanded.lastIndexOf(path.sep) + 1);
|
|
32
|
+
searchDir = dirPart || (path.sep === '\\' ? 'C:\\' : '/');
|
|
33
|
+
partial = raw.slice(lastSlashIndex + 1);
|
|
34
|
+
} else {
|
|
35
|
+
searchDir = process.cwd();
|
|
36
|
+
partial = raw;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const entries = fs.readdirSync(searchDir || '.', { withFileTypes: true });
|
|
42
|
+
const hits = [];
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
// Skip hidden files unless user started typing with a dot
|
|
46
|
+
if (!partial.startsWith('.') && entry.name.startsWith('.')) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (entry.name.startsWith(partial)) {
|
|
51
|
+
let isDir = entry.isDirectory();
|
|
52
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
53
|
+
try {
|
|
54
|
+
const fullPath = path.join(searchDir || '.', entry.name);
|
|
55
|
+
isDir = fs.statSync(fullPath).isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
isDir = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options.directoriesOnly && !isDir) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let name = entry.name;
|
|
66
|
+
if (isDir) {
|
|
67
|
+
name += '/';
|
|
68
|
+
}
|
|
69
|
+
hits.push(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hits.sort((a, b) => a.localeCompare(b));
|
|
74
|
+
return [hits, partial];
|
|
75
|
+
} catch {
|
|
76
|
+
return [[], partial];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pathCompleter = createPathCompleter();
|
|
82
|
+
|
|
83
|
+
async function promptSelect(options) {
|
|
84
|
+
const message = options.message || options.title || '';
|
|
85
|
+
const choices = options.choices || options.items || [];
|
|
86
|
+
const defaultIndex = options.defaultIndex || 0;
|
|
87
|
+
|
|
88
|
+
if (choices.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Normalize choices to { label, value, hint, description }
|
|
93
|
+
const normalizedChoices = choices.map((c) => {
|
|
94
|
+
if (typeof c === 'object' && c !== null) {
|
|
95
|
+
return {
|
|
96
|
+
label: c.label || c.title || String(c.value),
|
|
97
|
+
value: c.value !== undefined ? c.value : c,
|
|
98
|
+
hint: c.hint || c.description,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
label: String(c),
|
|
103
|
+
value: c,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!process.stdin.isTTY) {
|
|
108
|
+
// Non-interactive fallback
|
|
109
|
+
return normalizedChoices[Math.min(defaultIndex, normalizedChoices.length - 1)].value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Promise((resolvePromise) => {
|
|
113
|
+
let selectedIndex = Math.max(0, Math.min(defaultIndex, normalizedChoices.length - 1));
|
|
114
|
+
let isRendered = false;
|
|
115
|
+
|
|
116
|
+
const render = () => {
|
|
117
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
118
|
+
if (isRendered) {
|
|
119
|
+
process.stdout.write(`\r\x1b[${normalizedChoices.length + 1}A`);
|
|
120
|
+
}
|
|
121
|
+
isRendered = true;
|
|
122
|
+
|
|
123
|
+
const titleLine = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${message}${ANSI.reset} ${ANSI.gray}(Use arrow keys or numbers, Enter to select)${ANSI.reset}`;
|
|
124
|
+
process.stdout.write(ANSI.clearLine + truncate(titleLine, termWidth - 1) + '\n');
|
|
125
|
+
|
|
126
|
+
normalizedChoices.forEach((choice, index) => {
|
|
127
|
+
const isSelected = index === selectedIndex;
|
|
128
|
+
const pointer = isSelected ? `${ANSI.brightCyan}❯${ANSI.reset}` : ' ';
|
|
129
|
+
const numStr = `${ANSI.gray}${index + 1})${ANSI.reset}`;
|
|
130
|
+
const label = isSelected
|
|
131
|
+
? `${ANSI.bold}${ANSI.brightCyan}${choice.label}${ANSI.reset}`
|
|
132
|
+
: `${ANSI.white}${choice.label}${ANSI.reset}`;
|
|
133
|
+
const hint = choice.hint ? ` ${ANSI.gray}(${choice.hint})${ANSI.reset}` : '';
|
|
134
|
+
|
|
135
|
+
const line = ` ${pointer} ${numStr} ${label}${hint}`;
|
|
136
|
+
process.stdout.write(ANSI.clearLine + truncate(line, termWidth - 1) + '\n');
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
if (process.stdin.setRawMode) {
|
|
142
|
+
process.stdin.setRawMode(false);
|
|
143
|
+
}
|
|
144
|
+
process.stdin.pause();
|
|
145
|
+
process.stdin.removeListener('data', onData);
|
|
146
|
+
process.stdout.write(ANSI.showCursor);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const onData = (chunk) => {
|
|
150
|
+
const key = chunk.toString();
|
|
151
|
+
|
|
152
|
+
// Enter key
|
|
153
|
+
if (key === '\r' || key === '\n') {
|
|
154
|
+
cleanup();
|
|
155
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
156
|
+
const selected = normalizedChoices[selectedIndex];
|
|
157
|
+
const summary = `${ANSI.bold}${ANSI.green}✔${ANSI.reset} ${message} ${ANSI.cyan}${selected.label}${ANSI.reset}`;
|
|
158
|
+
process.stdout.write(ANSI.clearLine + truncate(summary, termWidth - 1) + '\n\n');
|
|
159
|
+
resolvePromise(selected.value);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Ctrl+C
|
|
164
|
+
if (key === '\u0003') {
|
|
165
|
+
cleanup();
|
|
166
|
+
process.exit(130);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Escape key or 'q'
|
|
170
|
+
if (key === '\u001b' || (chunk.length === 1 && chunk[0] === 0x1b)) {
|
|
171
|
+
cleanup();
|
|
172
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
173
|
+
const backSummary = `${ANSI.bold}${ANSI.gray}↩${ANSI.reset} ${ANSI.dim}${message} (Cancelled)${ANSI.reset}`;
|
|
174
|
+
process.stdout.write(ANSI.clearLine + truncate(backSummary, termWidth - 1) + '\n\n');
|
|
175
|
+
resolvePromise(null);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Number key selection (1..9)
|
|
180
|
+
const num = parseInt(key, 10);
|
|
181
|
+
if (!isNaN(num) && num >= 1 && num <= normalizedChoices.length) {
|
|
182
|
+
selectedIndex = num - 1;
|
|
183
|
+
cleanup();
|
|
184
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
185
|
+
const selected = normalizedChoices[selectedIndex];
|
|
186
|
+
const summary = `${ANSI.bold}${ANSI.green}✔${ANSI.reset} ${message} ${ANSI.cyan}${selected.label}${ANSI.reset}`;
|
|
187
|
+
process.stdout.write(ANSI.clearLine + truncate(summary, termWidth - 1) + '\n\n');
|
|
188
|
+
resolvePromise(selected.value);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Quick 'y' / 'Y' key selection for boolean / confirmation choices
|
|
193
|
+
if (key === 'y' || key === 'Y') {
|
|
194
|
+
const yesIdx = normalizedChoices.findIndex(
|
|
195
|
+
(c) => c.value === true || c.label.toLowerCase().startsWith('yes')
|
|
196
|
+
);
|
|
197
|
+
if (yesIdx >= 0) {
|
|
198
|
+
selectedIndex = yesIdx;
|
|
199
|
+
cleanup();
|
|
200
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
201
|
+
const selected = normalizedChoices[selectedIndex];
|
|
202
|
+
const summary = `${ANSI.bold}${ANSI.green}✔${ANSI.reset} ${message} ${ANSI.cyan}${selected.label}${ANSI.reset}`;
|
|
203
|
+
process.stdout.write(ANSI.clearLine + truncate(summary, termWidth - 1) + '\n\n');
|
|
204
|
+
resolvePromise(selected.value);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Quick 'n' / 'N' key selection for boolean / confirmation choices
|
|
210
|
+
if (key === 'n' || key === 'N') {
|
|
211
|
+
const noIdx = normalizedChoices.findIndex(
|
|
212
|
+
(c) => c.value === false || c.label.toLowerCase().startsWith('no')
|
|
213
|
+
);
|
|
214
|
+
if (noIdx >= 0) {
|
|
215
|
+
selectedIndex = noIdx;
|
|
216
|
+
cleanup();
|
|
217
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
218
|
+
const selected = normalizedChoices[selectedIndex];
|
|
219
|
+
const summary = `${ANSI.bold}${ANSI.green}✔${ANSI.reset} ${message} ${ANSI.cyan}${selected.label}${ANSI.reset}`;
|
|
220
|
+
process.stdout.write(ANSI.clearLine + truncate(summary, termWidth - 1) + '\n\n');
|
|
221
|
+
resolvePromise(selected.value);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Up arrow or k
|
|
227
|
+
if (key === '\u001b[A' || key === '\u001bOA' || key === 'k') {
|
|
228
|
+
selectedIndex = (selectedIndex - 1 + normalizedChoices.length) % normalizedChoices.length;
|
|
229
|
+
render();
|
|
230
|
+
}
|
|
231
|
+
// Down arrow or j
|
|
232
|
+
else if (key === '\u001b[B' || key === '\u001bOB' || key === 'j') {
|
|
233
|
+
selectedIndex = (selectedIndex + 1) % normalizedChoices.length;
|
|
234
|
+
render();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (process.stdin.setRawMode) {
|
|
239
|
+
process.stdin.setRawMode(true);
|
|
240
|
+
}
|
|
241
|
+
process.stdin.resume();
|
|
242
|
+
process.stdout.write(ANSI.hideCursor);
|
|
243
|
+
render();
|
|
244
|
+
process.stdin.on('data', onData);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function promptMultiSelect(options) {
|
|
249
|
+
const message = options.message || options.title || '';
|
|
250
|
+
const choices = options.choices || options.items || [];
|
|
251
|
+
const pageSize = options.pageSize || 8;
|
|
252
|
+
const allowCustomInput = options.allowCustomInput !== false;
|
|
253
|
+
|
|
254
|
+
if (choices.length === 0 && !allowCustomInput) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const allItems = choices.map((c) => {
|
|
259
|
+
if (typeof c === 'object' && c !== null) {
|
|
260
|
+
return {
|
|
261
|
+
label: c.label || c.title || String(c.value),
|
|
262
|
+
value: c.value !== undefined ? c.value : c,
|
|
263
|
+
hint: c.hint || c.description,
|
|
264
|
+
selected: Boolean(c.selected),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
label: String(c),
|
|
269
|
+
value: c,
|
|
270
|
+
selected: false,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!process.stdin.isTTY) {
|
|
275
|
+
const selected = allItems.filter((c) => c.selected).map((c) => c.value);
|
|
276
|
+
return selected.length > 0 ? selected : allItems.length > 0 ? [allItems[0].value] : [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new Promise((resolvePromise) => {
|
|
280
|
+
let filterText = '';
|
|
281
|
+
let selectedIndex = 0;
|
|
282
|
+
let lastRenderedLineCount = 0;
|
|
283
|
+
|
|
284
|
+
const getFilteredItems = () => {
|
|
285
|
+
const lower = filterText.toLowerCase().trim();
|
|
286
|
+
const matched = lower.length === 0
|
|
287
|
+
? allItems.slice()
|
|
288
|
+
: allItems.filter(
|
|
289
|
+
(item) =>
|
|
290
|
+
item.label.toLowerCase().includes(lower) ||
|
|
291
|
+
String(item.value).toLowerCase().includes(lower) ||
|
|
292
|
+
(item.hint && item.hint.toLowerCase().includes(lower))
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (allowCustomInput && lower.length > 0) {
|
|
296
|
+
const exactMatch = allItems.some(
|
|
297
|
+
(i) => i.label.toLowerCase() === lower || String(i.value).toLowerCase() === lower
|
|
298
|
+
);
|
|
299
|
+
if (!exactMatch) {
|
|
300
|
+
matched.push({
|
|
301
|
+
label: `➕ Add "${filterText}"`,
|
|
302
|
+
value: filterText,
|
|
303
|
+
hint: 'custom entry',
|
|
304
|
+
selected: false,
|
|
305
|
+
isCustom: true,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return matched;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const render = () => {
|
|
314
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
315
|
+
const filtered = getFilteredItems();
|
|
316
|
+
|
|
317
|
+
if (filtered.length === 0) {
|
|
318
|
+
selectedIndex = 0;
|
|
319
|
+
} else if (selectedIndex >= filtered.length) {
|
|
320
|
+
selectedIndex = filtered.length - 1;
|
|
321
|
+
} else if (selectedIndex < 0) {
|
|
322
|
+
selectedIndex = 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const total = filtered.length;
|
|
326
|
+
let startIndex = 0;
|
|
327
|
+
let endIndex = Math.min(pageSize, total);
|
|
328
|
+
|
|
329
|
+
if (selectedIndex >= endIndex) {
|
|
330
|
+
endIndex = selectedIndex + 1;
|
|
331
|
+
startIndex = Math.max(0, endIndex - pageSize);
|
|
332
|
+
} else if (selectedIndex < startIndex) {
|
|
333
|
+
startIndex = selectedIndex;
|
|
334
|
+
endIndex = Math.min(total, startIndex + pageSize);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const visibleItems = filtered.slice(startIndex, endIndex);
|
|
338
|
+
|
|
339
|
+
if (lastRenderedLineCount > 0) {
|
|
340
|
+
process.stdout.write(`\r\x1b[${lastRenderedLineCount}A`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const lines = [];
|
|
344
|
+
|
|
345
|
+
// 1. Title line
|
|
346
|
+
const title = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${message}${ANSI.reset} ${ANSI.dim}(<space> to toggle, "a" to toggle all, <enter> to confirm)${ANSI.reset}`;
|
|
347
|
+
lines.push(ANSI.clearLine + truncate(title, termWidth - 1));
|
|
348
|
+
|
|
349
|
+
// 2. Search Filter line
|
|
350
|
+
const searchDisplay = filterText.length > 0
|
|
351
|
+
? `${ANSI.yellow}${filterText}${ANSI.reset}`
|
|
352
|
+
: `${ANSI.dim}type to search...${ANSI.reset}`;
|
|
353
|
+
lines.push(ANSI.clearLine + ` ${ANSI.bold}Search:${ANSI.reset} ${searchDisplay}`);
|
|
354
|
+
|
|
355
|
+
// 3. Top scroll indicator
|
|
356
|
+
if (startIndex > 0) {
|
|
357
|
+
lines.push(ANSI.clearLine + ` ${ANSI.dim}▲ ${startIndex} more above...${ANSI.reset}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 4. Visible items
|
|
361
|
+
if (filtered.length === 0) {
|
|
362
|
+
lines.push(ANSI.clearLine + ` ${ANSI.gray}No matching items found${ANSI.reset}`);
|
|
363
|
+
} else {
|
|
364
|
+
visibleItems.forEach((item, idx) => {
|
|
365
|
+
const globalIdx = startIndex + idx;
|
|
366
|
+
const isCursor = globalIdx === selectedIndex;
|
|
367
|
+
const pointer = isCursor ? `${ANSI.brightCyan}❯${ANSI.reset}` : ' ';
|
|
368
|
+
const checkbox = item.selected
|
|
369
|
+
? `${ANSI.bold}${ANSI.green}[✔]${ANSI.reset}`
|
|
370
|
+
: `${ANSI.gray}[ ]${ANSI.reset}`;
|
|
371
|
+
|
|
372
|
+
const label = isCursor
|
|
373
|
+
? `${ANSI.bold}${ANSI.brightCyan}${item.label}${ANSI.reset}`
|
|
374
|
+
: item.selected
|
|
375
|
+
? `${ANSI.bold}${item.label}${ANSI.reset}`
|
|
376
|
+
: `${ANSI.white}${item.label}${ANSI.reset}`;
|
|
377
|
+
|
|
378
|
+
const hint = item.hint ? ` ${ANSI.gray}(${item.hint})${ANSI.reset}` : '';
|
|
379
|
+
lines.push(ANSI.clearLine + ` ${pointer} ${checkbox} ${label}${hint}`);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 5. Bottom scroll indicator
|
|
384
|
+
if (endIndex < total) {
|
|
385
|
+
lines.push(ANSI.clearLine + ` ${ANSI.dim}▼ ${total - endIndex} more below...${ANSI.reset}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 6. Selected counter footer
|
|
389
|
+
const selectedTotal = allItems.filter((i) => i.selected).length;
|
|
390
|
+
const countDisplay = `${ANSI.dim}(${selectedTotal} selected)${ANSI.reset}`;
|
|
391
|
+
lines.push(ANSI.clearLine + ` ${countDisplay}`);
|
|
392
|
+
|
|
393
|
+
lastRenderedLineCount = lines.length;
|
|
394
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const cleanup = () => {
|
|
398
|
+
if (process.stdin.setRawMode) {
|
|
399
|
+
process.stdin.setRawMode(false);
|
|
400
|
+
}
|
|
401
|
+
process.stdin.pause();
|
|
402
|
+
process.stdin.removeListener('data', onData);
|
|
403
|
+
process.stdout.write(ANSI.showCursor);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const onData = (chunk) => {
|
|
407
|
+
const key = chunk.toString();
|
|
408
|
+
|
|
409
|
+
// Enter key
|
|
410
|
+
if (key === '\r' || key === '\n') {
|
|
411
|
+
cleanup();
|
|
412
|
+
const selected = allItems.filter((i) => i.selected).map((i) => i.value);
|
|
413
|
+
const filtered = getFilteredItems();
|
|
414
|
+
let finalValues = selected;
|
|
415
|
+
if (finalValues.length === 0 && filtered.length > 0) {
|
|
416
|
+
const current = filtered[selectedIndex];
|
|
417
|
+
if (current) {
|
|
418
|
+
finalValues = [current.value];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
423
|
+
const summary = `${ANSI.bold}${ANSI.green}✔${ANSI.reset} ${message} ${ANSI.cyan}[${finalValues.join(', ')}]${ANSI.reset}`;
|
|
424
|
+
process.stdout.write(ANSI.clearLine + truncate(summary, termWidth - 1) + '\n\n');
|
|
425
|
+
resolvePromise(finalValues);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Ctrl+C
|
|
430
|
+
if (key === '\u0003') {
|
|
431
|
+
cleanup();
|
|
432
|
+
process.exit(130);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Standalone Escape
|
|
436
|
+
if (key === '\u001b' || (chunk.length === 1 && chunk[0] === 0x1b)) {
|
|
437
|
+
cleanup();
|
|
438
|
+
const termWidth = Math.max(20, process.stdout.columns || 80);
|
|
439
|
+
const backSummary = `${ANSI.bold}${ANSI.gray}↩${ANSI.reset} ${ANSI.dim}${message} (Cancelled)${ANSI.reset}`;
|
|
440
|
+
process.stdout.write(ANSI.clearLine + truncate(backSummary, termWidth - 1) + '\n\n');
|
|
441
|
+
resolvePromise(null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Up arrow or Ctrl+P
|
|
446
|
+
if (key === '\u001b[A' || key === '\u001bOA' || key === '\u0010') {
|
|
447
|
+
const filtered = getFilteredItems();
|
|
448
|
+
if (filtered.length > 0) {
|
|
449
|
+
selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length;
|
|
450
|
+
render();
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Down arrow or Ctrl+N
|
|
456
|
+
if (key === '\u001b[B' || key === '\u001bOB' || key === '\u000e') {
|
|
457
|
+
const filtered = getFilteredItems();
|
|
458
|
+
if (filtered.length > 0) {
|
|
459
|
+
selectedIndex = (selectedIndex + 1) % filtered.length;
|
|
460
|
+
render();
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Space key: Toggle checkbox
|
|
466
|
+
if (key === ' ' || (chunk.length === 1 && chunk[0] === 32)) {
|
|
467
|
+
const filtered = getFilteredItems();
|
|
468
|
+
const current = filtered[selectedIndex];
|
|
469
|
+
if (current) {
|
|
470
|
+
if (current.isCustom) {
|
|
471
|
+
const newItem = {
|
|
472
|
+
label: String(current.value),
|
|
473
|
+
value: current.value,
|
|
474
|
+
selected: true,
|
|
475
|
+
};
|
|
476
|
+
allItems.push(newItem);
|
|
477
|
+
filterText = '';
|
|
478
|
+
selectedIndex = allItems.length - 1;
|
|
479
|
+
} else {
|
|
480
|
+
current.selected = !current.selected;
|
|
481
|
+
const original = allItems.find((i) => i.value === current.value);
|
|
482
|
+
if (original) original.selected = current.selected;
|
|
483
|
+
}
|
|
484
|
+
render();
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Backspace: Delete from search filter
|
|
490
|
+
if (chunk.length === 1 && (chunk[0] === 0x7f || chunk[0] === 0x08)) {
|
|
491
|
+
if (filterText.length > 0) {
|
|
492
|
+
filterText = filterText.slice(0, -1);
|
|
493
|
+
selectedIndex = 0;
|
|
494
|
+
render();
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Ctrl+A (Toggle all visible items) or 'a' if filter is empty
|
|
500
|
+
if (chunk.length === 1 && (chunk[0] === 0x01 || (filterText === '' && (key === 'a' || key === 'A')))) {
|
|
501
|
+
const filtered = getFilteredItems();
|
|
502
|
+
const allSelected = filtered.every((i) => i.selected);
|
|
503
|
+
for (const item of filtered) {
|
|
504
|
+
if (!item.isCustom) {
|
|
505
|
+
item.selected = !allSelected;
|
|
506
|
+
const original = allItems.find((i) => i.value === item.value);
|
|
507
|
+
if (original) original.selected = item.selected;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
render();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Printable character typing (for real-time filter search)
|
|
515
|
+
if (chunk.length === 1 && chunk[0] >= 32 && chunk[0] <= 126) {
|
|
516
|
+
filterText += chunk.toString();
|
|
517
|
+
selectedIndex = 0;
|
|
518
|
+
render();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
if (process.stdin.setRawMode) {
|
|
524
|
+
process.stdin.setRawMode(true);
|
|
525
|
+
}
|
|
526
|
+
process.stdin.resume();
|
|
527
|
+
process.stdout.write(ANSI.hideCursor);
|
|
528
|
+
render();
|
|
529
|
+
process.stdin.on('data', onData);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function promptText(options) {
|
|
534
|
+
const message = options.message || options.question || '';
|
|
535
|
+
const defaultValue = options.defaultValue || '';
|
|
536
|
+
const validate = options.validate || null;
|
|
537
|
+
|
|
538
|
+
if (!process.stdin.isTTY) {
|
|
539
|
+
return defaultValue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return new Promise((resolvePromise) => {
|
|
543
|
+
let resolvedCompleter;
|
|
544
|
+
if (typeof options.completer === 'function') {
|
|
545
|
+
resolvedCompleter = options.completer;
|
|
546
|
+
} else if (options.completer === 'dir') {
|
|
547
|
+
resolvedCompleter = createPathCompleter({ directoriesOnly: true });
|
|
548
|
+
} else if (options.completer === 'path') {
|
|
549
|
+
resolvedCompleter = pathCompleter;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
process.stdin.resume();
|
|
553
|
+
|
|
554
|
+
const rl = readline.createInterface({
|
|
555
|
+
input: process.stdin,
|
|
556
|
+
output: process.stdout,
|
|
557
|
+
completer: resolvedCompleter,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const defaultHint = defaultValue ? `${ANSI.gray}(${defaultValue})${ANSI.reset} ` : '';
|
|
561
|
+
const promptMsg = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${message}${ANSI.reset} ${defaultHint}`;
|
|
562
|
+
|
|
563
|
+
const onData = (chunk) => {
|
|
564
|
+
// Standalone Escape key
|
|
565
|
+
if (chunk.length === 1 && chunk[0] === 0x1b) {
|
|
566
|
+
cleanup();
|
|
567
|
+
process.stdout.write(`\r${ANSI.clearLine}${ANSI.bold}${ANSI.gray}↩${ANSI.reset} ${ANSI.dim}${message} (Cancelled)${ANSI.reset}\n\n`);
|
|
568
|
+
resolvePromise(null);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const cleanup = () => {
|
|
573
|
+
process.stdin.removeListener('data', onData);
|
|
574
|
+
rl.close();
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
process.stdin.on('data', onData);
|
|
578
|
+
|
|
579
|
+
const ask = () => {
|
|
580
|
+
rl.question(promptMsg, (answer) => {
|
|
581
|
+
const val = answer.trim() || defaultValue;
|
|
582
|
+
if (validate) {
|
|
583
|
+
const res = validate(val);
|
|
584
|
+
if (res !== true) {
|
|
585
|
+
console.log(` ${ANSI.red}✖${ANSI.reset} ${res || 'Invalid input'}`);
|
|
586
|
+
ask();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
cleanup();
|
|
591
|
+
process.stdout.write('\n');
|
|
592
|
+
resolvePromise(val);
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
ask();
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function promptConfirm(options) {
|
|
601
|
+
const message = typeof options === 'string' ? options : (options.message || options.question || '');
|
|
602
|
+
const defaultYes = typeof options === 'object' ? Boolean(options.defaultYes) : false;
|
|
603
|
+
|
|
604
|
+
if (!process.stdin.isTTY) {
|
|
605
|
+
return defaultYes;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const choices = [
|
|
609
|
+
{ label: 'Yes', value: true },
|
|
610
|
+
{ label: 'No', value: false },
|
|
611
|
+
];
|
|
612
|
+
|
|
613
|
+
const result = await promptSelect({
|
|
614
|
+
message,
|
|
615
|
+
choices,
|
|
616
|
+
defaultIndex: defaultYes ? 0 : 1,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return result !== null ? result : defaultYes;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
module.exports = {
|
|
623
|
+
createPathCompleter,
|
|
624
|
+
pathCompleter,
|
|
625
|
+
promptSelect,
|
|
626
|
+
promptMultiSelect,
|
|
627
|
+
promptText,
|
|
628
|
+
promptConfirm,
|
|
629
|
+
};
|
package/package.json
CHANGED
package/skills/arise/SKILL.md
CHANGED
|
@@ -11,6 +11,12 @@ Use this skill to answer questions about `arise` and run worktree management com
|
|
|
11
11
|
|
|
12
12
|
## 1. CLI Quick Reference & Cheatsheet
|
|
13
13
|
|
|
14
|
+
### Interactive Mode
|
|
15
|
+
```bash
|
|
16
|
+
# Launch interactive menu (create, switch/open, list, nuke)
|
|
17
|
+
arise
|
|
18
|
+
```
|
|
19
|
+
|
|
14
20
|
### Creating Worktrees
|
|
15
21
|
```bash
|
|
16
22
|
# Create or open a worktree for a branch (auto-detects preset, boots Herdr workspace)
|