task-summary-extractor 9.4.0 → 9.6.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/.env.example +1 -1
- package/ARCHITECTURE.md +6 -0
- package/QUICK_START.md +4 -2
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/phases/discover.js +1 -0
- package/src/phases/init.js +63 -1
- package/src/phases/process-media.js +32 -5
- package/src/pipeline.js +4 -3
- package/src/services/video.js +116 -25
- package/src/utils/cli.js +114 -300
- package/src/utils/interactive.js +356 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt engine — zero dependencies, arrow-key navigation.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* selectOne() — single-select with ↑↓ arrows + Enter
|
|
6
|
+
* selectMany() — multi-select with ↑↓ arrows + Space (toggle) + Enter (confirm)
|
|
7
|
+
*
|
|
8
|
+
* Falls back to number input when stdin is not a TTY.
|
|
9
|
+
*
|
|
10
|
+
* Rendering strategy:
|
|
11
|
+
* After every draw(), the terminal cursor sits on the LAST rendered line
|
|
12
|
+
* (bottom of the block). Before each subsequent redraw we move UP by
|
|
13
|
+
* (totalRenderedLines − 1) to reach the first item line, then overwrite
|
|
14
|
+
* every line top-to-bottom. This keeps positioning deterministic and
|
|
15
|
+
* avoids the overlap / garble bugs of relative-to-highlight approaches.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { c, strip } = require('./colors');
|
|
21
|
+
|
|
22
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
25
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
26
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
27
|
+
|
|
28
|
+
/** Move cursor up N lines */
|
|
29
|
+
const UP = (n) => n > 0 ? `\x1b[${n}A` : '';
|
|
30
|
+
/** Move cursor down N lines */
|
|
31
|
+
const DOWN = (n) => n > 0 ? `\x1b[${n}B` : '';
|
|
32
|
+
/** Carriage return — column 0 */
|
|
33
|
+
const CR = '\r';
|
|
34
|
+
|
|
35
|
+
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build display strings for each item.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array<{label: string, hint?: string}>} items
|
|
41
|
+
* @param {number} cursor Currently highlighted index
|
|
42
|
+
* @param {Set<number>} [selected] For multi-select: selected indices
|
|
43
|
+
* @param {boolean} [multi=false]
|
|
44
|
+
* @returns {string[]}
|
|
45
|
+
*/
|
|
46
|
+
function renderList(items, cursor, selected, multi = false) {
|
|
47
|
+
return items.map((item, i) => {
|
|
48
|
+
const isCursor = i === cursor;
|
|
49
|
+
const prefix = isCursor ? c.cyan('❯') : ' ';
|
|
50
|
+
|
|
51
|
+
let checkbox = '';
|
|
52
|
+
if (multi) {
|
|
53
|
+
const isChecked = selected && selected.has(i);
|
|
54
|
+
checkbox = isChecked ? c.green(' ◉') : c.dim(' ○');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const label = isCursor ? c.bold(c.cyan(strip(item.label))) : item.label;
|
|
58
|
+
const hint = item.hint
|
|
59
|
+
? (isCursor ? c.dim(` ${strip(item.hint)}`) : c.dim(` ${item.hint}`))
|
|
60
|
+
: '';
|
|
61
|
+
|
|
62
|
+
return ` ${prefix}${checkbox} ${label}${hint}`;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write an array of strings to stdout, one per line.
|
|
68
|
+
* Each line is preceded by CR + CLEAR_LINE so the entire row is wiped first.
|
|
69
|
+
*/
|
|
70
|
+
function writeLines(lines) {
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
if (i > 0) process.stdout.write('\n');
|
|
73
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${lines[i]}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Key decoder ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decode a raw keypress buffer into a named action.
|
|
81
|
+
* @param {Buffer} buf
|
|
82
|
+
* @returns {'up'|'down'|'space'|'enter'|'escape'|'a'|null}
|
|
83
|
+
*/
|
|
84
|
+
function decodeKey(buf) {
|
|
85
|
+
if (buf[0] === 0x1b && buf[1] === 0x5b) {
|
|
86
|
+
if (buf[2] === 0x41) return 'up';
|
|
87
|
+
if (buf[2] === 0x42) return 'down';
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (buf[0] === 0x0d || buf[0] === 0x0a) return 'enter';
|
|
91
|
+
if (buf[0] === 0x20) return 'space';
|
|
92
|
+
if (buf[0] === 0x03) return 'escape';
|
|
93
|
+
if (buf[0] === 0x61 || buf[0] === 0x41) return 'a';
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Core: selectOne ───────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Interactive single-select with arrow-key navigation.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} opts
|
|
103
|
+
* @param {string} opts.title - Heading text (printed once)
|
|
104
|
+
* @param {Array<{label: string, hint?: string, value: any}>} opts.items
|
|
105
|
+
* @param {number} [opts.default=0] - Default highlighted index
|
|
106
|
+
* @param {string} [opts.footer] - Hint line below the list
|
|
107
|
+
* @returns {Promise<{index: number, value: any}>}
|
|
108
|
+
*/
|
|
109
|
+
function selectOne({ title, items, default: defaultIdx = 0, footer }) {
|
|
110
|
+
if (!process.stdin.isTTY) {
|
|
111
|
+
return _fallbackSelectOne({ title, items, default: defaultIdx });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let cursor = defaultIdx;
|
|
116
|
+
const total = items.length;
|
|
117
|
+
const hasFooter = !!footer;
|
|
118
|
+
const renderedLines = total + (hasFooter ? 1 : 0); // lines we overwrite
|
|
119
|
+
let firstDraw = true;
|
|
120
|
+
|
|
121
|
+
// ── Title (printed once, never overwritten) ────────
|
|
122
|
+
if (title) {
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(` ${title}`);
|
|
125
|
+
console.log(c.dim(' ' + '─'.repeat(60)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(HIDE_CURSOR);
|
|
129
|
+
|
|
130
|
+
// ── Draw / redraw ──────────────────────────────────
|
|
131
|
+
const draw = () => {
|
|
132
|
+
if (!firstDraw) {
|
|
133
|
+
// Terminal cursor is on the last rendered line — go back to first
|
|
134
|
+
process.stdout.write(UP(renderedLines - 1) + CR);
|
|
135
|
+
}
|
|
136
|
+
const lines = renderList(items, cursor);
|
|
137
|
+
writeLines(lines);
|
|
138
|
+
if (hasFooter) {
|
|
139
|
+
process.stdout.write('\n');
|
|
140
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${c.dim(` ${footer}`)}`);
|
|
141
|
+
}
|
|
142
|
+
// Terminal cursor is now on the LAST rendered line
|
|
143
|
+
firstDraw = false;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
draw(); // initial render
|
|
147
|
+
|
|
148
|
+
process.stdin.setRawMode(true);
|
|
149
|
+
process.stdin.resume();
|
|
150
|
+
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
process.stdin.setRawMode(false);
|
|
153
|
+
process.stdin.pause();
|
|
154
|
+
process.stdin.removeListener('data', onKey);
|
|
155
|
+
process.stdout.write(CR + SHOW_CURSOR + '\n');
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const onKey = (buf) => {
|
|
159
|
+
const key = decodeKey(buf);
|
|
160
|
+
if (key === 'up') {
|
|
161
|
+
cursor = (cursor - 1 + total) % total;
|
|
162
|
+
draw();
|
|
163
|
+
} else if (key === 'down') {
|
|
164
|
+
cursor = (cursor + 1) % total;
|
|
165
|
+
draw();
|
|
166
|
+
} else if (key === 'enter') {
|
|
167
|
+
cleanup();
|
|
168
|
+
const chosen = items[cursor];
|
|
169
|
+
console.log(c.success(`${strip(chosen.label)}`));
|
|
170
|
+
resolve({ index: cursor, value: chosen.value });
|
|
171
|
+
} else if (key === 'escape') {
|
|
172
|
+
cleanup();
|
|
173
|
+
const chosen = items[defaultIdx];
|
|
174
|
+
console.log(c.success(`${strip(chosen.label)}`));
|
|
175
|
+
resolve({ index: defaultIdx, value: chosen.value });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
process.stdin.on('data', onKey);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Core: selectMany ──────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Interactive multi-select with arrow-key navigation + Space toggle.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} opts
|
|
189
|
+
* @param {string} opts.title
|
|
190
|
+
* @param {Array<{label: string, hint?: string, value: any}>} opts.items
|
|
191
|
+
* @param {Set<number>} [opts.defaultSelected] - Pre-selected indices
|
|
192
|
+
* @param {string} [opts.footer]
|
|
193
|
+
* @returns {Promise<{indices: number[], values: any[]}>}
|
|
194
|
+
*/
|
|
195
|
+
function selectMany({ title, items, defaultSelected, footer }) {
|
|
196
|
+
if (!process.stdin.isTTY) {
|
|
197
|
+
return _fallbackSelectMany({ title, items, defaultSelected });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
const selected = new Set(defaultSelected || []);
|
|
203
|
+
const total = items.length;
|
|
204
|
+
|
|
205
|
+
const footerText = footer || '↑↓ navigate · Space toggle · A all/none · Enter confirm';
|
|
206
|
+
const renderedLines = total + 1; // items + footer (always shown)
|
|
207
|
+
let firstDraw = true;
|
|
208
|
+
|
|
209
|
+
if (title) {
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(` ${title}`);
|
|
212
|
+
console.log(c.dim(' ' + '─'.repeat(60)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process.stdout.write(HIDE_CURSOR);
|
|
216
|
+
|
|
217
|
+
const draw = () => {
|
|
218
|
+
if (!firstDraw) {
|
|
219
|
+
process.stdout.write(UP(renderedLines - 1) + CR);
|
|
220
|
+
}
|
|
221
|
+
const lines = renderList(items, cursor, selected, true);
|
|
222
|
+
writeLines(lines);
|
|
223
|
+
process.stdout.write('\n');
|
|
224
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${c.dim(` ${footerText}`)}`);
|
|
225
|
+
firstDraw = false;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
draw();
|
|
229
|
+
|
|
230
|
+
process.stdin.setRawMode(true);
|
|
231
|
+
process.stdin.resume();
|
|
232
|
+
|
|
233
|
+
const cleanup = () => {
|
|
234
|
+
process.stdin.setRawMode(false);
|
|
235
|
+
process.stdin.pause();
|
|
236
|
+
process.stdin.removeListener('data', onKey);
|
|
237
|
+
process.stdout.write(CR + SHOW_CURSOR + '\n');
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onKey = (buf) => {
|
|
241
|
+
const key = decodeKey(buf);
|
|
242
|
+
if (key === 'up') {
|
|
243
|
+
cursor = (cursor - 1 + total) % total;
|
|
244
|
+
draw();
|
|
245
|
+
} else if (key === 'down') {
|
|
246
|
+
cursor = (cursor + 1) % total;
|
|
247
|
+
draw();
|
|
248
|
+
} else if (key === 'space') {
|
|
249
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
250
|
+
else selected.add(cursor);
|
|
251
|
+
draw();
|
|
252
|
+
} else if (key === 'a') {
|
|
253
|
+
if (selected.size === total) selected.clear();
|
|
254
|
+
else { for (let i = 0; i < total; i++) selected.add(i); }
|
|
255
|
+
draw();
|
|
256
|
+
} else if (key === 'enter') {
|
|
257
|
+
cleanup();
|
|
258
|
+
const indices = [...selected].sort((a, b) => a - b);
|
|
259
|
+
const values = indices.map(i => items[i].value);
|
|
260
|
+
const labels = indices.map(i => strip(items[i].label));
|
|
261
|
+
if (labels.length === 0) {
|
|
262
|
+
console.log(c.dim(' None selected'));
|
|
263
|
+
} else if (labels.length === total) {
|
|
264
|
+
console.log(c.success('All selected'));
|
|
265
|
+
} else {
|
|
266
|
+
console.log(c.success(labels.join(', ')));
|
|
267
|
+
}
|
|
268
|
+
resolve({ indices, values });
|
|
269
|
+
} else if (key === 'escape') {
|
|
270
|
+
cleanup();
|
|
271
|
+
const indices = [...(defaultSelected || [])].sort((a, b) => a - b);
|
|
272
|
+
const values = indices.map(i => items[i].value);
|
|
273
|
+
resolve({ indices, values });
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
process.stdin.on('data', onKey);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Fallback implementations (non-TTY) ───────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function _fallbackSelectOne({ title, items, default: defaultIdx }) {
|
|
284
|
+
const readline = require('readline');
|
|
285
|
+
if (title) {
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(` ${title}`);
|
|
288
|
+
console.log(c.dim(' ' + '─'.repeat(50)));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
items.forEach((item, i) => {
|
|
292
|
+
const marker = i === defaultIdx ? c.green(' ← default') : '';
|
|
293
|
+
console.log(` ${c.cyan(`[${i + 1}]`)} ${item.label}${item.hint ? c.dim(` ${item.hint}`) : ''}${marker}`);
|
|
294
|
+
});
|
|
295
|
+
console.log('');
|
|
296
|
+
|
|
297
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
298
|
+
return new Promise(resolve => {
|
|
299
|
+
rl.question(` Select [1-${items.length}] (Enter = default): `, answer => {
|
|
300
|
+
rl.close();
|
|
301
|
+
const trimmed = (answer || '').trim();
|
|
302
|
+
if (!trimmed) {
|
|
303
|
+
resolve({ index: defaultIdx, value: items[defaultIdx].value });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const num = parseInt(trimmed, 10);
|
|
307
|
+
if (num >= 1 && num <= items.length) {
|
|
308
|
+
resolve({ index: num - 1, value: items[num - 1].value });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
resolve({ index: defaultIdx, value: items[defaultIdx].value });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function _fallbackSelectMany({ title, items, defaultSelected }) {
|
|
317
|
+
const readline = require('readline');
|
|
318
|
+
if (title) {
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log(` ${title}`);
|
|
321
|
+
console.log(c.dim(' ' + '─'.repeat(50)));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
items.forEach((item, i) => {
|
|
325
|
+
console.log(` ${c.cyan(`[${i + 1}]`)} ${item.label}${item.hint ? c.dim(` ${item.hint}`) : ''}`);
|
|
326
|
+
});
|
|
327
|
+
console.log(` ${c.cyan('[A]')} All`);
|
|
328
|
+
console.log('');
|
|
329
|
+
|
|
330
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
331
|
+
return new Promise(resolve => {
|
|
332
|
+
rl.question(' Select (e.g. 1,3 or A for all) [Enter = all]: ', answer => {
|
|
333
|
+
rl.close();
|
|
334
|
+
const trimmed = (answer || '').trim().toLowerCase();
|
|
335
|
+
if (!trimmed || trimmed === 'a' || trimmed === 'all') {
|
|
336
|
+
const all = items.map((_, i) => i);
|
|
337
|
+
resolve({ indices: all, values: items.map(it => it.value) });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
341
|
+
const indices = [];
|
|
342
|
+
for (const p of parts) {
|
|
343
|
+
const num = parseInt(p, 10);
|
|
344
|
+
if (num >= 1 && num <= items.length) indices.push(num - 1);
|
|
345
|
+
}
|
|
346
|
+
if (indices.length === 0) {
|
|
347
|
+
const all = items.map((_, i) => i);
|
|
348
|
+
resolve({ indices: all, values: items.map(it => it.value) });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
resolve({ indices, values: indices.map(i => items[i].value) });
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { selectOne, selectMany };
|