mixdog 0.7.6 → 0.7.7
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/CHANGELOG.md +18 -0
- package/README.md +8 -4
- package/hooks/session-start.cjs +73 -2
- package/package.json +1 -1
- package/scripts/bootstrap.mjs +5 -59
- package/scripts/ensure-deps.mjs +259 -0
- package/scripts/resolve-bun.mjs +60 -0
- package/scripts/run-mcp.mjs +13 -168
- package/setup/install.mjs +180 -9
- package/setup/launch.mjs +0 -0
- package/setup/locate-claude.mjs +38 -0
- package/setup/mixdog-cli.mjs +6 -42
- package/setup/setup-server.mjs +50 -2
- package/setup/setup.html +26 -12
- package/setup/tui.mjs +606 -0
- package/setup/wizard.mjs +117 -128
- package/src/agent/bridge-stall-watchdog.mjs +2 -2
- package/src/agent/index.mjs +3 -3
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +139 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +96 -0
- package/src/agent/orchestrator/session/manager.mjs +5 -3
- package/src/agent/orchestrator/session/store.mjs +9 -1
- package/src/channels/lib/runtime-paths.mjs +112 -74
- package/src/memory/index.mjs +30 -7
- package/src/memory/lib/pg/supervisor.mjs +12 -12
- package/src/shared/atomic-file.mjs +16 -0
- package/src/status/aggregator.mjs +3 -3
package/setup/setup.html
CHANGED
|
@@ -2607,8 +2607,8 @@ const AG_API_PROVIDERS = [
|
|
|
2607
2607
|
{id:'nvidia',name:'NVIDIA',env:'NVIDIA_API_KEY',url:'https://build.nvidia.com'},
|
|
2608
2608
|
];
|
|
2609
2609
|
const AG_OAUTH_PROVIDERS = [
|
|
2610
|
-
{id:'openai-oauth',name:'Codex',desc:'~/.codex/auth.json'},
|
|
2611
|
-
{id:'anthropic-oauth',name:'Claude Code',desc:'~/.claude/.credentials.json'},
|
|
2610
|
+
{id:'openai-oauth',name:'Codex',desc:'~/.codex/auth.json',login:true},
|
|
2611
|
+
{id:'anthropic-oauth',name:'Claude Code',desc:'~/.claude/.credentials.json',login:true},
|
|
2612
2612
|
{id:'grok-oauth',name:'Grok',desc:'~/.grok/auth.json — or browser OAuth (consent: "Grok Build")',login:true},
|
|
2613
2613
|
];
|
|
2614
2614
|
const AG_LOCAL_PROVIDERS = [
|
|
@@ -2673,8 +2673,9 @@ function agRenderAll() {
|
|
|
2673
2673
|
for (const p of AG_OAUTH_PROVIDERS) {
|
|
2674
2674
|
const detected = agOAuthDetected(p.id);
|
|
2675
2675
|
const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>';
|
|
2676
|
-
|
|
2677
|
-
|
|
2676
|
+
const btn = p.login
|
|
2677
|
+
? `<button class="save" style="width:auto;height:28px;padding:0 12px;font-size:11px;flex-shrink:0;margin-left:8px" type="button" onclick="oauthProviderLogin('${escapeAttr(p.id)}', this)">${detected ? 'Re-login' : 'Sign in'}</button>`
|
|
2678
|
+
: '';
|
|
2678
2679
|
oauthSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><span style="flex:1;font-size:12px;color:var(--text-4)">${p.desc}</span>${tag}${btn}</div>`;
|
|
2679
2680
|
}
|
|
2680
2681
|
const localSec = document.getElementById('ag-local-sec');
|
|
@@ -2733,18 +2734,31 @@ function agOAuthDetected(providerId, auth = agAuth) {
|
|
|
2733
2734
|
return false;
|
|
2734
2735
|
}
|
|
2735
2736
|
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2737
|
+
const OAUTH_LOGIN_ROUTES = {
|
|
2738
|
+
'openai-oauth': '/agent/openai-oauth/login',
|
|
2739
|
+
'anthropic-oauth': '/agent/anthropic-oauth/login',
|
|
2740
|
+
'grok-oauth': '/agent/grok-oauth/login',
|
|
2741
|
+
};
|
|
2742
|
+
const OAUTH_LOGIN_LABELS = {
|
|
2743
|
+
'openai-oauth': 'Codex',
|
|
2744
|
+
'anthropic-oauth': 'Claude',
|
|
2745
|
+
'grok-oauth': 'Grok',
|
|
2746
|
+
};
|
|
2747
|
+
|
|
2748
|
+
// Browser OAuth login for providers with login:true. CLI cred files (~/.codex,
|
|
2749
|
+
// ~/.claude, ~/.grok) show as "Set" automatically; Sign in runs when none exist.
|
|
2750
|
+
// Blocks until consent completes, then re-fetches auth so the row flips to Set.
|
|
2751
|
+
async function oauthProviderLogin(providerId, btn) {
|
|
2752
|
+
const route = OAUTH_LOGIN_ROUTES[providerId];
|
|
2753
|
+
if (!route) return;
|
|
2754
|
+
const label = OAUTH_LOGIN_LABELS[providerId] || providerId;
|
|
2741
2755
|
if (btn) { btn.disabled = true; btn.textContent = 'Waiting for browser…'; }
|
|
2742
2756
|
try {
|
|
2743
|
-
const r = await fetch(
|
|
2757
|
+
const r = await fetch(route, { method: 'POST' }).then(r => r.json());
|
|
2744
2758
|
if (r && r.ok) { await loadAgentData(); }
|
|
2745
|
-
else { alert('
|
|
2759
|
+
else { alert(label + ' login failed: ' + ((r && r.error) || 'unknown error')); if (btn) btn.disabled = false; }
|
|
2746
2760
|
} catch (e) {
|
|
2747
|
-
alert('
|
|
2761
|
+
alert(label + ' login failed: ' + (e?.message || e));
|
|
2748
2762
|
if (btn) btn.disabled = false;
|
|
2749
2763
|
}
|
|
2750
2764
|
}
|
package/setup/tui.mjs
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency clack-style terminal UI helpers for the mixdog setup wizard.
|
|
3
|
+
* Node built-ins only; assumes an interactive TTY (wizard guarantees this).
|
|
4
|
+
*/
|
|
5
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const stdin = process.stdin;
|
|
9
|
+
const stdout = process.stdout;
|
|
10
|
+
|
|
11
|
+
const ansi = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
hideCursor: '\x1b[?25l',
|
|
14
|
+
showCursor: '\x1b[?25h',
|
|
15
|
+
cyan: '\x1b[36m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
inverse: '\x1b[7m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
|
+
|
|
24
|
+
const STRIP_ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
25
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
26
|
+
|
|
27
|
+
function assertInteractiveTTY() {
|
|
28
|
+
if (!stdin.isTTY) {
|
|
29
|
+
throw new Error('setup/tui.mjs: stdin must be a TTY');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rail(active = true) {
|
|
34
|
+
return active ? `${ansi.dim}│${ansi.reset}` : `${ansi.dim}◇${ansi.reset}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function prefixGlyph(done) {
|
|
38
|
+
return done
|
|
39
|
+
? `${ansi.green}✔${ansi.reset}`
|
|
40
|
+
: `${ansi.cyan}◆${ansi.reset}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function codePointsOf(segment) {
|
|
44
|
+
const cps = [];
|
|
45
|
+
for (const ch of segment) {
|
|
46
|
+
cps.push(ch.codePointAt(0));
|
|
47
|
+
}
|
|
48
|
+
return cps;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isCombiningOrZeroWidth(cp) {
|
|
52
|
+
if (cp >= 0x0300 && cp <= 0x036f) return true;
|
|
53
|
+
if (cp >= 0x0483 && cp <= 0x0489) return true;
|
|
54
|
+
if (cp >= 0x0591 && cp <= 0x05bd) return true;
|
|
55
|
+
if (cp >= 0x064b && cp <= 0x065f) return true;
|
|
56
|
+
if (cp === 0x0670) return true;
|
|
57
|
+
if (cp >= 0x06d6 && cp <= 0x06dc) return true;
|
|
58
|
+
if (cp >= 0x1ab0 && cp <= 0x1aff) return true;
|
|
59
|
+
if (cp >= 0x1dc0 && cp <= 0x1dff) return true;
|
|
60
|
+
if (cp >= 0x20d0 && cp <= 0x20ff) return true;
|
|
61
|
+
if (cp >= 0xfe20 && cp <= 0xfe2f) return true;
|
|
62
|
+
if (cp === 0x200b || cp === 0x200d) return true;
|
|
63
|
+
if (cp >= 0xfe00 && cp <= 0xfe0f) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isWideBaseChar(cp) {
|
|
68
|
+
if (cp >= 0x1100 && cp <= 0x115f) return true;
|
|
69
|
+
if (cp >= 0x2e80 && cp <= 0x303e) return true;
|
|
70
|
+
if (cp >= 0x3041 && cp <= 0x33ff) return true;
|
|
71
|
+
if (cp >= 0x3400 && cp <= 0x4dbf) return true;
|
|
72
|
+
if (cp >= 0x4e00 && cp <= 0x9fff) return true;
|
|
73
|
+
if (cp >= 0xa000 && cp <= 0xa4cf) return true;
|
|
74
|
+
if (cp >= 0xac00 && cp <= 0xd7a3) return true;
|
|
75
|
+
if (cp >= 0xf900 && cp <= 0xfaff) return true;
|
|
76
|
+
if (cp >= 0xfe30 && cp <= 0xfe4f) return true;
|
|
77
|
+
if (cp >= 0xff00 && cp <= 0xff60) return true;
|
|
78
|
+
if (cp >= 0xffe0 && cp <= 0xffe6) return true;
|
|
79
|
+
if (cp >= 0x1f300) return true;
|
|
80
|
+
if (cp >= 0x20000 && cp <= 0x3fffd) return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function graphemeClusterWidth(segment) {
|
|
85
|
+
const cps = codePointsOf(segment);
|
|
86
|
+
if (cps.length === 0) return 0;
|
|
87
|
+
if (cps.every(isCombiningOrZeroWidth)) return 0;
|
|
88
|
+
|
|
89
|
+
const hasFe0f = cps.some((cp) => cp >= 0xfe00 && cp <= 0xfe0f);
|
|
90
|
+
const hasZwj = cps.includes(0x200d);
|
|
91
|
+
const hasRegional = cps.some((cp) => cp >= 0x1f1e6 && cp <= 0x1f1ff);
|
|
92
|
+
const hasModifier = cps.some((cp) => cp >= 0x1f3fb && cp <= 0x1f3ff);
|
|
93
|
+
const hasWideBase = cps.some((cp) => isWideBaseChar(cp));
|
|
94
|
+
|
|
95
|
+
if (hasFe0f || hasZwj || hasRegional || hasModifier || hasWideBase) return 2;
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cellWidth(str) {
|
|
100
|
+
const plain = str.replace(STRIP_ANSI_RE, '');
|
|
101
|
+
let width = 0;
|
|
102
|
+
for (const { segment } of GRAPHEME_SEGMENTER.segment(plain)) {
|
|
103
|
+
width += graphemeClusterWidth(segment);
|
|
104
|
+
}
|
|
105
|
+
return width;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function lineCount(text) {
|
|
109
|
+
if (!text) return 0;
|
|
110
|
+
const cols = stdout.columns || 80;
|
|
111
|
+
const logical = text.split('\n');
|
|
112
|
+
let total = 0;
|
|
113
|
+
for (const line of logical) {
|
|
114
|
+
const visible = cellWidth(line);
|
|
115
|
+
total += Math.max(1, Math.ceil(visible / cols));
|
|
116
|
+
}
|
|
117
|
+
return total;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeBlock(text) {
|
|
121
|
+
stdout.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function redrawUp(lines) {
|
|
125
|
+
if (lines > 0) {
|
|
126
|
+
stdout.write(`\x1b[${lines}A\x1b[0J`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function finishPrompt(lines, finalLine) {
|
|
131
|
+
redrawUp(lines);
|
|
132
|
+
stdout.write(`${ansi.green}✔${ansi.reset} ${finalLine}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {(ctx: {
|
|
137
|
+
* rerender: (draw: () => string) => void,
|
|
138
|
+
* onKey: (fn: (str: string, key: import('node:readline').Key) => boolean | void) => void,
|
|
139
|
+
* }) => Promise<unknown>} run
|
|
140
|
+
*/
|
|
141
|
+
async function withRawPrompt(run) {
|
|
142
|
+
assertInteractiveTTY();
|
|
143
|
+
const wasRaw = stdin.isRaw;
|
|
144
|
+
const wasPaused = stdin.isPaused();
|
|
145
|
+
let paintedLines = 0;
|
|
146
|
+
let keyHandler = null;
|
|
147
|
+
let settled = false;
|
|
148
|
+
let activePromptReject = null;
|
|
149
|
+
|
|
150
|
+
const promptReject = (rejectFn) => {
|
|
151
|
+
activePromptReject = rejectFn;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const rerender = (draw) => {
|
|
155
|
+
redrawUp(paintedLines);
|
|
156
|
+
const block = draw();
|
|
157
|
+
writeBlock(block);
|
|
158
|
+
paintedLines = lineCount(block);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const onKey = (fn) => {
|
|
162
|
+
keyHandler = fn;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (settled) return;
|
|
167
|
+
settled = true;
|
|
168
|
+
stdin.removeListener('keypress', onKeypress);
|
|
169
|
+
try {
|
|
170
|
+
stdin.setRawMode(!!wasRaw);
|
|
171
|
+
} catch {
|
|
172
|
+
stdin.setRawMode(false);
|
|
173
|
+
}
|
|
174
|
+
if (wasPaused) {
|
|
175
|
+
stdin.pause();
|
|
176
|
+
}
|
|
177
|
+
stdout.write(ansi.showCursor);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const onKeypress = (str, key) => {
|
|
181
|
+
if (!key) return;
|
|
182
|
+
if (key.ctrl && key.name === 'c') {
|
|
183
|
+
cleanup();
|
|
184
|
+
redrawUp(paintedLines);
|
|
185
|
+
stdout.write('\n');
|
|
186
|
+
process.exit(130);
|
|
187
|
+
}
|
|
188
|
+
if (keyHandler) {
|
|
189
|
+
try {
|
|
190
|
+
keyHandler(str, key || {});
|
|
191
|
+
} catch (err) {
|
|
192
|
+
cleanup();
|
|
193
|
+
if (activePromptReject) {
|
|
194
|
+
const rej = activePromptReject;
|
|
195
|
+
activePromptReject = null;
|
|
196
|
+
rej(err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
emitKeypressEvents(stdin);
|
|
204
|
+
stdin.setRawMode(true);
|
|
205
|
+
stdin.resume();
|
|
206
|
+
stdout.write(ansi.hideCursor);
|
|
207
|
+
stdin.on('keypress', onKeypress);
|
|
208
|
+
return await run({ rerender, onKey, promptReject });
|
|
209
|
+
} finally {
|
|
210
|
+
cleanup();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {string} message
|
|
216
|
+
* @param {{ value: string, label: string, hint?: string }[]} options
|
|
217
|
+
* @param {{ initial?: string }} [opts]
|
|
218
|
+
*/
|
|
219
|
+
export async function select(message, options, { initial } = {}) {
|
|
220
|
+
if (!options?.length) {
|
|
221
|
+
throw new Error('select: options must be a non-empty array');
|
|
222
|
+
}
|
|
223
|
+
let index = 0;
|
|
224
|
+
if (initial !== undefined) {
|
|
225
|
+
const i = options.findIndex((o) => o.value === initial);
|
|
226
|
+
if (i >= 0) index = i;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
230
|
+
const draw = () => {
|
|
231
|
+
const rows = [
|
|
232
|
+
`${prefixGlyph(false)} ${message}`,
|
|
233
|
+
rail(),
|
|
234
|
+
];
|
|
235
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
236
|
+
const opt = options[i];
|
|
237
|
+
const active = i === index;
|
|
238
|
+
const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
|
|
239
|
+
const label = active
|
|
240
|
+
? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
|
|
241
|
+
: opt.label;
|
|
242
|
+
const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
|
|
243
|
+
rows.push(`${rail()} ${cursor}${label}${hint}`);
|
|
244
|
+
}
|
|
245
|
+
return rows.join('\n');
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
rerender(draw);
|
|
249
|
+
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
promptReject(reject);
|
|
252
|
+
onKey((_str, key) => {
|
|
253
|
+
const name = key.name;
|
|
254
|
+
if (name === 'up' || name === 'k') {
|
|
255
|
+
index = (index - 1 + options.length) % options.length;
|
|
256
|
+
rerender(draw);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (name === 'down' || name === 'j') {
|
|
260
|
+
index = (index + 1) % options.length;
|
|
261
|
+
rerender(draw);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (name === 'return') {
|
|
265
|
+
const chosen = options[index];
|
|
266
|
+
finishPrompt(
|
|
267
|
+
lineCount(draw()),
|
|
268
|
+
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${chosen.label}${ansi.reset}`,
|
|
269
|
+
);
|
|
270
|
+
resolve(chosen.value);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string} message
|
|
279
|
+
* @param {{ value: string, label: string, hint?: string }[]} options
|
|
280
|
+
* @param {{ initial?: string[], min?: number }} [opts]
|
|
281
|
+
*/
|
|
282
|
+
export async function multiselect(message, options, { initial = [], min = 0 } = {}) {
|
|
283
|
+
if (!options?.length) {
|
|
284
|
+
throw new Error('multiselect: options must be a non-empty array');
|
|
285
|
+
}
|
|
286
|
+
const selected = new Set(
|
|
287
|
+
Array.isArray(initial) ? initial.filter((v) => options.some((o) => o.value === v)) : [],
|
|
288
|
+
);
|
|
289
|
+
let index = 0;
|
|
290
|
+
let error = '';
|
|
291
|
+
|
|
292
|
+
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
293
|
+
const draw = () => {
|
|
294
|
+
const rows = [
|
|
295
|
+
`${prefixGlyph(false)} ${message}`,
|
|
296
|
+
rail(),
|
|
297
|
+
];
|
|
298
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
299
|
+
const opt = options[i];
|
|
300
|
+
const active = i === index;
|
|
301
|
+
const checked = selected.has(opt.value);
|
|
302
|
+
const box = checked ? '◉' : '◯';
|
|
303
|
+
const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
|
|
304
|
+
const label = active
|
|
305
|
+
? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
|
|
306
|
+
: opt.label;
|
|
307
|
+
const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
|
|
308
|
+
rows.push(`${rail()} ${cursor}${box} ${label}${hint}`);
|
|
309
|
+
}
|
|
310
|
+
if (error) {
|
|
311
|
+
rows.push(`${rail()} ${ansi.red}${error}${ansi.reset}`);
|
|
312
|
+
}
|
|
313
|
+
return rows.join('\n');
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
rerender(draw);
|
|
317
|
+
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
promptReject(reject);
|
|
320
|
+
onKey((_str, key) => {
|
|
321
|
+
const name = key.name;
|
|
322
|
+
if (name === 'up' || name === 'k') {
|
|
323
|
+
index = (index - 1 + options.length) % options.length;
|
|
324
|
+
error = '';
|
|
325
|
+
rerender(draw);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (name === 'down' || name === 'j') {
|
|
329
|
+
index = (index + 1) % options.length;
|
|
330
|
+
error = '';
|
|
331
|
+
rerender(draw);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (name === 'space') {
|
|
335
|
+
const val = options[index].value;
|
|
336
|
+
if (selected.has(val)) selected.delete(val);
|
|
337
|
+
else selected.add(val);
|
|
338
|
+
error = '';
|
|
339
|
+
rerender(draw);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (name === 'return') {
|
|
343
|
+
if (selected.size < min) {
|
|
344
|
+
error = `Select at least ${min} option${min === 1 ? '' : 's'} (${selected.size}/${min})`;
|
|
345
|
+
rerender(draw);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const values = options
|
|
349
|
+
.filter((o) => selected.has(o.value))
|
|
350
|
+
.map((o) => o.value);
|
|
351
|
+
const labels = options
|
|
352
|
+
.filter((o) => selected.has(o.value))
|
|
353
|
+
.map((o) => o.label)
|
|
354
|
+
.join(', ');
|
|
355
|
+
finishPrompt(
|
|
356
|
+
lineCount(draw()),
|
|
357
|
+
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${labels || '(none)'}${ansi.reset}`,
|
|
358
|
+
);
|
|
359
|
+
resolve(values);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @param {string} message
|
|
368
|
+
* @param {{ initial?: boolean }} [opts]
|
|
369
|
+
*/
|
|
370
|
+
export async function confirm(message, { initial = false } = {}) {
|
|
371
|
+
let value = !!initial;
|
|
372
|
+
|
|
373
|
+
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
374
|
+
const draw = () => {
|
|
375
|
+
const yes = value
|
|
376
|
+
? `${ansi.cyan}${ansi.inverse} Yes ${ansi.reset}`
|
|
377
|
+
: `${ansi.dim}Yes${ansi.reset}`;
|
|
378
|
+
const no = !value
|
|
379
|
+
? `${ansi.cyan}${ansi.inverse} No ${ansi.reset}`
|
|
380
|
+
: `${ansi.dim}No${ansi.reset}`;
|
|
381
|
+
return [
|
|
382
|
+
`${prefixGlyph(false)} ${message}`,
|
|
383
|
+
rail(),
|
|
384
|
+
`${rail()} ${yes} / ${no}`,
|
|
385
|
+
`${rail()} ${ansi.dim}←/→ or y/n, Enter${ansi.reset}`,
|
|
386
|
+
].join('\n');
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
rerender(draw);
|
|
390
|
+
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
promptReject(reject);
|
|
393
|
+
onKey((str, key) => {
|
|
394
|
+
const name = key.name;
|
|
395
|
+
if (name === 'left' || name === 'y') {
|
|
396
|
+
value = true;
|
|
397
|
+
rerender(draw);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (name === 'right' || name === 'n') {
|
|
401
|
+
value = false;
|
|
402
|
+
rerender(draw);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (name === 'return') {
|
|
406
|
+
const label = value ? 'Yes' : 'No';
|
|
407
|
+
finishPrompt(
|
|
408
|
+
lineCount(draw()),
|
|
409
|
+
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${label}${ansi.reset}`,
|
|
410
|
+
);
|
|
411
|
+
resolve(value);
|
|
412
|
+
}
|
|
413
|
+
if (str === 'y' || str === 'Y') {
|
|
414
|
+
value = true;
|
|
415
|
+
rerender(draw);
|
|
416
|
+
}
|
|
417
|
+
if (str === 'n' || str === 'N') {
|
|
418
|
+
value = false;
|
|
419
|
+
rerender(draw);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @param {string} message
|
|
428
|
+
* @param {{ initial?: string, placeholder?: string }} [opts]
|
|
429
|
+
*/
|
|
430
|
+
export async function text(message, { initial = '', placeholder = '' } = {}) {
|
|
431
|
+
let value = String(initial ?? '');
|
|
432
|
+
|
|
433
|
+
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
434
|
+
const draw = () => {
|
|
435
|
+
const shown = value.length > 0
|
|
436
|
+
? value
|
|
437
|
+
: (placeholder ? `${ansi.dim}${placeholder}${ansi.reset}` : '');
|
|
438
|
+
return [
|
|
439
|
+
`${prefixGlyph(false)} ${message}`,
|
|
440
|
+
rail(),
|
|
441
|
+
`${rail()} ${shown}${ansi.dim}▌${ansi.reset}`,
|
|
442
|
+
].join('\n');
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
rerender(draw);
|
|
446
|
+
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
promptReject(reject);
|
|
449
|
+
onKey((str, key) => {
|
|
450
|
+
const name = key.name;
|
|
451
|
+
if (name === 'return') {
|
|
452
|
+
const out = value.length > 0 ? value : String(initial ?? '');
|
|
453
|
+
finishPrompt(
|
|
454
|
+
lineCount(draw()),
|
|
455
|
+
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${out || '(empty)'}${ansi.reset}`,
|
|
456
|
+
);
|
|
457
|
+
resolve(out);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (name === 'backspace') {
|
|
461
|
+
value = value.slice(0, -1);
|
|
462
|
+
rerender(draw);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (str && !key.ctrl && !key.meta && str >= ' ') {
|
|
466
|
+
value += str;
|
|
467
|
+
rerender(draw);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @param {string} message
|
|
476
|
+
*/
|
|
477
|
+
export async function password(message) {
|
|
478
|
+
let value = '';
|
|
479
|
+
|
|
480
|
+
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
481
|
+
const draw = () => {
|
|
482
|
+
const masked = value.length > 0 ? '•'.repeat(value.length) : '';
|
|
483
|
+
return [
|
|
484
|
+
`${prefixGlyph(false)} ${message}`,
|
|
485
|
+
rail(),
|
|
486
|
+
`${rail()} ${masked}${ansi.dim}▌${ansi.reset}`,
|
|
487
|
+
].join('\n');
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
rerender(draw);
|
|
491
|
+
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
promptReject(reject);
|
|
494
|
+
onKey((str, key) => {
|
|
495
|
+
const name = key.name;
|
|
496
|
+
if (name === 'return') {
|
|
497
|
+
finishPrompt(
|
|
498
|
+
lineCount(draw()),
|
|
499
|
+
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${'•'.repeat(value.length)}${ansi.reset}`,
|
|
500
|
+
);
|
|
501
|
+
resolve(value);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (name === 'backspace') {
|
|
505
|
+
value = value.slice(0, -1);
|
|
506
|
+
rerender(draw);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (str && !key.ctrl && !key.meta && str >= ' ') {
|
|
510
|
+
value += str;
|
|
511
|
+
rerender(draw);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {string} message
|
|
520
|
+
* @param {{ total?: number, width?: number }} [opts]
|
|
521
|
+
*/
|
|
522
|
+
export function createProgressBar(message, { total = 100, width = 24 } = {}) {
|
|
523
|
+
const safeTotal = total > 0 ? total : 100;
|
|
524
|
+
let lastDone = 0;
|
|
525
|
+
let closed = false;
|
|
526
|
+
|
|
527
|
+
const render = (done, tot) => {
|
|
528
|
+
const clamped = Math.max(0, Math.min(tot, done));
|
|
529
|
+
const pct = Math.min(100, Math.floor((clamped / tot) * 100));
|
|
530
|
+
const filled = Math.min(width, Math.round((clamped / tot) * width));
|
|
531
|
+
const bar = `${'█'.repeat(filled)}${'░'.repeat(width - filled)}`;
|
|
532
|
+
stdout.write(`\r${message} [${bar}] ${pct}%`);
|
|
533
|
+
lastDone = clamped;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
update(done, totalOverride) {
|
|
538
|
+
if (closed) return;
|
|
539
|
+
const tot = totalOverride !== undefined && totalOverride > 0 ? totalOverride : safeTotal;
|
|
540
|
+
render(done, tot);
|
|
541
|
+
},
|
|
542
|
+
done(finalMsg) {
|
|
543
|
+
if (closed) return;
|
|
544
|
+
closed = true;
|
|
545
|
+
const tail = finalMsg ? ` ${finalMsg}` : '';
|
|
546
|
+
stdout.write('\r\x1b[2K');
|
|
547
|
+
stdout.write(`${ansi.green}✔${ansi.reset} ${message}${tail}\n`);
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param {string} message
|
|
554
|
+
*/
|
|
555
|
+
export function createSpinner(message) {
|
|
556
|
+
let frame = 0;
|
|
557
|
+
let current = message;
|
|
558
|
+
let timer = null;
|
|
559
|
+
let stopped = false;
|
|
560
|
+
|
|
561
|
+
const tick = () => {
|
|
562
|
+
const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
563
|
+
frame += 1;
|
|
564
|
+
stdout.write(`\r${ansi.cyan}${glyph}${ansi.reset} ${current}`);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
timer = setInterval(tick, 80);
|
|
568
|
+
tick();
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
update(msg) {
|
|
572
|
+
if (stopped) return;
|
|
573
|
+
current = String(msg ?? '');
|
|
574
|
+
tick();
|
|
575
|
+
},
|
|
576
|
+
stop(finalMsg, ok = true) {
|
|
577
|
+
if (stopped) return;
|
|
578
|
+
stopped = true;
|
|
579
|
+
if (timer) clearInterval(timer);
|
|
580
|
+
stdout.write('\r\x1b[2K');
|
|
581
|
+
const mark = ok ? `${ansi.green}✔${ansi.reset}` : `${ansi.red}✖${ansi.reset}`;
|
|
582
|
+
const tail = finalMsg ? ` ${finalMsg}` : '';
|
|
583
|
+
stdout.write(`${mark} ${current}${tail}\n`);
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (process.argv[1] === fileURLToPath(import.meta.url) && process.env.TUI_SMOKE) {
|
|
589
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
590
|
+
(async () => {
|
|
591
|
+
const bar = createProgressBar('TUI smoke progress', { total: 100, width: 20 });
|
|
592
|
+
for (let i = 0; i <= 100; i += 10) {
|
|
593
|
+
bar.update(i);
|
|
594
|
+
await sleep(40);
|
|
595
|
+
}
|
|
596
|
+
bar.done('complete');
|
|
597
|
+
const spin = createSpinner('TUI smoke spinner');
|
|
598
|
+
await sleep(400);
|
|
599
|
+
spin.update('TUI smoke spinner (tick)');
|
|
600
|
+
await sleep(400);
|
|
601
|
+
spin.stop('stopped', true);
|
|
602
|
+
})().catch((err) => {
|
|
603
|
+
console.error(err);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
});
|
|
606
|
+
}
|