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.
- package/dist/registry/scripts/artifact-paths.js +12 -7
- package/dist/registry/scripts/learning/outcome-assessment.js +4 -3
- package/dist/registry/scripts/learning/run-application.js +19 -19
- package/dist/src/cli/commands/sync.js +1 -0
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +32 -181
- package/dist/src/cli/learning/inventory.js +2 -1
- package/dist/src/cli/learning/queue-operations.js +6 -5
- package/dist/src/cli/mcp/command-resolution.js +9 -4
- package/dist/src/cli/utils/agent-command-probe.js +148 -0
- package/dist/src/cli/utils/background-command.js +122 -0
- package/dist/src/cli/utils/machine-path.js +27 -0
- package/dist/src/cli/utils/managed-agent-install.js +76 -37
- package/dist/src/cli/utils/managed-agent-paths.js +99 -45
- package/dist/src/cli/utils/managed-node-runtime.js +94 -64
- package/dist/src/cli/utils/operation-scope.js +88 -0
- package/dist/src/cli/utils/pack-home.js +34 -1
- package/dist/src/cli/utils/win32-cmd-spawn.js +20 -12
- package/dist/src/first-run/server.js +4 -2
- package/dist/src/first-run/session-service.js +15 -55
- package/dist/src/local-mcp-server/learning-context-builder.js +31 -36
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.resolveMachinePathCached = exports.expandWindowsEnvRefs = exports.parseRegQueryPathValue = void 0;
|
|
7
7
|
exports.getManagedNodeRoot = getManagedNodeRoot;
|
|
8
|
+
exports.isOwnedManagedAgentTarget = isOwnedManagedAgentTarget;
|
|
8
9
|
exports.getPortableNodeBinPath = getPortableNodeBinPath;
|
|
9
10
|
exports.getManagedAgentBinDirs = getManagedAgentBinDirs;
|
|
10
11
|
exports.stripProjectLocalNodeBinDirs = stripProjectLocalNodeBinDirs;
|
|
@@ -18,10 +19,13 @@ exports.__setLoginShellPathResolverForTests = __setLoginShellPathResolverForTest
|
|
|
18
19
|
exports.resolveLoginShellPathCached = resolveLoginShellPathCached;
|
|
19
20
|
exports.__clearLoginShellPathCacheForTests = __clearLoginShellPathCacheForTests;
|
|
20
21
|
exports.invalidateMachinePathCache = invalidateMachinePathCache;
|
|
22
|
+
exports.invalidateAgentCommandRecovery = invalidateAgentCommandRecovery;
|
|
21
23
|
exports.__setMachinePathResolverForTests = __setMachinePathResolverForTests;
|
|
22
24
|
exports.buildRecoveredSystemPath = buildRecoveredSystemPath;
|
|
25
|
+
exports.buildRecoveredAgentPathAsync = buildRecoveredAgentPathAsync;
|
|
23
26
|
exports.buildRecoveredAgentPath = buildRecoveredAgentPath;
|
|
24
27
|
exports.versionFromProbeOutput = versionFromProbeOutput;
|
|
28
|
+
const operation_scope_1 = require("./operation-scope");
|
|
25
29
|
const fs_1 = __importDefault(require("fs"));
|
|
26
30
|
const path_1 = __importDefault(require("path"));
|
|
27
31
|
const child_process_1 = require("child_process");
|
|
@@ -36,6 +40,18 @@ Object.defineProperty(exports, "resolveMachinePathCached", { enumerable: true, g
|
|
|
36
40
|
function getManagedNodeRoot() {
|
|
37
41
|
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
38
42
|
}
|
|
43
|
+
/** Repair authority requires the real launcher to remain under the real FRAIM Node root. */
|
|
44
|
+
function isOwnedManagedAgentTarget(target) {
|
|
45
|
+
try {
|
|
46
|
+
const root = fs_1.default.realpathSync(getManagedNodeRoot());
|
|
47
|
+
const real = fs_1.default.realpathSync(target);
|
|
48
|
+
const relative = path_1.default.relative(root, real);
|
|
49
|
+
return Boolean(relative && !relative.startsWith('..') && !path_1.default.isAbsolute(relative));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
function getPortableNodeBinPath(platform = process.platform, arch = process.arch) {
|
|
40
56
|
const nodeRoot = getManagedNodeRoot();
|
|
41
57
|
if (platform === 'win32') {
|
|
@@ -228,9 +244,7 @@ function buildPathWithManagedAgentBins(basePath) {
|
|
|
228
244
|
// win32 rather than attempting a login-shell equivalent that doesn't exist.
|
|
229
245
|
const LOGIN_SHELL_PATH_TIMEOUT_MS = 3000;
|
|
230
246
|
const LOGIN_SHELL_PATH_CACHE_TTL_MS = 60_000;
|
|
231
|
-
function
|
|
232
|
-
if (process.platform === 'win32')
|
|
233
|
-
return null;
|
|
247
|
+
function loginShellInvocation() {
|
|
234
248
|
const shell = process.env.SHELL || '/bin/bash';
|
|
235
249
|
const marker = `__fraim_1256_shell_path_${process.pid}__`;
|
|
236
250
|
// `-lc`, not `-ilc`: login + command, deliberately NOT interactive (SOC 2 CC6.1 / ISO 27001
|
|
@@ -248,6 +262,20 @@ function defaultResolveLoginShellPath() {
|
|
|
248
262
|
'echo',
|
|
249
263
|
`echo "${marker}"`,
|
|
250
264
|
].join('; ');
|
|
265
|
+
return { shell, marker, script };
|
|
266
|
+
}
|
|
267
|
+
function parseLoginShellOutput(output, marker) {
|
|
268
|
+
const first = output.indexOf(marker);
|
|
269
|
+
if (first < 0)
|
|
270
|
+
return null;
|
|
271
|
+
const rest = output.slice(first + marker.length);
|
|
272
|
+
const end = rest.indexOf(marker);
|
|
273
|
+
return (end < 0 ? rest : rest.slice(0, end)).trim() || null;
|
|
274
|
+
}
|
|
275
|
+
function defaultResolveLoginShellPath() {
|
|
276
|
+
if (process.platform === 'win32')
|
|
277
|
+
return null;
|
|
278
|
+
const { shell, marker, script } = loginShellInvocation();
|
|
251
279
|
try {
|
|
252
280
|
const result = (0, child_process_1.spawnSync)(shell, ['-lc', script], {
|
|
253
281
|
encoding: 'utf8',
|
|
@@ -259,13 +287,7 @@ function defaultResolveLoginShellPath() {
|
|
|
259
287
|
if (result.status !== 0 || result.error)
|
|
260
288
|
return null;
|
|
261
289
|
const output = result.stdout || '';
|
|
262
|
-
|
|
263
|
-
if (firstIdx === -1)
|
|
264
|
-
return null;
|
|
265
|
-
const afterFirst = output.slice(firstIdx + marker.length);
|
|
266
|
-
const secondIdx = afterFirst.indexOf(marker);
|
|
267
|
-
const section = (secondIdx === -1 ? afterFirst : afterFirst.slice(0, secondIdx)).trim();
|
|
268
|
-
return section || null;
|
|
290
|
+
return parseLoginShellOutput(output, marker);
|
|
269
291
|
}
|
|
270
292
|
catch {
|
|
271
293
|
return null;
|
|
@@ -312,6 +334,7 @@ function resolveLoginShellPathCached() {
|
|
|
312
334
|
}
|
|
313
335
|
/** Test seam only. Clears the memoized login-shell PATH so the next call recomputes. */
|
|
314
336
|
function __clearLoginShellPathCacheForTests() {
|
|
337
|
+
recoveryGeneration++;
|
|
315
338
|
cachedLoginShellPath = undefined;
|
|
316
339
|
cachedLoginShellPathAtMs = 0;
|
|
317
340
|
cachedNpmGlobalBinDirsForRecovery.clear();
|
|
@@ -392,47 +415,78 @@ function resolveNpmGlobalBinDirsCached(basePath) {
|
|
|
392
415
|
function invalidateMachinePathCache() {
|
|
393
416
|
(0, machine_path_1.clearMachinePathCache)();
|
|
394
417
|
cachedNpmGlobalBinDirsForRecovery.clear();
|
|
418
|
+
recoveryGeneration++;
|
|
419
|
+
}
|
|
420
|
+
/** Refresh all discovery inputs after an explicit installation or setup retry. */
|
|
421
|
+
function invalidateAgentCommandRecovery() {
|
|
422
|
+
invalidateMachinePathCache();
|
|
423
|
+
cachedLoginShellPath = undefined;
|
|
424
|
+
cachedLoginShellPathAtMs = 0;
|
|
425
|
+
recoveryGeneration++;
|
|
395
426
|
}
|
|
396
427
|
function __setMachinePathResolverForTests(resolver) {
|
|
397
428
|
(0, machine_path_1.setMachinePathResolver)(resolver);
|
|
398
429
|
cachedNpmGlobalBinDirsForRecovery.clear();
|
|
399
430
|
}
|
|
431
|
+
function composeRecoveredSystemInputs(basePath, machinePath, loginPath) {
|
|
432
|
+
const inherited = stripProjectLocalNodeBinDirs(stripManagedAgentBinDirsFromPath(basePath));
|
|
433
|
+
const machine = machinePath ? appendBinDirsToPath(machinePath, inherited.split(path_1.default.delimiter).filter(Boolean)) : inherited;
|
|
434
|
+
const login = loginPath ? appendBinDirsToPath(machine, loginPath.split(path_1.default.delimiter).filter(Boolean)) : machine;
|
|
435
|
+
return stripManagedAgentBinDirsFromPath(stripProjectLocalNodeBinDirs(login));
|
|
436
|
+
}
|
|
400
437
|
function buildRecoveredSystemPath(basePath) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
const system = composeRecoveredSystemInputs(basePath, process.platform === 'win32' ? (0, machine_path_1.resolveMachinePathCached)() : null, resolveLoginShellPathCached());
|
|
439
|
+
return appendBinDirsToPath(system, resolveNpmGlobalBinDirsCached(system));
|
|
440
|
+
}
|
|
441
|
+
let recoveryGeneration = 0;
|
|
442
|
+
async function resolveLoginShellPathCachedAsync(signal) {
|
|
443
|
+
signal.throwIfAborted();
|
|
444
|
+
if (cachedLoginShellPath !== undefined && Date.now() - cachedLoginShellPathAtMs < LOGIN_SHELL_PATH_CACHE_TTL_MS)
|
|
445
|
+
return cachedLoginShellPath;
|
|
446
|
+
if (loginShellPathResolverOverride || process.platform === 'win32')
|
|
447
|
+
return resolveLoginShellPathCached();
|
|
448
|
+
const generation = recoveryGeneration;
|
|
449
|
+
const { captureAgentCommand } = require('./agent-command-probe');
|
|
450
|
+
const invocation = loginShellInvocation();
|
|
451
|
+
const output = await captureAgentCommand(invocation.shell, ['-lc', invocation.script], { env: { ...process.env }, signal, timeoutMs: LOGIN_SHELL_PATH_TIMEOUT_MS });
|
|
452
|
+
signal.throwIfAborted();
|
|
453
|
+
const value = output === null ? null : parseLoginShellOutput(output, invocation.marker);
|
|
454
|
+
if (generation === recoveryGeneration) {
|
|
455
|
+
cachedLoginShellPath = value;
|
|
456
|
+
cachedLoginShellPathAtMs = Date.now();
|
|
457
|
+
cachedNpmGlobalBinDirsForRecovery.clear();
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
const recoveredPaths = new operation_scope_1.SharedOperations();
|
|
462
|
+
async function buildRecoveredAgentPathAsync(basePath, options = {}) {
|
|
463
|
+
const generation = recoveryGeneration;
|
|
464
|
+
const key = JSON.stringify([basePath ?? process.env.PATH, (0, script_sync_utils_1.getUserFraimDir)(), generation]);
|
|
465
|
+
return (0, operation_scope_1.withOperation)(options, 30000, signal => recoveredPaths.run(key, async (owner) => {
|
|
466
|
+
// All children settle before cancellation releases the recovery owner.
|
|
467
|
+
const results = await Promise.allSettled([(0, machine_path_1.resolveMachinePathCachedAsync)(owner), resolveLoginShellPathCachedAsync(owner)]);
|
|
468
|
+
owner.throwIfAborted();
|
|
469
|
+
for (const result of results)
|
|
470
|
+
if (result.status === 'rejected')
|
|
471
|
+
throw result.reason;
|
|
472
|
+
const [machine, login] = results.map(result => result.value);
|
|
473
|
+
const system = composeRecoveredSystemInputs(basePath, machine, login);
|
|
474
|
+
let dirs = cachedNpmGlobalBinDirsForRecovery.get(system);
|
|
475
|
+
if (!dirs) {
|
|
476
|
+
const { getSystemCommandPath } = require('../mcp/command-resolution');
|
|
477
|
+
const { captureAgentCommand } = require('./agent-command-probe');
|
|
478
|
+
const npm = getSystemCommandPath('npm', system);
|
|
479
|
+
const output = npm ? await captureAgentCommand(npm, ['prefix', '-g'], { env: { ...process.env, PATH: system, Path: system }, signal: owner, timeoutMs: 5000 }) : null;
|
|
480
|
+
owner.throwIfAborted();
|
|
481
|
+
dirs = output && path_1.default.isAbsolute(output.trim()) ? getNpmGlobalBinDirsFromPrefix(output.trim()) : [];
|
|
482
|
+
if (generation === recoveryGeneration) {
|
|
483
|
+
if (cachedNpmGlobalBinDirsForRecovery.size >= NPM_GLOBAL_RECOVERY_CACHE_MAX_ENTRIES)
|
|
484
|
+
cachedNpmGlobalBinDirsForRecovery.clear();
|
|
485
|
+
cachedNpmGlobalBinDirsForRecovery.set(system, dirs);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return appendBinDirsToPath(appendBinDirsToPath(system, dirs), getManagedAgentBinDirs());
|
|
489
|
+
}, signal));
|
|
436
490
|
}
|
|
437
491
|
function buildRecoveredAgentPath(basePath) {
|
|
438
492
|
return appendBinDirsToPath(buildRecoveredSystemPath(basePath), getManagedAgentBinDirs());
|
|
@@ -14,8 +14,12 @@ exports.ensureManagedNpm = ensureManagedNpm;
|
|
|
14
14
|
const fs_1 = __importDefault(require("fs"));
|
|
15
15
|
const os_1 = __importDefault(require("os"));
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
|
-
const
|
|
17
|
+
const promises_1 = require("node:stream/promises");
|
|
18
|
+
const node_stream_1 = require("node:stream");
|
|
19
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
20
|
+
const operation_scope_1 = require("./operation-scope");
|
|
18
21
|
const crypto_1 = __importDefault(require("crypto"));
|
|
22
|
+
const agent_command_probe_1 = require("./agent-command-probe");
|
|
19
23
|
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
20
24
|
// Desktop-only users may have no working system npm/npx. Installer setup, Hub
|
|
21
25
|
// materialization, first-run setup, and agent installation share this managed
|
|
@@ -58,7 +62,12 @@ function findExecutableOnPath(name, pathValue = process.env.PATH) {
|
|
|
58
62
|
}
|
|
59
63
|
/** Whether `dir` already has a real, runnable node+npm — any version; this function's job is "a working npm", not "this exact one". */
|
|
60
64
|
function hasWorkingNodeAndNpm(dir, platform = process.platform) {
|
|
61
|
-
|
|
65
|
+
if (platform !== process.platform)
|
|
66
|
+
return false;
|
|
67
|
+
const runtimePath = [dir, process.env.SystemRoot ? path_1.default.join(process.env.SystemRoot, 'System32') : '/usr/bin'].join(path_1.default.delimiter);
|
|
68
|
+
const env = { ...process.env, PATH: runtimePath, Path: runtimePath };
|
|
69
|
+
return Boolean((0, agent_command_probe_1.selectAgentCommandSync)(path_1.default.join(dir, nodeExecutableName(platform)), { env })
|
|
70
|
+
&& (0, agent_command_probe_1.selectAgentCommandSync)(path_1.default.join(dir, npmExecutableName(platform)), { env }));
|
|
62
71
|
}
|
|
63
72
|
function nodeBinDir(root, platform) {
|
|
64
73
|
return platform === 'win32' ? root : path_1.default.join(root, 'bin');
|
|
@@ -108,55 +117,38 @@ function nodeArchiveFileName(request) {
|
|
|
108
117
|
function nodeDistBaseUrl(version) {
|
|
109
118
|
return `https://nodejs.org/dist/v${version}`;
|
|
110
119
|
}
|
|
111
|
-
function httpsGetBuffer(url) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}).on('error', reject);
|
|
128
|
-
});
|
|
120
|
+
async function httpsGetBuffer(url, signal) {
|
|
121
|
+
const response = await fetch(url, { signal, headers: { 'user-agent': 'fraim-hub-managed-node-runtime' } });
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
await response.body?.cancel();
|
|
124
|
+
throw new Error('Node release checksum download failed');
|
|
125
|
+
}
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let size = 0;
|
|
128
|
+
for await (const chunk of node_stream_1.Readable.fromWeb(response.body)) {
|
|
129
|
+
size += chunk.length;
|
|
130
|
+
if (size > 1024 * 1024)
|
|
131
|
+
throw new Error('Node checksum response exceeded its limit');
|
|
132
|
+
chunks.push(chunk);
|
|
133
|
+
}
|
|
134
|
+
signal?.throwIfAborted();
|
|
135
|
+
return Buffer.concat(chunks);
|
|
129
136
|
}
|
|
130
|
-
function httpsDownloadToFile(url, destPath) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (res.statusCode !== 200) {
|
|
140
|
-
res.resume();
|
|
141
|
-
file.close();
|
|
142
|
-
reject(new Error(`GET ${url} failed: HTTP ${res.statusCode}`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
res.pipe(file);
|
|
146
|
-
file.on('finish', () => file.close(() => resolve()));
|
|
147
|
-
}).on('error', (err) => {
|
|
148
|
-
file.close();
|
|
149
|
-
reject(err);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
137
|
+
async function httpsDownloadToFile(url, destPath, signal) {
|
|
138
|
+
const response = await fetch(url, { signal, headers: { 'user-agent': 'fraim-hub-managed-node-runtime' } });
|
|
139
|
+
if (!response.ok || !response.body) {
|
|
140
|
+
await response.body?.cancel();
|
|
141
|
+
throw new Error('Node runtime download failed');
|
|
142
|
+
}
|
|
143
|
+
await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(response.body), fs_1.default.createWriteStream(destPath), { signal });
|
|
152
144
|
}
|
|
153
145
|
/**
|
|
154
146
|
* Verify `filePath` against the sha256 line naming `fileName` in the release's own
|
|
155
147
|
* `SHASUMS256.txt` — the same integrity check `@electron/get` performs for Electron zips
|
|
156
148
|
* (`electron-dist.ts`), applied to Node's own equivalent publishing convention.
|
|
157
149
|
*/
|
|
158
|
-
async function verifyNodeArchiveChecksum(filePath, fileName, version) {
|
|
159
|
-
const shasums = (await httpsGetBuffer(`${nodeDistBaseUrl(version)}/SHASUMS256.txt
|
|
150
|
+
async function verifyNodeArchiveChecksum(filePath, fileName, version, signal) {
|
|
151
|
+
const shasums = (await httpsGetBuffer(`${nodeDistBaseUrl(version)}/SHASUMS256.txt`, signal)).toString('utf8');
|
|
160
152
|
const line = shasums.split('\n').find((entry) => entry.trim().endsWith(fileName));
|
|
161
153
|
if (!line) {
|
|
162
154
|
throw new Error(`SHASUMS256.txt for Node v${version} has no entry for ${fileName}; refusing to trust an unverified download`);
|
|
@@ -171,9 +163,9 @@ const defaultDownloadNodeArchive = async (request) => {
|
|
|
171
163
|
const fileName = nodeArchiveFileName(request);
|
|
172
164
|
const url = `${nodeDistBaseUrl(request.version)}/${fileName}`;
|
|
173
165
|
const tempPath = path_1.default.join(os_1.default.tmpdir(), `fraim-managed-node-${process.pid}-${Date.now()}-${fileName}`);
|
|
174
|
-
await httpsDownloadToFile(url, tempPath);
|
|
175
166
|
try {
|
|
176
|
-
await
|
|
167
|
+
await httpsDownloadToFile(url, tempPath, request.signal);
|
|
168
|
+
await verifyNodeArchiveChecksum(tempPath, fileName, request.version, request.signal);
|
|
177
169
|
}
|
|
178
170
|
catch (error) {
|
|
179
171
|
fs_1.default.rmSync(tempPath, { force: true });
|
|
@@ -182,16 +174,34 @@ const defaultDownloadNodeArchive = async (request) => {
|
|
|
182
174
|
return tempPath;
|
|
183
175
|
};
|
|
184
176
|
const defaultExtractNodeArchive = async (archivePath, destDir, request) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
177
|
+
request.signal?.throwIfAborted();
|
|
178
|
+
// Extraction owns file handles. A worker lets cancellation wait for their closure
|
|
179
|
+
// before removing the staging directory; a timer racing an extraction cannot do that.
|
|
180
|
+
await new Promise((resolve, reject) => {
|
|
181
|
+
const zip = request.platform === 'win32';
|
|
182
|
+
const worker = new node_worker_threads_1.Worker(`
|
|
183
|
+
const { workerData } = require('node:worker_threads');
|
|
184
|
+
const extractor = require(workerData.modulePath);
|
|
185
|
+
Promise.resolve(workerData.zip ? extractor(workerData.archivePath, { dir: workerData.destDir })
|
|
186
|
+
: extractor.x({ file: workerData.archivePath, cwd: workerData.destDir }))
|
|
187
|
+
.catch(error => { throw error; });
|
|
188
|
+
`, { eval: true, workerData: { zip, archivePath, destDir, modulePath: require.resolve(zip ? 'extract-zip' : 'tar') } });
|
|
189
|
+
let failure;
|
|
190
|
+
const abort = () => { void worker.terminate(); };
|
|
191
|
+
request.signal?.addEventListener('abort', abort, { once: true });
|
|
192
|
+
worker.once('error', error => { failure = error; });
|
|
193
|
+
worker.once('exit', code => {
|
|
194
|
+
request.signal?.removeEventListener('abort', abort);
|
|
195
|
+
if (request.signal?.aborted)
|
|
196
|
+
reject(request.signal.reason);
|
|
197
|
+
else if (failure || code !== 0)
|
|
198
|
+
reject(failure || new Error('Node extraction failed'));
|
|
199
|
+
else
|
|
200
|
+
resolve();
|
|
201
|
+
});
|
|
202
|
+
if (request.signal?.aborted)
|
|
203
|
+
abort();
|
|
204
|
+
});
|
|
195
205
|
};
|
|
196
206
|
/**
|
|
197
207
|
* Download and extract Node v`request.version` into `nodeRoot/<node-vX.Y.Z-platform-arch>/`,
|
|
@@ -211,24 +221,27 @@ async function downloadAndPromoteVersionedNodeDir(args) {
|
|
|
211
221
|
// tar requires an existing cwd. Each invocation owns a separate directory,
|
|
212
222
|
// including simultaneous requests within the same process.
|
|
213
223
|
stagingDir = fs_1.default.mkdtempSync(`${destDir}.staging-${process.pid}-`);
|
|
224
|
+
request.signal?.throwIfAborted();
|
|
214
225
|
await extractArchive(archivePath, stagingDir, request);
|
|
226
|
+
request.signal?.throwIfAborted();
|
|
215
227
|
// Official tar and ZIP archives preserve their top-level distribution folder.
|
|
216
228
|
// Also accept a flattened root from a supplied extractor.
|
|
217
229
|
const nestedCandidate = path_1.default.join(stagingDir, nodeVersionedDirName(request));
|
|
218
230
|
const extractedRoot = fs_1.default.existsSync(nestedCandidate) ? nestedCandidate : stagingDir;
|
|
219
231
|
fs_1.default.writeFileSync(path_1.default.join(extractedRoot, 'fraim-managed-node-version'), request.version, 'utf8');
|
|
220
|
-
if (!
|
|
232
|
+
if (!(await hasWorkingNodeAndNpmAsync(nodeBinDir(extractedRoot, request.platform), request.platform, request.signal))) {
|
|
221
233
|
throw new Error(`extracted Node v${request.version} is missing ${npmExecutableName(request.platform)}/${nodeExecutableName(request.platform)}; refusing to promote an incomplete install`);
|
|
222
234
|
}
|
|
223
235
|
// A concurrent launch may have finished downloading the same version already; the winner
|
|
224
236
|
// keeps its copy rather than being clobbered by this one.
|
|
225
|
-
if (!
|
|
237
|
+
if (!(await hasWorkingNodeAndNpmAsync(nodeBinDir(destDir, request.platform), request.platform, request.signal))) {
|
|
238
|
+
request.signal?.throwIfAborted();
|
|
226
239
|
fs_1.default.rmSync(destDir, { recursive: true, force: true });
|
|
227
240
|
try {
|
|
228
241
|
fs_1.default.renameSync(extractedRoot, destDir);
|
|
229
242
|
}
|
|
230
243
|
catch (error) {
|
|
231
|
-
if (!
|
|
244
|
+
if (!(await hasWorkingNodeAndNpmAsync(nodeBinDir(destDir, request.platform), request.platform, request.signal)))
|
|
232
245
|
throw error;
|
|
233
246
|
}
|
|
234
247
|
}
|
|
@@ -248,18 +261,24 @@ async function downloadAndPromoteVersionedNodeDir(args) {
|
|
|
248
261
|
* this launch," not a hard failure — never for "nothing found," which falls through to download.
|
|
249
262
|
*/
|
|
250
263
|
async function ensureManagedNpm(options = {}) {
|
|
264
|
+
return (0, operation_scope_1.withOperation)(options, 90000, signal => ensureManagedNpmCore({ ...options, signal }));
|
|
265
|
+
}
|
|
266
|
+
async function ensureManagedNpmCore(options) {
|
|
251
267
|
const version = options.version ?? exports.MANAGED_NODE_VERSION;
|
|
252
268
|
const platform = options.platform ?? process.platform;
|
|
253
269
|
const arch = options.arch ?? process.arch;
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
const systemPath = options.systemPath ?? process.env.PATH ?? '';
|
|
271
|
+
const env = { ...process.env, PATH: systemPath, Path: systemPath };
|
|
272
|
+
const systemNpm = platform === process.platform ? await (0, agent_command_probe_1.selectAgentCommand)('npm', { env, signal: options.signal }) : null;
|
|
273
|
+
if (systemNpm && await (0, agent_command_probe_1.selectAgentCommand)('node', { env, signal: options.signal })) {
|
|
274
|
+
return { npmPath: systemNpm.target, binDir: path_1.default.dirname(systemNpm.target), source: 'system' };
|
|
257
275
|
}
|
|
258
276
|
const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)(platform, arch);
|
|
259
|
-
|
|
277
|
+
options.signal?.throwIfAborted();
|
|
278
|
+
if (await hasWorkingNodeAndNpmAsync(existingBinDir, platform, options.signal)) {
|
|
260
279
|
return { npmPath: path_1.default.join(existingBinDir, npmExecutableName(platform)), binDir: existingBinDir, source: 'managed-existing' };
|
|
261
280
|
}
|
|
262
|
-
const request = { version, platform, arch };
|
|
281
|
+
const request = { version, platform, arch, signal: options.signal };
|
|
263
282
|
options.onProgress?.('Preparing FRAIM\'s bundled Node.js runtime (one-time, ~30MB)...');
|
|
264
283
|
const binDir = await downloadAndPromoteVersionedNodeDir({
|
|
265
284
|
nodeRoot: (0, managed_agent_paths_1.getManagedNodeRoot)(),
|
|
@@ -270,3 +289,14 @@ async function ensureManagedNpm(options = {}) {
|
|
|
270
289
|
options.onProgress?.('FRAIM Node.js runtime ready.');
|
|
271
290
|
return { npmPath: path_1.default.join(binDir, npmExecutableName(platform)), binDir, source: 'managed-downloaded' };
|
|
272
291
|
}
|
|
292
|
+
async function hasWorkingNodeAndNpmAsync(dir, platform, signal) {
|
|
293
|
+
signal?.throwIfAborted();
|
|
294
|
+
if (platform !== process.platform)
|
|
295
|
+
return false;
|
|
296
|
+
const runtimePath = [dir, process.env.SystemRoot ? path_1.default.join(process.env.SystemRoot, 'System32') : '/usr/bin'].join(path_1.default.delimiter);
|
|
297
|
+
const env = { ...process.env, PATH: runtimePath, Path: runtimePath };
|
|
298
|
+
const working = Boolean(await (0, agent_command_probe_1.selectAgentCommand)(path_1.default.join(dir, nodeExecutableName(platform)), { env, signal })
|
|
299
|
+
&& await (0, agent_command_probe_1.selectAgentCommand)(path_1.default.join(dir, npmExecutableName(platform)), { env, signal }));
|
|
300
|
+
signal?.throwIfAborted();
|
|
301
|
+
return working;
|
|
302
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SharedOperations = void 0;
|
|
4
|
+
exports.cancellationError = cancellationError;
|
|
5
|
+
exports.withOperation = withOperation;
|
|
6
|
+
function cancellationError(signal) {
|
|
7
|
+
return signal?.reason instanceof Error ? signal.reason : new Error('Operation cancelled');
|
|
8
|
+
}
|
|
9
|
+
async function withOperation(options, defaultTimeoutMs, task) {
|
|
10
|
+
options.signal?.throwIfAborted();
|
|
11
|
+
const owner = new AbortController();
|
|
12
|
+
const abort = () => owner.abort(cancellationError(options.signal));
|
|
13
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
14
|
+
const timer = setTimeout(() => owner.abort(new Error('Operation exceeded its deadline')), options.timeoutMs ?? defaultTimeoutMs);
|
|
15
|
+
if ((options.timeoutMs ?? defaultTimeoutMs) <= 0)
|
|
16
|
+
owner.abort(new Error('Operation exceeded its deadline'));
|
|
17
|
+
try {
|
|
18
|
+
owner.signal.throwIfAborted();
|
|
19
|
+
const value = await task(owner.signal);
|
|
20
|
+
owner.signal.throwIfAborted();
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
options.signal?.removeEventListener('abort', abort);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Subscribers own a lease. Only the last cancellation terminates shared work. */
|
|
29
|
+
class SharedOperations {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.active = new Map();
|
|
32
|
+
}
|
|
33
|
+
async run(key, task, signal) {
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
let work = this.active.get(key);
|
|
36
|
+
if (work?.controller.signal.aborted) {
|
|
37
|
+
// Do not overlap a replacement with the preceding owner's cleanup.
|
|
38
|
+
await work.promise.catch(() => undefined);
|
|
39
|
+
return this.run(key, task, signal);
|
|
40
|
+
}
|
|
41
|
+
if (!work) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
work = { controller, subscribers: 0, promise: undefined };
|
|
44
|
+
const entry = work;
|
|
45
|
+
// Invoke synchronously, as existing coalescer callers expect.
|
|
46
|
+
try {
|
|
47
|
+
work.promise = task(controller.signal);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
work.promise = Promise.reject(error);
|
|
51
|
+
}
|
|
52
|
+
work.promise = work.promise.finally(() => { if (this.active.get(key) === entry)
|
|
53
|
+
this.active.delete(key); });
|
|
54
|
+
this.active.set(key, work);
|
|
55
|
+
}
|
|
56
|
+
const entry = work;
|
|
57
|
+
entry.subscribers++;
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let released = false;
|
|
60
|
+
const release = () => {
|
|
61
|
+
if (released)
|
|
62
|
+
return false;
|
|
63
|
+
released = true;
|
|
64
|
+
entry.subscribers--;
|
|
65
|
+
signal?.removeEventListener('abort', abort);
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
const abort = () => {
|
|
69
|
+
if (!release())
|
|
70
|
+
return;
|
|
71
|
+
const error = cancellationError(signal);
|
|
72
|
+
if (entry.subscribers === 0) {
|
|
73
|
+
entry.controller.abort(error);
|
|
74
|
+
entry.promise.then(() => reject(error), reject);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
reject(error);
|
|
78
|
+
};
|
|
79
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
80
|
+
entry.promise.then(value => { if (release())
|
|
81
|
+
resolve(value); }, error => { if (release())
|
|
82
|
+
reject(error); });
|
|
83
|
+
if (signal?.aborted)
|
|
84
|
+
abort();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.SharedOperations = SharedOperations;
|
|
@@ -3,6 +3,7 @@ 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.resolvePackArtifactPaths = resolvePackArtifactPaths;
|
|
6
7
|
exports.packsDir = packsDir;
|
|
7
8
|
exports.defaultCloneDir = defaultCloneDir;
|
|
8
9
|
exports.resolvePackHome = resolvePackHome;
|
|
@@ -36,6 +37,7 @@ const child_process_1 = require("child_process");
|
|
|
36
37
|
const fs_1 = __importDefault(require("fs"));
|
|
37
38
|
const path_1 = __importDefault(require("path"));
|
|
38
39
|
const artifact_paths_1 = require("../../../registry/scripts/artifact-paths");
|
|
40
|
+
const local_runtime_1 = require("../../../registry/scripts/learning-usage/local-runtime");
|
|
39
41
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
40
42
|
const user_config_1 = require("./user-config");
|
|
41
43
|
/**
|
|
@@ -48,6 +50,37 @@ const user_config_1 = require("./user-config");
|
|
|
48
50
|
function absolutize(p) {
|
|
49
51
|
return path_1.default.isAbsolute(p) ? p : path_1.default.resolve((0, project_fraim_paths_1.getUserFraimDirPath)(), p);
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve artifact paths for a home, reading artifact path overrides from the
|
|
55
|
+
* machine-level config (`~/.fraim/config.json`) for Manager and Org homes.
|
|
56
|
+
*
|
|
57
|
+
* For Manager/Org homes the config path (overrides) and the artifact root
|
|
58
|
+
* (storage location) are separate concerns: overrides always live in the
|
|
59
|
+
* machine-level config while artifacts resolve relative to the synced homeRoot.
|
|
60
|
+
* `resolveArtifactPaths` handles Project homes correctly on its own; this helper
|
|
61
|
+
* is only needed for Manager/Org to avoid reading a non-existent synced config.
|
|
62
|
+
*/
|
|
63
|
+
function resolvePackArtifactPaths(home) {
|
|
64
|
+
const layer = 'layer' in home ? home.layer : 'level' in home ? home.level.toLowerCase() : 'project';
|
|
65
|
+
if (layer === 'manager' || layer === 'org') {
|
|
66
|
+
const machineConfigPath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json');
|
|
67
|
+
// readJsonIfObject returns {} for ENOENT in strict mode; non-ENOENT errors propagate.
|
|
68
|
+
const machineConfig = (0, local_runtime_1.readJsonIfObject)(machineConfigPath, { strict: true });
|
|
69
|
+
const sectionKey = layer === 'manager' ? 'managerArtifactPaths' : 'orgArtifactPaths';
|
|
70
|
+
// Use property-presence check: an explicitly invalid value (false, null, "") must reach
|
|
71
|
+
// resolveArtifactPaths so it can be validated and rejected there, not silently replaced.
|
|
72
|
+
const artifactPaths = sectionKey in machineConfig
|
|
73
|
+
? machineConfig[sectionKey]
|
|
74
|
+
: 'artifactPaths' in machineConfig
|
|
75
|
+
? machineConfig.artifactPaths
|
|
76
|
+
: undefined;
|
|
77
|
+
return (0, artifact_paths_1.resolveArtifactPaths)(home.root, home.contentRoot, machineConfigPath, artifactPaths !== undefined ? { artifactPaths } : {});
|
|
78
|
+
}
|
|
79
|
+
// Project OwnerHome carries artifactRoot = <project> while root = <project>/fraim.
|
|
80
|
+
// Use artifactRoot when present so paths like docs/evidence resolve relative to the project root.
|
|
81
|
+
const homeRoot = ('artifactRoot' in home && home.artifactRoot !== undefined) ? home.artifactRoot : home.root;
|
|
82
|
+
return (0, artifact_paths_1.resolveArtifactPaths)(homeRoot, home.contentRoot);
|
|
83
|
+
}
|
|
51
84
|
/** FRAIM-managed directory for pack copies that have no user-chosen location. */
|
|
52
85
|
function packsDir() {
|
|
53
86
|
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'packs');
|
|
@@ -86,7 +119,7 @@ function migrateOrgFlatContent(orgRoot) {
|
|
|
86
119
|
const dest = path_1.default.join(orgRoot, 'personalized-employee');
|
|
87
120
|
try {
|
|
88
121
|
// Root-relative output trees are authoritative, even when their names resemble legacy capabilities.
|
|
89
|
-
const artifacts = (
|
|
122
|
+
const artifacts = resolvePackArtifactPaths({ level: 'Org', root: orgRoot, contentRoot: dest });
|
|
90
123
|
const protectedDirectories = [artifacts.evidence, artifacts.raw, artifacts.retrospectives, path_1.default.dirname(artifacts.archiveRaw)];
|
|
91
124
|
const contains = (root, target) => {
|
|
92
125
|
const relative = path_1.default.relative(root, target);
|