@openagents-org/agent-launcher 0.2.168 → 0.2.170
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/icons/deepseek.svg +1 -0
- package/package.json +2 -2
- package/registry.json +93 -2
- package/src/adapters/deepseek-runtime.js +634 -0
- package/src/adapters/deepseek.js +1190 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/utils.js +24 -4
- package/src/adapters/workspace-prompt.js +63 -0
- package/src/daemon.js +21 -6
- package/src/install-preflight.js +233 -0
- package/src/installer.js +199 -12
- package/src/node-shims.js +103 -0
package/src/installer.js
CHANGED
|
@@ -8,10 +8,32 @@ const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, a
|
|
|
8
8
|
const { EnvManager } = require('./env');
|
|
9
9
|
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
10
10
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
11
|
+
const { checkInstallPrereqs, missingPrereqError } = require('./install-preflight');
|
|
12
|
+
const { detectShadowedNodeShims, shadowedNodeWarning } = require('./node-shims');
|
|
11
13
|
|
|
12
14
|
const STATUS_CACHE_TTL_MS = 10000;
|
|
13
15
|
const statusCache = new Map();
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* How long a streaming install may produce NO output before it is treated as
|
|
19
|
+
* hung and killed. Generous on purpose: a slow npm postinstall or a large
|
|
20
|
+
* extraction can legitimately go quiet for a while, and killing a working
|
|
21
|
+
* install is worse than waiting. Set OPENAGENTS_INSTALL_STALL_MS=0 to disable.
|
|
22
|
+
*/
|
|
23
|
+
const INSTALL_STALL_MS = (() => {
|
|
24
|
+
const raw = process.env.OPENAGENTS_INSTALL_STALL_MS;
|
|
25
|
+
if (raw === undefined || raw === '') return 10 * 60 * 1000;
|
|
26
|
+
const n = Number(raw);
|
|
27
|
+
return Number.isFinite(n) && n >= 0 ? n : 10 * 60 * 1000;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How often the stall watchdog checks. Capped at half the stall budget so the
|
|
32
|
+
* poll granularity never dominates the deadline (matters for a short budget in
|
|
33
|
+
* tests; in production the 15s ceiling applies).
|
|
34
|
+
*/
|
|
35
|
+
const STALL_POLL_MS = Math.max(50, Math.min(15000, Math.floor(INSTALL_STALL_MS / 2) || 15000));
|
|
36
|
+
|
|
15
37
|
// Version detection is comparatively expensive (spawns `<bin> --version`), so
|
|
16
38
|
// results are cached briefly and invalidated on install/uninstall.
|
|
17
39
|
const VERSION_CACHE_TTL_MS = 60000;
|
|
@@ -609,7 +631,12 @@ class Installer {
|
|
|
609
631
|
env: getEnhancedEnv(),
|
|
610
632
|
timeout: 10000,
|
|
611
633
|
}).trim();
|
|
612
|
-
|
|
634
|
+
// The optional `-prerelease` tail is NOT decoration. An agent pinned to a
|
|
635
|
+
// preview release (`@deepseek-ai/dsh@0.1.0-rc.6`) is gated on the exact
|
|
636
|
+
// version, and dropping the suffix reported a correctly installed rc.6 as
|
|
637
|
+
// plain `0.1.0` — which then failed its own gate. Agents without a
|
|
638
|
+
// prerelease segment match exactly as before.
|
|
639
|
+
const match = raw.match(/(\d+[\d.]+\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
613
640
|
version = match ? match[1] : (raw.split('\n')[0] || null);
|
|
614
641
|
} catch {}
|
|
615
642
|
|
|
@@ -618,14 +645,52 @@ class Installer {
|
|
|
618
645
|
}
|
|
619
646
|
|
|
620
647
|
/**
|
|
621
|
-
* Evaluate
|
|
648
|
+
* Evaluate a registry version gate against a detected version. Generic for any
|
|
622
649
|
* agent. Returns { compatible: true|false|null, minVersion, message }.
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
650
|
+
*
|
|
651
|
+
* Two gate shapes, in priority order:
|
|
652
|
+
*
|
|
653
|
+
* `install.supported_version` — EXACT match. For agents pinned to an
|
|
654
|
+
* upstream preview whose configuration can change between releases, a floor
|
|
655
|
+
* is the wrong shape: it happily admits the next preview, which is precisely
|
|
656
|
+
* the version the adapter has not been written against. Declared by
|
|
657
|
+
* deepseek (dsh is a developer preview).
|
|
658
|
+
*
|
|
659
|
+
* `install.min_version` / `check_ready.min_version` — a floor (>=). The
|
|
660
|
+
* original behaviour, unchanged for every agent that does not declare
|
|
661
|
+
* supported_version.
|
|
627
662
|
*/
|
|
628
663
|
_evaluateCompatibility(entry, version) {
|
|
664
|
+
const supported = (entry && entry.install && entry.install.supported_version) || null;
|
|
665
|
+
if (supported) {
|
|
666
|
+
// Compare the version-shaped token from each side, prerelease included.
|
|
667
|
+
// `compareVersions` cannot be used: it strips `-rc.N`, so rc.5 and rc.6
|
|
668
|
+
// would compare equal and the exact gate would admit the wrong preview.
|
|
669
|
+
const norm = (v) => {
|
|
670
|
+
const m = String(v == null ? '' : v).match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
671
|
+
return m ? m[0] : null;
|
|
672
|
+
};
|
|
673
|
+
const got = norm(version);
|
|
674
|
+
const want = norm(supported);
|
|
675
|
+
if (!got) {
|
|
676
|
+
return {
|
|
677
|
+
compatible: null,
|
|
678
|
+
minVersion: supported,
|
|
679
|
+
message: `Version unknown (requires exactly ${supported})`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (got !== want) {
|
|
683
|
+
return {
|
|
684
|
+
compatible: false,
|
|
685
|
+
minVersion: supported,
|
|
686
|
+
message:
|
|
687
|
+
`${(entry.label || entry.name || 'This agent')} ${got} is not supported — ` +
|
|
688
|
+
`it is pinned to ${want} exactly (upstream ships it as a developer preview).`,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return { compatible: true, minVersion: supported, message: null };
|
|
692
|
+
}
|
|
693
|
+
|
|
629
694
|
// Opt-in floor declared under either install.min_version or
|
|
630
695
|
// check_ready.min_version (e.g. cline pins check_ready.min_version).
|
|
631
696
|
const minVersion = (entry && (
|
|
@@ -825,6 +890,17 @@ class Installer {
|
|
|
825
890
|
}
|
|
826
891
|
return 'absent';
|
|
827
892
|
}
|
|
893
|
+
// A named field, for a file that exists in both states. Gemini's
|
|
894
|
+
// google_accounts.json is written on install and records a signed-OUT
|
|
895
|
+
// account as `active: null`, so its existence proves nothing — only the
|
|
896
|
+
// field does. Still content-free: the value is tested for emptiness, never
|
|
897
|
+
// read out, logged or returned.
|
|
898
|
+
if (checkReady.creds_key) {
|
|
899
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
900
|
+
const value = parsed && typeof parsed === 'object' ? parsed[checkReady.creds_key] : null;
|
|
901
|
+
const filled = typeof value === 'string' ? !!value.trim() : !!value;
|
|
902
|
+
return filled ? 'present' : 'absent';
|
|
903
|
+
}
|
|
828
904
|
return st.size > 0 ? 'present' : 'absent';
|
|
829
905
|
} catch {
|
|
830
906
|
return 'unreadable';
|
|
@@ -922,6 +998,8 @@ class Installer {
|
|
|
922
998
|
return { success: true, output: `${entry.label || agentType} uses direct API mode; no binary install needed.` };
|
|
923
999
|
}
|
|
924
1000
|
|
|
1001
|
+
this._assertPrereqs(agentType, entry);
|
|
1002
|
+
|
|
925
1003
|
let cmd = this._getInstallCommand(entry.install);
|
|
926
1004
|
if (!cmd) {
|
|
927
1005
|
throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
|
|
@@ -991,6 +1069,8 @@ class Installer {
|
|
|
991
1069
|
return { success: true, command: 'api-only' };
|
|
992
1070
|
}
|
|
993
1071
|
|
|
1072
|
+
this._assertPrereqs(agentType, entry, onData);
|
|
1073
|
+
|
|
994
1074
|
let rawCmd = this._getInstallCommand(entry.install);
|
|
995
1075
|
if (!rawCmd) {
|
|
996
1076
|
throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
|
|
@@ -1073,14 +1153,30 @@ class Installer {
|
|
|
1073
1153
|
|
|
1074
1154
|
return new Promise((resolve, reject) => {
|
|
1075
1155
|
// windowsHide stops a console window from flashing up on every install.
|
|
1076
|
-
|
|
1156
|
+
//
|
|
1157
|
+
// detached (Unix): puts the installer in its own process group, which is
|
|
1158
|
+
// what makes the stall watchdog able to stop the WHOLE tree. Without it
|
|
1159
|
+
// the direct child is a shell and the actual work — curl, git, uv — runs
|
|
1160
|
+
// in grandchildren that survive a signal aimed at the shell. It does not
|
|
1161
|
+
// change lifetime semantics: an install was never killed by the launcher
|
|
1162
|
+
// exiting, before or after this.
|
|
1163
|
+
const proc = spawn(spawnFile, spawnArgs, {
|
|
1164
|
+
shell: useShell,
|
|
1165
|
+
env,
|
|
1166
|
+
cwd: installCwd,
|
|
1167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1168
|
+
windowsHide: true,
|
|
1169
|
+
detached: process.platform !== 'win32',
|
|
1170
|
+
});
|
|
1077
1171
|
|
|
1078
1172
|
if (proc.stdout) proc.stdout.setEncoding('utf-8');
|
|
1079
1173
|
if (proc.stderr) proc.stderr.setEncoding('utf-8');
|
|
1080
1174
|
|
|
1081
1175
|
let outputTail = '';
|
|
1176
|
+
let lastOutputAt = Date.now();
|
|
1082
1177
|
const captureOutput = (d) => {
|
|
1083
1178
|
const text = String(d);
|
|
1179
|
+
lastOutputAt = Date.now();
|
|
1084
1180
|
outputTail = (outputTail + text).slice(-4000);
|
|
1085
1181
|
if (onData) onData(text);
|
|
1086
1182
|
};
|
|
@@ -1088,8 +1184,40 @@ class Installer {
|
|
|
1088
1184
|
if (proc.stdout) proc.stdout.on('data', captureOutput);
|
|
1089
1185
|
if (proc.stderr) proc.stderr.on('data', captureOutput);
|
|
1090
1186
|
|
|
1091
|
-
|
|
1187
|
+
// Stall watchdog. A third-party installer that decides to wait — hermes's
|
|
1188
|
+
// install.sh polls for up to 900s after asking macOS for the developer
|
|
1189
|
+
// tools — otherwise hangs this promise indefinitely, and the only visible
|
|
1190
|
+
// symptom is a progress bar that stopped moving. The preflight now
|
|
1191
|
+
// prevents that specific case, but the general shape (a download that
|
|
1192
|
+
// never returns, a prompt on a stdin nobody is reading) is not specific
|
|
1193
|
+
// to any one installer, so time is bounded here for all of them.
|
|
1194
|
+
let stallTimer = null;
|
|
1195
|
+
let stalled = false;
|
|
1196
|
+
const clearStallTimer = () => {
|
|
1197
|
+
if (stallTimer) { clearInterval(stallTimer); stallTimer = null; }
|
|
1198
|
+
};
|
|
1199
|
+
if (INSTALL_STALL_MS > 0) {
|
|
1200
|
+
stallTimer = setInterval(() => {
|
|
1201
|
+
if (Date.now() - lastOutputAt < INSTALL_STALL_MS) return;
|
|
1202
|
+
stalled = true;
|
|
1203
|
+
clearStallTimer();
|
|
1204
|
+
const minutes = Math.round(INSTALL_STALL_MS / 60000);
|
|
1205
|
+
const msg = `Install stalled: no output for ${minutes} minutes. `
|
|
1206
|
+
+ 'The installer was stopped. It may be waiting on a prompt, or on a '
|
|
1207
|
+
+ 'download that never returns.';
|
|
1208
|
+
if (onData) onData(`\n${msg}\n`);
|
|
1209
|
+
this._killProcessTree(proc);
|
|
1210
|
+
reject(new Error(msg));
|
|
1211
|
+
}, STALL_POLL_MS);
|
|
1212
|
+
// Never hold the event loop open for the watchdog alone.
|
|
1213
|
+
if (typeof stallTimer.unref === 'function') stallTimer.unref();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
proc.on('error', (err) => { clearStallTimer(); reject(err); });
|
|
1092
1217
|
proc.on('close', (code) => {
|
|
1218
|
+
clearStallTimer();
|
|
1219
|
+
// The watchdog already rejected; this close is the kill it performed.
|
|
1220
|
+
if (stalled) return;
|
|
1093
1221
|
if (code === 0) {
|
|
1094
1222
|
// Aider-only: confirm a real, genuine binary exists before recording
|
|
1095
1223
|
// the install (verify-before-mark; never writes a marker on
|
|
@@ -1127,6 +1255,13 @@ class Installer {
|
|
|
1127
1255
|
return;
|
|
1128
1256
|
}
|
|
1129
1257
|
if (onData) onData(`\nHermes CLI resolved: ${hermes.path}\n`);
|
|
1258
|
+
// Its installer may have repointed the machine's node/npm/npx at
|
|
1259
|
+
// its own Node. See node-shims.js — we report, never rewrite.
|
|
1260
|
+
const shimWarning = shadowedNodeWarning(
|
|
1261
|
+
entry.label || agentType,
|
|
1262
|
+
detectShadowedNodeShims(),
|
|
1263
|
+
);
|
|
1264
|
+
if (shimWarning && onData) onData(`${shimWarning}\n`);
|
|
1130
1265
|
}
|
|
1131
1266
|
// Cursor-only: same failure shape as hermes above — its installer
|
|
1132
1267
|
// exits 0 after a failed download. See _verifyCursorBinary.
|
|
@@ -1307,6 +1442,51 @@ class Installer {
|
|
|
1307
1442
|
});
|
|
1308
1443
|
}
|
|
1309
1444
|
|
|
1445
|
+
/**
|
|
1446
|
+
* Refuse to run a third-party installer whose hard dependencies are missing.
|
|
1447
|
+
*
|
|
1448
|
+
* See install-preflight.js for why this is worth doing before the spawn: the
|
|
1449
|
+
* scripts we shell out to handle a missing dependency by opening OS dialogs
|
|
1450
|
+
* and sleeping for minutes, which the launcher can only render as a frozen
|
|
1451
|
+
* progress bar.
|
|
1452
|
+
*/
|
|
1453
|
+
_assertPrereqs(agentType, entry, onData, check = checkInstallPrereqs) {
|
|
1454
|
+
const { ok, missing } = check(entry);
|
|
1455
|
+
if (ok) return;
|
|
1456
|
+
const err = missingPrereqError(entry.label || agentType, missing);
|
|
1457
|
+
if (onData) onData(`${err.message}\n`);
|
|
1458
|
+
throw err;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Stop a spawned installer and everything it started.
|
|
1463
|
+
*
|
|
1464
|
+
* The child is usually a shell (`curl … | bash`, powershell) whose real work
|
|
1465
|
+
* happens in grandchildren, so signalling the direct child alone leaves the
|
|
1466
|
+
* download running. On Unix the process group is signalled; on Windows
|
|
1467
|
+
* taskkill /T walks the tree.
|
|
1468
|
+
*/
|
|
1469
|
+
_killProcessTree(proc) {
|
|
1470
|
+
if (!proc || proc.exitCode !== null) return;
|
|
1471
|
+
try {
|
|
1472
|
+
if (process.platform === 'win32') {
|
|
1473
|
+
try {
|
|
1474
|
+
execSync(`taskkill /F /T /PID ${proc.pid}`, {
|
|
1475
|
+
stdio: 'ignore', timeout: 5000, windowsHide: true,
|
|
1476
|
+
});
|
|
1477
|
+
} catch { proc.kill(); }
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch { proc.kill('SIGTERM'); }
|
|
1481
|
+
setTimeout(() => {
|
|
1482
|
+
if (proc.exitCode !== null) return;
|
|
1483
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch { proc.kill('SIGKILL'); }
|
|
1484
|
+
}, 5000).unref();
|
|
1485
|
+
} catch {
|
|
1486
|
+
/* the process is already gone */
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1310
1490
|
/**
|
|
1311
1491
|
* Get install command for current platform.
|
|
1312
1492
|
*/
|
|
@@ -1326,11 +1506,18 @@ class Installer {
|
|
|
1326
1506
|
// npm install -g <pkg> → npm uninstall --prefix <runtimeDir> <pkg>
|
|
1327
1507
|
if (installCmd.includes('npm install')) {
|
|
1328
1508
|
const prefixDir = agentType ? getRuntimePrefix(agentType) : path.join(os.homedir(), '.openagents', 'nodejs');
|
|
1329
|
-
|
|
1509
|
+
const cmd = installCmd
|
|
1330
1510
|
.replace('npm install -g', `npm uninstall --prefix "${prefixDir}"`)
|
|
1331
|
-
.replace('npm install', 'npm uninstall')
|
|
1332
|
-
|
|
1333
|
-
|
|
1511
|
+
.replace('npm install', 'npm uninstall');
|
|
1512
|
+
// Drop the version spec, whatever shape it takes. This used to be two
|
|
1513
|
+
// blind global replaces — `@latest` and `@[\d.]+` — and the second one
|
|
1514
|
+
// understood only digits and dots, so a PRERELEASE pin was cut in half:
|
|
1515
|
+
// @deepseek-ai/dsh@0.1.0-rc.6 → @deepseek-ai/dsh-rc.6
|
|
1516
|
+
// npm exits 0 when asked to remove a package that is not installed, so
|
|
1517
|
+
// the uninstall reported success, removed nothing, and left the agent
|
|
1518
|
+
// looking installed forever. Anchoring to the package token at the end of
|
|
1519
|
+
// the command makes the shape of the version irrelevant.
|
|
1520
|
+
return cmd.replace(/(\s@?[\w-]+(?:\/[\w-]+)?)@\S+\s*$/, '$1');
|
|
1334
1521
|
}
|
|
1335
1522
|
|
|
1336
1523
|
// pip install <pkg> → pip uninstall -y <pkg>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detecting an agent installer that has taken over the machine's `node`.
|
|
3
|
+
*
|
|
4
|
+
* Hermes's install.sh, when it decides the system Node is too old, downloads
|
|
5
|
+
* its own and then does this:
|
|
6
|
+
*
|
|
7
|
+
* ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node"
|
|
8
|
+
* ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm"
|
|
9
|
+
* ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx"
|
|
10
|
+
*
|
|
11
|
+
* `ln -sf` overwrites. On a machine where ~/.local/bin comes before nvm or
|
|
12
|
+
* Homebrew on PATH — a very common layout — the user's default `node` silently
|
|
13
|
+
* becomes the agent's, for every shell and every unrelated project, and npm's
|
|
14
|
+
* global prefix is rewritten to ~/.local along with it. Nothing announces this,
|
|
15
|
+
* and nothing connects a project that suddenly builds differently back to
|
|
16
|
+
* having installed an agent.
|
|
17
|
+
*
|
|
18
|
+
* This module only looks and reports. Rewriting a user's PATH or deleting their
|
|
19
|
+
* symlinks would be a second uninvited change on top of the first, and the
|
|
20
|
+
* agent's Node may be exactly what they want. Telling them precisely what
|
|
21
|
+
* happened, and how to undo it, is the part that was missing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* node/npm/npx under `linkDir` that are symlinks into `ownerDir`, together with
|
|
32
|
+
* whether they shadow another installation the user already had.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} [deps] - seams for tests; production passes nothing.
|
|
35
|
+
* @returns {{ shims: Array<{name: string, link: string, target: string}>, shadowed: string|null }}
|
|
36
|
+
*/
|
|
37
|
+
function detectShadowedNodeShims(deps = {}) {
|
|
38
|
+
const home = deps.home || os.homedir();
|
|
39
|
+
const linkDir = deps.linkDir || path.join(home, '.local', 'bin');
|
|
40
|
+
const ownerDir = deps.ownerDir || path.join(home, '.hermes', 'node', 'bin');
|
|
41
|
+
const readLink = deps.readLink || defaultReadLink;
|
|
42
|
+
const exists = deps.exists || ((p) => fs.existsSync(p));
|
|
43
|
+
const entries = deps.pathEntries || (process.env.PATH || '').split(path.delimiter);
|
|
44
|
+
|
|
45
|
+
const shims = [];
|
|
46
|
+
for (const name of ['node', 'npm', 'npx']) {
|
|
47
|
+
const link = path.join(linkDir, name);
|
|
48
|
+
const target = readLink(link);
|
|
49
|
+
if (!target) continue;
|
|
50
|
+
const resolved = path.resolve(linkDir, target);
|
|
51
|
+
if (resolved === path.join(ownerDir, name) || resolved.startsWith(ownerDir + path.sep)) {
|
|
52
|
+
shims.push({ name, link, target: resolved });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (shims.length === 0) return { shims: [], shadowed: null };
|
|
56
|
+
|
|
57
|
+
// Only a problem if the user HAD another node. On a machine with none, these
|
|
58
|
+
// symlinks are the installer doing something useful.
|
|
59
|
+
const normalized = entries.map((d) => d.trim()).filter(Boolean);
|
|
60
|
+
const linkIndex = normalized.indexOf(linkDir);
|
|
61
|
+
const after = linkIndex === -1 ? normalized : normalized.slice(linkIndex + 1);
|
|
62
|
+
for (const dir of after) {
|
|
63
|
+
if (dir === linkDir) continue;
|
|
64
|
+
const candidate = path.join(dir, 'node');
|
|
65
|
+
try {
|
|
66
|
+
if (exists(candidate)) return { shims, shadowed: candidate };
|
|
67
|
+
} catch {
|
|
68
|
+
/* unreadable dir on PATH — skip */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { shims, shadowed: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** lstat + readlink, or null when the path is absent or not a symlink. */
|
|
75
|
+
function defaultReadLink(p) {
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.lstatSync(p).isSymbolicLink()) return null;
|
|
78
|
+
return fs.readlinkSync(p);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The warning text, or null when there is nothing worth saying. Written to be
|
|
86
|
+
* read in an install log by someone who did not expect any of this.
|
|
87
|
+
*/
|
|
88
|
+
function shadowedNodeWarning(agentLabel, detection) {
|
|
89
|
+
if (!detection || detection.shims.length === 0 || !detection.shadowed) return null;
|
|
90
|
+
const names = detection.shims.map((s) => s.name).join(', ');
|
|
91
|
+
const dir = path.dirname(detection.shims[0].link);
|
|
92
|
+
return [
|
|
93
|
+
'',
|
|
94
|
+
`Note: the ${agentLabel} installer replaced ${names} in ${dir} with links to its own Node.`,
|
|
95
|
+
`That now takes precedence over ${detection.shadowed} for every shell on this machine,`,
|
|
96
|
+
'so other projects will build with the agent\'s Node version from here on.',
|
|
97
|
+
'',
|
|
98
|
+
'To put your own Node back, remove the links (this does not affect the agent):',
|
|
99
|
+
` rm -f ${detection.shims.map((s) => s.link).join(' ')}`,
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { detectShadowedNodeShims, shadowedNodeWarning };
|