@calltelemetry/cli 0.5.18 → 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 +638 -177
  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,25 +145,26 @@ 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() {
141
- console.log('');
142
- console.log(chalk.bold.cyan(' Call Telemetry — First-Boot Setup'));
143
- console.log(chalk.dim(' ────────────────────────────────────────'));
144
- // Always run network wizard — even if DHCP already provided an IP,
145
- // the operator should confirm or switch to static.
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) {
146
163
  console.log('');
147
- console.log(chalk.bold.cyan(' Step 1 — Network Configuration'));
164
+ console.log(chalk.bold.cyan(' Step 1 of 6 — Network'));
148
165
  console.log(chalk.dim(' ────────────────────────────────────────'));
149
166
  const iface = detectIface();
167
+ state.iface = iface;
150
168
  const existingIp = detectCurrentIp();
151
169
  console.log(chalk.dim(` Interface : ${iface}`));
152
170
  if (existingIp) {
@@ -156,98 +174,640 @@ export async function runNetworkOnboarding() {
156
174
  console.log(chalk.yellow(' No IP address detected.'));
157
175
  }
158
176
  console.log('');
159
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
160
- const ask = makeAsker(rl);
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'));
182
+ console.log('');
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'));
209
+ console.log(chalk.dim(' ────────────────────────────────────────'));
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
161
253
  try {
162
- if (existingIp) {
163
- console.log(chalk.cyan(' Network mode:'));
164
- console.log(chalk.dim(' 1 Keep current DHCP configuration'));
165
- console.log(chalk.dim(' 2 Configure static IP'));
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'));
338
+ console.log('');
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
+ }
166
351
  console.log('');
167
- const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
168
- if (mode === '2') {
169
- await runStaticIp(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}`));
170
356
  }
171
- else {
172
- writeSentinel();
173
- console.log(chalk.green(` ✓ Keeping DHCP: ${existingIp}`));
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'));
174
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)`));
175
585
  }
176
586
  else {
177
- console.log(chalk.cyan(' Network mode:'));
178
- console.log(chalk.dim(' 1 Static IP'));
179
- console.log(chalk.dim(' 2 DHCP (auto)'));
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>'));
603
+ }
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'));
180
707
  console.log('');
181
- const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
182
- if (mode === '2') {
183
- await runDhcp(rl, ask, iface);
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
184
728
  }
185
- else {
186
- await runStaticIp(rl, ask, iface);
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;
187
750
  }
751
+ console.log(chalk.red(' ✗ Invalid choice — enter A, E, or C'));
188
752
  }
189
753
  }
190
754
  finally {
191
755
  rl.close();
192
756
  }
193
- // Run preferences wizard if prefs haven't been configured yet
194
- if (!loadPrefs()) {
195
- await runPreferencesOnboarding();
196
- }
197
757
  console.log(chalk.dim(' Continuing to management shell...\n'));
198
758
  }
199
- // ── Preferences Onboarding ──────────────────────────────────────────────────
759
+ // ── Preferences-only onboarding ──────────────────────────────────────────────
200
760
  /**
201
- * Interactive preferences wizard for first-boot configuration.
202
- * 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.
203
763
  */
204
764
  export async function runPreferencesOnboarding() {
205
765
  console.log('');
206
- console.log(chalk.bold.cyan(' Step 2Appliance Preferences'));
207
- console.log(chalk.dim(' ────────────────────────────────────────'));
766
+ console.log(chalk.bold.cyan(' Call TelemetryPreferences Setup'));
767
+ console.log(chalk.dim(' ═══════════════════════════════════════'));
208
768
  console.log(chalk.dim(' Configure optional features. Press Enter to accept defaults.'));
209
- console.log('');
210
769
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
211
770
  const ask = makeAsker(rl);
212
771
  try {
213
- const prefs = {
214
- 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,
215
785
  ipv6: false,
216
- otel: true, // Always enabled — not a user preference
217
786
  jtapi: false,
218
- deploymentMode: 'docker-compose',
219
- k8sNamespace: 'ct',
220
- k8sEnvironment: 'ct-dev',
221
787
  };
222
- // Auto-updates
223
- const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
224
- prefs.autoUpdate = autoUpdate !== 'n' && autoUpdate !== 'no';
225
- console.log(prefs.autoUpdate ? chalk.green(' ✓ Auto-updates enabled') : chalk.dim(' ○ Auto-updates disabled'));
226
- // IPv6
227
- const ipv6 = (await ask(chalk.bold(' Enable IPv6 support? [y/N]: '))).trim().toLowerCase();
228
- prefs.ipv6 = ipv6 === 'y' || ipv6 === 'yes';
229
- console.log(prefs.ipv6 ? chalk.green(' ✓ IPv6 enabled') : chalk.dim(' ○ IPv6 disabled'));
230
- // JTAPI Greeting Injection
231
- console.log('');
232
- console.log(chalk.dim(' JTAPI enables greeting injection — play custom audio greetings'));
233
- console.log(chalk.dim(' to callers via Cisco JTAPI phone control. Requires a CUCM user'));
234
- console.log(chalk.dim(' with CTI permissions and a JTAPI license.'));
235
- const jtapi = (await ask(chalk.bold(' Enable JTAPI greeting injection? [y/N]: '))).trim().toLowerCase();
236
- prefs.jtapi = jtapi === 'y' || jtapi === 'yes';
237
- console.log(prefs.jtapi ? chalk.green(' ✓ JTAPI greeting injection enabled') : chalk.dim(' ○ JTAPI greeting injection disabled'));
788
+ await stepPreferences(ask, state);
238
789
  // Summary
239
790
  console.log('');
240
791
  console.log(chalk.cyan(' Preferences Summary'));
241
792
  console.log(chalk.dim(' ────────────────────────────────────────'));
242
- console.log(` Auto-updates : ${prefs.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
243
- console.log(` IPv6 : ${prefs.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
244
- console.log(` JTAPI Greetings : ${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')}`);
245
796
  console.log('');
246
797
  const confirm = (await ask(chalk.bold(' Save preferences? [Y/n]: '))).trim().toLowerCase();
247
798
  if (confirm === 'n' || confirm === 'no') {
248
799
  console.log(chalk.yellow(' Skipped — defaults will be used. Edit later with: config preferences'));
249
800
  return;
250
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
+ };
251
811
  savePrefs(prefs);
252
812
  console.log(chalk.green(' ✓ Preferences saved'));
253
813
  }
@@ -255,103 +815,4 @@ export async function runPreferencesOnboarding() {
255
815
  rl.close();
256
816
  }
257
817
  }
258
- async function runDhcp(_rl, _ask, iface) {
259
- console.log('');
260
- console.log(chalk.dim(' Requesting IP via DHCP...'));
261
- const ok = applyDhcp(iface);
262
- if (!ok) {
263
- console.log(chalk.red(' ✗ DHCP configuration failed.'));
264
- console.log(chalk.dim(' Retry from the shell: network dhcp'));
265
- return;
266
- }
267
- // Wait briefly for DHCP lease
268
- await new Promise((r) => setTimeout(r, 2000));
269
- const ip = detectCurrentIp();
270
- if (ip) {
271
- writeSentinel();
272
- console.log(chalk.green(` ✓ Network ready (DHCP): ${ip}`));
273
- }
274
- else {
275
- console.log(chalk.yellow(' ⚠ No IP obtained via DHCP yet — lease may still be pending.'));
276
- console.log(chalk.dim(' Check status: show interfaces'));
277
- }
278
- console.log('');
279
- }
280
- async function runStaticIp(_rl, ask, iface) {
281
- console.log('');
282
- console.log(chalk.cyan(' Static IP Configuration'));
283
- console.log(chalk.dim(' ────────────────────────────────────────'));
284
- // ── Collect fields ──────────────────────────────────────────────────
285
- let ip = '';
286
- while (!isValidIp(ip)) {
287
- ip = (await ask(chalk.bold(' IP Address (e.g. 192.168.1.100) : '))).trim();
288
- if (!isValidIp(ip))
289
- console.log(chalk.red(' ✗ Invalid IP address — enter four octets, e.g. 192.168.1.100'));
290
- }
291
- let prefix = '';
292
- while (!isValidPrefix(prefix)) {
293
- prefix = (await ask(chalk.bold(' Prefix (e.g. 24 for /24) : '))).trim();
294
- if (!isValidPrefix(prefix))
295
- console.log(chalk.red(' ✗ Invalid prefix — enter a number between 1 and 30'));
296
- }
297
- let gateway = '';
298
- while (!isValidGateway(gateway)) {
299
- gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
300
- if (!isValidGateway(gateway))
301
- console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
302
- }
303
- const dns1Raw = await ask(chalk.bold(` Primary DNS [${DEFAULT_DNS1}] : `));
304
- const dns1 = parseDnsField(dns1Raw, DEFAULT_DNS1);
305
- const dns2Raw = await ask(chalk.bold(` Secondary DNS [${DEFAULT_DNS2}] : `));
306
- const dns2 = parseDnsField(dns2Raw, DEFAULT_DNS2);
307
- const ipPrefix = `${ip}/${prefix}`;
308
- // ── Summary ─────────────────────────────────────────────────────────
309
- console.log('');
310
- console.log(chalk.cyan(' Summary'));
311
- console.log(chalk.dim(' ────────────────────────────────────────'));
312
- console.log(` IP Address : ${chalk.bold(ipPrefix)}`);
313
- console.log(` Gateway : ${chalk.bold(gateway)}`);
314
- console.log(` Primary DNS : ${chalk.bold(dns1)}`);
315
- console.log(` Secondary : ${chalk.bold(dns2)}`);
316
- console.log(` Interface : ${chalk.dim(iface)}`);
317
- console.log('');
318
- const confirm = (await ask(chalk.bold(' Apply? [Y/n]: '))).trim().toLowerCase();
319
- if (confirm === 'n' || confirm === 'no') {
320
- console.log(chalk.yellow('\n Cancelled. Run network address <ip/prefix> <gateway> to configure manually.\n'));
321
- return;
322
- }
323
- // ── Apply ────────────────────────────────────────────────────────────
324
- console.log('');
325
- console.log(chalk.dim(' Applying static IP...'));
326
- const ok = applyStaticIp(iface, ipPrefix, gateway, dns1, dns2);
327
- if (!ok) {
328
- console.log(chalk.red(' ✗ Failed to apply network configuration.'));
329
- console.log(chalk.dim(' Retry from the shell:'));
330
- console.log(chalk.dim(` network address ${iface} ${ipPrefix} ${gateway}`));
331
- console.log(chalk.dim(` network dns-servers ${dns1} ${dns2}`));
332
- console.log('');
333
- return;
334
- }
335
- // ── Verify IP ────────────────────────────────────────────────────────
336
- const newIp = detectCurrentIp();
337
- if (newIp) {
338
- writeSentinel();
339
- console.log(chalk.green(` ✓ Network configured: ${newIp}`));
340
- }
341
- else {
342
- console.log(chalk.yellow(' ⚠ Configuration applied but no IP detected yet.'));
343
- }
344
- // ── Ping gateway ─────────────────────────────────────────────────────
345
- process.stdout.write(chalk.dim(` Checking gateway ${gateway}...`));
346
- const ms = pingGateway(gateway);
347
- if (ms !== null) {
348
- console.log(chalk.green(` ✓ reachable (${ms}ms)`));
349
- }
350
- else {
351
- console.log(chalk.yellow(' ⚠ no response — check gateway address'));
352
- }
353
- console.log('');
354
- console.log(chalk.dim(` Web UI: https://${newIp ?? ip}`));
355
- console.log('');
356
- }
357
818
  //# sourceMappingURL=network-onboarding.js.map