@calltelemetry/cli 0.5.17 → 0.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.
Files changed (37) hide show
  1. package/dist/lib/certs.d.ts +16 -2
  2. package/dist/lib/certs.d.ts.map +1 -1
  3. package/dist/lib/certs.js +80 -8
  4. package/dist/lib/certs.js.map +1 -1
  5. package/dist/lib/identity.d.ts +38 -0
  6. package/dist/lib/identity.d.ts.map +1 -0
  7. package/dist/lib/identity.js +85 -0
  8. package/dist/lib/identity.js.map +1 -0
  9. package/dist/lib/prefs.d.ts +4 -0
  10. package/dist/lib/prefs.d.ts.map +1 -1
  11. package/dist/lib/prefs.js.map +1 -1
  12. package/dist/lib/time.d.ts +57 -0
  13. package/dist/lib/time.d.ts.map +1 -0
  14. package/dist/lib/time.js +200 -0
  15. package/dist/lib/time.js.map +1 -0
  16. package/dist/lib/users.d.ts.map +1 -1
  17. package/dist/lib/users.js +13 -5
  18. package/dist/lib/users.js.map +1 -1
  19. package/dist/lib/version.d.ts +1 -1
  20. package/dist/lib/version.d.ts.map +1 -1
  21. package/dist/lib/version.js +1 -1
  22. package/dist/lib/version.js.map +1 -1
  23. package/dist/shell/commands/config.d.ts +3 -0
  24. package/dist/shell/commands/config.d.ts.map +1 -1
  25. package/dist/shell/commands/config.js +76 -3
  26. package/dist/shell/commands/config.js.map +1 -1
  27. package/dist/shell/commands/diag.d.ts.map +1 -1
  28. package/dist/shell/commands/diag.js +73 -0
  29. package/dist/shell/commands/diag.js.map +1 -1
  30. package/dist/shell/commands/registry.d.ts.map +1 -1
  31. package/dist/shell/commands/registry.js +16 -2
  32. package/dist/shell/commands/registry.js.map +1 -1
  33. package/dist/shell/network-onboarding.d.ts +40 -10
  34. package/dist/shell/network-onboarding.d.ts.map +1 -1
  35. package/dist/shell/network-onboarding.js +648 -173
  36. package/dist/shell/network-onboarding.js.map +1 -1
  37. package/package.json +1 -1
@@ -1,22 +1,32 @@
1
1
  /**
2
- * network-onboarding.ts — First-boot network setup wizard for ct shell.
2
+ * network-onboarding.ts — First-boot setup wizard for ct shell.
3
3
  *
4
- * Guides the operator through IP / gateway / DNS configuration with:
5
- * - Field-by-field prompts (IP, prefix, gateway, DNS)
6
- * - Default values shown in brackets (press Enter to accept)
7
- * - Input validation before applying
8
- * - Summary + confirm before writing to NetworkManager
9
- * - Gateway ping after apply to verify reachability
4
+ * Six-step wizard that collects all configuration atomically before applying:
5
+ * 1. Network (IP/DHCP, DNS, search domain)
6
+ * 2. System Identity (hostname, location)
7
+ * 3. Admin Credentials (root password)
8
+ * 4. Timezone
9
+ * 5. NTP Time Servers
10
+ * 6. Preferences (auto-update, IPv6, JTAPI)
11
+ *
12
+ * After collection, shows a unified summary and offers Apply/Edit/Cancel.
13
+ * Apply phase runs all changes sequentially with progress output.
14
+ * Verify phase checks connectivity and reports warnings (not blockers).
10
15
  */
11
16
  import * as readline from 'node:readline';
12
17
  import { execSync, spawnSync } from 'node:child_process';
13
18
  import { writeFileSync } from 'node:fs';
14
19
  import chalk from 'chalk';
15
- import { loadPrefs, savePrefs } from '../lib/prefs.js';
20
+ import { savePrefs } from '../lib/prefs.js';
21
+ import { getHostname, isValidHostname, buildFqdn, applyHostname, applyAdminPassword, isStrongPassword, } from '../lib/identity.js';
22
+ import { getSystemTimezone, TIMEZONE_REGIONS, searchTimezones, isValidTimezone, applyTimezone, applyNtpServers, checkNtpSync, DEFAULT_NTP, } from '../lib/time.js';
23
+ import { generateSelfSignedCerts, getCertInfo } from '../lib/certs.js';
24
+ export { buildFqdn } from '../lib/identity.js';
16
25
  export const NETWORK_SENTINEL = '/etc/ct-network-configured';
17
26
  const DEFAULT_DNS1 = '8.8.8.8';
18
27
  const DEFAULT_DNS2 = '8.8.4.4';
19
- // ── Validation helpers (exported for testing) ──────────────────────────────
28
+ const DEFAULT_SEARCH_DOMAIN = 'local';
29
+ // ── Validation helpers (exported for testing) ────────────────────────────────
20
30
  const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
21
31
  export function isValidIp(ip) {
22
32
  const m = IPV4_RE.exec(ip.trim());
@@ -48,7 +58,14 @@ export function parsePingMs(stdout) {
48
58
  // macOS: round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.123 ms
49
59
  return null;
50
60
  }
51
- // ── System helpers ─────────────────────────────────────────────────────────
61
+ /** Validate a DNS search domain (e.g. 'local', 'corp.example.com'). */
62
+ export function isValidSearchDomain(domain) {
63
+ if (domain.length === 0)
64
+ return true; // empty means no search domain
65
+ // RFC 1123-ish: labels separated by dots, alphanumeric + hyphens
66
+ return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(domain) && domain.length <= 253;
67
+ }
68
+ // ── System helpers ───────────────────────────────────────────────────────────
52
69
  /** Return current global IPv4 address if DHCP already provided one. */
53
70
  export function detectCurrentIp() {
54
71
  try {
@@ -128,112 +145,669 @@ export function applyDhcp(iface) {
128
145
  const up = spawnSync('sudo', ['nmcli', 'connection', 'up', 'ct-network'], { stdio: 'pipe' });
129
146
  return up.status === 0;
130
147
  }
131
- // ── Readline helper ────────────────────────────────────────────────────────
148
+ // ── Readline helper ──────────────────────────────────────────────────────────
132
149
  function makeAsker(rl) {
133
150
  return (question) => new Promise((resolve) => rl.question(question, resolve));
134
151
  }
135
- // ── Wizard ─────────────────────────────────────────────────────────────────
136
- /**
137
- * Interactive first-boot network setup wizard.
138
- * Runs once (when NETWORK_SENTINEL does not exist) before the shell starts.
139
- */
140
- export async function runNetworkOnboarding() {
152
+ // ── Step progress helper ─────────────────────────────────────────────────────
153
+ function stepProgress(label, ok) {
154
+ if (ok) {
155
+ console.log(chalk.green(` ✓ ${label}`));
156
+ }
157
+ else {
158
+ console.log(chalk.red(` ✗ ${label}`));
159
+ }
160
+ }
161
+ // ── Step 1: Network ──────────────────────────────────────────────────────────
162
+ async function stepNetwork(ask, state) {
141
163
  console.log('');
142
- console.log(chalk.bold.cyan(' Call TelemetryFirst-Boot Setup'));
164
+ console.log(chalk.bold.cyan(' Step 1 of 6 Network'));
143
165
  console.log(chalk.dim(' ────────────────────────────────────────'));
144
- // If DHCP already provided an IP, skip network wizard
166
+ const iface = detectIface();
167
+ state.iface = iface;
145
168
  const existingIp = detectCurrentIp();
169
+ console.log(chalk.dim(` Interface : ${iface}`));
146
170
  if (existingIp) {
147
- writeSentinel();
148
- console.log(chalk.green(` ✓ Network ready (DHCP): ${existingIp}`));
171
+ console.log(chalk.green(` Current IP: ${existingIp} (via DHCP)`));
149
172
  }
150
173
  else {
151
- // Run network setup
174
+ console.log(chalk.yellow(' No IP address detected.'));
175
+ }
176
+ console.log('');
177
+ // Mode selection — order depends on whether DHCP already has an IP
178
+ if (existingIp) {
179
+ console.log(chalk.cyan(' Network mode:'));
180
+ console.log(chalk.dim(' 1 Keep current DHCP configuration'));
181
+ console.log(chalk.dim(' 2 Configure static IP'));
152
182
  console.log('');
153
- console.log(chalk.bold.cyan(' Step 1 Network Configuration'));
183
+ const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
184
+ if (mode === '2') {
185
+ state.mode = 'static';
186
+ }
187
+ else {
188
+ state.mode = 'dhcp';
189
+ state.ip = existingIp;
190
+ }
191
+ }
192
+ else {
193
+ console.log(chalk.cyan(' Network mode:'));
194
+ console.log(chalk.dim(' 1 Static IP'));
195
+ console.log(chalk.dim(' 2 DHCP (auto)'));
196
+ console.log('');
197
+ const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
198
+ if (mode === '2') {
199
+ state.mode = 'dhcp';
200
+ }
201
+ else {
202
+ state.mode = 'static';
203
+ }
204
+ }
205
+ // Collect static IP fields
206
+ if (state.mode === 'static') {
207
+ console.log('');
208
+ console.log(chalk.cyan(' Static IP Configuration'));
154
209
  console.log(chalk.dim(' ────────────────────────────────────────'));
155
- const iface = detectIface();
156
- console.log(chalk.dim(` Interface : ${iface}`));
157
- console.log(chalk.dim(' No IP address detected — configure network to reach the web UI.'));
210
+ let ip = '';
211
+ while (!isValidIp(ip)) {
212
+ ip = (await ask(chalk.bold(' IP Address (e.g. 192.168.1.100) : '))).trim();
213
+ if (!isValidIp(ip))
214
+ console.log(chalk.red(' ✗ Invalid IP address — enter four octets, e.g. 192.168.1.100'));
215
+ }
216
+ state.ip = ip;
217
+ let prefix = '';
218
+ while (!isValidPrefix(prefix)) {
219
+ prefix = (await ask(chalk.bold(' Prefix (e.g. 24 for /24) : '))).trim();
220
+ if (!isValidPrefix(prefix))
221
+ console.log(chalk.red(' ✗ Invalid prefix — enter a number between 1 and 30'));
222
+ }
223
+ state.prefix = prefix;
224
+ let gateway = '';
225
+ while (!isValidGateway(gateway)) {
226
+ gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
227
+ if (!isValidGateway(gateway))
228
+ console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
229
+ }
230
+ state.gateway = gateway;
231
+ }
232
+ // DNS (applies to both modes)
233
+ const dns1Raw = await ask(chalk.bold(` Primary DNS [${DEFAULT_DNS1}] : `));
234
+ state.dns1 = parseDnsField(dns1Raw, DEFAULT_DNS1);
235
+ const dns2Raw = await ask(chalk.bold(` Secondary DNS [${DEFAULT_DNS2}] : `));
236
+ state.dns2 = parseDnsField(dns2Raw, DEFAULT_DNS2);
237
+ // Search domain
238
+ const domainRaw = await ask(chalk.bold(` Search domain [${DEFAULT_SEARCH_DOMAIN}] : `));
239
+ state.searchDomain = parseDnsField(domainRaw, DEFAULT_SEARCH_DOMAIN);
240
+ // Quick connectivity check
241
+ console.log('');
242
+ console.log(chalk.dim(' Quick connectivity check...'));
243
+ if (state.mode === 'static' && state.gateway) {
244
+ const ms = pingGateway(state.gateway);
245
+ if (ms !== null) {
246
+ console.log(chalk.green(` ✓ Gateway ${state.gateway} reachable (${ms}ms)`));
247
+ }
248
+ else {
249
+ console.log(chalk.yellow(` ⚠ Gateway ${state.gateway} not reachable — may work after apply`));
250
+ }
251
+ }
252
+ // DNS resolve test
253
+ try {
254
+ const result = spawnSync('nslookup', ['google.com', state.dns1], {
255
+ encoding: 'utf-8',
256
+ timeout: 5000,
257
+ });
258
+ if (result.status === 0) {
259
+ console.log(chalk.green(` ✓ DNS resolution via ${state.dns1} working`));
260
+ }
261
+ else {
262
+ console.log(chalk.yellow(` ⚠ DNS resolution via ${state.dns1} failed — may work after apply`));
263
+ }
264
+ }
265
+ catch {
266
+ console.log(chalk.yellow(` ⚠ DNS check skipped — nslookup not available`));
267
+ }
268
+ }
269
+ // ── Step 2: System Identity ──────────────────────────────────────────────────
270
+ async function stepIdentity(ask, state) {
271
+ console.log('');
272
+ console.log(chalk.bold.cyan(' Step 2 of 6 — System Identity'));
273
+ console.log(chalk.dim(' ────────────────────────────────────────'));
274
+ const currentHostname = getHostname();
275
+ // Hostname
276
+ let hostname = '';
277
+ while (true) {
278
+ const raw = await ask(chalk.bold(` Hostname [${currentHostname}]: `));
279
+ hostname = raw.trim() || currentHostname;
280
+ if (isValidHostname(hostname))
281
+ break;
282
+ console.log(chalk.red(' ✗ Invalid hostname — alphanumeric + hyphens, 1-63 chars, no leading/trailing hyphen'));
283
+ }
284
+ state.hostname = hostname;
285
+ // Show FQDN
286
+ const fqdn = buildFqdn(hostname, state.searchDomain);
287
+ console.log(chalk.dim(` FQDN: ${fqdn}`));
288
+ // Location
289
+ const locationRaw = await ask(chalk.bold(' Location (optional): '));
290
+ state.location = locationRaw.trim();
291
+ // Cert info
292
+ console.log('');
293
+ console.log(chalk.dim(` A self-signed SSL certificate will be generated for ${chalk.bold(fqdn)}`));
294
+ console.log(chalk.dim(' To import a CA-signed cert later:'));
295
+ console.log(chalk.dim(' config certs import <cert.pem> <key.pem>'));
296
+ }
297
+ // ── Step 3: Admin Credentials ────────────────────────────────────────────────
298
+ async function stepAdmin(ask, state) {
299
+ console.log('');
300
+ console.log(chalk.bold.cyan(' Step 3 of 6 — Admin Credentials'));
301
+ console.log(chalk.dim(' ────────────────────────────────────────'));
302
+ console.log(chalk.yellow(' Default passwords must be changed before production use.'));
303
+ console.log(chalk.dim(' Minimum 12 characters, uppercase, lowercase, and digit required.'));
304
+ console.log('');
305
+ while (true) {
306
+ const pw = (await ask(chalk.bold(' New admin password: '))).trim();
307
+ const check = isStrongPassword(pw);
308
+ if (!check.valid) {
309
+ console.log(chalk.red(` ✗ ${check.reason}`));
310
+ continue;
311
+ }
312
+ const confirm = (await ask(chalk.bold(' Confirm password: '))).trim();
313
+ if (confirm !== pw) {
314
+ console.log(chalk.red(' ✗ Passwords do not match'));
315
+ continue;
316
+ }
317
+ state.adminPassword = pw;
318
+ console.log(chalk.green(' ✓ Password accepted'));
319
+ break;
320
+ }
321
+ }
322
+ // ── Step 4: Timezone ─────────────────────────────────────────────────────────
323
+ async function stepTimezone(ask, state) {
324
+ console.log('');
325
+ console.log(chalk.bold.cyan(' Step 4 of 6 — Timezone'));
326
+ console.log(chalk.dim(' ────────────────────────────────────────'));
327
+ const currentTz = getSystemTimezone();
328
+ console.log(chalk.dim(` Current timezone: ${currentTz}`));
329
+ console.log('');
330
+ // Region menu
331
+ while (true) {
332
+ console.log(chalk.cyan(' Select region:'));
333
+ for (let i = 0; i < TIMEZONE_REGIONS.length; i++) {
334
+ console.log(chalk.dim(` ${i + 1} ${TIMEZONE_REGIONS[i].name}`));
335
+ }
336
+ console.log(chalk.dim(' 4 Other / Custom IANA zone'));
337
+ console.log(chalk.dim(' s Search'));
158
338
  console.log('');
159
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
160
- const ask = makeAsker(rl);
161
- try {
162
- console.log(chalk.cyan(' Network mode:'));
163
- console.log(chalk.dim(' 1 Static IP'));
164
- console.log(chalk.dim(' 2 DHCP (auto)'));
339
+ const choice = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
340
+ if (choice === 's') {
341
+ // Search mode
342
+ const query = (await ask(chalk.bold(' Search timezone: '))).trim();
343
+ if (!query)
344
+ continue;
345
+ const results = searchTimezones(query);
346
+ if (results.length === 0) {
347
+ console.log(chalk.yellow(' No matches found.'));
348
+ console.log('');
349
+ continue;
350
+ }
165
351
  console.log('');
166
- const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
167
- if (mode === '2') {
168
- await runDhcp(rl, ask, iface);
352
+ for (let i = 0; i < results.length; i++) {
353
+ const tz = results[i];
354
+ const note = tz.note ? chalk.dim(` (${tz.note})`) : '';
355
+ console.log(chalk.dim(` ${i + 1} ${tz.label.padEnd(14)} ${tz.offset.padEnd(10)} ${tz.value}${note}`));
169
356
  }
170
- else {
171
- await runStaticIp(rl, ask, iface);
357
+ console.log(chalk.dim(' b Back'));
358
+ console.log('');
359
+ const pick = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
360
+ if (pick === 'b')
361
+ continue;
362
+ const idx = parseInt(pick, 10) - 1;
363
+ if (idx >= 0 && idx < results.length) {
364
+ state.timezone = results[idx].value;
365
+ console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
366
+ return;
367
+ }
368
+ console.log(chalk.red(' ✗ Invalid selection'));
369
+ continue;
370
+ }
371
+ if (choice === '4') {
372
+ // Custom IANA zone
373
+ while (true) {
374
+ const tz = (await ask(chalk.bold(' IANA timezone (e.g. America/Chicago): '))).trim();
375
+ if (!tz)
376
+ break;
377
+ if (isValidTimezone(tz)) {
378
+ state.timezone = tz;
379
+ console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
380
+ return;
381
+ }
382
+ console.log(chalk.red(' ✗ Invalid timezone — must be a valid IANA zone'));
172
383
  }
384
+ continue;
385
+ }
386
+ const regionIdx = parseInt(choice, 10) - 1;
387
+ if (regionIdx < 0 || regionIdx >= TIMEZONE_REGIONS.length) {
388
+ console.log(chalk.red(' ✗ Invalid selection'));
389
+ continue;
390
+ }
391
+ // Show zones in selected region
392
+ const region = TIMEZONE_REGIONS[regionIdx];
393
+ console.log('');
394
+ console.log(chalk.cyan(` ${region.name}:`));
395
+ for (let i = 0; i < region.zones.length; i++) {
396
+ const tz = region.zones[i];
397
+ const note = tz.note ? chalk.dim(` (${tz.note})`) : '';
398
+ console.log(chalk.dim(` ${i + 1} ${tz.label.padEnd(14)} ${tz.offset.padEnd(10)} ${tz.value}${note}`));
399
+ }
400
+ console.log(chalk.dim(' b Back'));
401
+ console.log('');
402
+ const zonePick = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
403
+ if (zonePick === 'b')
404
+ continue;
405
+ const zoneIdx = parseInt(zonePick, 10) - 1;
406
+ if (zoneIdx >= 0 && zoneIdx < region.zones.length) {
407
+ state.timezone = region.zones[zoneIdx].value;
408
+ console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
409
+ return;
410
+ }
411
+ console.log(chalk.red(' ✗ Invalid selection'));
412
+ }
413
+ }
414
+ // ── Step 5: NTP ──────────────────────────────────────────────────────────────
415
+ async function stepNtp(ask, state) {
416
+ console.log('');
417
+ console.log(chalk.bold.cyan(' Step 5 of 6 — NTP Time Servers'));
418
+ console.log(chalk.dim(' ────────────────────────────────────────'));
419
+ const ntp1Raw = await ask(chalk.bold(` NTP Server 1 [${DEFAULT_NTP[0]}]: `));
420
+ state.ntp1 = parseDnsField(ntp1Raw, DEFAULT_NTP[0]);
421
+ const ntp2Raw = await ask(chalk.bold(` NTP Server 2 [${DEFAULT_NTP[1]}]: `));
422
+ state.ntp2 = parseDnsField(ntp2Raw, DEFAULT_NTP[1]);
423
+ console.log(chalk.green(` ✓ NTP: ${state.ntp1}, ${state.ntp2}`));
424
+ }
425
+ // ── Step 6: Preferences ──────────────────────────────────────────────────────
426
+ async function stepPreferences(ask, state) {
427
+ console.log('');
428
+ console.log(chalk.bold.cyan(' Step 6 of 6 — Preferences'));
429
+ console.log(chalk.dim(' ────────────────────────────────────────'));
430
+ // Auto-updates
431
+ const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
432
+ state.autoUpdate = autoUpdate !== 'n' && autoUpdate !== 'no';
433
+ console.log(state.autoUpdate ? chalk.green(' ✓ Auto-updates enabled') : chalk.dim(' ○ Auto-updates disabled'));
434
+ // IPv6
435
+ const ipv6 = (await ask(chalk.bold(' Enable IPv6 support? [y/N]: '))).trim().toLowerCase();
436
+ state.ipv6 = ipv6 === 'y' || ipv6 === 'yes';
437
+ console.log(state.ipv6 ? chalk.green(' ✓ IPv6 enabled') : chalk.dim(' ○ IPv6 disabled'));
438
+ // JTAPI
439
+ console.log('');
440
+ console.log(chalk.dim(' JTAPI enables greeting injection — play custom audio greetings'));
441
+ console.log(chalk.dim(' to callers via Cisco JTAPI phone control. Requires a CUCM user'));
442
+ console.log(chalk.dim(' with CTI permissions and a JTAPI license.'));
443
+ const jtapi = (await ask(chalk.bold(' Enable JTAPI greeting injection? [y/N]: '))).trim().toLowerCase();
444
+ state.jtapi = jtapi === 'y' || jtapi === 'yes';
445
+ console.log(state.jtapi ? chalk.green(' ✓ JTAPI greeting injection enabled') : chalk.dim(' ○ JTAPI greeting injection disabled'));
446
+ }
447
+ // ── Summary ──────────────────────────────────────────────────────────────────
448
+ /** Render a full summary of the wizard state. Exported for testing. */
449
+ export function renderSummary(state) {
450
+ const lines = [];
451
+ const add = (s) => lines.push(s);
452
+ add('');
453
+ add(chalk.bold.cyan(' Configuration Summary'));
454
+ add(chalk.dim(' ════════════════════════════════════════'));
455
+ // NETWORK
456
+ add('');
457
+ add(chalk.cyan(' NETWORK'));
458
+ add(chalk.dim(' ────────────────────────────────────────'));
459
+ add(` Mode : ${chalk.bold(state.mode.toUpperCase())}`);
460
+ if (state.mode === 'static') {
461
+ add(` IP Address : ${chalk.bold(`${state.ip}/${state.prefix}`)}`);
462
+ add(` Gateway : ${chalk.bold(state.gateway)}`);
463
+ }
464
+ else if (state.ip) {
465
+ add(` IP Address : ${chalk.bold(state.ip)} ${chalk.dim('(DHCP)')}`);
466
+ }
467
+ add(` Primary DNS : ${chalk.bold(state.dns1)}`);
468
+ add(` Secondary DNS : ${chalk.bold(state.dns2)}`);
469
+ add(` Search Domain : ${chalk.bold(state.searchDomain)}`);
470
+ add(` Interface : ${chalk.dim(state.iface)}`);
471
+ // SYSTEM
472
+ add('');
473
+ add(chalk.cyan(' SYSTEM'));
474
+ add(chalk.dim(' ────────────────────────────────────────'));
475
+ add(` Hostname : ${chalk.bold(state.hostname)}`);
476
+ add(` FQDN : ${chalk.bold(buildFqdn(state.hostname, state.searchDomain))}`);
477
+ add(` Location : ${state.location ? chalk.bold(state.location) : chalk.dim('(not set)')}`);
478
+ add(` Admin Password : ${chalk.bold('(changed)')}`);
479
+ // TIME
480
+ add('');
481
+ add(chalk.cyan(' TIME'));
482
+ add(chalk.dim(' ────────────────────────────────────────'));
483
+ add(` Timezone : ${chalk.bold(state.timezone)}`);
484
+ add(` NTP Server 1 : ${chalk.bold(state.ntp1)}`);
485
+ add(` NTP Server 2 : ${chalk.bold(state.ntp2)}`);
486
+ // PREFERENCES
487
+ add('');
488
+ add(chalk.cyan(' PREFERENCES'));
489
+ add(chalk.dim(' ────────────────────────────────────────'));
490
+ add(` Auto-updates : ${state.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
491
+ add(` IPv6 : ${state.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
492
+ add(` JTAPI Greetings: ${state.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
493
+ add('');
494
+ return lines.join('\n');
495
+ }
496
+ // ── Apply phase ──────────────────────────────────────────────────────────────
497
+ async function applyWizardState(state) {
498
+ console.log('');
499
+ console.log(chalk.bold.cyan(' Applying configuration...'));
500
+ console.log(chalk.dim(' ────────────────────────────────────────'));
501
+ let allOk = true;
502
+ // 1. Hostname
503
+ const hostnameOk = applyHostname(state.hostname);
504
+ stepProgress('Set hostname', hostnameOk);
505
+ if (!hostnameOk)
506
+ allOk = false;
507
+ // 2. Network
508
+ if (state.mode === 'static') {
509
+ const ipPrefix = `${state.ip}/${state.prefix}`;
510
+ const netOk = applyStaticIp(state.iface, ipPrefix, state.gateway, state.dns1, state.dns2);
511
+ stepProgress('Apply static IP', netOk);
512
+ if (!netOk)
513
+ allOk = false;
514
+ }
515
+ else {
516
+ const netOk = applyDhcp(state.iface);
517
+ stepProgress('Apply DHCP', netOk);
518
+ if (!netOk)
519
+ allOk = false;
520
+ }
521
+ // 3. Search domain
522
+ const dnsSearchResult = spawnSync('sudo', [
523
+ 'nmcli', 'con', 'mod', 'ct-network', 'ipv4.dns-search', state.searchDomain,
524
+ ], { stdio: 'pipe' });
525
+ const searchOk = dnsSearchResult.status === 0;
526
+ stepProgress('Set DNS search domain', searchOk);
527
+ if (!searchOk)
528
+ allOk = false;
529
+ // 4. Admin password
530
+ const pwOk = applyAdminPassword(state.adminPassword);
531
+ stepProgress('Set admin password', pwOk);
532
+ if (!pwOk)
533
+ allOk = false;
534
+ // 5. Timezone
535
+ const tzOk = applyTimezone(state.timezone);
536
+ stepProgress('Set timezone', tzOk);
537
+ if (!tzOk)
538
+ allOk = false;
539
+ // 6. NTP servers
540
+ const ntpOk = applyNtpServers(state.ntp1, state.ntp2);
541
+ stepProgress('Configure NTP servers', ntpOk);
542
+ if (!ntpOk)
543
+ allOk = false;
544
+ // 7. Self-signed certs
545
+ try {
546
+ const fqdn = buildFqdn(state.hostname, state.searchDomain);
547
+ await generateSelfSignedCerts(fqdn, state.ip);
548
+ stepProgress('Generate SSL certificate', true);
549
+ }
550
+ catch {
551
+ stepProgress('Generate SSL certificate', false);
552
+ allOk = false;
553
+ }
554
+ // 8. Save preferences
555
+ const prefs = {
556
+ autoUpdate: state.autoUpdate,
557
+ ipv6: state.ipv6,
558
+ otel: true,
559
+ jtapi: state.jtapi,
560
+ deploymentMode: 'docker-compose',
561
+ k8sNamespace: 'ct',
562
+ k8sEnvironment: 'ct-dev',
563
+ timezone: state.timezone,
564
+ ntpServers: [state.ntp1, state.ntp2],
565
+ hostname: state.hostname,
566
+ location: state.location || undefined,
567
+ };
568
+ savePrefs(prefs);
569
+ stepProgress('Save preferences', true);
570
+ // 9. Write sentinel
571
+ writeSentinel();
572
+ stepProgress('Mark setup complete', true);
573
+ return allOk;
574
+ }
575
+ // ── Verify phase ─────────────────────────────────────────────────────────────
576
+ function runVerification(state) {
577
+ console.log('');
578
+ console.log(chalk.bold.cyan(' Verification'));
579
+ console.log(chalk.dim(' ────────────────────────────────────────'));
580
+ // 1. Ping gateway (static only)
581
+ if (state.mode === 'static' && state.gateway) {
582
+ const ms = pingGateway(state.gateway);
583
+ if (ms !== null) {
584
+ console.log(chalk.green(` ✓ Gateway ${state.gateway} reachable (${ms}ms)`));
585
+ }
586
+ else {
587
+ console.log(chalk.yellow(` ⚠ Gateway ${state.gateway} not reachable`));
588
+ console.log(chalk.dim(' Remediation: network address <ip/prefix> <gateway>'));
589
+ }
590
+ }
591
+ // 2. DNS resolution — search domain
592
+ try {
593
+ const result = spawnSync('nslookup', [state.searchDomain, state.dns1], {
594
+ encoding: 'utf-8',
595
+ timeout: 5000,
596
+ });
597
+ if (result.status === 0) {
598
+ console.log(chalk.green(` ✓ DNS resolves ${state.searchDomain} via ${state.dns1}`));
599
+ }
600
+ else {
601
+ console.log(chalk.yellow(` ⚠ DNS cannot resolve ${state.searchDomain}`));
602
+ console.log(chalk.dim(' Remediation: network dns-servers <dns1> <dns2>'));
173
603
  }
174
- finally {
175
- rl.close();
604
+ }
605
+ catch {
606
+ console.log(chalk.yellow(' ⚠ DNS search domain check skipped'));
607
+ }
608
+ // 3. External DNS
609
+ try {
610
+ const result = spawnSync('nslookup', ['google.com', state.dns1], {
611
+ encoding: 'utf-8',
612
+ timeout: 5000,
613
+ });
614
+ if (result.status === 0) {
615
+ console.log(chalk.green(` ✓ External DNS resolution working`));
616
+ }
617
+ else {
618
+ console.log(chalk.yellow(' ⚠ External DNS resolution failed'));
619
+ console.log(chalk.dim(' Remediation: network dns-servers <dns1> <dns2>'));
620
+ }
621
+ }
622
+ catch {
623
+ console.log(chalk.yellow(' ⚠ External DNS check skipped'));
624
+ }
625
+ // 4. NTP sync
626
+ const ntpSynced = checkNtpSync();
627
+ if (ntpSynced) {
628
+ console.log(chalk.green(' ✓ NTP synchronized'));
629
+ }
630
+ else {
631
+ console.log(chalk.yellow(' ⚠ NTP not yet synchronized — may take a few minutes'));
632
+ console.log(chalk.dim(' Remediation: config ntp <server1> <server2>'));
633
+ }
634
+ // 5. Certificate check
635
+ const certInfo = getCertInfo();
636
+ if (certInfo) {
637
+ const fqdn = buildFqdn(state.hostname, state.searchDomain);
638
+ if (certInfo.cn === fqdn) {
639
+ console.log(chalk.green(` ✓ SSL certificate CN matches ${fqdn}`));
640
+ }
641
+ else {
642
+ console.log(chalk.yellow(` ⚠ SSL certificate CN (${certInfo.cn}) does not match FQDN (${fqdn})`));
643
+ console.log(chalk.dim(' Remediation: config certs regenerate'));
644
+ }
645
+ }
646
+ else {
647
+ console.log(chalk.yellow(' ⚠ No SSL certificate found'));
648
+ console.log(chalk.dim(' Remediation: config certs regenerate'));
649
+ }
650
+ }
651
+ // ── Step runner mapping ──────────────────────────────────────────────────────
652
+ const STEP_FUNCTIONS = [
653
+ stepNetwork,
654
+ stepIdentity,
655
+ stepAdmin,
656
+ stepTimezone,
657
+ stepNtp,
658
+ stepPreferences,
659
+ ];
660
+ const STEP_NAMES = [
661
+ 'Network',
662
+ 'System Identity',
663
+ 'Admin Credentials',
664
+ 'Timezone',
665
+ 'NTP Time Servers',
666
+ 'Preferences',
667
+ ];
668
+ // ── Main entry point ─────────────────────────────────────────────────────────
669
+ /**
670
+ * Interactive first-boot setup wizard.
671
+ * Runs once (when NETWORK_SENTINEL does not exist) before the shell starts.
672
+ * Collects all configuration, then applies atomically.
673
+ */
674
+ export async function runNetworkOnboarding() {
675
+ console.log('');
676
+ console.log(chalk.bold.cyan(' Call Telemetry — First-Boot Setup'));
677
+ console.log(chalk.dim(' ═══════════════════════════════════════'));
678
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
679
+ const ask = makeAsker(rl);
680
+ try {
681
+ // Initialize state with defaults
682
+ const state = {
683
+ mode: 'dhcp',
684
+ dns1: DEFAULT_DNS1,
685
+ dns2: DEFAULT_DNS2,
686
+ searchDomain: DEFAULT_SEARCH_DOMAIN,
687
+ iface: 'eth0',
688
+ hostname: getHostname(),
689
+ location: '',
690
+ adminPassword: '',
691
+ timezone: getSystemTimezone(),
692
+ ntp1: DEFAULT_NTP[0],
693
+ ntp2: DEFAULT_NTP[1],
694
+ autoUpdate: true,
695
+ ipv6: false,
696
+ jtapi: false,
697
+ };
698
+ // Run steps 1-6 sequentially
699
+ for (const stepFn of STEP_FUNCTIONS) {
700
+ await stepFn(ask, state);
701
+ }
702
+ // Show/edit/apply loop
703
+ while (true) {
704
+ // Show unified summary
705
+ console.log(renderSummary(state));
706
+ console.log(chalk.bold(' [A]pply [E]dit [C]ancel'));
707
+ console.log('');
708
+ const action = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
709
+ if (action === 'c') {
710
+ console.log(chalk.yellow('\n Cancelled — no changes applied.\n'));
711
+ return;
712
+ }
713
+ if (action === 'e') {
714
+ console.log('');
715
+ for (let i = 0; i < STEP_NAMES.length; i++) {
716
+ console.log(chalk.dim(` ${i + 1} ${STEP_NAMES[i]}`));
717
+ }
718
+ console.log('');
719
+ const stepChoice = (await ask(chalk.bold(' Which step? [1-6]: '))).trim();
720
+ const stepIdx = parseInt(stepChoice, 10) - 1;
721
+ if (stepIdx >= 0 && stepIdx < STEP_FUNCTIONS.length) {
722
+ await STEP_FUNCTIONS[stepIdx](ask, state);
723
+ }
724
+ else {
725
+ console.log(chalk.red(' ✗ Invalid step number'));
726
+ }
727
+ continue; // re-show summary
728
+ }
729
+ if (action === 'a' || action === '') {
730
+ // Apply all
731
+ const ok = await applyWizardState(state);
732
+ if (ok) {
733
+ console.log('');
734
+ console.log(chalk.green(' ✓ All configuration applied successfully'));
735
+ }
736
+ else {
737
+ console.log('');
738
+ console.log(chalk.yellow(' ⚠ Some steps had errors — check messages above'));
739
+ }
740
+ // Verify
741
+ runVerification(state);
742
+ // Show Web UI + SSH info
743
+ const displayIp = state.ip ?? detectCurrentIp() ?? state.hostname;
744
+ console.log('');
745
+ console.log(chalk.dim(' ────────────────────────────────────────'));
746
+ console.log(chalk.bold(` Web UI : https://${displayIp}`));
747
+ console.log(chalk.bold(` SSH : ssh root@${displayIp}`));
748
+ console.log('');
749
+ break;
750
+ }
751
+ console.log(chalk.red(' ✗ Invalid choice — enter A, E, or C'));
176
752
  }
177
753
  }
178
- // Run preferences wizard if prefs haven't been configured yet
179
- if (!loadPrefs()) {
180
- await runPreferencesOnboarding();
754
+ finally {
755
+ rl.close();
181
756
  }
182
757
  console.log(chalk.dim(' Continuing to management shell...\n'));
183
758
  }
184
- // ── Preferences Onboarding ──────────────────────────────────────────────────
759
+ // ── Preferences-only onboarding ──────────────────────────────────────────────
185
760
  /**
186
- * Interactive preferences wizard for first-boot configuration.
187
- * Asks about optional features and saves to ~/.ct/preferences.json.
761
+ * Interactive preferences wizard for when network is already configured
762
+ * but preferences haven't been set yet. Runs only Step 6.
188
763
  */
189
764
  export async function runPreferencesOnboarding() {
190
765
  console.log('');
191
- console.log(chalk.bold.cyan(' Step 2Appliance Preferences'));
192
- console.log(chalk.dim(' ────────────────────────────────────────'));
766
+ console.log(chalk.bold.cyan(' Call TelemetryPreferences Setup'));
767
+ console.log(chalk.dim(' ═══════════════════════════════════════'));
193
768
  console.log(chalk.dim(' Configure optional features. Press Enter to accept defaults.'));
194
- console.log('');
195
769
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
196
770
  const ask = makeAsker(rl);
197
771
  try {
198
- const prefs = {
199
- autoUpdate: false,
772
+ const state = {
773
+ mode: 'dhcp',
774
+ dns1: DEFAULT_DNS1,
775
+ dns2: DEFAULT_DNS2,
776
+ searchDomain: DEFAULT_SEARCH_DOMAIN,
777
+ iface: 'eth0',
778
+ hostname: getHostname(),
779
+ location: '',
780
+ adminPassword: '',
781
+ timezone: getSystemTimezone(),
782
+ ntp1: DEFAULT_NTP[0],
783
+ ntp2: DEFAULT_NTP[1],
784
+ autoUpdate: true,
200
785
  ipv6: false,
201
- otel: false,
202
786
  jtapi: false,
203
- deploymentMode: 'docker-compose',
204
- k8sNamespace: 'ct',
205
- k8sEnvironment: 'ct-dev',
206
787
  };
207
- // Auto-updates
208
- const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
209
- prefs.autoUpdate = autoUpdate !== 'n' && autoUpdate !== 'no';
210
- console.log(prefs.autoUpdate ? chalk.green(' ✓ Auto-updates enabled') : chalk.dim(' ○ Auto-updates disabled'));
211
- // IPv6
212
- const ipv6 = (await ask(chalk.bold(' Enable IPv6 support? [y/N]: '))).trim().toLowerCase();
213
- prefs.ipv6 = ipv6 === 'y' || ipv6 === 'yes';
214
- console.log(prefs.ipv6 ? chalk.green(' ✓ IPv6 enabled') : chalk.dim(' ○ IPv6 disabled'));
215
- // OpenTelemetry
216
- const otel = (await ask(chalk.bold(' Enable OpenTelemetry tracing? [y/N]: '))).trim().toLowerCase();
217
- prefs.otel = otel === 'y' || otel === 'yes';
218
- console.log(prefs.otel ? chalk.green(' ✓ OpenTelemetry enabled') : chalk.dim(' ○ OpenTelemetry disabled'));
219
- // JTAPI phone control
220
- const jtapi = (await ask(chalk.bold(' Enable JTAPI phone control? [y/N]: '))).trim().toLowerCase();
221
- prefs.jtapi = jtapi === 'y' || jtapi === 'yes';
222
- console.log(prefs.jtapi ? chalk.green(' ✓ JTAPI enabled') : chalk.dim(' ○ JTAPI disabled'));
788
+ await stepPreferences(ask, state);
223
789
  // Summary
224
790
  console.log('');
225
791
  console.log(chalk.cyan(' Preferences Summary'));
226
792
  console.log(chalk.dim(' ────────────────────────────────────────'));
227
- console.log(` Auto-updates : ${prefs.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
228
- console.log(` IPv6 : ${prefs.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
229
- console.log(` OpenTelemetry : ${prefs.otel ? chalk.green('Yes') : chalk.dim('No')}`);
230
- console.log(` JTAPI : ${prefs.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
793
+ console.log(` Auto-updates : ${state.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
794
+ console.log(` IPv6 : ${state.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
795
+ console.log(` JTAPI Greetings : ${state.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
231
796
  console.log('');
232
797
  const confirm = (await ask(chalk.bold(' Save preferences? [Y/n]: '))).trim().toLowerCase();
233
798
  if (confirm === 'n' || confirm === 'no') {
234
799
  console.log(chalk.yellow(' Skipped — defaults will be used. Edit later with: config preferences'));
235
800
  return;
236
801
  }
802
+ const prefs = {
803
+ autoUpdate: state.autoUpdate,
804
+ ipv6: state.ipv6,
805
+ otel: true,
806
+ jtapi: state.jtapi,
807
+ deploymentMode: 'docker-compose',
808
+ k8sNamespace: 'ct',
809
+ k8sEnvironment: 'ct-dev',
810
+ };
237
811
  savePrefs(prefs);
238
812
  console.log(chalk.green(' ✓ Preferences saved'));
239
813
  }
@@ -241,103 +815,4 @@ export async function runPreferencesOnboarding() {
241
815
  rl.close();
242
816
  }
243
817
  }
244
- async function runDhcp(_rl, _ask, iface) {
245
- console.log('');
246
- console.log(chalk.dim(' Requesting IP via DHCP...'));
247
- const ok = applyDhcp(iface);
248
- if (!ok) {
249
- console.log(chalk.red(' ✗ DHCP configuration failed.'));
250
- console.log(chalk.dim(' Retry from the shell: network dhcp'));
251
- return;
252
- }
253
- // Wait briefly for DHCP lease
254
- await new Promise((r) => setTimeout(r, 2000));
255
- const ip = detectCurrentIp();
256
- if (ip) {
257
- writeSentinel();
258
- console.log(chalk.green(` ✓ Network ready (DHCP): ${ip}`));
259
- }
260
- else {
261
- console.log(chalk.yellow(' ⚠ No IP obtained via DHCP yet — lease may still be pending.'));
262
- console.log(chalk.dim(' Check status: show interfaces'));
263
- }
264
- console.log('');
265
- }
266
- async function runStaticIp(_rl, ask, iface) {
267
- console.log('');
268
- console.log(chalk.cyan(' Static IP Configuration'));
269
- console.log(chalk.dim(' ────────────────────────────────────────'));
270
- // ── Collect fields ──────────────────────────────────────────────────
271
- let ip = '';
272
- while (!isValidIp(ip)) {
273
- ip = (await ask(chalk.bold(' IP Address (e.g. 192.168.1.100) : '))).trim();
274
- if (!isValidIp(ip))
275
- console.log(chalk.red(' ✗ Invalid IP address — enter four octets, e.g. 192.168.1.100'));
276
- }
277
- let prefix = '';
278
- while (!isValidPrefix(prefix)) {
279
- prefix = (await ask(chalk.bold(' Prefix (e.g. 24 for /24) : '))).trim();
280
- if (!isValidPrefix(prefix))
281
- console.log(chalk.red(' ✗ Invalid prefix — enter a number between 1 and 30'));
282
- }
283
- let gateway = '';
284
- while (!isValidGateway(gateway)) {
285
- gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
286
- if (!isValidGateway(gateway))
287
- console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
288
- }
289
- const dns1Raw = await ask(chalk.bold(` Primary DNS [${DEFAULT_DNS1}] : `));
290
- const dns1 = parseDnsField(dns1Raw, DEFAULT_DNS1);
291
- const dns2Raw = await ask(chalk.bold(` Secondary DNS [${DEFAULT_DNS2}] : `));
292
- const dns2 = parseDnsField(dns2Raw, DEFAULT_DNS2);
293
- const ipPrefix = `${ip}/${prefix}`;
294
- // ── Summary ─────────────────────────────────────────────────────────
295
- console.log('');
296
- console.log(chalk.cyan(' Summary'));
297
- console.log(chalk.dim(' ────────────────────────────────────────'));
298
- console.log(` IP Address : ${chalk.bold(ipPrefix)}`);
299
- console.log(` Gateway : ${chalk.bold(gateway)}`);
300
- console.log(` Primary DNS : ${chalk.bold(dns1)}`);
301
- console.log(` Secondary : ${chalk.bold(dns2)}`);
302
- console.log(` Interface : ${chalk.dim(iface)}`);
303
- console.log('');
304
- const confirm = (await ask(chalk.bold(' Apply? [Y/n]: '))).trim().toLowerCase();
305
- if (confirm === 'n' || confirm === 'no') {
306
- console.log(chalk.yellow('\n Cancelled. Run network address <ip/prefix> <gateway> to configure manually.\n'));
307
- return;
308
- }
309
- // ── Apply ────────────────────────────────────────────────────────────
310
- console.log('');
311
- console.log(chalk.dim(' Applying static IP...'));
312
- const ok = applyStaticIp(iface, ipPrefix, gateway, dns1, dns2);
313
- if (!ok) {
314
- console.log(chalk.red(' ✗ Failed to apply network configuration.'));
315
- console.log(chalk.dim(' Retry from the shell:'));
316
- console.log(chalk.dim(` network address ${iface} ${ipPrefix} ${gateway}`));
317
- console.log(chalk.dim(` network dns-servers ${dns1} ${dns2}`));
318
- console.log('');
319
- return;
320
- }
321
- // ── Verify IP ────────────────────────────────────────────────────────
322
- const newIp = detectCurrentIp();
323
- if (newIp) {
324
- writeSentinel();
325
- console.log(chalk.green(` ✓ Network configured: ${newIp}`));
326
- }
327
- else {
328
- console.log(chalk.yellow(' ⚠ Configuration applied but no IP detected yet.'));
329
- }
330
- // ── Ping gateway ─────────────────────────────────────────────────────
331
- process.stdout.write(chalk.dim(` Checking gateway ${gateway}...`));
332
- const ms = pingGateway(gateway);
333
- if (ms !== null) {
334
- console.log(chalk.green(` ✓ reachable (${ms}ms)`));
335
- }
336
- else {
337
- console.log(chalk.yellow(' ⚠ no response — check gateway address'));
338
- }
339
- console.log('');
340
- console.log(chalk.dim(` Web UI: https://${newIp ?? ip}`));
341
- console.log('');
342
- }
343
818
  //# sourceMappingURL=network-onboarding.js.map