pi-famulus 0.1.8 → 0.1.9
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/bin/pi-famulus.js +2 -2
- package/package.json +7 -7
- package/src/config.ts +2 -6
- package/src/index.ts +36 -6
- package/src/manager-client.ts +7 -2
- package/src/native-manager.d.ts +30 -0
- package/src/native-manager.js +260 -11
package/bin/pi-famulus.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureNativeManagerInstalled, nativePackageName } from "../src/native-manager.js";
|
|
4
4
|
|
|
5
|
-
const binary =
|
|
5
|
+
const binary = ensureNativeManagerInstalled();
|
|
6
6
|
if (!binary) {
|
|
7
7
|
const name = nativePackageName(process.platform, process.arch);
|
|
8
8
|
console.error(name
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-famulus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Keep your main agent's context clean: subagents on cheaper models, long bash in the background, and monitors that wake the agent. One Rust daemon runs the shells.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"pi-famulus": "./bin/pi-famulus.js"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"pi-famulus-linux-x64": "0.1.
|
|
58
|
-
"pi-famulus-linux-arm64": "0.1.
|
|
59
|
-
"pi-famulus-darwin-x64": "0.1.
|
|
60
|
-
"pi-famulus-darwin-arm64": "0.1.
|
|
61
|
-
"pi-famulus-win-x64": "0.1.
|
|
62
|
-
"pi-famulus-win-arm64": "0.1.
|
|
57
|
+
"pi-famulus-linux-x64": "0.1.9",
|
|
58
|
+
"pi-famulus-linux-arm64": "0.1.9",
|
|
59
|
+
"pi-famulus-darwin-x64": "0.1.9",
|
|
60
|
+
"pi-famulus-darwin-arm64": "0.1.9",
|
|
61
|
+
"pi-famulus-win-x64": "0.1.9",
|
|
62
|
+
"pi-famulus-win-arm64": "0.1.9"
|
|
63
63
|
},
|
|
64
64
|
"pi": {
|
|
65
65
|
"extensions": [
|
package/src/config.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { delimiter, join, win32 } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { describeNativeLookup, resolveNativeManagerPath } from "./native-manager.js";
|
|
11
11
|
|
|
12
12
|
export interface FamulusConfig {
|
|
13
13
|
/** Foreground budget for bash before auto-backgrounding (ms). */
|
|
@@ -290,11 +290,7 @@ export function describeManagerSearch(
|
|
|
290
290
|
const tried: string[] = [];
|
|
291
291
|
if (config.managerPath) tried.push(`config managerPath ${describeCandidate(config.managerPath)}`);
|
|
292
292
|
if (env.PI_FAMULUS_MANAGER_PATH) tried.push(`PI_FAMULUS_MANAGER_PATH ${describeCandidate(env.PI_FAMULUS_MANAGER_PATH)}`);
|
|
293
|
-
|
|
294
|
-
const native = resolveNativeManagerPath();
|
|
295
|
-
tried.push(nativeName
|
|
296
|
-
? `npm ${nativeName}${native ? ` ${native}` : " (missing or unusable, ignored)"}`
|
|
297
|
-
: `npm native manager (unsupported ${process.platform}/${process.arch})`);
|
|
293
|
+
tried.push(describeNativeLookup());
|
|
298
294
|
tried.push(describeCandidate(join(home, "bin", MANAGER_FILE_NAME)));
|
|
299
295
|
tried.push("pi-famulus on PATH");
|
|
300
296
|
return tried.join("; ");
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { createRegistryCommsHost } from "./comms/registry-host";
|
|
|
24
24
|
import { createAgentMessageTool, createContactSupervisorTool } from "./comms/tools";
|
|
25
25
|
import { registerReplyCommand } from "./comms/reply-command";
|
|
26
26
|
import { describeManagerSearch, getFamulusHome, loadConfig, resolveManagerPath, resolveSubagentConfig } from "./config";
|
|
27
|
+
import { ensureNativeManagerInstalled } from "./native-manager.js";
|
|
27
28
|
import type { TaskExitInfo } from "./format";
|
|
28
29
|
import { EXTENSION_VERSION, ManagerClient, type ManagerEvent, type TaskRecord } from "./manager-client";
|
|
29
30
|
import { createMonitorTool, exitEventFromRecord, MonitorRegistry } from "./monitor";
|
|
@@ -99,7 +100,28 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
|
|
|
99
100
|
registerBehaviorGuidelines(pi);
|
|
100
101
|
const home = getFamulusHome();
|
|
101
102
|
const config = loadConfig(home);
|
|
102
|
-
|
|
103
|
+
// pi update reinstalls extensions with npm. omit=optional drops the platform
|
|
104
|
+
// package during that install. Fetch it in the background so a slow or
|
|
105
|
+
// unreachable registry cannot stall extension startup.
|
|
106
|
+
let managerPath = resolveManagerPath(config, home);
|
|
107
|
+
let famulusInfo: {
|
|
108
|
+
extensionVersion: string;
|
|
109
|
+
managerPath: string | null;
|
|
110
|
+
describeSearch: () => string;
|
|
111
|
+
getClient: () => ManagerClient | null;
|
|
112
|
+
} | null = null;
|
|
113
|
+
const nativeRestore: Promise<string | null> = managerPath
|
|
114
|
+
? Promise.resolve(managerPath)
|
|
115
|
+
: Promise.resolve()
|
|
116
|
+
.then(() => ensureNativeManagerInstalled())
|
|
117
|
+
.then((path) => {
|
|
118
|
+
if (path) {
|
|
119
|
+
managerPath = path;
|
|
120
|
+
if (famulusInfo) famulusInfo.managerPath = path;
|
|
121
|
+
}
|
|
122
|
+
return path;
|
|
123
|
+
})
|
|
124
|
+
.catch(() => null);
|
|
103
125
|
const clock = realClock;
|
|
104
126
|
|
|
105
127
|
// Claude-style transcript pills for notifications (TUI only; no-ops elsewhere).
|
|
@@ -392,12 +414,13 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
|
|
|
392
414
|
clock,
|
|
393
415
|
syncWithManager: deps.syncWithManager,
|
|
394
416
|
});
|
|
395
|
-
|
|
417
|
+
famulusInfo = {
|
|
396
418
|
extensionVersion: EXTENSION_VERSION,
|
|
397
419
|
managerPath,
|
|
398
420
|
describeSearch: () => describeManagerSearch(config, home),
|
|
399
421
|
getClient: () => client,
|
|
400
|
-
}
|
|
422
|
+
};
|
|
423
|
+
registerFamulusCommand(pi, famulusInfo);
|
|
401
424
|
monitorRegistry.onChange(() => {
|
|
402
425
|
for (const mon of monitorRegistry.listActive()) {
|
|
403
426
|
const existing = workIndex.get(mon.taskId);
|
|
@@ -819,11 +842,18 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
|
|
|
819
842
|
startFleetWidget(startCtx);
|
|
820
843
|
|
|
821
844
|
// Connect in the background: a cold manager spawn must not stall the
|
|
822
|
-
// first prompt.
|
|
845
|
+
// first prompt. The cache install, if one is running, finishes first so a
|
|
846
|
+
// missing optional package is not reported before it can be restored.
|
|
847
|
+
// Tools call ensureAvailable() lazily before use.
|
|
823
848
|
const c = client;
|
|
824
|
-
void
|
|
825
|
-
.
|
|
849
|
+
void nativeRestore
|
|
850
|
+
.then((path) => {
|
|
851
|
+
if (client !== c || !c) return null;
|
|
852
|
+
if (path) c.useManagerPath(path);
|
|
853
|
+
return c.connect();
|
|
854
|
+
})
|
|
826
855
|
.then((ok) => {
|
|
856
|
+
if (ok === null || client !== c || !c) return;
|
|
827
857
|
if (ok) {
|
|
828
858
|
if (c.protocolLevel() >= 4) void reconcileAgentLeases(c).catch(() => {});
|
|
829
859
|
return;
|
package/src/manager-client.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { famulusPaths } from "./config";
|
|
|
18
18
|
import { realClock, type Clock, type ClockTimer } from "./clock";
|
|
19
19
|
|
|
20
20
|
const MAX_FRAME_BYTES = 4 * 1024 * 1024; // 4 MiB (§3.3)
|
|
21
|
-
const EXTENSION_VERSION = "0.1.
|
|
21
|
+
const EXTENSION_VERSION = "0.1.9";
|
|
22
22
|
export { EXTENSION_VERSION };
|
|
23
23
|
// 5: speaks per-kind agent admission and queued acquire.
|
|
24
24
|
const PROTOCOL = 5;
|
|
@@ -285,7 +285,7 @@ function delay(clock: Clock, ms: number): Promise<void> {
|
|
|
285
285
|
export class ManagerClient {
|
|
286
286
|
private readonly home: string;
|
|
287
287
|
private readonly sessionId: string;
|
|
288
|
-
private
|
|
288
|
+
private managerPath: string | null;
|
|
289
289
|
private readonly piPid: number;
|
|
290
290
|
private readonly cwd: string | undefined;
|
|
291
291
|
private readonly log: (message: string) => void;
|
|
@@ -327,6 +327,11 @@ export class ManagerClient {
|
|
|
327
327
|
return this.state === "connected";
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/** A binary found after construction. The first path wins. */
|
|
331
|
+
useManagerPath(path: string): void {
|
|
332
|
+
if (!this.managerPath) this.managerPath = path;
|
|
333
|
+
}
|
|
334
|
+
|
|
330
335
|
/** Maximum protocol level reported by the connected manager; 0 means unknown/legacy. */
|
|
331
336
|
protocolLevel(): number {
|
|
332
337
|
return this.protocolLevel_;
|
package/src/native-manager.d.ts
CHANGED
|
@@ -3,7 +3,37 @@ export interface NativeManagerOptions {
|
|
|
3
3
|
arch?: string;
|
|
4
4
|
/** Node package resolver; tests can use createRequire from an isolated consumer. */
|
|
5
5
|
resolve?: (specifier: string) => string;
|
|
6
|
+
/** Walk node_modules from this file instead of this module. */
|
|
7
|
+
from?: string;
|
|
8
|
+
/** Do not walk above this directory. */
|
|
9
|
+
stopAt?: string;
|
|
10
|
+
/** Versioned platform cache. `false` disables the cache lookup. */
|
|
11
|
+
cacheDir?: string | false;
|
|
12
|
+
}
|
|
13
|
+
export interface NativeCacheOptions {
|
|
14
|
+
version?: string;
|
|
15
|
+
platform?: string;
|
|
16
|
+
arch?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
home?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EnsureNativeOptions extends NativeManagerOptions {
|
|
21
|
+
env?: NodeJS.ProcessEnv;
|
|
22
|
+
npmCommand?: string;
|
|
23
|
+
spawn?: (
|
|
24
|
+
command: string,
|
|
25
|
+
args: string[],
|
|
26
|
+
options?: { timeout?: number; windowsHide?: boolean; encoding?: "utf8" },
|
|
27
|
+
) => { status: number | null; error?: Error };
|
|
6
28
|
}
|
|
7
29
|
export function nativePackageName(platform: string, arch: string): string | null;
|
|
30
|
+
export function nativeCacheDir(options?: NativeCacheOptions): string;
|
|
8
31
|
/** Null when unsupported, missing, corrupt, non-executable, or a different version. */
|
|
9
32
|
export function resolveNativeManagerPath(options?: NativeManagerOptions): string | null;
|
|
33
|
+
/** `npm <package> <path>` or why the exact platform package was not usable. */
|
|
34
|
+
export function describeNativeLookup(options?: NativeManagerOptions): string;
|
|
35
|
+
/**
|
|
36
|
+
* Install the exact platform package into the versioned cache when it is missing.
|
|
37
|
+
* Returns the binary path, or null when it is still unusable.
|
|
38
|
+
*/
|
|
39
|
+
export function ensureNativeManagerInstalled(options?: EnsureNativeOptions): string | null;
|
package/src/native-manager.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, closeSync, constants, mkdirSync, openSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
3
7
|
import { getSystemErrorMap } from "node:util";
|
|
4
8
|
|
|
5
9
|
// Filesystem failures mean this optional package is unusable, regardless of the syscall code.
|
|
@@ -11,8 +15,10 @@ const resolutionErrorCodes = new Set([
|
|
|
11
15
|
"ERR_INVALID_MODULE_SPECIFIER", "ERR_UNSUPPORTED_RESOLVE_REQUEST",
|
|
12
16
|
]);
|
|
13
17
|
|
|
18
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
14
19
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
20
|
const resolveInstalled = createRequire(import.meta.url).resolve;
|
|
21
|
+
const RESTORE_HINT = "not installed; run `npm install --include=optional pi-famulus`";
|
|
16
22
|
|
|
17
23
|
/** The native optional packages are selected by npm's os/cpu constraints. */
|
|
18
24
|
export function nativePackageName(platform, arch) {
|
|
@@ -23,20 +29,263 @@ export function nativePackageName(platform, arch) {
|
|
|
23
29
|
return `pi-famulus-${packageOs}-${arch}`;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Cache for the exact platform package. `pi update --extensions` reinstalls
|
|
34
|
+
* with npm, and `omit=optional` (user or project npmrc) deletes optional
|
|
35
|
+
* dependencies during that install. The cache is outside the famulus runtime
|
|
36
|
+
* home and outside pi's package tree, so the next update does not remove it.
|
|
37
|
+
*/
|
|
38
|
+
export function nativeCacheDir({
|
|
39
|
+
version = packageVersion,
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
arch = process.arch,
|
|
42
|
+
env = process.env,
|
|
43
|
+
home = homedir(),
|
|
44
|
+
} = {}) {
|
|
45
|
+
const base = platform === "win32"
|
|
46
|
+
? join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "pi-famulus", "native")
|
|
47
|
+
: join(env.XDG_CACHE_HOME || join(home, ".cache"), "pi-famulus", "native");
|
|
48
|
+
return join(base, version, `${platform}-${arch}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isIgnoredResolveError(error) {
|
|
52
|
+
return error instanceof SyntaxError || systemErrorCodes.has(error?.code) || resolutionErrorCodes.has(error?.code);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function executableFile(path) {
|
|
56
|
+
try {
|
|
57
|
+
const stat = statSync(path);
|
|
58
|
+
if (!stat.isFile() || stat.size <= 0) return false;
|
|
59
|
+
accessSync(path, constants.X_OK);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (isIgnoredResolveError(error)) return false;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Usable exact-version binary inside one package directory, or null. */
|
|
68
|
+
function binaryInPackage(dir, name) {
|
|
69
|
+
let metadata;
|
|
70
|
+
try {
|
|
71
|
+
metadata = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (isIgnoredResolveError(error)) return null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (!metadata || metadata.name !== name || metadata.version !== packageVersion) return null;
|
|
77
|
+
const exported = metadata.exports?.["./bin/pi-famulus"];
|
|
78
|
+
const rel = typeof exported === "string" ? exported : metadata.main;
|
|
79
|
+
if (typeof rel !== "string" || !rel.startsWith("./") || rel.split("/").includes("..")) return null;
|
|
80
|
+
const binary = join(dir, rel);
|
|
81
|
+
const root = dir.endsWith(sep) ? dir : dir + sep;
|
|
82
|
+
if (!binary.startsWith(root)) return null;
|
|
83
|
+
return executableFile(binary) ? binary : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function walkNodeModules(startFile, stopAt) {
|
|
87
|
+
const dirs = [];
|
|
88
|
+
let dir = dirname(startFile);
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const boundary = stopAt ? resolve(stopAt) : null;
|
|
91
|
+
while (!seen.has(dir)) {
|
|
92
|
+
seen.add(dir);
|
|
93
|
+
dirs.push(join(dir, "node_modules"));
|
|
94
|
+
if (boundary && (dir === boundary || !dir.startsWith(boundary.endsWith(sep) ? boundary : boundary + sep))) break;
|
|
95
|
+
const parent = dirname(dir);
|
|
96
|
+
if (parent === dir) break;
|
|
97
|
+
dir = parent;
|
|
98
|
+
}
|
|
99
|
+
return dirs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function legacyResolve(name, resolve) {
|
|
103
|
+
const metadata = JSON.parse(readFileSync(resolve(`${name}/package.json`), "utf8"));
|
|
104
|
+
if (!metadata || metadata.name !== name || metadata.version !== packageVersion) return null;
|
|
105
|
+
const binary = resolve(`${name}/bin/pi-famulus`);
|
|
106
|
+
const stat = statSync(binary);
|
|
107
|
+
if (!stat.isFile() || stat.size <= 0) return null;
|
|
108
|
+
accessSync(binary, constants.X_OK);
|
|
109
|
+
return binary;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function searchNative({ platform = process.platform, arch = process.arch, from, stopAt, cacheDir } = {}) {
|
|
113
|
+
const name = nativePackageName(platform, arch);
|
|
114
|
+
if (!name) return { name: null, path: null, detail: `unsupported ${platform}/${arch}` };
|
|
115
|
+
let start = from ?? modulePath;
|
|
116
|
+
if (!from) {
|
|
117
|
+
try { start = realpathSync(modulePath); } catch { /* keep the URL path */ }
|
|
118
|
+
}
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
for (const nodeModules of walkNodeModules(start, stopAt)) {
|
|
121
|
+
const dir = join(nodeModules, name);
|
|
122
|
+
if (seen.has(dir)) continue;
|
|
123
|
+
seen.add(dir);
|
|
124
|
+
const binary = binaryInPackage(dir, name);
|
|
125
|
+
if (binary) return { name, path: binary, detail: "" };
|
|
126
|
+
}
|
|
127
|
+
// NODE_PATH and other resolver roots. An explicit `from` is a hermetic walk.
|
|
128
|
+
if (!from) {
|
|
129
|
+
try {
|
|
130
|
+
const resolved = dirname(resolveInstalled(`${name}/package.json`));
|
|
131
|
+
if (!seen.has(resolved)) {
|
|
132
|
+
const binary = binaryInPackage(resolved, name);
|
|
133
|
+
if (binary) return { name, path: binary, detail: "" };
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!isIgnoredResolveError(error)) throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (cacheDir !== false) {
|
|
140
|
+
const dir = join(cacheDir ?? nativeCacheDir({ platform, arch }), "node_modules", name);
|
|
141
|
+
const binary = binaryInPackage(dir, name);
|
|
142
|
+
if (binary) return { name, path: binary, detail: "" };
|
|
143
|
+
}
|
|
144
|
+
return { name, path: null, detail: RESTORE_HINT };
|
|
145
|
+
}
|
|
146
|
+
|
|
26
147
|
/** Resolve the exact-version optional package without loading native executable bytes. */
|
|
27
|
-
export function resolveNativeManagerPath({ platform = process.platform, arch = process.arch, resolve
|
|
148
|
+
export function resolveNativeManagerPath({ platform = process.platform, arch = process.arch, resolve, from, stopAt, cacheDir } = {}) {
|
|
28
149
|
const name = nativePackageName(platform, arch);
|
|
29
150
|
if (!name) return null;
|
|
151
|
+
// Injected resolvers stay hermetic: tests use them to simulate one install tree.
|
|
152
|
+
if (resolve) {
|
|
153
|
+
try {
|
|
154
|
+
return legacyResolve(name, resolve);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (isIgnoredResolveError(error)) return null;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return searchNative({ platform, arch, from, stopAt, cacheDir }).path;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `npm <package> <path>` or why that exact platform package was not usable. */
|
|
164
|
+
export function describeNativeLookup(options) {
|
|
165
|
+
const name = nativePackageName(options?.platform ?? process.platform, options?.arch ?? process.arch);
|
|
166
|
+
if (!name) return `npm native manager (unsupported ${options?.platform ?? process.platform}/${options?.arch ?? process.arch})`;
|
|
167
|
+
if (options?.resolve) {
|
|
168
|
+
const path = resolveNativeManagerPath(options);
|
|
169
|
+
return path ? `npm ${name} ${path}` : `npm ${name} (missing or unusable, ignored)`;
|
|
170
|
+
}
|
|
171
|
+
const found = searchNative(options);
|
|
172
|
+
return found.path ? `npm ${name} ${found.path}` : `npm ${name} (${found.detail})`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pidAlive(pid) {
|
|
30
176
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const binary = resolve(`${name}/bin/pi-famulus`);
|
|
34
|
-
const stat = statSync(binary);
|
|
35
|
-
if (!stat.isFile() || stat.size <= 0) return null;
|
|
36
|
-
accessSync(binary, constants.X_OK);
|
|
37
|
-
return binary;
|
|
177
|
+
process.kill(pid, 0);
|
|
178
|
+
return true;
|
|
38
179
|
} catch (error) {
|
|
39
|
-
|
|
40
|
-
|
|
180
|
+
return error?.code === "EPERM";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sleepMs(ms) {
|
|
185
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A lock left by a dead process, or held far longer than one npm install, can be replaced. */
|
|
189
|
+
function installLockIsStale(lockPath) {
|
|
190
|
+
try {
|
|
191
|
+
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
192
|
+
const pid = Number(readFileSync(lockPath, "utf8").trim());
|
|
193
|
+
if (Number.isInteger(pid) && pid > 0) return !pidAlive(pid) || age > 150_000;
|
|
194
|
+
return age > 5_000;
|
|
195
|
+
} catch {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One npm install per cache directory. Another startup either waits until that
|
|
202
|
+
* install publishes a binary or takes over a stale lock. Returns the open lock,
|
|
203
|
+
* or null when waiting itself produced a usable binary.
|
|
204
|
+
*/
|
|
205
|
+
function acquireInstallLock(dir, lookup) {
|
|
206
|
+
mkdirSync(dir, { recursive: true });
|
|
207
|
+
const lockPath = `${dir}.install.lock`;
|
|
208
|
+
const deadline = Date.now() + 130_000;
|
|
209
|
+
while (Date.now() <= deadline) {
|
|
210
|
+
try {
|
|
211
|
+
const fd = openSync(lockPath, "wx");
|
|
212
|
+
try { writeSync(fd, String(process.pid)); } catch { /* the fd is still the lock */ }
|
|
213
|
+
return { fd, lockPath };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error?.code !== "EEXIST") throw error;
|
|
216
|
+
const ready = resolveNativeManagerPath(lookup);
|
|
217
|
+
if (ready) return null;
|
|
218
|
+
if (installLockIsStale(lockPath)) {
|
|
219
|
+
rmSync(lockPath, { force: true });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
sleepMs(200);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function releaseInstallLock(lock) {
|
|
229
|
+
if (!lock) return;
|
|
230
|
+
try { closeSync(lock.fd); } catch { /* already closed */ }
|
|
231
|
+
rmSync(lock.lockPath, { force: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Install the exact platform package into the versioned cache when the
|
|
236
|
+
* optional dependency is missing. No-ops when a usable binary is already
|
|
237
|
+
* resolvable. `PI_FAMULUS_NO_NATIVE_FETCH=1`, `PI_OFFLINE=1`, and Vitest skip
|
|
238
|
+
* the install. Directory, manifest, and npm failures return null so startup
|
|
239
|
+
* degrades instead of throwing.
|
|
240
|
+
*/
|
|
241
|
+
export function ensureNativeManagerInstalled({
|
|
242
|
+
platform = process.platform,
|
|
243
|
+
arch = process.arch,
|
|
244
|
+
cacheDir,
|
|
245
|
+
env = process.env,
|
|
246
|
+
npmCommand,
|
|
247
|
+
spawn = spawnSync,
|
|
248
|
+
from,
|
|
249
|
+
stopAt,
|
|
250
|
+
} = {}) {
|
|
251
|
+
const lookup = { platform, arch, cacheDir, from, stopAt };
|
|
252
|
+
try {
|
|
253
|
+
const existing = resolveNativeManagerPath(lookup);
|
|
254
|
+
if (existing) return existing;
|
|
255
|
+
if (!nativePackageName(platform, arch)) return null;
|
|
256
|
+
if (env.PI_FAMULUS_NO_NATIVE_FETCH === "1" || env.PI_OFFLINE === "1" || env.VITEST) return null;
|
|
257
|
+
if (cacheDir === false) return null;
|
|
258
|
+
const dir = cacheDir ?? nativeCacheDir({ platform, arch, env });
|
|
259
|
+
const lockedLookup = { ...lookup, cacheDir: dir };
|
|
260
|
+
const lock = acquireInstallLock(dir, lockedLookup);
|
|
261
|
+
if (!lock) return resolveNativeManagerPath(lockedLookup);
|
|
262
|
+
try {
|
|
263
|
+
const manifest = join(dir, "package.json");
|
|
264
|
+
try {
|
|
265
|
+
readFileSync(manifest);
|
|
266
|
+
} catch {
|
|
267
|
+
writeFileSync(manifest, `${JSON.stringify({ private: true, name: "pi-famulus-native-cache", version: "0.0.0" })}\n`);
|
|
268
|
+
}
|
|
269
|
+
const command = npmCommand ?? (platform === "win32" ? "npm.cmd" : "npm");
|
|
270
|
+
const name = nativePackageName(platform, arch);
|
|
271
|
+
let result;
|
|
272
|
+
try {
|
|
273
|
+
result = spawn(command, [
|
|
274
|
+
"install", `${name}@${packageVersion}`,
|
|
275
|
+
"--prefix", dir,
|
|
276
|
+
"--ignore-scripts", "--no-fund", "--no-audit", "--loglevel=error",
|
|
277
|
+
], { timeout: 120_000, windowsHide: true, encoding: "utf8" });
|
|
278
|
+
} catch {
|
|
279
|
+
return resolveNativeManagerPath(lockedLookup);
|
|
280
|
+
}
|
|
281
|
+
const found = resolveNativeManagerPath(lockedLookup);
|
|
282
|
+
if (found) return found;
|
|
283
|
+
if (!result || result.error || result.status !== 0) return null;
|
|
284
|
+
return null;
|
|
285
|
+
} finally {
|
|
286
|
+
releaseInstallLock(lock);
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
41
290
|
}
|
|
42
291
|
}
|