instar 1.2.25 → 1.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +43 -5
- package/dist/commands/server.js.map +1 -1
- package/dist/core/OrgIntentDriftAnalyzer.d.ts +87 -0
- package/dist/core/OrgIntentDriftAnalyzer.d.ts.map +1 -0
- package/dist/core/OrgIntentDriftAnalyzer.js +191 -0
- package/dist/core/OrgIntentDriftAnalyzer.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +30 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +49 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/utils/resolveNodeBinary.d.ts +71 -0
- package/dist/utils/resolveNodeBinary.d.ts.map +1 -0
- package/dist/utils/resolveNodeBinary.js +152 -0
- package/dist/utils/resolveNodeBinary.js.map +1 -0
- package/package.json +1 -1
- package/scripts/fix-better-sqlite3.cjs +24 -5
- package/scripts/resolve-node-binary.cjs +119 -0
- package/src/data/builtin-manifest.json +61 -61
- package/src/scaffold/templates/jobs/instar/org-intent-drift-audit.md +34 -0
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.2.26.md +58 -0
- package/upgrades/1.2.27.md +40 -0
- package/upgrades/side-effects/fix-heal-execpath-staleness.md +92 -0
- package/upgrades/side-effects/org-intent-drift-detection.md +101 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveNodeBinary — find a stable, currently-existing Node.js executable.
|
|
3
|
+
*
|
|
4
|
+
* Background:
|
|
5
|
+
* The native-module heal path spawns child processes with `process.execPath`
|
|
6
|
+
* as the executable target. That works UNTIL the underlying file disappears
|
|
7
|
+
* from disk — most commonly when Homebrew (or any package manager) updates
|
|
8
|
+
* Node while the server is still running. Brew swaps the
|
|
9
|
+
* /opt/homebrew/bin/node symlink and removes the previous Cellar directory.
|
|
10
|
+
* The running process keeps an open FD to the deleted binary so it continues
|
|
11
|
+
* executing normally, but any NEW `spawnSync(process.execPath, ...)` returns
|
|
12
|
+
* ENOENT — the file is gone from the filesystem.
|
|
13
|
+
*
|
|
14
|
+
* Observed live on luna 2026-05-21: heal detected NODE_MODULE_VERSION
|
|
15
|
+
* mismatch correctly, then logged
|
|
16
|
+
* "rebuild failed (spawnSync /opt/homebrew/Cellar/node/25.6.1/bin/node ENOENT)"
|
|
17
|
+
* even though `node --version` from a fresh shell still worked — Homebrew
|
|
18
|
+
* had moved Node forward and removed the original Cellar path the process
|
|
19
|
+
* was launched from.
|
|
20
|
+
*
|
|
21
|
+
* Strategy:
|
|
22
|
+
* 1. Try `process.execPath` first (cheapest, most likely correct).
|
|
23
|
+
* 2. If it's ENOENT, try `fs.realpathSync(process.execPath)` — if execPath
|
|
24
|
+
* was a symlink at startup, the underlying target might still exist.
|
|
25
|
+
* 3. Otherwise fall back to a list of known-stable absolute paths in order:
|
|
26
|
+
* - the bundled agent Node at <agentStateDir>/bin/node (passed in)
|
|
27
|
+
* - /opt/homebrew/bin/node (macOS Homebrew's stable symlink)
|
|
28
|
+
* - /usr/local/bin/node, /usr/bin/node
|
|
29
|
+
* 4. Final fallback: `which node` from PATH.
|
|
30
|
+
*
|
|
31
|
+
* Returns the first existing-and-executable absolute path, or null when
|
|
32
|
+
* none of the candidates resolve. Callers that get null should NOT silently
|
|
33
|
+
* proceed — the heal cannot continue without a working Node, and the failure
|
|
34
|
+
* mode is operator-visible (DegradationReporter event with a recovery hint).
|
|
35
|
+
*
|
|
36
|
+
* Spec / origin: heal-execpath-staleness fix (Luna self-heal failure, 2026-05-21).
|
|
37
|
+
*/
|
|
38
|
+
export interface ResolveOptions {
|
|
39
|
+
/** Optional path to the agent's bundled Node (e.g. <stateDir>/bin/node). */
|
|
40
|
+
agentBundledNode?: string;
|
|
41
|
+
/** Override the in-process execPath. For testing only. */
|
|
42
|
+
execPathOverride?: string;
|
|
43
|
+
/** Override the platform string. For testing only. */
|
|
44
|
+
platformOverride?: NodeJS.Platform;
|
|
45
|
+
/** Override fs.existsSync. For testing only. */
|
|
46
|
+
existsSyncOverride?: (p: string) => boolean;
|
|
47
|
+
/** Override the `which`/`where` lookup. For testing only. */
|
|
48
|
+
whichOverride?: () => string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Result of a resolution attempt. Includes the source so callers can log
|
|
52
|
+
* which fallback fired (useful when investigating why heal needed help).
|
|
53
|
+
*/
|
|
54
|
+
export interface ResolvedNodeBinary {
|
|
55
|
+
/** Absolute path to the resolved Node executable. */
|
|
56
|
+
path: string;
|
|
57
|
+
/** Which strategy produced this path. */
|
|
58
|
+
source: 'execPath' | 'execPath-realpath' | 'agent-bundled' | 'homebrew' | 'usr-local' | 'usr-bin' | 'which';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a stable Node binary path. Returns the first viable candidate;
|
|
62
|
+
* null only when every strategy fails (rare — usually means Node was
|
|
63
|
+
* completely uninstalled mid-session).
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveStableNodeBinary(opts?: ResolveOptions): ResolvedNodeBinary | null;
|
|
66
|
+
/**
|
|
67
|
+
* Convenience: returns just the path string, or null. Use the full
|
|
68
|
+
* resolver when you need to log which fallback fired.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveStableNodeBinaryPath(opts?: ResolveOptions): string | null;
|
|
71
|
+
//# sourceMappingURL=resolveNodeBinary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveNodeBinary.d.ts","sourceRoot":"","sources":["../../src/utils/resolveNodeBinary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAOH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACnC,gDAAgD;IAChD,kBAAkB,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,MAAM,EACF,UAAU,GACV,mBAAmB,GACnB,eAAe,GACf,UAAU,GACV,WAAW,GACX,SAAS,GACT,OAAO,CAAC;CACb;AA8BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,GAAE,cAAmB,GACxB,kBAAkB,GAAG,IAAI,CAmD3B;AAwBD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,GAAG,IAAI,CAGpF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveNodeBinary — find a stable, currently-existing Node.js executable.
|
|
3
|
+
*
|
|
4
|
+
* Background:
|
|
5
|
+
* The native-module heal path spawns child processes with `process.execPath`
|
|
6
|
+
* as the executable target. That works UNTIL the underlying file disappears
|
|
7
|
+
* from disk — most commonly when Homebrew (or any package manager) updates
|
|
8
|
+
* Node while the server is still running. Brew swaps the
|
|
9
|
+
* /opt/homebrew/bin/node symlink and removes the previous Cellar directory.
|
|
10
|
+
* The running process keeps an open FD to the deleted binary so it continues
|
|
11
|
+
* executing normally, but any NEW `spawnSync(process.execPath, ...)` returns
|
|
12
|
+
* ENOENT — the file is gone from the filesystem.
|
|
13
|
+
*
|
|
14
|
+
* Observed live on luna 2026-05-21: heal detected NODE_MODULE_VERSION
|
|
15
|
+
* mismatch correctly, then logged
|
|
16
|
+
* "rebuild failed (spawnSync /opt/homebrew/Cellar/node/25.6.1/bin/node ENOENT)"
|
|
17
|
+
* even though `node --version` from a fresh shell still worked — Homebrew
|
|
18
|
+
* had moved Node forward and removed the original Cellar path the process
|
|
19
|
+
* was launched from.
|
|
20
|
+
*
|
|
21
|
+
* Strategy:
|
|
22
|
+
* 1. Try `process.execPath` first (cheapest, most likely correct).
|
|
23
|
+
* 2. If it's ENOENT, try `fs.realpathSync(process.execPath)` — if execPath
|
|
24
|
+
* was a symlink at startup, the underlying target might still exist.
|
|
25
|
+
* 3. Otherwise fall back to a list of known-stable absolute paths in order:
|
|
26
|
+
* - the bundled agent Node at <agentStateDir>/bin/node (passed in)
|
|
27
|
+
* - /opt/homebrew/bin/node (macOS Homebrew's stable symlink)
|
|
28
|
+
* - /usr/local/bin/node, /usr/bin/node
|
|
29
|
+
* 4. Final fallback: `which node` from PATH.
|
|
30
|
+
*
|
|
31
|
+
* Returns the first existing-and-executable absolute path, or null when
|
|
32
|
+
* none of the candidates resolve. Callers that get null should NOT silently
|
|
33
|
+
* proceed — the heal cannot continue without a working Node, and the failure
|
|
34
|
+
* mode is operator-visible (DegradationReporter event with a recovery hint).
|
|
35
|
+
*
|
|
36
|
+
* Spec / origin: heal-execpath-staleness fix (Luna self-heal failure, 2026-05-21).
|
|
37
|
+
*/
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
/**
|
|
42
|
+
* Check whether a path exists AND is executable (or at least file-like).
|
|
43
|
+
* On macOS/Linux, regular files with the exec bit set qualify; on Windows
|
|
44
|
+
* we only check existence because exec semantics differ. Symlinks are
|
|
45
|
+
* followed via fs.statSync (not lstat).
|
|
46
|
+
*/
|
|
47
|
+
function isExecutableFile(p, existsSync) {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(p))
|
|
50
|
+
return false;
|
|
51
|
+
const stat = fs.statSync(p);
|
|
52
|
+
if (!stat.isFile())
|
|
53
|
+
return false;
|
|
54
|
+
// accessSync throws on missing exec bit; treat throw as "not executable".
|
|
55
|
+
if (os.platform() !== 'win32') {
|
|
56
|
+
try {
|
|
57
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a stable Node binary path. Returns the first viable candidate;
|
|
71
|
+
* null only when every strategy fails (rare — usually means Node was
|
|
72
|
+
* completely uninstalled mid-session).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveStableNodeBinary(opts = {}) {
|
|
75
|
+
const existsSync = opts.existsSyncOverride ?? fs.existsSync;
|
|
76
|
+
const platform = opts.platformOverride ?? os.platform();
|
|
77
|
+
const execPath = opts.execPathOverride ?? process.execPath;
|
|
78
|
+
// 1. process.execPath as-is. Cheapest and almost always right.
|
|
79
|
+
if (isExecutableFile(execPath, existsSync)) {
|
|
80
|
+
return { path: execPath, source: 'execPath' };
|
|
81
|
+
}
|
|
82
|
+
// 2. Realpath of process.execPath. If execPath was a symlink at startup,
|
|
83
|
+
// Node resolves it during init — but in some Homebrew failure modes,
|
|
84
|
+
// the realpath target survives even when the original execPath has
|
|
85
|
+
// been swept up. Try it.
|
|
86
|
+
try {
|
|
87
|
+
const real = fs.realpathSync(execPath);
|
|
88
|
+
if (real !== execPath && isExecutableFile(real, existsSync)) {
|
|
89
|
+
return { path: real, source: 'execPath-realpath' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// realpath fails if execPath is gone; continue to fallbacks.
|
|
94
|
+
}
|
|
95
|
+
// 3. Agent's bundled Node, if the caller knows where it lives.
|
|
96
|
+
if (opts.agentBundledNode && isExecutableFile(opts.agentBundledNode, existsSync)) {
|
|
97
|
+
return { path: opts.agentBundledNode, source: 'agent-bundled' };
|
|
98
|
+
}
|
|
99
|
+
// 4. Platform-stable absolute paths in order of preference.
|
|
100
|
+
if (platform !== 'win32') {
|
|
101
|
+
const candidates = [
|
|
102
|
+
['/opt/homebrew/bin/node', 'homebrew'],
|
|
103
|
+
['/usr/local/bin/node', 'usr-local'],
|
|
104
|
+
['/usr/bin/node', 'usr-bin'],
|
|
105
|
+
];
|
|
106
|
+
for (const [candidate, source] of candidates) {
|
|
107
|
+
if (isExecutableFile(candidate, existsSync)) {
|
|
108
|
+
return { path: candidate, source };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 5. Last resort: ask the OS where `node` lives on PATH.
|
|
113
|
+
const whichResult = opts.whichOverride
|
|
114
|
+
? opts.whichOverride()
|
|
115
|
+
: whichNode(platform);
|
|
116
|
+
if (whichResult && isExecutableFile(whichResult, existsSync)) {
|
|
117
|
+
return { path: whichResult, source: 'which' };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Locate the first `node` on PATH via the platform's `which`/`where` tool.
|
|
123
|
+
* Returns null on any failure — the caller has its own fallback chain.
|
|
124
|
+
*/
|
|
125
|
+
function whichNode(platform) {
|
|
126
|
+
try {
|
|
127
|
+
const cmd = platform === 'win32' ? 'where' : 'which';
|
|
128
|
+
const result = spawnSync(cmd, ['node'], {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: 5000,
|
|
131
|
+
});
|
|
132
|
+
if (result.status === 0 && result.stdout) {
|
|
133
|
+
// `where` (Windows) can return multiple paths; take the first.
|
|
134
|
+
const first = result.stdout.trim().split(/\r?\n/)[0];
|
|
135
|
+
if (first)
|
|
136
|
+
return first;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Convenience: returns just the path string, or null. Use the full
|
|
146
|
+
* resolver when you need to log which fallback fired.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveStableNodeBinaryPath(opts = {}) {
|
|
149
|
+
const resolved = resolveStableNodeBinary(opts);
|
|
150
|
+
return resolved ? resolved.path : null;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=resolveNodeBinary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveNodeBinary.js","sourceRoot":"","sources":["../../src/utils/resolveNodeBinary.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -28,11 +28,25 @@ const { execSync, execFileSync } = require('child_process');
|
|
|
28
28
|
const fs = require('fs');
|
|
29
29
|
const os = require('os');
|
|
30
30
|
const path = require('path');
|
|
31
|
+
const { resolveStableNodeBinary } = require('./resolve-node-binary.cjs');
|
|
31
32
|
|
|
32
33
|
const MODULE_VERSION = process.versions.modules;
|
|
33
34
|
const ARCH = process.arch;
|
|
34
35
|
const PLATFORM = process.platform;
|
|
35
36
|
|
|
37
|
+
// Resolve a stable Node binary up front. process.execPath is preferred when
|
|
38
|
+
// it still exists; we fall back to /opt/homebrew/bin/node (or PATH-derived
|
|
39
|
+
// node) when Homebrew has swept the original Cellar dir out from under us.
|
|
40
|
+
// See scripts/resolve-node-binary.cjs and src/utils/resolveNodeBinary.ts.
|
|
41
|
+
const _resolvedNode = resolveStableNodeBinary();
|
|
42
|
+
const NODE_BIN = _resolvedNode ? _resolvedNode.path : process.execPath;
|
|
43
|
+
if (_resolvedNode && _resolvedNode.source !== 'execPath') {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[fix-better-sqlite3] process.execPath (${process.execPath}) is unreachable; ` +
|
|
46
|
+
`using ${_resolvedNode.source} fallback at ${NODE_BIN}.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
function findBetterSqlite3() {
|
|
37
51
|
try {
|
|
38
52
|
const resolved = require.resolve('better-sqlite3/package.json');
|
|
@@ -72,7 +86,7 @@ function getBetterSqliteVersion(pkgDir) {
|
|
|
72
86
|
function verifyChildAbiMatches() {
|
|
73
87
|
try {
|
|
74
88
|
const out = execFileSync(
|
|
75
|
-
|
|
89
|
+
NODE_BIN,
|
|
76
90
|
['-e', "process.stdout.write(process.versions.modules)"],
|
|
77
91
|
{ stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }
|
|
78
92
|
);
|
|
@@ -94,7 +108,7 @@ function verifyChildAbiMatches() {
|
|
|
94
108
|
function testBinary(pkgDir) {
|
|
95
109
|
try {
|
|
96
110
|
execFileSync(
|
|
97
|
-
|
|
111
|
+
NODE_BIN,
|
|
98
112
|
[
|
|
99
113
|
'-e',
|
|
100
114
|
"const Database = require('better-sqlite3'); const db = new Database(':memory:'); db.pragma('journal_mode = WAL'); db.close();",
|
|
@@ -180,7 +194,10 @@ function tryPrebuild(pkgDir, betterSqliteVersion) {
|
|
|
180
194
|
|
|
181
195
|
/** Locate npm's CLI entry without needing a shell. */
|
|
182
196
|
function findNpmCli() {
|
|
183
|
-
|
|
197
|
+
// Prefer the npm sibling of the resolved Node binary — when execPath is
|
|
198
|
+
// stale, NODE_BIN points at the live Node, and its sibling npm is the right
|
|
199
|
+
// pair for that ABI.
|
|
200
|
+
const nodeDir = path.dirname(NODE_BIN);
|
|
184
201
|
const candidates = [
|
|
185
202
|
path.resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
186
203
|
'/opt/homebrew/lib/node_modules/npm/bin/npm-cli.js',
|
|
@@ -215,13 +232,15 @@ function trySourceBuild(pkgDir) {
|
|
|
215
232
|
// we're building FOR. Without this, a mixed environment (e.g. asdf Node 22
|
|
216
233
|
// on PATH + server's Node 25 as execPath) compiles against the wrong
|
|
217
234
|
// headers and produces an ABI-mismatched binary that silently fails later.
|
|
218
|
-
|
|
235
|
+
// Use NODE_BIN (the resolved Node) so the PATH prefix points at a Node that
|
|
236
|
+
// actually exists on disk — process.execPath may be stale.
|
|
237
|
+
const execDir = path.dirname(NODE_BIN);
|
|
219
238
|
const childPath = `${execDir}${path.delimiter}${process.env.PATH || ''}`;
|
|
220
239
|
|
|
221
240
|
try {
|
|
222
241
|
console.log(`[fix-better-sqlite3] Source-building better-sqlite3 in ${rebuildCwd} (~30s)`);
|
|
223
242
|
execFileSync(
|
|
224
|
-
|
|
243
|
+
NODE_BIN,
|
|
225
244
|
[npmCli, 'rebuild', 'better-sqlite3', '--build-from-source'],
|
|
226
245
|
{
|
|
227
246
|
cwd: rebuildCwd,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolve-node-binary.cjs — CommonJS twin of src/utils/resolveNodeBinary.ts.
|
|
3
|
+
*
|
|
4
|
+
* Both files implement the same fallback chain. They are kept in sync because
|
|
5
|
+
* fix-better-sqlite3.cjs runs as a child process via execFileSync and cannot
|
|
6
|
+
* import the TypeScript module; the server-side ensureSqliteBindings code
|
|
7
|
+
* imports the TS module directly. If you change the resolution chain in one,
|
|
8
|
+
* mirror it in the other.
|
|
9
|
+
*
|
|
10
|
+
* Rationale: when Homebrew updates Node mid-session, process.execPath becomes
|
|
11
|
+
* ENOENT for any NEW spawnSync (even though the running process keeps its open
|
|
12
|
+
* FD). The heal path needs a stable fallback chain so it can rebuild and the
|
|
13
|
+
* subsystems can recover after restart.
|
|
14
|
+
*
|
|
15
|
+
* Spec / origin: heal-execpath-staleness fix (Luna self-heal failure, 2026-05-21).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const { spawnSync } = require('node:child_process');
|
|
21
|
+
|
|
22
|
+
function isExecutableFile(p, existsSync) {
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(p)) return false;
|
|
25
|
+
const stat = fs.statSync(p);
|
|
26
|
+
if (!stat.isFile()) return false;
|
|
27
|
+
if (os.platform() !== 'win32') {
|
|
28
|
+
try {
|
|
29
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function whichNode(platform) {
|
|
41
|
+
try {
|
|
42
|
+
const cmd = platform === 'win32' ? 'where' : 'which';
|
|
43
|
+
const result = spawnSync(cmd, ['node'], {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
});
|
|
47
|
+
if (result.status === 0 && result.stdout) {
|
|
48
|
+
const first = result.stdout.trim().split(/\r?\n/)[0];
|
|
49
|
+
if (first) return first;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{agentBundledNode?: string, execPathOverride?: string,
|
|
59
|
+
* platformOverride?: string, existsSyncOverride?: (p:string)=>boolean,
|
|
60
|
+
* whichOverride?: () => string|null}} [opts]
|
|
61
|
+
* @returns {{path:string, source:string} | null}
|
|
62
|
+
*/
|
|
63
|
+
function resolveStableNodeBinary(opts = {}) {
|
|
64
|
+
const existsSync = opts.existsSyncOverride || fs.existsSync;
|
|
65
|
+
const platform = opts.platformOverride || os.platform();
|
|
66
|
+
const execPath = opts.execPathOverride || process.execPath;
|
|
67
|
+
|
|
68
|
+
// 1. process.execPath as-is.
|
|
69
|
+
if (isExecutableFile(execPath, existsSync)) {
|
|
70
|
+
return { path: execPath, source: 'execPath' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Realpath of process.execPath.
|
|
74
|
+
try {
|
|
75
|
+
const real = fs.realpathSync(execPath);
|
|
76
|
+
if (real !== execPath && isExecutableFile(real, existsSync)) {
|
|
77
|
+
return { path: real, source: 'execPath-realpath' };
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* realpath fails when execPath is gone; continue */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Agent's bundled Node.
|
|
84
|
+
if (opts.agentBundledNode && isExecutableFile(opts.agentBundledNode, existsSync)) {
|
|
85
|
+
return { path: opts.agentBundledNode, source: 'agent-bundled' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Platform-stable absolute paths.
|
|
89
|
+
if (platform !== 'win32') {
|
|
90
|
+
const candidates = [
|
|
91
|
+
['/opt/homebrew/bin/node', 'homebrew'],
|
|
92
|
+
['/usr/local/bin/node', 'usr-local'],
|
|
93
|
+
['/usr/bin/node', 'usr-bin'],
|
|
94
|
+
];
|
|
95
|
+
for (const [candidate, source] of candidates) {
|
|
96
|
+
if (isExecutableFile(candidate, existsSync)) {
|
|
97
|
+
return { path: candidate, source };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. PATH lookup.
|
|
103
|
+
const whichResult = opts.whichOverride ? opts.whichOverride() : whichNode(platform);
|
|
104
|
+
if (whichResult && isExecutableFile(whichResult, existsSync)) {
|
|
105
|
+
return { path: whichResult, source: 'which' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveStableNodeBinaryPath(opts = {}) {
|
|
112
|
+
const resolved = resolveStableNodeBinary(opts);
|
|
113
|
+
return resolved ? resolved.path : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
resolveStableNodeBinary,
|
|
118
|
+
resolveStableNodeBinaryPath,
|
|
119
|
+
};
|