@calltelemetry/cli 0.6.12 → 0.6.13
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/commands/appliance.d.ts +3 -0
- package/dist/commands/appliance.d.ts.map +1 -0
- package/dist/commands/appliance.js +99 -0
- package/dist/commands/appliance.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +19 -17
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/migration-001-cgnat.d.ts +2 -0
- package/dist/lib/migration-001-cgnat.d.ts.map +1 -0
- package/dist/lib/migration-001-cgnat.js +59 -0
- package/dist/lib/migration-001-cgnat.js.map +1 -0
- package/dist/lib/migration-002-swap.d.ts +12 -0
- package/dist/lib/migration-002-swap.d.ts.map +1 -0
- package/dist/lib/migration-002-swap.js +103 -0
- package/dist/lib/migration-002-swap.js.map +1 -0
- package/dist/lib/migration-003-nm-heal.d.ts +12 -0
- package/dist/lib/migration-003-nm-heal.d.ts.map +1 -0
- package/dist/lib/migration-003-nm-heal.js +75 -0
- package/dist/lib/migration-003-nm-heal.js.map +1 -0
- package/dist/lib/migration-004-systemd-docker.d.ts +13 -0
- package/dist/lib/migration-004-systemd-docker.d.ts.map +1 -0
- package/dist/lib/migration-004-systemd-docker.js +48 -0
- package/dist/lib/migration-004-systemd-docker.js.map +1 -0
- package/dist/lib/migration-005-ssh-keys.d.ts +13 -0
- package/dist/lib/migration-005-ssh-keys.d.ts.map +1 -0
- package/dist/lib/migration-005-ssh-keys.js +49 -0
- package/dist/lib/migration-005-ssh-keys.js.map +1 -0
- package/dist/lib/migration-006-console-loglevel.d.ts +10 -0
- package/dist/lib/migration-006-console-loglevel.d.ts.map +1 -0
- package/dist/lib/migration-006-console-loglevel.js +27 -0
- package/dist/lib/migration-006-console-loglevel.js.map +1 -0
- package/dist/lib/migration-007-nm-keyfile.d.ts +10 -0
- package/dist/lib/migration-007-nm-keyfile.d.ts.map +1 -0
- package/dist/lib/migration-007-nm-keyfile.js +30 -0
- package/dist/lib/migration-007-nm-keyfile.js.map +1 -0
- package/dist/lib/migration-008-systemd-restart.d.ts +13 -0
- package/dist/lib/migration-008-systemd-restart.d.ts.map +1 -0
- package/dist/lib/migration-008-systemd-restart.js +41 -0
- package/dist/lib/migration-008-systemd-restart.js.map +1 -0
- package/dist/lib/migration-009-bind-mount-files.d.ts +11 -0
- package/dist/lib/migration-009-bind-mount-files.d.ts.map +1 -0
- package/dist/lib/migration-009-bind-mount-files.js +86 -0
- package/dist/lib/migration-009-bind-mount-files.js.map +1 -0
- package/dist/lib/migration-010-jtapi-state.d.ts +11 -0
- package/dist/lib/migration-010-jtapi-state.d.ts.map +1 -0
- package/dist/lib/migration-010-jtapi-state.js +61 -0
- package/dist/lib/migration-010-jtapi-state.js.map +1 -0
- package/dist/lib/migration-011-docker-memory-limit.d.ts +13 -0
- package/dist/lib/migration-011-docker-memory-limit.d.ts.map +1 -0
- package/dist/lib/migration-011-docker-memory-limit.js +45 -0
- package/dist/lib/migration-011-docker-memory-limit.js.map +1 -0
- package/dist/lib/migration-012-db-pool-sizes.d.ts +12 -0
- package/dist/lib/migration-012-db-pool-sizes.d.ts.map +1 -0
- package/dist/lib/migration-012-db-pool-sizes.js +64 -0
- package/dist/lib/migration-012-db-pool-sizes.js.map +1 -0
- package/dist/lib/migrations.d.ts +34 -0
- package/dist/lib/migrations.d.ts.map +1 -0
- package/dist/lib/migrations.js +179 -0
- package/dist/lib/migrations.js.map +1 -0
- package/dist/lib/update-steps.d.ts.map +1 -1
- package/dist/lib/update-steps.js +12 -0
- package/dist/lib/update-steps.js.map +1 -1
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/shell/commands/appliance-performance.d.ts +36 -0
- package/dist/shell/commands/appliance-performance.d.ts.map +1 -0
- package/dist/shell/commands/appliance-performance.js +221 -0
- package/dist/shell/commands/appliance-performance.js.map +1 -0
- package/dist/shell/commands/registry.d.ts.map +1 -1
- package/dist/shell/commands/registry.js +19 -0
- package/dist/shell/commands/registry.js.map +1 -1
- package/dist/shell/commands/users.d.ts.map +1 -1
- package/dist/shell/commands/users.js +42 -1
- package/dist/shell/commands/users.js.map +1 -1
- package/dist/shell/network-onboarding.d.ts +29 -7
- package/dist/shell/network-onboarding.d.ts.map +1 -1
- package/dist/shell/network-onboarding.js +332 -81
- package/dist/shell/network-onboarding.js.map +1 -1
- package/dist/ui/views/JtapiDisableView.d.ts +6 -0
- package/dist/ui/views/JtapiDisableView.d.ts.map +1 -0
- package/dist/ui/views/JtapiDisableView.js +52 -0
- package/dist/ui/views/JtapiDisableView.js.map +1 -0
- package/dist/ui/views/JtapiEnableView.d.ts +6 -0
- package/dist/ui/views/JtapiEnableView.d.ts.map +1 -0
- package/dist/ui/views/JtapiEnableView.js +40 -0
- package/dist/ui/views/JtapiEnableView.js.map +1 -0
- package/dist/ui/views/JtapiStatusView.d.ts +6 -0
- package/dist/ui/views/JtapiStatusView.d.ts.map +1 -0
- package/dist/ui/views/JtapiStatusView.js +11 -0
- package/dist/ui/views/JtapiStatusView.js.map +1 -0
- package/dist/ui/views/JtapiTroubleshootView.d.ts +6 -0
- package/dist/ui/views/JtapiTroubleshootView.d.ts.map +1 -0
- package/dist/ui/views/JtapiTroubleshootView.js +114 -0
- package/dist/ui/views/JtapiTroubleshootView.js.map +1 -0
- package/dist/ui/views/MainMenu.d.ts.map +1 -1
- package/dist/ui/views/MainMenu.js +11 -0
- package/dist/ui/views/MainMenu.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* network-onboarding.ts — First-boot setup wizard for ct shell.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Seven-step wizard that collects all configuration atomically before applying:
|
|
5
5
|
* 1. Network (IP/DHCP, DNS, search domain)
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
6
|
+
* 2. Timezone
|
|
7
|
+
* 3. NTP Time Servers
|
|
8
|
+
* 4. System Identity (hostname, location)
|
|
9
|
+
* 5. Admin Credentials (root password)
|
|
10
10
|
* 6. Preferences (auto-update, IPv6, JTAPI)
|
|
11
|
+
* 7. Performance Profile (small/medium/large)
|
|
11
12
|
*
|
|
12
13
|
* After collection, shows a unified summary and offers Apply/Edit/Cancel.
|
|
13
14
|
* Apply phase runs all changes sequentially with progress output.
|
|
@@ -15,12 +16,13 @@
|
|
|
15
16
|
*/
|
|
16
17
|
import * as readline from 'node:readline';
|
|
17
18
|
import { execSync, spawnSync } from 'node:child_process';
|
|
18
|
-
import { writeFileSync } from 'node:fs';
|
|
19
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
19
20
|
import chalk from 'chalk';
|
|
20
|
-
import { savePrefs } from '../lib/prefs.js';
|
|
21
|
+
import { loadPrefs, savePrefs } from '../lib/prefs.js';
|
|
21
22
|
import { getHostname, isValidHostname, buildFqdn, applyHostname, applyAdminPassword, isStrongPassword, } from '../lib/identity.js';
|
|
22
23
|
import { getSystemTimezone, TIMEZONE_REGIONS, searchTimezones, isValidTimezone, applyTimezone, applyNtpServers, checkNtpSync, DEFAULT_NTP, } from '../lib/time.js';
|
|
23
24
|
import { generateSelfSignedCerts, getCertInfo } from '../lib/certs.js';
|
|
25
|
+
import { PROFILES, getSystemRamMb, getSystemCpuCount, recommendedProfile, meetsRamRequirement, applyProfile, detectCurrentProfile, } from './commands/appliance-performance.js';
|
|
24
26
|
export { buildFqdn } from '../lib/identity.js';
|
|
25
27
|
export const NETWORK_SENTINEL = '/etc/ct-network-configured';
|
|
26
28
|
const DEFAULT_DNS1 = '8.8.8.8';
|
|
@@ -99,13 +101,86 @@ export function pingGateway(gateway) {
|
|
|
99
101
|
return null;
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
+
/** Test if a DNS server can resolve a well-known domain. Returns true on success. */
|
|
105
|
+
function testDnsServer(server) {
|
|
104
106
|
try {
|
|
105
|
-
|
|
107
|
+
const result = spawnSync('nslookup', ['google.com', server], {
|
|
108
|
+
encoding: 'utf-8',
|
|
109
|
+
timeout: 5000,
|
|
110
|
+
});
|
|
111
|
+
return result.status === 0;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Ask for a DNS server, test resolution, offer retry on failure. */
|
|
118
|
+
async function askAndTestDns(ask, label, defaultValue) {
|
|
119
|
+
const padded = label.length < 13 ? label.padEnd(13) : label;
|
|
120
|
+
while (true) {
|
|
121
|
+
const raw = await ask(chalk.bold(` ${padded} [${defaultValue}] : `));
|
|
122
|
+
const server = parseDnsField(raw, defaultValue);
|
|
123
|
+
if (!isValidIp(server)) {
|
|
124
|
+
console.log(chalk.red(` ✗ Invalid IP address`));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
process.stdout.write(chalk.dim(` Testing DNS resolution via ${server} ... `));
|
|
128
|
+
if (testDnsServer(server)) {
|
|
129
|
+
console.log(chalk.green('✓ working'));
|
|
130
|
+
return server;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk.yellow('✗ resolution failed'));
|
|
134
|
+
const retry = (await ask(chalk.yellow(' Re-enter DNS server? [Y/n]: '))).trim().toLowerCase();
|
|
135
|
+
if (retry === 'n') {
|
|
136
|
+
console.log(chalk.dim(' Continuing with unresponsive DNS — may work after config is applied'));
|
|
137
|
+
return server;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Write the network sentinel with config state (no sensitive values). */
|
|
143
|
+
export function writeSentinel(state) {
|
|
144
|
+
const sentinel = state ? {
|
|
145
|
+
mode: state.mode ?? 'dhcp',
|
|
146
|
+
ip: state.ip,
|
|
147
|
+
prefix: state.prefix,
|
|
148
|
+
gateway: state.gateway,
|
|
149
|
+
dns1: state.dns1,
|
|
150
|
+
dns2: state.dns2,
|
|
151
|
+
searchDomain: state.searchDomain,
|
|
152
|
+
iface: state.iface,
|
|
153
|
+
hostname: state.hostname,
|
|
154
|
+
location: state.location,
|
|
155
|
+
timezone: state.timezone,
|
|
156
|
+
ntp1: state.ntp1,
|
|
157
|
+
ntp2: state.ntp2,
|
|
158
|
+
configuredAt: new Date().toISOString(),
|
|
159
|
+
} : { mode: 'dhcp', configuredAt: new Date().toISOString() };
|
|
160
|
+
const content = JSON.stringify(sentinel, null, 2);
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(NETWORK_SENTINEL, content);
|
|
106
163
|
}
|
|
107
164
|
catch {
|
|
108
|
-
|
|
165
|
+
// Fall back to sudo write via temp file
|
|
166
|
+
const tmp = `/tmp/ct-sentinel-${Date.now()}.json`;
|
|
167
|
+
writeFileSync(tmp, content);
|
|
168
|
+
spawnSync('sudo', ['mv', tmp, NETWORK_SENTINEL]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Read saved sentinel state, or null if not configured / empty. */
|
|
172
|
+
export function readSentinel() {
|
|
173
|
+
try {
|
|
174
|
+
if (!existsSync(NETWORK_SENTINEL))
|
|
175
|
+
return null;
|
|
176
|
+
const { readFileSync } = require('node:fs');
|
|
177
|
+
const raw = readFileSync(NETWORK_SENTINEL, 'utf-8').trim();
|
|
178
|
+
if (!raw || raw.length < 2)
|
|
179
|
+
return null;
|
|
180
|
+
return JSON.parse(raw);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
109
184
|
}
|
|
110
185
|
}
|
|
111
186
|
/** Apply static IP via nmcli. Returns true on success. */
|
|
@@ -149,6 +224,44 @@ export function applyDhcp(iface) {
|
|
|
149
224
|
function makeAsker(rl) {
|
|
150
225
|
return (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
151
226
|
}
|
|
227
|
+
/** Prompt for password with masked input (shows * for each character) */
|
|
228
|
+
function askMasked(prompt) {
|
|
229
|
+
return new Promise(resolve => {
|
|
230
|
+
const { stdin, stdout } = process;
|
|
231
|
+
stdout.write(prompt);
|
|
232
|
+
const chars = [];
|
|
233
|
+
const wasRaw = stdin.isRaw;
|
|
234
|
+
stdin.setRawMode?.(true);
|
|
235
|
+
stdin.resume();
|
|
236
|
+
const onData = (buf) => {
|
|
237
|
+
const ch = buf.toString('utf8');
|
|
238
|
+
if (ch === '\r' || ch === '\n') {
|
|
239
|
+
stdin.removeListener('data', onData);
|
|
240
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
241
|
+
stdout.write('\n');
|
|
242
|
+
resolve(chars.join(''));
|
|
243
|
+
}
|
|
244
|
+
else if (ch === '\x7f' || ch === '\b') {
|
|
245
|
+
if (chars.length > 0) {
|
|
246
|
+
chars.pop();
|
|
247
|
+
stdout.write('\b \b');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (ch === '\x03') {
|
|
251
|
+
// Ctrl+C
|
|
252
|
+
stdin.removeListener('data', onData);
|
|
253
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
254
|
+
stdout.write('\n');
|
|
255
|
+
resolve('');
|
|
256
|
+
}
|
|
257
|
+
else if (ch >= ' ') {
|
|
258
|
+
chars.push(ch);
|
|
259
|
+
stdout.write('*');
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
stdin.on('data', onData);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
152
265
|
// ── Step progress helper ─────────────────────────────────────────────────────
|
|
153
266
|
function stepProgress(label, ok) {
|
|
154
267
|
if (ok) {
|
|
@@ -161,7 +274,7 @@ function stepProgress(label, ok) {
|
|
|
161
274
|
// ── Step 1: Network ──────────────────────────────────────────────────────────
|
|
162
275
|
async function stepNetwork(ask, state) {
|
|
163
276
|
console.log('');
|
|
164
|
-
console.log(chalk.bold.cyan(' Step 1 of
|
|
277
|
+
console.log(chalk.bold.cyan(' Step 1 of 7 — Network'));
|
|
165
278
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
166
279
|
const iface = detectIface();
|
|
167
280
|
state.iface = iface;
|
|
@@ -179,8 +292,11 @@ async function stepNetwork(ask, state) {
|
|
|
179
292
|
console.log(chalk.cyan(' Network mode:'));
|
|
180
293
|
console.log(chalk.dim(' 1 Keep current DHCP configuration'));
|
|
181
294
|
console.log(chalk.dim(' 2 Configure static IP'));
|
|
295
|
+
console.log(chalk.dim(' 3 Skip setup and exit to shell'));
|
|
182
296
|
console.log('');
|
|
183
297
|
const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
|
|
298
|
+
if (mode === '3')
|
|
299
|
+
return 'skip';
|
|
184
300
|
if (mode === '2') {
|
|
185
301
|
state.mode = 'static';
|
|
186
302
|
}
|
|
@@ -193,8 +309,11 @@ async function stepNetwork(ask, state) {
|
|
|
193
309
|
console.log(chalk.cyan(' Network mode:'));
|
|
194
310
|
console.log(chalk.dim(' 1 Static IP'));
|
|
195
311
|
console.log(chalk.dim(' 2 DHCP (auto)'));
|
|
312
|
+
console.log(chalk.dim(' 3 Skip setup and exit to shell'));
|
|
196
313
|
console.log('');
|
|
197
314
|
const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
|
|
315
|
+
if (mode === '3')
|
|
316
|
+
return 'skip';
|
|
198
317
|
if (mode === '2') {
|
|
199
318
|
state.mode = 'dhcp';
|
|
200
319
|
}
|
|
@@ -243,55 +362,43 @@ async function stepNetwork(ask, state) {
|
|
|
243
362
|
console.log(chalk.green(` ✓ Subnet: /${prefix}`));
|
|
244
363
|
}
|
|
245
364
|
state.prefix = prefix;
|
|
246
|
-
|
|
247
|
-
while (
|
|
248
|
-
gateway =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
365
|
+
// Gateway — validate format, then test reachability with retry option
|
|
366
|
+
while (true) {
|
|
367
|
+
let gateway = '';
|
|
368
|
+
while (!isValidGateway(gateway)) {
|
|
369
|
+
gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
|
|
370
|
+
if (!isValidGateway(gateway))
|
|
371
|
+
console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
|
|
372
|
+
}
|
|
373
|
+
process.stdout.write(chalk.dim(` Checking connectivity to ${gateway} ... `));
|
|
374
|
+
const latency = pingGateway(gateway);
|
|
375
|
+
if (latency !== null) {
|
|
376
|
+
console.log(chalk.green(`✓ reachable (${latency}ms)`));
|
|
377
|
+
state.gateway = gateway;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.log(chalk.yellow(`✗ unreachable`));
|
|
382
|
+
const retry = (await ask(chalk.yellow(' Re-enter gateway? [Y/n]: '))).trim().toLowerCase();
|
|
383
|
+
if (retry === 'n') {
|
|
384
|
+
console.log(chalk.dim(' Continuing with unreachable gateway — may work after config is applied'));
|
|
385
|
+
state.gateway = gateway;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// DNS — validate each server can resolve, with retry option
|
|
392
|
+
state.dns1 = await askAndTestDns(ask, 'Primary DNS', DEFAULT_DNS1);
|
|
393
|
+
state.dns2 = await askAndTestDns(ask, 'Secondary DNS', DEFAULT_DNS2);
|
|
259
394
|
// Search domain
|
|
260
395
|
const domainRaw = await ask(chalk.bold(` Search domain [${DEFAULT_SEARCH_DOMAIN}] : `));
|
|
261
396
|
state.searchDomain = parseDnsField(domainRaw, DEFAULT_SEARCH_DOMAIN);
|
|
262
|
-
// Quick connectivity check
|
|
263
|
-
console.log('');
|
|
264
|
-
console.log(chalk.dim(' Quick connectivity check...'));
|
|
265
|
-
if (state.mode === 'static' && state.gateway) {
|
|
266
|
-
const ms = pingGateway(state.gateway);
|
|
267
|
-
if (ms !== null) {
|
|
268
|
-
console.log(chalk.green(` ✓ Gateway ${state.gateway} reachable (${ms}ms)`));
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
console.log(chalk.yellow(` ⚠ Gateway ${state.gateway} not reachable — may work after apply`));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
// DNS resolve test
|
|
275
|
-
try {
|
|
276
|
-
const result = spawnSync('nslookup', ['google.com', state.dns1], {
|
|
277
|
-
encoding: 'utf-8',
|
|
278
|
-
timeout: 5000,
|
|
279
|
-
});
|
|
280
|
-
if (result.status === 0) {
|
|
281
|
-
console.log(chalk.green(` ✓ DNS resolution via ${state.dns1} working`));
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
console.log(chalk.yellow(` ⚠ DNS resolution via ${state.dns1} failed — may work after apply`));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
console.log(chalk.yellow(` ⚠ DNS check skipped — nslookup not available`));
|
|
289
|
-
}
|
|
290
397
|
}
|
|
291
398
|
// ── Step 2: System Identity ──────────────────────────────────────────────────
|
|
292
399
|
async function stepIdentity(ask, state) {
|
|
293
400
|
console.log('');
|
|
294
|
-
console.log(chalk.bold.cyan(' Step 4 of
|
|
401
|
+
console.log(chalk.bold.cyan(' Step 4 of 7 — System Identity'));
|
|
295
402
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
296
403
|
const currentHostname = getHostname();
|
|
297
404
|
// Hostname
|
|
@@ -317,21 +424,21 @@ async function stepIdentity(ask, state) {
|
|
|
317
424
|
console.log(chalk.dim(' config certs import <cert.pem> <key.pem>'));
|
|
318
425
|
}
|
|
319
426
|
// ── Step 3: Admin Credentials ────────────────────────────────────────────────
|
|
320
|
-
async function stepAdmin(
|
|
427
|
+
async function stepAdmin(_ask, state) {
|
|
321
428
|
console.log('');
|
|
322
|
-
console.log(chalk.bold.cyan(' Step 5 of
|
|
429
|
+
console.log(chalk.bold.cyan(' Step 5 of 7 — Admin Credentials'));
|
|
323
430
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
324
431
|
console.log(chalk.yellow(' Default passwords should be changed before production use.'));
|
|
325
432
|
console.log(chalk.dim(' Minimum 8 characters.'));
|
|
326
433
|
console.log('');
|
|
327
434
|
while (true) {
|
|
328
|
-
const pw = (await
|
|
435
|
+
const pw = (await askMasked(chalk.bold(' New admin password: '))).trim();
|
|
329
436
|
const check = isStrongPassword(pw);
|
|
330
437
|
if (!check.valid) {
|
|
331
438
|
console.log(chalk.red(` ✗ ${check.reason}`));
|
|
332
439
|
continue;
|
|
333
440
|
}
|
|
334
|
-
const confirm = (await
|
|
441
|
+
const confirm = (await askMasked(chalk.bold(' Confirm password: '))).trim();
|
|
335
442
|
if (confirm !== pw) {
|
|
336
443
|
console.log(chalk.red(' ✗ Passwords do not match'));
|
|
337
444
|
continue;
|
|
@@ -344,7 +451,7 @@ async function stepAdmin(ask, state) {
|
|
|
344
451
|
// ── Step 4: Timezone ─────────────────────────────────────────────────────────
|
|
345
452
|
async function stepTimezone(ask, state) {
|
|
346
453
|
console.log('');
|
|
347
|
-
console.log(chalk.bold.cyan(' Step 2 of
|
|
454
|
+
console.log(chalk.bold.cyan(' Step 2 of 7 — Timezone'));
|
|
348
455
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
349
456
|
const currentTz = getSystemTimezone();
|
|
350
457
|
console.log(chalk.dim(` Current timezone: ${currentTz}`));
|
|
@@ -436,7 +543,7 @@ async function stepTimezone(ask, state) {
|
|
|
436
543
|
// ── Step 5: NTP ──────────────────────────────────────────────────────────────
|
|
437
544
|
async function stepNtp(ask, state) {
|
|
438
545
|
console.log('');
|
|
439
|
-
console.log(chalk.bold.cyan(' Step 3 of
|
|
546
|
+
console.log(chalk.bold.cyan(' Step 3 of 7 — NTP Time Servers'));
|
|
440
547
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
441
548
|
const ntp1Raw = await ask(chalk.bold(` NTP Server 1 [${DEFAULT_NTP[0]}]: `));
|
|
442
549
|
state.ntp1 = parseDnsField(ntp1Raw, DEFAULT_NTP[0]);
|
|
@@ -447,7 +554,7 @@ async function stepNtp(ask, state) {
|
|
|
447
554
|
// ── Step 6: Preferences ──────────────────────────────────────────────────────
|
|
448
555
|
async function stepPreferences(ask, state) {
|
|
449
556
|
console.log('');
|
|
450
|
-
console.log(chalk.bold.cyan(' Step 6 of
|
|
557
|
+
console.log(chalk.bold.cyan(' Step 6 of 7 — Preferences'));
|
|
451
558
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
452
559
|
// Auto-updates
|
|
453
560
|
const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
|
|
@@ -466,6 +573,67 @@ async function stepPreferences(ask, state) {
|
|
|
466
573
|
state.jtapi = jtapi === 'y' || jtapi === 'yes';
|
|
467
574
|
console.log(state.jtapi ? chalk.green(' ✓ JTAPI greeting injection enabled') : chalk.dim(' ○ JTAPI greeting injection disabled'));
|
|
468
575
|
}
|
|
576
|
+
// ── Step 7: Performance Profile ──────────────────────────────────────────
|
|
577
|
+
const PROFILE_NAMES = ['small', 'medium', 'large'];
|
|
578
|
+
async function stepPerformance(ask, state) {
|
|
579
|
+
console.log('');
|
|
580
|
+
console.log(chalk.bold.cyan(' Step 7 of 7 — Performance Profile'));
|
|
581
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
582
|
+
// Check if already configured
|
|
583
|
+
const existing = detectCurrentProfile();
|
|
584
|
+
if (existing) {
|
|
585
|
+
console.log(chalk.dim(` Current profile: ${PROFILES[existing].description}`));
|
|
586
|
+
}
|
|
587
|
+
// Detect hardware
|
|
588
|
+
const ramMb = getSystemRamMb();
|
|
589
|
+
const cpus = getSystemCpuCount();
|
|
590
|
+
const ramStr = ramMb > 0 ? `${(ramMb / 1024).toFixed(0)} GB` : 'unknown';
|
|
591
|
+
const cpuStr = cpus > 0 ? `${cpus} vCPUs` : 'unknown';
|
|
592
|
+
console.log(chalk.dim(` Detected: ${ramStr} RAM, ${cpuStr}`));
|
|
593
|
+
console.log('');
|
|
594
|
+
// Determine recommended profile
|
|
595
|
+
const rec = ramMb > 0 ? recommendedProfile(ramMb) : 'small';
|
|
596
|
+
console.log(chalk.cyan(' Choose a performance profile:'));
|
|
597
|
+
console.log('');
|
|
598
|
+
for (let i = 0; i < PROFILE_NAMES.length; i++) {
|
|
599
|
+
const name = PROFILE_NAMES[i];
|
|
600
|
+
const p = PROFILES[name];
|
|
601
|
+
const isRec = name === rec;
|
|
602
|
+
const label = isRec ? `${p.description} (recommended for this hardware)` : p.description;
|
|
603
|
+
const marker = isRec ? chalk.green(` > ${i + 1} ${label}`) : chalk.dim(` ${i + 1} ${label}`);
|
|
604
|
+
console.log(marker);
|
|
605
|
+
console.log(chalk.dim(` ${p.capacity}`));
|
|
606
|
+
console.log(chalk.dim(` Minimum: ${p.hardware}`));
|
|
607
|
+
console.log('');
|
|
608
|
+
}
|
|
609
|
+
// Default to recommended
|
|
610
|
+
const recIdx = PROFILE_NAMES.indexOf(rec) + 1;
|
|
611
|
+
const choice = (await ask(chalk.bold(` Choice [${recIdx}]: `))).trim() || String(recIdx);
|
|
612
|
+
const idx = parseInt(choice, 10) - 1;
|
|
613
|
+
if (isNaN(idx) || idx < 0 || idx >= PROFILE_NAMES.length) {
|
|
614
|
+
console.log(chalk.red(' ✗ Invalid selection — using recommended profile'));
|
|
615
|
+
state.performanceProfile = rec;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
state.performanceProfile = PROFILE_NAMES[idx];
|
|
619
|
+
}
|
|
620
|
+
const selected = state.performanceProfile;
|
|
621
|
+
const profile = PROFILES[selected];
|
|
622
|
+
// Warn if hardware doesn't meet requirements
|
|
623
|
+
if (ramMb > 0 && !meetsRamRequirement(ramMb, selected)) {
|
|
624
|
+
const ramGb = (ramMb / 1024).toFixed(1);
|
|
625
|
+
console.log('');
|
|
626
|
+
console.log(chalk.yellow(` ⚠ Your system has ${ramGb} GB RAM but ${profile.description} requires ${profile.hardware}.`));
|
|
627
|
+
console.log(chalk.yellow(' Performance may suffer.'));
|
|
628
|
+
const confirm = (await ask(chalk.bold(' Continue anyway? [y/N]: '))).trim().toLowerCase();
|
|
629
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
630
|
+
console.log(chalk.dim(' Falling back to recommended profile.'));
|
|
631
|
+
state.performanceProfile = rec;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const final = PROFILES[state.performanceProfile];
|
|
635
|
+
console.log(chalk.green(` ✓ Performance profile: ${final.description}`));
|
|
636
|
+
}
|
|
469
637
|
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
470
638
|
/** Render a full summary of the wizard state. Exported for testing. */
|
|
471
639
|
export function renderSummary(state) {
|
|
@@ -512,6 +680,15 @@ export function renderSummary(state) {
|
|
|
512
680
|
add(` Auto-updates : ${state.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
513
681
|
add(` IPv6 : ${state.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
514
682
|
add(` JTAPI Greetings: ${state.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
683
|
+
// PERFORMANCE
|
|
684
|
+
if (state.performanceProfile) {
|
|
685
|
+
const perf = PROFILES[state.performanceProfile];
|
|
686
|
+
add('');
|
|
687
|
+
add(chalk.cyan(' PERFORMANCE'));
|
|
688
|
+
add(chalk.dim(' ────────────────────────────────────────'));
|
|
689
|
+
add(` Profile : ${chalk.bold(perf.description)}`);
|
|
690
|
+
add(` Capacity : ${perf.capacity}`);
|
|
691
|
+
}
|
|
515
692
|
add('');
|
|
516
693
|
return lines.join('\n');
|
|
517
694
|
}
|
|
@@ -563,7 +740,18 @@ async function applyWizardState(state) {
|
|
|
563
740
|
stepProgress('Configure NTP servers', ntpOk);
|
|
564
741
|
if (!ntpOk)
|
|
565
742
|
allOk = false;
|
|
566
|
-
// 7.
|
|
743
|
+
// 7. Performance profile
|
|
744
|
+
if (state.performanceProfile) {
|
|
745
|
+
try {
|
|
746
|
+
await applyProfile(state.performanceProfile);
|
|
747
|
+
stepProgress('Apply performance profile', true);
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
stepProgress('Apply performance profile', false);
|
|
751
|
+
allOk = false;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// 8. Save preferences + write sentinel BEFORE cert gen
|
|
567
755
|
// (so wizard doesn't re-run even if cert generation fails)
|
|
568
756
|
const prefs = {
|
|
569
757
|
autoUpdate: state.autoUpdate,
|
|
@@ -682,6 +870,7 @@ const STEP_FUNCTIONS = [
|
|
|
682
870
|
stepIdentity, // 4. Hostname + location
|
|
683
871
|
stepAdmin, // 5. Admin password
|
|
684
872
|
stepPreferences, // 6. Feature preferences
|
|
873
|
+
stepPerformance, // 7. Performance profile
|
|
685
874
|
];
|
|
686
875
|
const STEP_NAMES = [
|
|
687
876
|
'Network',
|
|
@@ -690,6 +879,7 @@ const STEP_NAMES = [
|
|
|
690
879
|
'System Identity',
|
|
691
880
|
'Admin Credentials',
|
|
692
881
|
'Preferences',
|
|
882
|
+
'Performance Profile',
|
|
693
883
|
];
|
|
694
884
|
// ── Main entry point ─────────────────────────────────────────────────────────
|
|
695
885
|
/**
|
|
@@ -698,32 +888,91 @@ const STEP_NAMES = [
|
|
|
698
888
|
* Collects all configuration, then applies atomically.
|
|
699
889
|
*/
|
|
700
890
|
export async function runNetworkOnboarding() {
|
|
891
|
+
const saved = readSentinel();
|
|
892
|
+
const isReconfigure = saved !== null;
|
|
701
893
|
console.log('');
|
|
702
|
-
console.log(chalk.bold.cyan(
|
|
894
|
+
console.log(chalk.bold.cyan(isReconfigure
|
|
895
|
+
? ' Call Telemetry — Reconfigure'
|
|
896
|
+
: ' Call Telemetry — First-Boot Setup'));
|
|
703
897
|
console.log(chalk.dim(' ═══════════════════════════════════════'));
|
|
704
898
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
705
899
|
const ask = makeAsker(rl);
|
|
706
900
|
try {
|
|
707
|
-
// Initialize state
|
|
901
|
+
// Initialize state — restore from sentinel if reconfiguring
|
|
902
|
+
const prefs = loadPrefs();
|
|
708
903
|
const state = {
|
|
709
|
-
mode: 'dhcp',
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
904
|
+
mode: saved?.mode ?? 'dhcp',
|
|
905
|
+
ip: saved?.ip,
|
|
906
|
+
prefix: saved?.prefix,
|
|
907
|
+
gateway: saved?.gateway,
|
|
908
|
+
dns1: saved?.dns1 ?? DEFAULT_DNS1,
|
|
909
|
+
dns2: saved?.dns2 ?? DEFAULT_DNS2,
|
|
910
|
+
searchDomain: saved?.searchDomain ?? DEFAULT_SEARCH_DOMAIN,
|
|
911
|
+
iface: saved?.iface ?? detectIface(),
|
|
912
|
+
hostname: saved?.hostname ?? getHostname(),
|
|
913
|
+
location: saved?.location ?? '',
|
|
716
914
|
adminPassword: '',
|
|
717
|
-
timezone: getSystemTimezone(),
|
|
718
|
-
ntp1: DEFAULT_NTP[0],
|
|
719
|
-
ntp2: DEFAULT_NTP[1],
|
|
720
|
-
autoUpdate: true,
|
|
721
|
-
ipv6: false,
|
|
722
|
-
jtapi: false,
|
|
915
|
+
timezone: saved?.timezone ?? getSystemTimezone(),
|
|
916
|
+
ntp1: saved?.ntp1 ?? DEFAULT_NTP[0],
|
|
917
|
+
ntp2: saved?.ntp2 ?? DEFAULT_NTP[1],
|
|
918
|
+
autoUpdate: prefs?.autoUpdate ?? true,
|
|
919
|
+
ipv6: prefs?.ipv6 ?? false,
|
|
920
|
+
jtapi: prefs?.jtapi ?? false,
|
|
723
921
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
922
|
+
if (isReconfigure) {
|
|
923
|
+
// Show current config and let user pick what to change
|
|
924
|
+
console.log(renderSummary(state));
|
|
925
|
+
console.log(chalk.bold(' Select a section to reconfigure, or press Enter to exit:'));
|
|
926
|
+
console.log('');
|
|
927
|
+
for (let i = 0; i < STEP_NAMES.length; i++) {
|
|
928
|
+
console.log(chalk.dim(` ${i + 1} ${STEP_NAMES[i]}`));
|
|
929
|
+
}
|
|
930
|
+
console.log(chalk.dim(' A Reconfigure everything'));
|
|
931
|
+
console.log('');
|
|
932
|
+
const choice = (await ask(chalk.bold(' Choice [Enter=done]: '))).trim().toLowerCase();
|
|
933
|
+
if (choice === '') {
|
|
934
|
+
console.log(chalk.dim('\n No changes. Current configuration retained.\n'));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (choice === 'a') {
|
|
938
|
+
// Run all steps
|
|
939
|
+
for (const stepFn of STEP_FUNCTIONS) {
|
|
940
|
+
await stepFn(ask, state);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
const stepIdx = parseInt(choice, 10) - 1;
|
|
945
|
+
if (stepIdx >= 0 && stepIdx < STEP_FUNCTIONS.length) {
|
|
946
|
+
await STEP_FUNCTIONS[stepIdx](ask, state);
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
console.log(chalk.red(' ✗ Invalid choice'));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
// First boot — run all steps sequentially
|
|
956
|
+
for (const stepFn of STEP_FUNCTIONS) {
|
|
957
|
+
const result = await stepFn(ask, state);
|
|
958
|
+
// Step 1 returns 'skip' if user chose to skip setup entirely
|
|
959
|
+
if (result === 'skip') {
|
|
960
|
+
writeSentinel(state);
|
|
961
|
+
if (!loadPrefs()) {
|
|
962
|
+
savePrefs({
|
|
963
|
+
autoUpdate: true,
|
|
964
|
+
ipv6: false,
|
|
965
|
+
otel: false,
|
|
966
|
+
jtapi: false,
|
|
967
|
+
deploymentMode: 'docker-compose',
|
|
968
|
+
k8sNamespace: 'default',
|
|
969
|
+
k8sEnvironment: 'ct-prod',
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
console.log(chalk.green('\n ✓ Setup skipped. Run "ct setup" anytime to configure.\n'));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
727
976
|
}
|
|
728
977
|
// Show/edit/apply loop
|
|
729
978
|
while (true) {
|
|
@@ -742,7 +991,7 @@ export async function runNetworkOnboarding() {
|
|
|
742
991
|
console.log(chalk.dim(` ${i + 1} ${STEP_NAMES[i]}`));
|
|
743
992
|
}
|
|
744
993
|
console.log('');
|
|
745
|
-
const stepChoice = (await ask(chalk.bold(' Which step? [1-
|
|
994
|
+
const stepChoice = (await ask(chalk.bold(' Which step? [1-7]: '))).trim();
|
|
746
995
|
const stepIdx = parseInt(stepChoice, 10) - 1;
|
|
747
996
|
if (stepIdx >= 0 && stepIdx < STEP_FUNCTIONS.length) {
|
|
748
997
|
await STEP_FUNCTIONS[stepIdx](ask, state);
|
|
@@ -755,6 +1004,8 @@ export async function runNetworkOnboarding() {
|
|
|
755
1004
|
if (action === 'a' || action === '') {
|
|
756
1005
|
// Apply all
|
|
757
1006
|
const ok = await applyWizardState(state);
|
|
1007
|
+
// Save config to sentinel (no sensitive values)
|
|
1008
|
+
writeSentinel(state);
|
|
758
1009
|
if (ok) {
|
|
759
1010
|
console.log('');
|
|
760
1011
|
console.log(chalk.green(' ✓ All configuration applied successfully'));
|