@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.
- package/dist/lib/certs.d.ts +16 -2
- package/dist/lib/certs.d.ts.map +1 -1
- package/dist/lib/certs.js +80 -8
- package/dist/lib/certs.js.map +1 -1
- package/dist/lib/identity.d.ts +38 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +85 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/prefs.d.ts +4 -0
- package/dist/lib/prefs.d.ts.map +1 -1
- package/dist/lib/prefs.js.map +1 -1
- package/dist/lib/time.d.ts +57 -0
- package/dist/lib/time.d.ts.map +1 -0
- package/dist/lib/time.js +200 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/lib/users.d.ts.map +1 -1
- package/dist/lib/users.js +13 -5
- package/dist/lib/users.js.map +1 -1
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/shell/commands/config.d.ts +3 -0
- package/dist/shell/commands/config.d.ts.map +1 -1
- package/dist/shell/commands/config.js +76 -3
- package/dist/shell/commands/config.js.map +1 -1
- package/dist/shell/commands/diag.d.ts.map +1 -1
- package/dist/shell/commands/diag.js +73 -0
- package/dist/shell/commands/diag.js.map +1 -1
- package/dist/shell/commands/registry.d.ts.map +1 -1
- package/dist/shell/commands/registry.js +16 -2
- package/dist/shell/commands/registry.js.map +1 -1
- package/dist/shell/network-onboarding.d.ts +40 -10
- package/dist/shell/network-onboarding.d.ts.map +1 -1
- package/dist/shell/network-onboarding.js +638 -177
- package/dist/shell/network-onboarding.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* network-onboarding.ts — First-boot
|
|
2
|
+
* network-onboarding.ts — First-boot setup wizard for ct shell.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
178
|
-
console.log(chalk.dim('
|
|
179
|
-
|
|
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
|
|
182
|
-
if (
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
|
759
|
+
// ── Preferences-only onboarding ──────────────────────────────────────────────
|
|
200
760
|
/**
|
|
201
|
-
* Interactive preferences wizard for
|
|
202
|
-
*
|
|
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('
|
|
207
|
-
console.log(chalk.dim('
|
|
766
|
+
console.log(chalk.bold.cyan(' Call Telemetry — Preferences 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
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
243
|
-
console.log(` IPv6
|
|
244
|
-
console.log(` JTAPI Greetings
|
|
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
|