fraim 2.0.360 → 2.0.361-next.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.
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.agentProbeFailureMessage = agentProbeFailureMessage;
7
+ exports.agentProbeEnvironment = agentProbeEnvironment;
8
+ exports.agentProbeEnvironmentAsync = agentProbeEnvironmentAsync;
9
+ exports.commandInvocation = commandInvocation;
10
+ exports.selectAgentCommandSync = selectAgentCommandSync;
11
+ exports.selectAgentCommand = selectAgentCommand;
12
+ exports.captureAgentCommand = captureAgentCommand;
13
+ exports.probeAgentProcess = probeAgentProcess;
14
+ const operation_scope_1 = require("./operation-scope");
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const child_process_1 = require("child_process");
18
+ const background_command_1 = require("./background-command");
19
+ const command_resolution_1 = require("../mcp/command-resolution");
20
+ const managed_agent_paths_1 = require("./managed-agent-paths");
21
+ const win32_cmd_spawn_1 = require("./win32-cmd-spawn");
22
+ function agentProbeFailureMessage(reason) {
23
+ switch (reason) {
24
+ case 'access-denied': return 'Windows denied access to the agent launcher. Check installation permissions or use an accessible installation.';
25
+ case 'policy-blocked': return 'A system policy blocked the agent launcher. Ask your administrator to allow the selected installation.';
26
+ case 'unsupported-launcher': return 'No supported agent launcher was found. Install the Windows executable or CMD launcher.';
27
+ case 'timeout': return 'The agent version check timed out. Retry Check; the installation has not been proven broken.';
28
+ case 'cancelled': return 'The agent check was cancelled. Retry Check.';
29
+ case 'output-limit': return 'The agent launcher exceeded the output limit. Inspect the installation before retrying.';
30
+ case 'exit': return 'The agent launcher failed with a nonzero exit. Inspect or repair the installation, then check again.';
31
+ case 'no-version': return 'The agent launcher returned no version. Inspect its installation and dependencies, then check again.';
32
+ case 'spawn': return 'The agent launcher could not start. Check its executable and dependencies.';
33
+ case 'cleanup-failed': return 'The agent check could not terminate its process. Close that process before retrying.';
34
+ default: return 'No working agent installation was found. Install the agent and check again.';
35
+ }
36
+ }
37
+ function agentProbeEnvironment(basePath, env = process.env) {
38
+ const recovered = (0, managed_agent_paths_1.buildRecoveredAgentPath)(basePath ?? env.PATH ?? env.Path);
39
+ return { ...env, PATH: recovered, Path: recovered };
40
+ }
41
+ async function agentProbeEnvironmentAsync(basePath, env = process.env, options = {}) {
42
+ const recovered = await (0, managed_agent_paths_1.buildRecoveredAgentPathAsync)(basePath ?? env.PATH ?? env.Path, options);
43
+ return { ...env, PATH: recovered, Path: recovered };
44
+ }
45
+ function commandInvocation(target, args, env = process.env) {
46
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(target)) {
47
+ const invocation = (0, win32_cmd_spawn_1.buildCmdExeArgs)(target, args);
48
+ return { command: env.ComSpec || env.COMSPEC || invocation.command, args: invocation.args, windowsVerbatimArguments: true };
49
+ }
50
+ return { command: target, args, windowsVerbatimArguments: false };
51
+ }
52
+ function candidates(command, env, onFailure) {
53
+ const possible = (0, command_resolution_1.getSystemCommandCandidates)(command, env.PATH ?? env.Path, env.PATHEXT);
54
+ if (process.platform === 'win32' && !path_1.default.isAbsolute(command) && !command.includes('.')) {
55
+ possible.push(...(0, command_resolution_1.getSystemCommandCandidates)(command + '.ps1', env.PATH ?? env.Path));
56
+ }
57
+ return [...new Set(possible)].filter(target => {
58
+ try {
59
+ if (!fs_1.default.statSync(target).isFile())
60
+ return false;
61
+ if (process.platform === 'win32' && !/\.(exe|com|cmd|bat)$/i.test(target)) {
62
+ onFailure?.('unsupported-launcher');
63
+ return false;
64
+ }
65
+ fs_1.default.accessSync(target, process.platform === 'win32' ? fs_1.default.constants.R_OK : fs_1.default.constants.X_OK);
66
+ return true;
67
+ }
68
+ catch (error) {
69
+ if (['EACCES', 'EPERM'].includes(error.code || ''))
70
+ onFailure?.('access-denied');
71
+ return false;
72
+ }
73
+ });
74
+ }
75
+ function selection(target, version, options) {
76
+ const env = options.env || agentProbeEnvironment();
77
+ const relative = path_1.default.relative((0, managed_agent_paths_1.getPortableNodeBinPath)(), target);
78
+ return { target, version, env, cwd: options.cwd,
79
+ source: relative && !relative.startsWith('..') && !path_1.default.isAbsolute(relative) ? 'managed' : 'system' };
80
+ }
81
+ /** Synchronous compatibility for CLI callers. Server discovery/launch use the async selector. */
82
+ function selectAgentCommandSync(command, options = {}) {
83
+ const env = options.env || agentProbeEnvironment();
84
+ const deadline = Date.now() + (options.totalTimeoutMs ?? 30000);
85
+ for (const target of candidates(command, env, options.onFailure)) {
86
+ const remaining = deadline - Date.now();
87
+ if (remaining <= 0 || options.signal?.aborted)
88
+ break;
89
+ const invocation = commandInvocation(target, ['--version'], env);
90
+ const result = (0, child_process_1.spawnSync)(invocation.command, invocation.args, {
91
+ env, cwd: options.cwd, windowsHide: true, windowsVerbatimArguments: invocation.windowsVerbatimArguments,
92
+ encoding: 'utf8', timeout: Math.min(options.timeoutMs ?? 10000, remaining), maxBuffer: 65536,
93
+ });
94
+ const version = result.status === 0 && !result.error ? (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr) : null;
95
+ if (version)
96
+ return selection(target, version, { ...options, env });
97
+ }
98
+ return null;
99
+ }
100
+ async function selectAgentCommand(command, options = {}) {
101
+ try {
102
+ return await (0, operation_scope_1.withOperation)({ signal: options.signal, timeoutMs: options.totalTimeoutMs }, 30000, async (signal) => {
103
+ const env = options.env || await agentProbeEnvironmentAsync(undefined, process.env, { signal });
104
+ for (const target of candidates(command, env, options.onFailure)) {
105
+ signal.throwIfAborted();
106
+ const version = await probeTarget(target, { ...options, env, signal, timeoutMs: options.timeoutMs ?? 10000 });
107
+ signal.throwIfAborted();
108
+ if (version)
109
+ return selection(target, version, { ...options, env });
110
+ }
111
+ return null;
112
+ });
113
+ }
114
+ catch (error) {
115
+ if (options.signal?.aborted || /deadline|abort|cancel/i.test(error instanceof Error ? error.message : String(error))) {
116
+ options.onFailure?.(options.signal?.aborted ? 'cancelled' : 'timeout');
117
+ return null;
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ function captureAgentCommand(target, args, options) {
123
+ return probeTarget(target, options, args, true);
124
+ }
125
+ async function probeAgentProcess(target, args, options) {
126
+ const invocation = commandInvocation(target, args, options.env);
127
+ return (0, background_command_1.runBackgroundCommand)(invocation.command, invocation.args, { ...options,
128
+ timeoutMs: options.timeoutMs ?? 10000, maxOutputBytes: 65536,
129
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments });
130
+ }
131
+ async function probeTarget(target, options, args = ['--version'], raw = false) {
132
+ const result = await probeAgentProcess(target, args, options);
133
+ if (result.outcome === 'cleanup-failed')
134
+ throw new Error('Agent probe cleanup failed; refusing another launch');
135
+ if (result.error || result.status !== 0) {
136
+ const diagnostic = `${result.stderr}\n${result.error?.message || ''}`;
137
+ const reason = result.outcome === 'cancelled' ? 'cancelled' : result.outcome === 'timeout' ? 'timeout'
138
+ : result.outcome === 'output-limit' ? 'output-limit' : result.error ? 'spawn' : 'exit';
139
+ const classified = /access is denied|permission denied|EACCES|EPERM/i.test(diagnostic) ? 'access-denied'
140
+ : /blocked by (?:group|system) policy|execution of scripts is disabled/i.test(diagnostic) ? 'policy-blocked' : reason;
141
+ options.onFailure?.(classified, result.status);
142
+ return null;
143
+ }
144
+ const answer = raw ? result.stdout : (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
145
+ if (!raw && !answer)
146
+ options.onFailure?.('no-version', result.status);
147
+ return answer;
148
+ }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.killBackgroundProcessTree = killBackgroundProcessTree;
7
+ exports.terminateOwnedChild = terminateOwnedChild;
8
+ exports.runBackgroundCommand = runBackgroundCommand;
9
+ const child_process_1 = require("child_process");
10
+ const path_1 = __importDefault(require("path"));
11
+ const tree_kill_1 = __importDefault(require("tree-kill"));
12
+ function killBackgroundProcessTree(pid, signal = 'SIGTERM', callback = () => undefined) {
13
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
14
+ callback(new Error('Refusing invalid or current process termination'));
15
+ return;
16
+ }
17
+ if (process.platform === 'win32') {
18
+ (0, child_process_1.execFile)(path_1.default.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe'), ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 5000 }, callback);
19
+ }
20
+ else
21
+ (0, tree_kill_1.default)(pid, signal, callback);
22
+ }
23
+ /** Wait for both tree termination and pipe closure, with a finite cleanup ceiling. */
24
+ function terminateOwnedChild(child, cleanupMs = 5000) {
25
+ if (!child.pid)
26
+ return Promise.resolve(true);
27
+ return new Promise(resolve => {
28
+ let closed = false, killed = false, failed = false, finished = false;
29
+ const finish = (success) => {
30
+ if (finished)
31
+ return;
32
+ finished = true;
33
+ clearTimeout(timer);
34
+ child.removeListener('close', onClose);
35
+ resolve(success);
36
+ };
37
+ const check = () => { if (closed && killed)
38
+ finish(!failed); };
39
+ const onClose = () => { closed = true; check(); };
40
+ const timer = setTimeout(() => {
41
+ // Never interpret an unconfirmed cleanup as permission to start another process.
42
+ try {
43
+ child.kill('SIGKILL');
44
+ }
45
+ catch { /* report the failed cleanup below */ }
46
+ finish(false);
47
+ }, cleanupMs);
48
+ child.once('close', onClose);
49
+ killBackgroundProcessTree(child.pid, 'SIGKILL', error => {
50
+ killed = true;
51
+ failed = Boolean(error && !closed && child.exitCode === null);
52
+ check();
53
+ });
54
+ });
55
+ }
56
+ /** Own a captured child until close and tree cleanup have both completed. */
57
+ function runBackgroundCommand(command, args, options = {}) {
58
+ if (options.signal?.aborted)
59
+ return Promise.resolve({ status: null, stdout: '', stderr: '', outcome: 'cancelled', error: new Error('Command cancelled') });
60
+ return new Promise(resolve => {
61
+ let child;
62
+ try {
63
+ child = (0, child_process_1.spawn)(command, args, { env: options.env ?? process.env, cwd: options.cwd,
64
+ windowsHide: true, windowsVerbatimArguments: options.windowsVerbatimArguments, stdio: ['ignore', 'pipe', 'pipe'] });
65
+ }
66
+ catch (failure) {
67
+ resolve({ status: null, stdout: '', stderr: '', outcome: 'spawn-error', error: failure });
68
+ return;
69
+ }
70
+ let stdout = '', stderr = '', bytes = 0;
71
+ let error, outcome;
72
+ let termination;
73
+ let finished = false;
74
+ const finish = (status, childSignal) => {
75
+ if (finished)
76
+ return;
77
+ finished = true;
78
+ clearTimeout(timer);
79
+ options.signal?.removeEventListener('abort', abort);
80
+ resolve({ status: error ? null : status, stdout, stderr, error,
81
+ outcome: outcome ?? (status === 0 ? 'success' : childSignal ? 'terminated' : 'nonzero-exit') });
82
+ };
83
+ const terminate = (reason) => {
84
+ if (termination)
85
+ return;
86
+ outcome = reason;
87
+ error = new Error(reason === 'timeout' ? 'Background command exceeded its deadline' : `Command ${reason}`);
88
+ if (!child.pid)
89
+ return;
90
+ termination = terminateOwnedChild(child).then(cleaned => {
91
+ if (!cleaned) {
92
+ outcome = 'cleanup-failed';
93
+ error = new Error('Command tree cleanup failed');
94
+ finish(null);
95
+ }
96
+ });
97
+ };
98
+ const abort = () => terminate('cancelled');
99
+ const timer = setTimeout(() => terminate('timeout'), options.timeoutMs ?? 15000);
100
+ options.signal?.addEventListener('abort', abort, { once: true });
101
+ if (options.signal?.aborted)
102
+ abort();
103
+ const capture = (chunk, channel) => {
104
+ bytes += chunk.length;
105
+ if (bytes > (options.maxOutputBytes ?? 1024 * 1024)) {
106
+ terminate('output-limit');
107
+ return;
108
+ }
109
+ if (channel === 'stdout')
110
+ stdout += chunk.toString();
111
+ else
112
+ stderr += chunk.toString();
113
+ };
114
+ child.stdout?.on('data', chunk => capture(chunk, 'stdout'));
115
+ child.stderr?.on('data', chunk => capture(chunk, 'stderr'));
116
+ child.once('error', failure => { error = failure; outcome = 'spawn-error'; });
117
+ child.once('close', async (status, signal) => {
118
+ await termination;
119
+ finish(status, signal);
120
+ });
121
+ });
122
+ }
@@ -7,6 +7,7 @@ exports.parseRegQueryPathValue = parseRegQueryPathValue;
7
7
  exports.expandWindowsEnvRefs = expandWindowsEnvRefs;
8
8
  exports.composeMachinePath = composeMachinePath;
9
9
  exports.resolveMachinePathCached = resolveMachinePathCached;
10
+ exports.resolveMachinePathCachedAsync = resolveMachinePathCachedAsync;
10
11
  exports.clearMachinePathCache = clearMachinePathCache;
11
12
  exports.setMachinePathResolver = setMachinePathResolver;
12
13
  exports.readWindowsEnvVarScopes = readWindowsEnvVarScopes;
@@ -161,7 +162,33 @@ function resolveMachinePathCached(nowMs = Date.now()) {
161
162
  cachedMachinePathAtMs = nowMs;
162
163
  return cachedMachinePath;
163
164
  }
165
+ /** Populate the shared recovery cache without blocking the Hub event loop on registry reads. */
166
+ let machinePathGeneration = 0;
167
+ async function resolveMachinePathCachedAsync(signal) {
168
+ signal?.throwIfAborted();
169
+ if (cachedMachinePath !== undefined && Date.now() - cachedMachinePathAtMs < MACHINE_PATH_CACHE_TTL_MS)
170
+ return cachedMachinePath;
171
+ if (machinePathResolverOverride || process.platform !== 'win32' || process.env.FRAIM_TEST_SANDBOX === '1')
172
+ return resolveMachinePathCached();
173
+ const generation = machinePathGeneration;
174
+ const { captureAgentCommand } = require('./agent-command-probe');
175
+ const results = await Promise.allSettled(WINDOWS_PATH_REGISTRY_KEYS.map(async ([key, value]) => {
176
+ const output = await captureAgentCommand(resolveRegExePath(), ['query', key, '/v', value], { env: { ...process.env }, signal, timeoutMs: MACHINE_PATH_TIMEOUT_MS });
177
+ return output === null ? null : parseRegQueryPathValue(output);
178
+ }));
179
+ signal?.throwIfAborted();
180
+ for (const result of results)
181
+ if (result.status === 'rejected')
182
+ throw result.reason;
183
+ const value = composeMachinePath(results.map(result => result.value));
184
+ if (generation === machinePathGeneration) {
185
+ cachedMachinePath = value;
186
+ cachedMachinePathAtMs = Date.now();
187
+ }
188
+ return value;
189
+ }
164
190
  function clearMachinePathCache() {
191
+ machinePathGeneration++;
165
192
  cachedMachinePath = undefined;
166
193
  cachedMachinePathAtMs = 0;
167
194
  }
@@ -3,11 +3,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.verifyInstalledAgentCommand = verifyInstalledAgentCommand;
7
+ exports.runAgentInstallCommand = runAgentInstallCommand;
6
8
  exports.installManagedAgent = installManagedAgent;
7
9
  const fs_1 = __importDefault(require("fs"));
10
+ const agent_command_probe_1 = require("./agent-command-probe");
11
+ const background_command_1 = require("./background-command");
8
12
  const persist_shell_path_1 = require("./persist-shell-path");
9
13
  const path_1 = __importDefault(require("path"));
14
+ const command_resolution_1 = require("../mcp/command-resolution");
10
15
  const managed_agent_paths_1 = require("./managed-agent-paths");
16
+ /** Verify the exact installed target without blocking cancellation or recovering another launcher. */
17
+ async function verifyInstalledAgentCommand(command, _extraBinDirs, basePath, options = {}) {
18
+ const env = { ...process.env, PATH: basePath ?? process.env.PATH, Path: basePath ?? process.env.Path };
19
+ const selected = await (0, agent_command_probe_1.selectAgentCommand)(command, { env, signal: options.signal, totalTimeoutMs: options.timeoutMs });
20
+ options.signal?.throwIfAborted();
21
+ return selected?.version ?? null;
22
+ }
23
+ async function runAgentInstallCommand(command, args, env, options = {}) {
24
+ const childEnv = { ...process.env };
25
+ for (const [key, value] of Object.entries(env || {})) {
26
+ if (value === undefined)
27
+ delete childEnv[key];
28
+ else
29
+ childEnv[key] = value;
30
+ }
31
+ const target = (0, command_resolution_1.getSystemCommandPath)(command, childEnv.PATH || childEnv.Path) || command;
32
+ const invocation = (0, agent_command_probe_1.commandInvocation)(target, args, childEnv);
33
+ const result = await (0, background_command_1.runBackgroundCommand)(invocation.command, invocation.args, {
34
+ ...options, env: childEnv, timeoutMs: options.timeoutMs ?? 90000, maxOutputBytes: 65536,
35
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
36
+ });
37
+ options.signal?.throwIfAborted();
38
+ if (result.error || result.status !== 0)
39
+ throw Object.assign(result.error || new Error('Agent installation command failed'), {
40
+ code: result.outcome === 'cleanup-failed' ? result.outcome : /\bEEXIST\b/.test(result.stderr) ? 'EEXIST' : result.outcome,
41
+ stderr: result.stderr,
42
+ });
43
+ return { stdout: result.stdout, stderr: result.stderr };
44
+ }
11
45
  function errorMessage(error) {
12
46
  return error instanceof Error ? error.message : 'Unknown error';
13
47
  }
@@ -66,7 +100,9 @@ function cleanupManagedInstallArtifacts(option, prefix) {
66
100
  }
67
101
  const removed = [];
68
102
  for (const candidate of candidates) {
69
- if (!isInsideDirectory(resolvedPrefix, candidate) || !fs_1.default.existsSync(candidate))
103
+ if (!isInsideDirectory(resolvedPrefix, candidate) || !fs_1.default.existsSync(candidate)
104
+ || !(0, managed_agent_paths_1.isOwnedManagedAgentTarget)(candidate)
105
+ || !isInsideDirectory(fs_1.default.realpathSync(resolvedPrefix), fs_1.default.realpathSync(candidate)))
70
106
  continue;
71
107
  fs_1.default.rmSync(candidate, { recursive: true, force: true });
72
108
  removed.push(candidate);
@@ -79,6 +115,7 @@ async function runInstallWithStaleCleanupRetry(option, prefix, runInstall) {
79
115
  return;
80
116
  }
81
117
  catch (error) {
118
+ option.signal?.throwIfAborted();
82
119
  if (!isStaleNpmInstallError(error))
83
120
  throw error;
84
121
  const removed = cleanupManagedInstallArtifacts(option, prefix);
@@ -104,50 +141,52 @@ async function runInstallWithStaleCleanupRetry(option, prefix, runInstall) {
104
141
  // lives in one place. Callers are responsible for the "already installed"
105
142
  // check before invoking this — it only covers the install-then-fallback path.
106
143
  async function installManagedAgent(option, systemPath, deps) {
144
+ option.signal?.throwIfAborted();
107
145
  let standardInstallError = null;
108
146
  const portableBin = (0, managed_agent_paths_1.getPortableNodeBinPath)();
109
- // npm writes Windows shims at the prefix root, but POSIX executables in prefix/bin.
110
147
  const prefix = process.platform === 'win32' ? portableBin : path_1.default.dirname(portableBin);
111
- try {
112
- await deps.runProcess('npm', ['install', '-g', option.installPackage], {
113
- PATH: systemPath,
114
- npm_config_prefix: undefined,
115
- NPM_CONFIG_PREFIX: undefined,
116
- });
117
- const standardVersion = deps.commandVersion(option.launchCommand, undefined, systemPath);
118
- // Only look up npm global bin dirs when the CLI was NOT already runnable: that lookup
119
- // is a blocking npm-prefix subprocess, and on the success path there is nothing to find.
120
- // (Commit 16cea56ac made this unconditional so the caller could persist the directory onto
121
- // process.env.PATH; issue #1748 removed that in-process PATH write, so the unconditional
122
- // lookup has no consumer and only costs the happy path a blocking subprocess.)
123
- const npmGlobalBinDirs = standardVersion
124
- ? []
125
- : (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)(systemPath, {
126
- npm_config_prefix: undefined,
127
- NPM_CONFIG_PREFIX: undefined,
128
- });
129
- const standardVersionWithNpmBin = standardVersion
130
- || (npmGlobalBinDirs.length > 0
131
- ? deps.commandVersion(option.launchCommand, npmGlobalBinDirs, systemPath)
132
- : null);
133
- if (standardVersionWithNpmBin) {
148
+ const npm = option.npmPath || (0, command_resolution_1.getSystemCommandPath)('npm', systemPath) || 'npm';
149
+ const env = { PATH: systemPath, Path: systemPath, npm_config_prefix: undefined, NPM_CONFIG_PREFIX: undefined };
150
+ const verifyPrefix = async (destination) => {
151
+ const bin = process.platform === 'win32' ? destination : path_1.default.join(destination, 'bin');
152
+ const target = (0, command_resolution_1.getSystemCommandPath)(option.launchCommand, bin);
153
+ const version = target ? await deps.commandVersion(target, undefined, systemPath, { signal: option.signal, timeoutMs: 30000 }) : null;
154
+ option.signal?.throwIfAborted();
155
+ return version;
156
+ };
157
+ if (!option.managedOnly) {
158
+ try {
159
+ const response = await deps.runProcess(npm, ['prefix', '-g'], env, { signal: option.signal, timeoutMs: 10000 });
160
+ option.signal?.throwIfAborted();
161
+ const destination = response.stdout.trim();
162
+ if (!path_1.default.isAbsolute(destination))
163
+ throw new Error('npm did not report an absolute installation prefix.');
164
+ await deps.runProcess(npm, ['install', '-g', option.installPackage], {
165
+ ...env, npm_config_prefix: destination, NPM_CONFIG_PREFIX: destination,
166
+ }, { signal: option.signal });
167
+ option.signal?.throwIfAborted();
168
+ if (!(await verifyPrefix(destination)))
169
+ throw new Error(option.label + ' installed target failed verification.');
170
+ (0, managed_agent_paths_1.invalidateAgentCommandRecovery)();
134
171
  return { outcome: 'standard' };
135
172
  }
136
- standardInstallError = `${option.label} standard install completed, but the CLI is not runnable from the user PATH.`;
137
- }
138
- catch (error) {
139
- if (isStaleNpmInstallError(error))
140
- cleanupManagedInstallArtifacts(option, prefix);
141
- standardInstallError = errorMessage(error);
173
+ catch (error) {
174
+ option.signal?.throwIfAborted();
175
+ if (error?.code === 'cleanup-failed')
176
+ throw error;
177
+ if (isStaleNpmInstallError(error))
178
+ cleanupManagedInstallArtifacts(option, prefix);
179
+ standardInstallError = errorMessage(error);
180
+ }
142
181
  }
143
- // Defect A fix: co-locate npm-global shims with the node.exe/npm.cmd that
144
- // actually runs the install, instead of the flat legacy directory.
145
182
  fs_1.default.mkdirSync(prefix, { recursive: true });
146
- await runInstallWithStaleCleanupRetry(option, prefix, () => deps.runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix }).then(() => undefined));
147
- const ver = deps.commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
148
- if (!ver) {
149
- throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH. Standard install failure: ${standardInstallError}`);
183
+ await runInstallWithStaleCleanupRetry(option, prefix, () => deps.runProcess(npm, ['install', '-g', option.installPackage], {
184
+ ...env, npm_config_prefix: prefix, NPM_CONFIG_PREFIX: prefix,
185
+ }, { signal: option.signal }).then(() => { option.signal?.throwIfAborted(); }));
186
+ if (!(await verifyPrefix(prefix))) {
187
+ throw new Error(option.label + ' managed installed target failed verification. Standard install: ' + (standardInstallError || 'not requested'));
150
188
  }
151
189
  (0, persist_shell_path_1.persistShellPath)();
190
+ (0, managed_agent_paths_1.invalidateAgentCommandRecovery)();
152
191
  return { outcome: 'managed' };
153
192
  }