@newrelic/preflight 1.0.6 → 1.0.8
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/README.md +6 -4
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -5
- package/dist/config.js.map +1 -1
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +3 -0
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +318 -76
- package/dist/install/cli.js.map +1 -1
- package/dist/install/diagnostics.d.ts.map +1 -1
- package/dist/install/diagnostics.js +224 -159
- package/dist/install/diagnostics.js.map +1 -1
- package/dist/install/json-utils.d.ts +1 -0
- package/dist/install/json-utils.d.ts.map +1 -1
- package/dist/install/json-utils.js +3 -0
- package/dist/install/json-utils.js.map +1 -1
- package/dist/install/schedule.d.ts +15 -2
- package/dist/install/schedule.d.ts.map +1 -1
- package/dist/install/schedule.js +122 -19
- package/dist/install/schedule.js.map +1 -1
- package/dist/install/setup-wizard.d.ts.map +1 -1
- package/dist/install/setup-wizard.js +4 -2
- package/dist/install/setup-wizard.js.map +1 -1
- package/package.json +1 -1
package/dist/install/cli.js
CHANGED
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
* Dynamically imported from collector-script.ts when argv[2] is install/uninstall,
|
|
5
5
|
* so commander and other heavy deps are never loaded on the hot hook path.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import { existsSync, copyFileSync, realpathSync } from 'node:fs';
|
|
9
|
-
import {
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { Command } from 'commander';
|
|
13
|
+
import { createLogger } from '../shared/index.js';
|
|
12
14
|
import { mergeSettings, removeSettings, mergeMcpConfig, removeMcpConfig, detectSettingsPath, detectMcpConfigPath, generateNrConfig, } from './install-helper.js';
|
|
13
15
|
import { isWsl, resolveWindowsHome } from './platform.js';
|
|
14
16
|
import { validateConfigFile, DEFAULT_STORAGE_PATH, ConfigFileSchema } from '../config.js';
|
|
15
17
|
import { migrateStoragePath } from './migrate.js';
|
|
16
18
|
import { installSchedule, removeSchedule, getScheduleStatus, removeDashboardDaemon, getDashboardDaemonStatus, resolveBinaryPath, } from './schedule.js';
|
|
17
|
-
import {
|
|
19
|
+
import { readJsonFileStrict, writeJsonFile, errMsg } from './json-utils.js';
|
|
20
|
+
const logger = createLogger('cli');
|
|
18
21
|
const NR_CONFIG_PATH = resolve(DEFAULT_STORAGE_PATH, 'config.json');
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
20
23
|
// Platform persistence helpers — read/write platformTarget in config.json
|
|
@@ -31,7 +34,7 @@ function clearSavedPlatform() {
|
|
|
31
34
|
writeJsonFile(NR_CONFIG_PATH, rest, DEFAULT_STORAGE_PATH);
|
|
32
35
|
}
|
|
33
36
|
catch (err) {
|
|
34
|
-
eprint(`\n⚠ Could not clear saved platform target: ${
|
|
37
|
+
eprint(`\n⚠ Could not clear saved platform target: ${errMsg(err)}`);
|
|
35
38
|
eprint(' The next install may use the stale platform target. Fix the issue and re-run uninstall.');
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -110,13 +113,7 @@ function eprint(msg = '') {
|
|
|
110
113
|
// PATH verification
|
|
111
114
|
// ---------------------------------------------------------------------------
|
|
112
115
|
export function verifyBinaryOnPath() {
|
|
113
|
-
|
|
114
|
-
execSync('which preflight', { stdio: 'pipe' });
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
116
|
+
return resolveBinaryPath() !== null;
|
|
120
117
|
}
|
|
121
118
|
function printPathWarning() {
|
|
122
119
|
print('\n⚠ preflight is not on your PATH.');
|
|
@@ -154,10 +151,54 @@ function handleUpdate() {
|
|
|
154
151
|
print('✗ Could not locate the repo root. Run this command from within the cloned repo or after npm link.');
|
|
155
152
|
process.exit(1);
|
|
156
153
|
}
|
|
154
|
+
let gitRoot;
|
|
155
|
+
try {
|
|
156
|
+
gitRoot = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--show-toplevel'], {
|
|
157
|
+
stdio: 'pipe',
|
|
158
|
+
env: { ...process.env, GIT_DIR: undefined, GIT_WORK_TREE: undefined },
|
|
159
|
+
})
|
|
160
|
+
.toString()
|
|
161
|
+
.trim();
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (err.code === 'ENOENT') {
|
|
165
|
+
print('✗ git is not installed or not found on PATH.');
|
|
166
|
+
print(' Install git (https://git-scm.com) then retry: preflight update');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
print('✗ preflight was installed via a package manager, not cloned from source.');
|
|
170
|
+
print(' (If your .git directory is missing or corrupt, re-clone the repo instead.)');
|
|
171
|
+
print(' To update, reinstall using your package manager, e.g.:');
|
|
172
|
+
print(' npm install -g @newrelic/preflight@latest');
|
|
173
|
+
print(' pnpm add -g @newrelic/preflight@latest');
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
// If repoRoot sits below a node_modules directory within the git tree,
|
|
178
|
+
// preflight is installed as a dependency — not a source clone.
|
|
179
|
+
// path.relative() normalises separators on all platforms (robust on Windows).
|
|
180
|
+
if (relative(gitRoot, repoRoot).split(sep).includes('node_modules')) {
|
|
181
|
+
print('✗ preflight was installed via a package manager, not cloned from source.');
|
|
182
|
+
print(' To update, reinstall using your package manager, e.g.:');
|
|
183
|
+
print(' npm install -g @newrelic/preflight@latest');
|
|
184
|
+
print(' pnpm add -g @newrelic/preflight@latest');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
157
187
|
print(`Updating Preflight from ${repoRoot}...\n`);
|
|
158
188
|
try {
|
|
159
189
|
print('→ git pull');
|
|
160
190
|
execFileSync('git', ['pull'], { cwd: repoRoot, stdio: 'inherit' });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
print('\n✗ git pull failed. Check the output above for details.');
|
|
194
|
+
print(' If the output shows diverged branches and you have no local commits to keep,');
|
|
195
|
+
print(' you can reset to the remote HEAD (replace <branch> with your default branch):');
|
|
196
|
+
print(` git -C "${repoRoot}" fetch origin`);
|
|
197
|
+
print(` git -C "${repoRoot}" reset --hard origin/<branch>`);
|
|
198
|
+
print(' WARNING: reset --hard permanently discards any local commits not yet on origin.');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
161
202
|
print('\n→ npm run build');
|
|
162
203
|
execFileSync('npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
|
|
163
204
|
print('\n✓ Update complete.');
|
|
@@ -165,7 +206,7 @@ function handleUpdate() {
|
|
|
165
206
|
print(' Run `preflight install` to update the MCP server key in ~/.mcp.json.');
|
|
166
207
|
}
|
|
167
208
|
catch {
|
|
168
|
-
print('\n✗
|
|
209
|
+
print('\n✗ Build failed. Check the output above for details.');
|
|
169
210
|
process.exit(1);
|
|
170
211
|
}
|
|
171
212
|
}
|
|
@@ -178,9 +219,14 @@ function handleSchedule(options) {
|
|
|
178
219
|
process.exit(1);
|
|
179
220
|
}
|
|
180
221
|
if (options.disable) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
222
|
+
try {
|
|
223
|
+
const removed = removeSchedule();
|
|
224
|
+
print(removed ? '✓ Auto-update schedule removed.' : 'No schedule was installed.');
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
eprint(`⚠ Could not remove schedule: ${errMsg(err)}`);
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
}
|
|
184
230
|
return;
|
|
185
231
|
}
|
|
186
232
|
if (options.time !== undefined) {
|
|
@@ -210,10 +256,15 @@ function handleSchedule(options) {
|
|
|
210
256
|
// No flags — show status.
|
|
211
257
|
const status = getScheduleStatus();
|
|
212
258
|
if (status.installed) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
259
|
+
if (status.readable === false) {
|
|
260
|
+
print('Auto-update schedule: installed (plist unreadable — reinstall with: preflight schedule --time HH:MM)');
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const hh = String(status.hour ?? 0).padStart(2, '0');
|
|
264
|
+
const mm = String(status.minute ?? 0).padStart(2, '0');
|
|
265
|
+
print(`Auto-update schedule: ${hh}:${mm} daily`);
|
|
266
|
+
print(` Binary: ${status.binaryPath ?? 'unknown'}`);
|
|
267
|
+
}
|
|
217
268
|
print(' To change: preflight schedule --time HH:MM');
|
|
218
269
|
print(' To remove: preflight schedule --disable');
|
|
219
270
|
}
|
|
@@ -248,13 +299,13 @@ function handleInstall(options) {
|
|
|
248
299
|
catch (err) {
|
|
249
300
|
const isSyntaxError = err instanceof SyntaxError;
|
|
250
301
|
if (isSyntaxError || (inWsl && !explicitPlatform) || credentialsProvided) {
|
|
251
|
-
eprint(`\n✗ Cannot read existing NR config to determine install target: ${
|
|
302
|
+
eprint(`\n✗ Cannot read existing NR config to determine install target: ${errMsg(err)}`);
|
|
252
303
|
eprint(isSyntaxError
|
|
253
304
|
? ' config.json contains invalid JSON — fix or delete it, then re-run install.'
|
|
254
305
|
: ' Fix file permissions then re-run install.');
|
|
255
306
|
throw err;
|
|
256
307
|
}
|
|
257
|
-
eprint(`\n⚠ Could not read existing NR config to persist platform target: ${
|
|
308
|
+
eprint(`\n⚠ Could not read existing NR config to persist platform target: ${errMsg(err)}`);
|
|
258
309
|
eprint(' Platform target not persisted — hook installation will continue. Re-run to save it.');
|
|
259
310
|
skipNrConfigWrite = true;
|
|
260
311
|
}
|
|
@@ -268,21 +319,21 @@ function handleInstall(options) {
|
|
|
268
319
|
mergedMcp = mergeMcpConfig(readJsonFileStrict(mcpPath), binPath, { platform });
|
|
269
320
|
}
|
|
270
321
|
catch (err) {
|
|
271
|
-
eprint(`\n✗ Failed to prepare config: ${
|
|
322
|
+
eprint(`\n✗ Failed to prepare config: ${errMsg(err)}`);
|
|
272
323
|
throw err;
|
|
273
324
|
}
|
|
274
325
|
try {
|
|
275
326
|
writeJsonFile(settingsPath, mergedSettings, allowedBase);
|
|
276
327
|
}
|
|
277
328
|
catch (err) {
|
|
278
|
-
eprint(`\n✗ Failed to write hooks config (${settingsPath}): ${
|
|
329
|
+
eprint(`\n✗ Failed to write hooks config (${settingsPath}): ${errMsg(err)}`);
|
|
279
330
|
throw err;
|
|
280
331
|
}
|
|
281
332
|
try {
|
|
282
333
|
writeJsonFile(mcpPath, mergedMcp, allowedBase);
|
|
283
334
|
}
|
|
284
335
|
catch (err) {
|
|
285
|
-
eprint(`\n✗ Failed to write MCP config (${mcpPath}): ${
|
|
336
|
+
eprint(`\n✗ Failed to write MCP config (${mcpPath}): ${errMsg(err)}`);
|
|
286
337
|
throw err;
|
|
287
338
|
}
|
|
288
339
|
// Persist platformTarget (and credentials if provided) — only after both hook files written.
|
|
@@ -298,10 +349,10 @@ function handleInstall(options) {
|
|
|
298
349
|
}
|
|
299
350
|
catch (err) {
|
|
300
351
|
if (credentialsProvided) {
|
|
301
|
-
eprint(`\n✗ Failed to save New Relic config: ${
|
|
352
|
+
eprint(`\n✗ Failed to save New Relic config: ${errMsg(err)}`);
|
|
302
353
|
throw err;
|
|
303
354
|
}
|
|
304
|
-
eprint(`\n⚠ Could not persist platform target: ${
|
|
355
|
+
eprint(`\n⚠ Could not persist platform target: ${errMsg(err)}`);
|
|
305
356
|
eprint(' The next install will re-detect the target platform from scratch.');
|
|
306
357
|
}
|
|
307
358
|
}
|
|
@@ -344,9 +395,12 @@ function handleInstall(options) {
|
|
|
344
395
|
print(' to check your config file for typos or unsupported fields.');
|
|
345
396
|
}
|
|
346
397
|
// ---------------------------------------------------------------------------
|
|
347
|
-
// Uninstall
|
|
398
|
+
// Uninstall helpers
|
|
348
399
|
// ---------------------------------------------------------------------------
|
|
349
|
-
|
|
400
|
+
// Resolves which settings and MCP config paths to clean based on platform
|
|
401
|
+
// flags and the saved install target. Calls process.exit(1) on invalid
|
|
402
|
+
// flag combinations or unresolvable state.
|
|
403
|
+
function resolveUninstallPaths(options) {
|
|
350
404
|
const scope = options.project ? 'project' : 'user';
|
|
351
405
|
if (options.windowsCc && options.linuxCc) {
|
|
352
406
|
print('\n ⚠ --windows-cc and --linux-cc are mutually exclusive. Pass only one.');
|
|
@@ -448,68 +502,255 @@ function handleUninstall(options) {
|
|
|
448
502
|
settingsPathsToClean.set(detectSettingsPath(scope, null), undefined);
|
|
449
503
|
mcpPathsToClean.set(detectMcpConfigPath(scope, null), undefined);
|
|
450
504
|
}
|
|
451
|
-
|
|
452
|
-
|
|
505
|
+
return { settingsPathsToClean, mcpPathsToClean };
|
|
506
|
+
}
|
|
507
|
+
// Backs up a config file then writes the transformed result. Returns whether
|
|
508
|
+
// the write succeeded and any error encountered — never throws.
|
|
509
|
+
function backupAndWrite(path, allowedBase, transform, errorLabel) {
|
|
510
|
+
const backup = `${path}.backup-${Date.now()}`;
|
|
511
|
+
try {
|
|
512
|
+
const data = readJsonFileStrict(path);
|
|
513
|
+
copyFileSync(path, backup);
|
|
514
|
+
print(` Backup saved: ${backup}`);
|
|
515
|
+
writeJsonFile(path, transform(data), allowedBase);
|
|
516
|
+
return { written: true, error: null };
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
// If copyFileSync and writeJsonFile both succeeded the backup is no longer
|
|
520
|
+
// needed (the write path handles it). If copyFileSync succeeded but
|
|
521
|
+
// writeJsonFile failed, the backup is the user's only recovery copy —
|
|
522
|
+
// preserve it. If readJsonFileStrict failed, no backup was created and the
|
|
523
|
+
// original is untouched, so there is nothing to recover.
|
|
524
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
525
|
+
eprint(`\n✗ Failed to clean ${errorLabel} (${path}): ${errMsg(err)}`);
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
return { written: false, error };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Removes preflight hooks and MCP config entries from the given paths.
|
|
531
|
+
// Returns a RemovalResult — never throws. Partial completion is possible:
|
|
532
|
+
// removed=true even when error is non-null if at least one file succeeded.
|
|
533
|
+
function removeClaudeCodeConfig(settingsPathsToClean, mcpPathsToClean) {
|
|
534
|
+
let removed = false;
|
|
535
|
+
let firstError = null;
|
|
536
|
+
let hadSettingsFile = false;
|
|
453
537
|
for (const [settingsPath, allowedBase] of settingsPathsToClean) {
|
|
454
|
-
if (existsSync(settingsPath))
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
eprint(`\n✗ Failed to clean hooks config (${settingsPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
settingsFound = true;
|
|
538
|
+
if (!existsSync(settingsPath))
|
|
539
|
+
continue;
|
|
540
|
+
hadSettingsFile = true;
|
|
541
|
+
const { written, error } = backupAndWrite(settingsPath, allowedBase, removeSettings, 'hooks config');
|
|
542
|
+
if (written) {
|
|
543
|
+
removed = true;
|
|
466
544
|
print(`✓ Hooks removed: ${settingsPath}`);
|
|
467
545
|
}
|
|
546
|
+
else {
|
|
547
|
+
firstError ??= error;
|
|
548
|
+
}
|
|
468
549
|
}
|
|
469
|
-
if (!
|
|
550
|
+
if (!hadSettingsFile) {
|
|
470
551
|
print(`No settings file found at ${[...settingsPathsToClean.keys()].join(', ')}. Skipping hooks.`);
|
|
471
552
|
}
|
|
472
|
-
let
|
|
553
|
+
let hadMcpFile = false;
|
|
473
554
|
for (const [mcpPath, allowedBase] of mcpPathsToClean) {
|
|
474
|
-
if (existsSync(mcpPath))
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
catch (err) {
|
|
482
|
-
eprint(`\n✗ Failed to clean MCP config (${mcpPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
483
|
-
throw err;
|
|
484
|
-
}
|
|
485
|
-
mcpFound = true;
|
|
555
|
+
if (!existsSync(mcpPath))
|
|
556
|
+
continue;
|
|
557
|
+
hadMcpFile = true;
|
|
558
|
+
const { written, error } = backupAndWrite(mcpPath, allowedBase, removeMcpConfig, 'MCP config');
|
|
559
|
+
if (written) {
|
|
560
|
+
removed = true;
|
|
486
561
|
print(`✓ MCP server removed: ${mcpPath}`);
|
|
487
562
|
}
|
|
563
|
+
else {
|
|
564
|
+
firstError ??= error;
|
|
565
|
+
}
|
|
488
566
|
}
|
|
489
|
-
if (!
|
|
567
|
+
if (!hadMcpFile) {
|
|
490
568
|
print(`No MCP config found at ${[...mcpPathsToClean.keys()].join(', ')}. Skipping MCP server.`);
|
|
491
569
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
570
|
+
return { label: 'Claude Code config', removed, error: firstError, requiresRestart: true };
|
|
571
|
+
}
|
|
572
|
+
// Runs a single uninstall step. Returns a RemovalResult — never throws.
|
|
573
|
+
// process.exitCode is set to 1 and a warning is printed on error.
|
|
574
|
+
function runStep(label, requiresRestart, fn) {
|
|
575
|
+
try {
|
|
576
|
+
const removed = fn();
|
|
577
|
+
return { label, removed, error: null, requiresRestart };
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
582
|
+
logger.warn('uninstall step failed', { label, error });
|
|
583
|
+
eprint(`⚠ Could not remove ${label}: ${error.message}`);
|
|
584
|
+
return { label, removed: false, error, requiresRestart };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Prompts the user for uninstall confirmation. Returns one of four outcomes.
|
|
588
|
+
async function promptConfirm(yes) {
|
|
589
|
+
if (yes)
|
|
590
|
+
return 'confirmed';
|
|
591
|
+
if (process.stdin.isTTY !== true)
|
|
592
|
+
return 'non-interactive';
|
|
593
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
594
|
+
try {
|
|
595
|
+
const answer = (await rl.question('Continue? [y/N]: ')).trim().toLowerCase();
|
|
596
|
+
return answer === 'y' || answer === 'yes' ? 'confirmed' : 'declined';
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return 'stdin-closed';
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
rl.close();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Handles a promptConfirm result: prints cancellation message and sets
|
|
606
|
+
// exitCode=1 on any non-confirmed outcome. Returns true only when confirmed.
|
|
607
|
+
function handleConfirm(result) {
|
|
608
|
+
if (result === 'confirmed')
|
|
609
|
+
return true;
|
|
610
|
+
const message = result === 'non-interactive'
|
|
611
|
+
? 'Uninstall cancelled (non-interactive stdin — rerun with --yes to confirm).'
|
|
612
|
+
: result === 'stdin-closed'
|
|
613
|
+
? 'Uninstall cancelled (stdin closed).'
|
|
614
|
+
: 'Uninstall cancelled.';
|
|
615
|
+
print(message);
|
|
616
|
+
process.exitCode = 1;
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Uninstall handler
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
async function handleUninstall(options) {
|
|
623
|
+
// --daemon: targeted removal of just the background dashboard daemon plist.
|
|
624
|
+
// Does not touch hooks, MCP config, schedules, or session history.
|
|
625
|
+
if (options.daemon) {
|
|
626
|
+
if (options.project || options.windowsCc || options.linuxCc) {
|
|
627
|
+
print(' ⚠ --daemon cannot be combined with --project, --windows-cc, or --linux-cc.');
|
|
628
|
+
print(' Use bare `preflight uninstall` to remove hooks and the daemon together.');
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
const daemonStatus = getDashboardDaemonStatus();
|
|
632
|
+
if (!daemonStatus.installed) {
|
|
633
|
+
print('No background dashboard daemon installed — nothing to remove.');
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
print('preflight uninstall --daemon will remove the background dashboard daemon.\n');
|
|
637
|
+
if (!handleConfirm(await promptConfirm(options.yes ?? false)))
|
|
638
|
+
return;
|
|
639
|
+
const step = runStep('background dashboard daemon', false, () => removeDashboardDaemon());
|
|
640
|
+
if (step.error !== null) {
|
|
641
|
+
print('\nUninstall incomplete — see errors above.\n');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!step.removed) {
|
|
645
|
+
process.exitCode = 1;
|
|
646
|
+
print('Background dashboard daemon already absent — nothing to remove.');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
print('✓ Background dashboard daemon removed.');
|
|
650
|
+
print(' The dashboard is now only available while Claude Code is running.');
|
|
651
|
+
print(' To reinstall, run: preflight setup');
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const { settingsPathsToClean, mcpPathsToClean } = resolveUninstallPaths(options);
|
|
655
|
+
// Build a human-readable summary of what will change, then ask for
|
|
656
|
+
// confirmation before touching anything.
|
|
657
|
+
const changeSummary = [];
|
|
658
|
+
let hadConfigFiles = false;
|
|
659
|
+
for (const settingsPath of settingsPathsToClean.keys()) {
|
|
660
|
+
if (existsSync(settingsPath)) {
|
|
661
|
+
changeSummary.push(` • Remove hooks from ${settingsPath}`);
|
|
662
|
+
hadConfigFiles = true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const mcpPath of mcpPathsToClean.keys()) {
|
|
666
|
+
if (existsSync(mcpPath)) {
|
|
667
|
+
changeSummary.push(` • Remove MCP server from ${mcpPath}`);
|
|
668
|
+
hadConfigFiles = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const scheduleStatus = getScheduleStatus();
|
|
672
|
+
const daemonStatus = getDashboardDaemonStatus();
|
|
673
|
+
if (scheduleStatus.installed) {
|
|
674
|
+
const action = scheduleStatus.readable
|
|
675
|
+
? 'Unload and delete'
|
|
676
|
+
: 'Remove (plist unreadable — label removal)';
|
|
677
|
+
changeSummary.push(` • ${action} auto-update schedule`);
|
|
678
|
+
}
|
|
679
|
+
if (daemonStatus.installed) {
|
|
680
|
+
const action = daemonStatus.readable
|
|
681
|
+
? 'Unload and delete'
|
|
682
|
+
: 'Remove (plist unreadable — label removal)';
|
|
683
|
+
changeSummary.push(` • ${action} background dashboard daemon`);
|
|
684
|
+
}
|
|
685
|
+
if (changeSummary.length === 0) {
|
|
686
|
+
print('Nothing installed — no changes to make.');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
print('preflight uninstall will make the following changes:\n');
|
|
690
|
+
for (const line of changeSummary)
|
|
691
|
+
print(line);
|
|
692
|
+
print('');
|
|
693
|
+
print(` Your session history and config at ${DEFAULT_STORAGE_PATH} will NOT be deleted.`);
|
|
694
|
+
print('');
|
|
695
|
+
if (!handleConfirm(await promptConfirm(options.yes ?? false)))
|
|
696
|
+
return;
|
|
697
|
+
print('');
|
|
698
|
+
const configStep = removeClaudeCodeConfig(settingsPathsToClean, mcpPathsToClean);
|
|
498
699
|
if (options.windowsCc) {
|
|
499
700
|
print(' To reinstall Windows Claude Code mode, re-run: preflight install --windows-cc');
|
|
500
701
|
}
|
|
501
|
-
|
|
702
|
+
const scheduleStep = runStep('auto-update schedule', false, () => {
|
|
703
|
+
const removed = removeSchedule();
|
|
704
|
+
if (removed)
|
|
705
|
+
print('✓ Auto-update schedule removed');
|
|
706
|
+
else if (scheduleStatus.installed) {
|
|
707
|
+
// Plist vanished between status-check and removal (TOCTOU). Throw so
|
|
708
|
+
// runStep captures this as an error and anyFailed reflects it.
|
|
709
|
+
throw new Error('Auto-update schedule already absent — may have been removed by another process');
|
|
710
|
+
}
|
|
711
|
+
return removed;
|
|
712
|
+
});
|
|
713
|
+
const daemonStep = runStep('background dashboard daemon', false, () => {
|
|
714
|
+
const removed = removeDashboardDaemon();
|
|
715
|
+
if (removed)
|
|
716
|
+
print('✓ Background dashboard daemon removed');
|
|
717
|
+
else if (daemonStatus.installed) {
|
|
718
|
+
// Plist vanished between status-check and removal (TOCTOU). Throw so
|
|
719
|
+
// runStep captures this as an error and anyFailed reflects it — without
|
|
720
|
+
// this, process.exitCode=1 and the success message are contradictory.
|
|
721
|
+
throw new Error('Background dashboard daemon already absent — may have been removed by another process');
|
|
722
|
+
}
|
|
723
|
+
return removed;
|
|
724
|
+
});
|
|
725
|
+
const steps = [configStep, scheduleStep, daemonStep];
|
|
726
|
+
const anyRemoved = steps.some((s) => s.removed);
|
|
727
|
+
const anyFailed = steps.some((s) => s.error !== null);
|
|
728
|
+
const requiresRestart = steps.some((s) => s.removed && s.requiresRestart);
|
|
729
|
+
// Clear the saved platform record when hooks/MCP config was removed (even if
|
|
730
|
+
// schedule/daemon cleanup also failed — those are independent). Also clear on
|
|
731
|
+
// schedule/daemon-only removal when config files existed on disk at status-check
|
|
732
|
+
// time: absent config files mean hooks were never written (or already manually
|
|
733
|
+
// deleted) and the platform record should be preserved for a re-install attempt.
|
|
734
|
+
// Skip on --linux-cc (sibling wsl-windows-cc record may still be in use).
|
|
735
|
+
const shouldClearPlatform = configStep.removed || (anyRemoved && !anyFailed && hadConfigFiles);
|
|
736
|
+
if (!options.linuxCc && shouldClearPlatform) {
|
|
502
737
|
clearSavedPlatform();
|
|
503
738
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
739
|
+
if (requiresRestart) {
|
|
740
|
+
if (anyFailed) {
|
|
741
|
+
print('\nRestart Claude Code to apply hook changes. Uninstall incomplete — see errors above.\n');
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
print('\nRestart Claude Code for changes to take effect.\n');
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (anyRemoved) {
|
|
748
|
+
// Schedule/daemon only — launchd unloaded immediately; no restart needed.
|
|
749
|
+
print(anyFailed ? '\nUninstall incomplete — see errors above.\n' : '\nUninstall complete.\n');
|
|
750
|
+
}
|
|
751
|
+
else if (anyFailed) {
|
|
752
|
+
print('\nUninstall incomplete — see errors above.\n');
|
|
753
|
+
}
|
|
513
754
|
}
|
|
514
755
|
// ---------------------------------------------------------------------------
|
|
515
756
|
// Validate handler
|
|
@@ -556,8 +797,7 @@ function handleValidate(options) {
|
|
|
556
797
|
async function handleDoctor(options) {
|
|
557
798
|
const { runDiagnostics } = await import('./diagnostics.js');
|
|
558
799
|
const configPath = options.config ?? resolve(DEFAULT_STORAGE_PATH, 'config.json');
|
|
559
|
-
const
|
|
560
|
-
const storagePath = typeof raw.storagePath === 'string' ? raw.storagePath : undefined;
|
|
800
|
+
const storagePath = process.env.NEW_RELIC_AI_MCP_STORAGE_PATH ?? undefined;
|
|
561
801
|
print('Running diagnostics...');
|
|
562
802
|
const checks = await runDiagnostics({ configPath, storagePath });
|
|
563
803
|
const ICON = { ok: '✓', warn: '⚠', fail: '✗', skip: '-' };
|
|
@@ -605,6 +845,8 @@ export function createInstallProgram() {
|
|
|
605
845
|
.option('--project', 'Remove from project-level .claude/settings.json instead of user-level')
|
|
606
846
|
.option('--windows-cc', 'Remove Windows Claude Code hooks only (WSL only)')
|
|
607
847
|
.option('--linux-cc', 'Remove Linux Claude Code hooks only (WSL only)')
|
|
848
|
+
.option('--daemon', 'Remove only the background dashboard daemon (preserves hooks, MCP config, and session history)')
|
|
849
|
+
.option('--yes', 'Skip the confirmation prompt (useful for scripts and CI)')
|
|
608
850
|
.action(handleUninstall);
|
|
609
851
|
program
|
|
610
852
|
.command('setup')
|
|
@@ -615,7 +857,7 @@ export function createInstallProgram() {
|
|
|
615
857
|
await runSetupWizard();
|
|
616
858
|
}
|
|
617
859
|
catch (err) {
|
|
618
|
-
print(`\n✗ Setup failed: ${
|
|
860
|
+
print(`\n✗ Setup failed: ${errMsg(err)}`);
|
|
619
861
|
process.exitCode = 1;
|
|
620
862
|
}
|
|
621
863
|
});
|