ciphermesh 2.0.0 → 2.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.
@@ -0,0 +1,111 @@
1
+ // First-run setup wizard: ~30 seconds from `npx ciphermesh` to chatting, with
2
+ // just enough context to use the security features. Runs when no config file
3
+ // exists yet (or on demand via --setup) and persists the answers so it never
4
+ // asks twice.
5
+ import chalk from 'chalk';
6
+ import { SERVER_PORT } from './constants.js';
7
+ import { promptLabel, promptDim, promptError } from './banner.js';
8
+ import { themeNames, setTheme, getThemeName, THEMES } from './themes.js';
9
+ import { parseInvite } from './invite.js';
10
+ import { configPath, saveConfig } from './config.js';
11
+
12
+ /**
13
+ * Resolve a theme answer: a number from the printed list ("2"), a name
14
+ * ("matrix"), or empty → fallback. Pure — exported for testing.
15
+ */
16
+ export function parseThemeChoice(input, names, fallback) {
17
+ const clean = (input || '').trim().toLowerCase();
18
+ if (!clean) {
19
+ return fallback;
20
+ }
21
+ if (/^\d+$/.test(clean)) {
22
+ const idx = Number(clean) - 1;
23
+ return names[idx] || fallback;
24
+ }
25
+ return names.includes(clean) ? clean : fallback;
26
+ }
27
+
28
+ /**
29
+ * Resolve the server answer into what to use this session and what to save
30
+ * as the default. Invites are used as-is for the session but saved as their
31
+ * host:port (a room invite is one-shot, the host is worth keeping).
32
+ * Pure — exported for testing.
33
+ */
34
+ export function resolveServerAnswer(input, fallback = `localhost:${SERVER_PORT}`) {
35
+ const clean = (input || '').trim();
36
+ if (!clean) {
37
+ return { session: fallback, save: fallback };
38
+ }
39
+ const invite = parseInvite(clean);
40
+ if (invite) {
41
+ return { session: clean, save: invite.wsUrl.replace(/^wss?:\/\//, '') };
42
+ }
43
+ return { session: clean, save: clean };
44
+ }
45
+
46
+ /**
47
+ * Interactive first-run wizard. Uses the same readline interface as the rest
48
+ * of startup. Returns `{ nickname, server, theme }` — the caller uses them
49
+ * directly for this session (no duplicate prompts) — after persisting them
50
+ * to the config file.
51
+ */
52
+ export async function runOnboarding(rl, { savePath = configPath() } = {}) {
53
+ const dim = (t) => console.log(promptDim(` ${t}`));
54
+
55
+ console.log();
56
+ console.log(chalk.bold.white(' First time here? Quick setup — 30 seconds.'));
57
+ dim(`Everything is saved to ${savePath} (re-run anytime with: ciphermesh --setup)`);
58
+ console.log();
59
+ console.log(chalk.white(' How CipherMesh works, in three lines:'));
60
+ dim('• Everything is end-to-end encrypted — the relay only ever sees ciphertext.');
61
+ dim('• Your identity is a keypair; its short fingerprint is shown when you connect.');
62
+ dim('• Verify friends out-of-band with /verify — a green ✓ appears next to their name.');
63
+ console.log();
64
+
65
+ // 1. Nickname
66
+ let nickname = '';
67
+ while (!nickname) {
68
+ const raw = await rl.question(promptLabel(`Nickname ${promptDim('(a-z, 0-9, _, -)')}: `));
69
+ const clean = raw.trim().replace(/[^a-zA-Z0-9_-]/g, '');
70
+ if (clean.length >= 1 && clean.length <= 20) {
71
+ nickname = clean;
72
+ } else {
73
+ console.log(promptError('Invalid nickname. Use 1-20 alphanumeric characters.'));
74
+ }
75
+ }
76
+
77
+ // 2. Theme
78
+ const names = themeNames();
79
+ console.log();
80
+ console.log(chalk.white(' Colour theme for nicknames:'));
81
+ names.forEach((name, i) => {
82
+ const swatch = THEMES[name]
83
+ .slice(0, 5)
84
+ .map((c) => (c.startsWith('#') ? chalk.hex(c)('█') : chalk[c]?.('█') || '█'))
85
+ .join('');
86
+ console.log(promptDim(` ${i + 1}. ${name.padEnd(8)} ${swatch}`));
87
+ });
88
+ const themeRaw = await rl.question(
89
+ promptLabel(`Theme ${promptDim(`(1-${names.length} or name, Enter = ${getThemeName()})`)}: `),
90
+ );
91
+ const theme = parseThemeChoice(themeRaw, names, getThemeName());
92
+ setTheme(theme);
93
+
94
+ // 3. Default server
95
+ console.log();
96
+ console.log(chalk.white(' Which server should be your default?'));
97
+ dim(`• Same machine as the relay → localhost:${SERVER_PORT}`);
98
+ dim(`• Someone else hosts it (LAN/Tailscale) → their IP, e.g. 100.64.0.9:${SERVER_PORT}`);
99
+ dim('• Got a ciphermesh:// invite? Paste it here.');
100
+ const serverRaw = await rl.question(
101
+ promptLabel(`Server ${promptDim(`(Enter = localhost:${SERVER_PORT})`)}: `),
102
+ );
103
+ const { session: server, save: serverToSave } = resolveServerAnswer(serverRaw);
104
+
105
+ saveConfig({ nickname, theme, server: serverToSave }, savePath);
106
+ console.log();
107
+ console.log(promptLabel('Setup saved — next time you go straight to the chat.'));
108
+ console.log();
109
+
110
+ return { nickname, server, theme };
111
+ }