joinhive 2.0.1 → 2.1.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 +5 -3
- package/bin/derive-evm-key.mjs +13 -0
- package/bin/hive +26 -6
- package/bin/hive-buzz.mjs +73 -0
- package/bin/hive-join.mjs +342 -91
- package/bin/hive-key.mjs +131 -0
- package/bin/hive-net.mjs +13 -0
- package/daemon/fanout.mjs +5 -2
- package/daemon/hived.mjs +21 -2
- package/docs/cli.md +3 -1
- package/package.json +3 -2
- package/server/api.mjs +41 -1
- package/server/join-page.mjs +7 -5
- package/server/provision.mjs +140 -4
- package/server/supervisor.mjs +27 -0
- package/shared/prompt.mjs +168 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// shared/prompt — the CLI's interactive moments (select/ask/confirm/pause)
|
|
2
|
+
// and progress rendering (spinner/checkpoints), hand-rolled to keep the pack
|
|
3
|
+
// dependency-light.
|
|
4
|
+
//
|
|
5
|
+
// TTY rules:
|
|
6
|
+
// - The arrow-key select needs a real TTY; when stdin/stdout isn't one
|
|
7
|
+
// (CI, pipes, heredocs) every prompt degrades to numbered/plain line
|
|
8
|
+
// input with identical semantics. install-remote.sh re-attaches /dev/tty
|
|
9
|
+
// before exec'ing the join, so `curl | bash` lands on the TTY path.
|
|
10
|
+
// - Ctrl-C during a raw-mode select exits 130 — a half-answered wizard must
|
|
11
|
+
// never half-provision.
|
|
12
|
+
// Every function accepts {input, output} for tests (fake streams exercise the
|
|
13
|
+
// non-TTY paths without a terminal).
|
|
14
|
+
import { createInterface } from 'node:readline/promises';
|
|
15
|
+
|
|
16
|
+
const isTTY = (s) => !!(s && s.isTTY);
|
|
17
|
+
const DIM = '\x1b[2m';
|
|
18
|
+
const CYAN = '\x1b[36m';
|
|
19
|
+
const RESET = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
const line = async ({ input, output, prompt }) => {
|
|
22
|
+
// On stdin EOF (</dev/null, closed pipes) a pending question() neither
|
|
23
|
+
// resolves nor rejects — the event loop just drains and node exits 0
|
|
24
|
+
// mid-flow. Two guards: an input that ALREADY ended ('end' fires once, so
|
|
25
|
+
// a second readline on it would wait forever) throws straight away, and a
|
|
26
|
+
// mid-question EOF is turned into a rejection by racing the close event.
|
|
27
|
+
// Every caller maps the rejection to "use the default".
|
|
28
|
+
if (!input || input.readableEnded || input.destroyed || input.closed) {
|
|
29
|
+
output.write(`${prompt}\n`);
|
|
30
|
+
throw new Error('input closed');
|
|
31
|
+
}
|
|
32
|
+
const rl = createInterface({ input, output });
|
|
33
|
+
try {
|
|
34
|
+
return await Promise.race([
|
|
35
|
+
rl.question(prompt),
|
|
36
|
+
new Promise((_, reject) => rl.once('close', () => reject(new Error('input closed')))),
|
|
37
|
+
]);
|
|
38
|
+
} finally { rl.close(); }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// One line of text. Empty input takes `def`; `validate` (sync or async)
|
|
42
|
+
// returns true or an error string and re-prompts on failure.
|
|
43
|
+
export const ask = async ({ prompt, def = '', validate = null, input = process.stdin, output = process.stdout }) => {
|
|
44
|
+
for (;;) {
|
|
45
|
+
let v;
|
|
46
|
+
try { v = (await line({ input, output, prompt: `? ${prompt}${def ? ` ${DIM}(${def})${RESET}` : ''} › ` })).trim(); }
|
|
47
|
+
catch { v = ''; }
|
|
48
|
+
if (!v) v = def;
|
|
49
|
+
if (!validate) return v;
|
|
50
|
+
const ok = await validate(v);
|
|
51
|
+
if (ok === true) return v;
|
|
52
|
+
output.write(` ✗ ${ok || 'invalid'}\n`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const confirm = async ({ prompt, def = true, input = process.stdin, output = process.stdout }) => {
|
|
57
|
+
let v;
|
|
58
|
+
try { v = (await line({ input, output, prompt: `? ${prompt} ${def ? '(Y/n)' : '(y/N)'} › ` })).trim().toLowerCase(); }
|
|
59
|
+
catch { v = ''; }
|
|
60
|
+
if (!v) return def;
|
|
61
|
+
return v === 'y' || v === 'yes';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Hard pause: nothing continues until the human presses Enter.
|
|
65
|
+
export const pause = async ({ prompt, input = process.stdin, output = process.stdout }) => {
|
|
66
|
+
try { await line({ input, output, prompt: `${prompt} ` }); } catch {}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Arrow-key select on a TTY; numbered list everywhere else.
|
|
70
|
+
// options: [{label, hint?, value}] -> resolves to the chosen option's value.
|
|
71
|
+
export const select = async ({ title, options, defaultIndex = 0, input = process.stdin, output = process.stdout }) => {
|
|
72
|
+
if (!Array.isArray(options) || !options.length) throw new Error('select needs options');
|
|
73
|
+
let idx = Math.min(Math.max(defaultIndex, 0), options.length - 1);
|
|
74
|
+
|
|
75
|
+
if (!isTTY(input) || !isTTY(output) || typeof input.setRawMode !== 'function') {
|
|
76
|
+
output.write(`? ${title}\n`);
|
|
77
|
+
options.forEach((o, i) => output.write(` ${i + 1}. ${o.label}${o.hint ? ` ${DIM}— ${o.hint}${RESET}` : ''}${i === idx ? ' (default)' : ''}\n`));
|
|
78
|
+
let v;
|
|
79
|
+
try { v = (await line({ input, output, prompt: ` pick 1-${options.length} › ` })).trim(); } catch { v = ''; }
|
|
80
|
+
const n = Number(v);
|
|
81
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) idx = n - 1;
|
|
82
|
+
return options[idx].value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lines = options.length + 1;
|
|
86
|
+
const render = (first) => {
|
|
87
|
+
if (!first) output.write(`\x1b[${lines}A`);
|
|
88
|
+
output.write(`\x1b[0J? ${title} ${DIM}(↑/↓, Enter)${RESET}\n`);
|
|
89
|
+
options.forEach((o, i) => {
|
|
90
|
+
const on = i === idx;
|
|
91
|
+
const label = on ? `${CYAN}❯ ${o.label}${RESET}` : ` ${o.label}`;
|
|
92
|
+
output.write(` ${label}${o.hint ? ` ${DIM}— ${o.hint}${RESET}` : ''}\n`);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
render(true);
|
|
97
|
+
input.setRawMode(true);
|
|
98
|
+
input.resume();
|
|
99
|
+
const picked = await new Promise((resolve) => {
|
|
100
|
+
const cleanup = () => { input.off('data', onData); input.setRawMode(false); input.pause(); };
|
|
101
|
+
const onData = (buf) => {
|
|
102
|
+
const s = buf.toString();
|
|
103
|
+
if (s === '\x03') { cleanup(); output.write('\n'); process.exit(130); }
|
|
104
|
+
if (s === '\r' || s === '\n') { cleanup(); return resolve(idx); }
|
|
105
|
+
if (/^[1-9]$/.test(s) && Number(s) <= options.length) { idx = Number(s) - 1; cleanup(); return resolve(idx); }
|
|
106
|
+
if (s === '\x1b[A' || s === 'k') idx = (idx - 1 + options.length) % options.length;
|
|
107
|
+
else if (s === '\x1b[B' || s === 'j' || s === '\t') idx = (idx + 1) % options.length;
|
|
108
|
+
else return;
|
|
109
|
+
render(false);
|
|
110
|
+
};
|
|
111
|
+
input.on('data', onData);
|
|
112
|
+
});
|
|
113
|
+
output.write(`\x1b[${lines}A\x1b[0J? ${title} › ${CYAN}${options[picked].label}${RESET}\n`);
|
|
114
|
+
return options[picked].value;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
118
|
+
|
|
119
|
+
// Single-line spinner. On non-TTY output each text() prints its own line.
|
|
120
|
+
export const spinner = (text, { output = process.stdout } = {}) => {
|
|
121
|
+
const tty = isTTY(output);
|
|
122
|
+
let cur = text;
|
|
123
|
+
let i = 0;
|
|
124
|
+
let timer = null;
|
|
125
|
+
const draw = () => output.write(`\r\x1b[K${CYAN}${FRAMES[i = (i + 1) % FRAMES.length]}${RESET} ${cur}`);
|
|
126
|
+
if (tty) { draw(); timer = setInterval(draw, 100); } else output.write(`… ${cur}\n`);
|
|
127
|
+
return {
|
|
128
|
+
text(t) { cur = t; if (!tty) output.write(`… ${t}\n`); },
|
|
129
|
+
stop(final) {
|
|
130
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
131
|
+
if (tty) output.write('\r\x1b[K');
|
|
132
|
+
if (final) output.write(`${final}\n`);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Named checkpoints on one live line: `invite ✓ → bee key ⠸ → grants`.
|
|
138
|
+
// items: [{key, label}]. set(key, 'active'|'done'|'skip') re-renders; stop()
|
|
139
|
+
// finalizes the line. Non-TTY output prints one line per completed step.
|
|
140
|
+
export const checkpoints = (items, { output = process.stdout } = {}) => {
|
|
141
|
+
const tty = isTTY(output);
|
|
142
|
+
const state = new Map();
|
|
143
|
+
let spin = 0;
|
|
144
|
+
let timer = null;
|
|
145
|
+
const renderLine = () => items.map(({ key, label }) => {
|
|
146
|
+
const st = state.get(key);
|
|
147
|
+
if (st === 'done') return `${label} ✓`;
|
|
148
|
+
if (st === 'skip') return `${label} ⤼`;
|
|
149
|
+
if (st === 'active') return `${label} ${CYAN}${FRAMES[spin % FRAMES.length]}${RESET}`;
|
|
150
|
+
return `${DIM}${label}${RESET}`;
|
|
151
|
+
}).join(' → ');
|
|
152
|
+
const draw = () => { spin++; output.write(`\r\x1b[K▸ ${renderLine()}`); };
|
|
153
|
+
if (tty) { draw(); timer = setInterval(draw, 120); }
|
|
154
|
+
return {
|
|
155
|
+
set(key, st = 'done') {
|
|
156
|
+
if (state.get(key) === st) return;
|
|
157
|
+
state.set(key, st);
|
|
158
|
+
if (tty) draw();
|
|
159
|
+
else if (st === 'done' || st === 'skip') output.write(` ${st === 'done' ? '✓' : '⤼'} ${items.find((i) => i.key === key)?.label || key}\n`);
|
|
160
|
+
},
|
|
161
|
+
has(key) { return state.has(key); },
|
|
162
|
+
stop(final) {
|
|
163
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
164
|
+
if (tty) output.write('\r\x1b[K');
|
|
165
|
+
if (final) output.write(`${final}\n`);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
};
|