@parall/daemon 1.32.1 → 1.34.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/bundle/bb-browser-daemon.js +15628 -0
- package/bundle/buildDomTree.js +1501 -0
- package/bundle/manifest.json +19 -11
- package/bundle/parall-claude-agent.js +185 -25
- package/bundle/parall-codex-agent.js +185 -25
- package/bundle/parall-daemon.js +4647 -2856
- package/bundle/parall-openclaw-agent.js +4 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/clip-runtime/browser-dependency.d.ts +20 -0
- package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
- package/dist/clip-runtime/browser-dependency.js +52 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +70 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-manager.js +608 -0
- package/dist/clip-runtime/clip-provider.d.ts +10 -1
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +63 -21
- package/dist/clip-runtime/hub-client.d.ts +79 -0
- package/dist/clip-runtime/hub-client.d.ts.map +1 -0
- package/dist/clip-runtime/hub-client.js +320 -0
- package/dist/clip-runtime/index.d.ts +2 -0
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/clip-runtime/index.js +2 -0
- package/dist/clip-runtime/ipc.d.ts +6 -0
- package/dist/clip-runtime/ipc.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.d.ts +16 -8
- package/dist/clip-runtime/manifest.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.js +13 -0
- package/dist/clip-runtime/process-manager.d.ts +55 -3
- package/dist/clip-runtime/process-manager.d.ts.map +1 -1
- package/dist/clip-runtime/process-manager.js +226 -48
- package/dist/clip-runtime/process.d.ts +15 -1
- package/dist/clip-runtime/process.d.ts.map +1 -1
- package/dist/clip-runtime/process.js +73 -10
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/index.js +46 -5
- package/dist/runtime-bin-resolver.d.ts +7 -0
- package/dist/runtime-bin-resolver.d.ts.map +1 -0
- package/dist/runtime-bin-resolver.js +292 -0
- package/dist/supervisor.d.ts +53 -4
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +449 -117
- package/package.json +7 -6
|
@@ -12,11 +12,24 @@ export interface InvokeEvent {
|
|
|
12
12
|
export interface InvokeResult {
|
|
13
13
|
output: unknown;
|
|
14
14
|
}
|
|
15
|
+
export interface ClipInvokeContext {
|
|
16
|
+
/**
|
|
17
|
+
* The executing Clip id. It shards the process pool and fixes the caller
|
|
18
|
+
* identity for nested dependency invokes, so the execution side can resolve
|
|
19
|
+
* the Clip's browser binding via the hub. Browser
|
|
20
|
+
* profile / account identity is NOT carried here — it is resolved from the
|
|
21
|
+
* hub binding (GetBindings) and rides on clip_token end-to-end.
|
|
22
|
+
*/
|
|
23
|
+
clipId?: string;
|
|
24
|
+
}
|
|
25
|
+
export type DependencyInvokeResolver = (clipName: string, command: string, input: unknown, context: ClipInvokeContext) => Promise<unknown>;
|
|
15
26
|
export declare class ClipCommandError extends Error {
|
|
16
27
|
readonly code?: string;
|
|
17
28
|
constructor(message: string, code?: string);
|
|
18
29
|
}
|
|
19
30
|
export declare class ClipProcess {
|
|
31
|
+
private readonly context;
|
|
32
|
+
private readonly dependencyResolver?;
|
|
20
33
|
readonly clip: ClipConfig;
|
|
21
34
|
private child;
|
|
22
35
|
private reader;
|
|
@@ -32,7 +45,7 @@ export declare class ClipProcess {
|
|
|
32
45
|
private doneDeferred;
|
|
33
46
|
private doneResolved;
|
|
34
47
|
private exitError;
|
|
35
|
-
constructor(clip: ClipConfig);
|
|
48
|
+
constructor(clip: ClipConfig, context?: ClipInvokeContext, dependencyResolver?: DependencyInvokeResolver | undefined);
|
|
36
49
|
/**
|
|
37
50
|
* Spawn the Bun subprocess and start the IPC read loop.
|
|
38
51
|
* Corresponds to Pinix startLocked().
|
|
@@ -51,6 +64,7 @@ export declare class ClipProcess {
|
|
|
51
64
|
private handleRegister;
|
|
52
65
|
private dispatchInvokeEvent;
|
|
53
66
|
private handleData;
|
|
67
|
+
private handleDependencyInvoke;
|
|
54
68
|
private send;
|
|
55
69
|
private signalReady;
|
|
56
70
|
private finish;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;AAmBvB,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,wBAAwB,GAAG,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAK3C;AAgBD,qBAAa,WAAW;IAsBpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAtBtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAE1B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,OAAO,CAAmD;IAClE,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAoB;gBAGnC,IAAI,EAAE,UAAU,EACC,OAAO,GAAE,iBAAsB,EAC/B,kBAAkB,CAAC,EAAE,wBAAwB,YAAA;IAKhE;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAwCnD,SAAS,CAAC,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAInB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC;IAI/D,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,OAAO,CAAC,YAAY,CAAC;IA2DlB,IAAI,CAAC,SAAS,GAAE,MAA6B,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBnE,KAAK,IAAI,OAAO;IAShB,WAAW,IAAI,aAAa,GAAG,IAAI;IAInC,QAAQ,IAAI,KAAK,GAAG,SAAS;YAMf,QAAQ;IAWtB,OAAO,CAAC,aAAa;IAqDrB,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,UAAU;YASJ,sBAAsB;YAsDtB,IAAI;IAKlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,MAAM;IAiCd,OAAO,CAAC,KAAK;CAOd"}
|
|
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { clearAllProviderCreds } from '../runtimes.js';
|
|
4
4
|
import { NdjsonReader, NdjsonWriter, MessageType } from './ipc.js';
|
|
5
|
-
import {
|
|
5
|
+
import { enrichManifest, manifestFromIpc, resolveEntrypoint, } from './manifest.js';
|
|
6
|
+
import { manifestDeclaresBrowserDependency, manifestMapsDependencyToBrowser, } from './browser-dependency.js';
|
|
6
7
|
const CLIP_REGISTER_TIMEOUT_MS = 10_000;
|
|
7
8
|
const CLIP_STOP_TIMEOUT_MS = 5_000;
|
|
8
9
|
function sanitizeEnvForClip(extra) {
|
|
@@ -32,6 +33,8 @@ function makeDeferred() {
|
|
|
32
33
|
return { promise, resolve, reject };
|
|
33
34
|
}
|
|
34
35
|
export class ClipProcess {
|
|
36
|
+
context;
|
|
37
|
+
dependencyResolver;
|
|
35
38
|
clip;
|
|
36
39
|
child = null;
|
|
37
40
|
reader = null;
|
|
@@ -47,7 +50,9 @@ export class ClipProcess {
|
|
|
47
50
|
doneDeferred = makeDeferred();
|
|
48
51
|
doneResolved = false;
|
|
49
52
|
exitError;
|
|
50
|
-
constructor(clip) {
|
|
53
|
+
constructor(clip, context = {}, dependencyResolver) {
|
|
54
|
+
this.context = context;
|
|
55
|
+
this.dependencyResolver = dependencyResolver;
|
|
51
56
|
this.clip = clip;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
@@ -231,20 +236,27 @@ export class ClipProcess {
|
|
|
231
236
|
this.handleData(msg);
|
|
232
237
|
break;
|
|
233
238
|
case MessageType.ListClips:
|
|
234
|
-
//
|
|
239
|
+
// Advertise the "browser" dependency to the child whenever its manifest
|
|
240
|
+
// declares it. Binding resolution + cross-machine routing happen at
|
|
241
|
+
// invoke time via the hub (GetBindings → Invoke); an unbound or
|
|
242
|
+
// unavailable profile surfaces as an invoke-time error, not here.
|
|
235
243
|
this.send({
|
|
236
244
|
id: msg.id,
|
|
237
245
|
type: MessageType.ListClipsResult,
|
|
238
|
-
clips:
|
|
246
|
+
clips: this.context.clipId && manifestDeclaresBrowserDependency(this.manifest)
|
|
247
|
+
? [
|
|
248
|
+
{
|
|
249
|
+
name: 'browser',
|
|
250
|
+
package: '@pinixai/bb-browser-pro',
|
|
251
|
+
version: '0.15.0',
|
|
252
|
+
description: 'Profile-bound browser capability',
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: [],
|
|
239
256
|
}).catch(() => { });
|
|
240
257
|
break;
|
|
241
258
|
case MessageType.Invoke:
|
|
242
|
-
|
|
243
|
-
this.send({
|
|
244
|
-
id: msg.id,
|
|
245
|
-
type: MessageType.Error,
|
|
246
|
-
error: 'clip-to-clip invoke not supported yet',
|
|
247
|
-
}).catch(() => { });
|
|
259
|
+
void this.handleDependencyInvoke(msg);
|
|
248
260
|
break;
|
|
249
261
|
case MessageType.Heartbeat:
|
|
250
262
|
break;
|
|
@@ -291,6 +303,8 @@ export class ClipProcess {
|
|
|
291
303
|
event.output = msg.output;
|
|
292
304
|
if (msg.error)
|
|
293
305
|
event.error = { message: msg.error };
|
|
306
|
+
if (msg.processExit || msg.process_exit)
|
|
307
|
+
event.processExit = true;
|
|
294
308
|
callback(event);
|
|
295
309
|
}
|
|
296
310
|
handleData(msg) {
|
|
@@ -301,6 +315,55 @@ export class ClipProcess {
|
|
|
301
315
|
error: 'data operations not implemented yet',
|
|
302
316
|
}).catch(() => { });
|
|
303
317
|
}
|
|
318
|
+
async handleDependencyInvoke(msg) {
|
|
319
|
+
const dependencyName = msg.clip ?? msg.alias ?? '';
|
|
320
|
+
if (!msg.id) {
|
|
321
|
+
console.warn(`[clip:${this.clip.name}] dependency invoke missing id`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!dependencyName || !msg.command) {
|
|
325
|
+
await this.send({
|
|
326
|
+
id: msg.id,
|
|
327
|
+
type: MessageType.Error,
|
|
328
|
+
error: 'dependency clip and command are required',
|
|
329
|
+
}).catch(() => { });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!manifestMapsDependencyToBrowser(this.manifest, dependencyName)) {
|
|
333
|
+
await this.send({
|
|
334
|
+
id: msg.id,
|
|
335
|
+
type: MessageType.Error,
|
|
336
|
+
error: 'browser dependency is not declared by this clip',
|
|
337
|
+
}).catch(() => { });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!this.dependencyResolver) {
|
|
341
|
+
await this.send({
|
|
342
|
+
id: msg.id,
|
|
343
|
+
type: MessageType.Error,
|
|
344
|
+
error: 'clip dependency invoke is not configured',
|
|
345
|
+
}).catch(() => { });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const output = await this.dependencyResolver('browser', msg.command, msg.input ?? {}, this.context);
|
|
350
|
+
await this.send({
|
|
351
|
+
id: msg.id,
|
|
352
|
+
type: MessageType.Result,
|
|
353
|
+
output,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const isCommandError = err instanceof ClipCommandError;
|
|
358
|
+
await this.send({
|
|
359
|
+
id: msg.id,
|
|
360
|
+
type: MessageType.Error,
|
|
361
|
+
error: err instanceof Error ? err.message : String(err),
|
|
362
|
+
processExit: !isCommandError,
|
|
363
|
+
process_exit: !isCommandError,
|
|
364
|
+
}).catch(() => { });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
304
367
|
async send(msg) {
|
|
305
368
|
if (!this.writer)
|
|
306
369
|
throw new Error('writer not initialized');
|
package/dist/config.d.ts
CHANGED
|
@@ -54,6 +54,15 @@ export type ClaudeDaemonConfig = {
|
|
|
54
54
|
/** Disable self-update entirely (K8s env auto-disables). */
|
|
55
55
|
updateDisabled: boolean;
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Root dir for all on-disk daemon state: `config.json`, the self-update
|
|
59
|
+
* `bundle/` overlay (versions + current symlink + update-state.json), and
|
|
60
|
+
* bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
|
|
61
|
+
* overrides it so multiple daemons can coexist on one host without colliding —
|
|
62
|
+
* e.g. a staging desktop install points its launchd agent at
|
|
63
|
+
* `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
|
|
64
|
+
* Unset → default (no behavior change for existing single-env installs).
|
|
65
|
+
*/
|
|
57
66
|
export declare function daemonConfigDir(env?: NodeJS.ProcessEnv): string;
|
|
58
67
|
export declare function daemonConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
59
68
|
export declare function resolveClaudeDaemonConfig(env?: NodeJS.ProcessEnv): ClaudeDaemonConfig;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC,6BAA6B,EAAE,MAAM,CAAC;IACtC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AA2BF,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC,6BAA6B,EAAE,MAAM,CAAC;IACtC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AA2BF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAI5E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAyBD,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CA0DpB;AASD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;mEAEmE;AACnE,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,+EAA+E;AAC/E,wBAAgB,8BAA8B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,uEAAuE;AACvE,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG7E;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAMR"}
|
package/dist/config.js
CHANGED
|
@@ -24,7 +24,19 @@ function parseMsAllowZero(value, fallback) {
|
|
|
24
24
|
const n = Number(value);
|
|
25
25
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Root dir for all on-disk daemon state: `config.json`, the self-update
|
|
29
|
+
* `bundle/` overlay (versions + current symlink + update-state.json), and
|
|
30
|
+
* bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
|
|
31
|
+
* overrides it so multiple daemons can coexist on one host without colliding —
|
|
32
|
+
* e.g. a staging desktop install points its launchd agent at
|
|
33
|
+
* `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
|
|
34
|
+
* Unset → default (no behavior change for existing single-env installs).
|
|
35
|
+
*/
|
|
27
36
|
export function daemonConfigDir(env = process.env) {
|
|
37
|
+
const override = env.PRLL_DAEMON_CONFIG_DIR?.trim();
|
|
38
|
+
if (override)
|
|
39
|
+
return resolvePath(override);
|
|
28
40
|
return path.join(env.HOME || os.homedir(), '.parall-daemon');
|
|
29
41
|
}
|
|
30
42
|
export function daemonConfigPath(env = process.env) {
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { createLogger, createOtelLogger, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
4
|
import { ParallClient } from '@parall/sdk';
|
|
4
|
-
import {
|
|
5
|
+
import { runCLI } from './cli.js';
|
|
6
|
+
import { resolveBundleDir, resolveClaudeDaemonConfig, resolveWsUrl, } from './config.js';
|
|
5
7
|
import { DaemonSupervisor, sleepCancellable } from './supervisor.js';
|
|
6
8
|
import { DaemonUpdater } from './updater.js';
|
|
7
|
-
import { runCLI } from './cli.js';
|
|
8
9
|
const UPDATE_EXIT_CODE = 42;
|
|
9
10
|
let log = createLogger('daemon');
|
|
10
11
|
function formatError(reason) {
|
|
@@ -13,6 +14,39 @@ function formatError(reason) {
|
|
|
13
14
|
}
|
|
14
15
|
return String(reason);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Self-update should run only when a service manager supervises the daemon —
|
|
19
|
+
* `parall-daemon service install` (launchd/systemd) or the hosted entrypoint.sh
|
|
20
|
+
* keepalive loop. Those wrappers re-exec the overlay bundle on restart, and
|
|
21
|
+
* exit(UPDATE_EXIT_CODE) is precisely what triggers that restart so the new code
|
|
22
|
+
* loads.
|
|
23
|
+
*
|
|
24
|
+
* A bare `npx @parall/daemon` foreground run has neither property: it never
|
|
25
|
+
* exec's the overlay (so a downloaded bundle is dead weight on disk — this very
|
|
26
|
+
* process keeps running the npm launcher code), and nothing restarts it after
|
|
27
|
+
* exit(42). Self-updating there would just kill the daemon with no recovery. So
|
|
28
|
+
* we gate on two positive "managed" signals:
|
|
29
|
+
* - PRLL_DAEMON_MANAGED=1 — injected into the plist / systemd unit by
|
|
30
|
+
* `service install`; the authoritative marker.
|
|
31
|
+
* - launched from the overlay bundle dir — a service wrapper already exec'd
|
|
32
|
+
* `…/.parall-daemon/bundle/current/parall-daemon.js`. Covers service
|
|
33
|
+
* installs predating the PRLL_DAEMON_MANAGED marker.
|
|
34
|
+
* Foreground users stay current via `npx @parall/daemon@latest`, which pulls the
|
|
35
|
+
* newest launcher on every run.
|
|
36
|
+
*/
|
|
37
|
+
function isSelfUpdateManaged(env, bundleDir) {
|
|
38
|
+
if (env.PRLL_DAEMON_MANAGED === '1' || env.PRLL_DAEMON_MANAGED === 'true')
|
|
39
|
+
return true;
|
|
40
|
+
const entry = process.argv[1];
|
|
41
|
+
if (entry) {
|
|
42
|
+
const resolvedEntry = path.resolve(entry);
|
|
43
|
+
const resolvedBundle = path.resolve(bundleDir);
|
|
44
|
+
if (resolvedEntry === resolvedBundle || resolvedEntry.startsWith(resolvedBundle + path.sep)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
16
50
|
/**
|
|
17
51
|
* Outer keepalive: runs `supervisor.run()` in a loop, restarting it with
|
|
18
52
|
* exponential backoff if it rejects. The daemon process only exits when
|
|
@@ -61,7 +95,7 @@ async function runForever(config, client, log, signal, updater) {
|
|
|
61
95
|
log.error('supervisor keepalive disabled (PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0) — exiting');
|
|
62
96
|
throw err;
|
|
63
97
|
}
|
|
64
|
-
const delay = Math.min(config.supervisorRestartBackoffMs *
|
|
98
|
+
const delay = Math.min(config.supervisorRestartBackoffMs * 2 ** attempt, config.supervisorRestartBackoffMaxMs);
|
|
65
99
|
attempt += 1;
|
|
66
100
|
log.warn(`restarting supervisor in ${delay}ms (attempt ${attempt})`);
|
|
67
101
|
const slept = await sleepCancellable(delay, signal);
|
|
@@ -77,9 +111,13 @@ async function main() {
|
|
|
77
111
|
log.info(`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`);
|
|
78
112
|
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
79
113
|
// --- Self-update: rollback check (before any network calls) ---
|
|
114
|
+
// Only run when a service manager supervises us (see isSelfUpdateManaged):
|
|
115
|
+
// a bare `npx` foreground run can't load the overlay and won't be restarted
|
|
116
|
+
// after exit(42), so self-update there would just kill the daemon.
|
|
80
117
|
let updater = null;
|
|
81
|
-
|
|
82
|
-
|
|
118
|
+
const bundleDir = resolveBundleDir(process.env);
|
|
119
|
+
const selfUpdateEnabled = !config.updateDisabled && isSelfUpdateManaged(process.env, bundleDir);
|
|
120
|
+
if (selfUpdateEnabled) {
|
|
83
121
|
updater = new DaemonUpdater(bundleDir, config.updateCdnUrl, log, true);
|
|
84
122
|
if (updater.checkRollback()) {
|
|
85
123
|
log.info('rollback applied — exiting for service manager restart');
|
|
@@ -88,6 +126,9 @@ async function main() {
|
|
|
88
126
|
}
|
|
89
127
|
log.info(`update: bundleDir=${bundleDir} cdn=${config.updateCdnUrl} interval=${config.updateIntervalMs}ms`);
|
|
90
128
|
}
|
|
129
|
+
else if (!config.updateDisabled) {
|
|
130
|
+
log.info('update: skipped (foreground mode) — run `parall-daemon service install` for managed auto-update');
|
|
131
|
+
}
|
|
91
132
|
// The daemon talks to the API as a Machine — the bearer is mck_*.
|
|
92
133
|
// No orgId is configured here; per-agent subprocesses get their
|
|
93
134
|
// own org_id via the spawn env.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type RuntimeBinLogger = {
|
|
2
|
+
info: (msg: string) => void;
|
|
3
|
+
warn: (msg: string) => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function applyRuntimeBinaryEnv(runtimeType: string, baseEnv: NodeJS.ProcessEnv, log: RuntimeBinLogger): NodeJS.ProcessEnv;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=runtime-bin-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-bin-resolver.d.ts","sourceRoot":"","sources":["../src/runtime-bin-resolver.ts"],"names":[],"mappings":"AAUA,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,CAAC;AA0BF,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,GAAG,EAAE,gBAAgB,GACpB,MAAM,CAAC,UAAU,CAmCnB"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
const RUNTIME_BINARIES = {
|
|
6
|
+
'claude-code': { command: 'claude', envVar: 'PRLL_CLAUDE_BIN' },
|
|
7
|
+
codex: { command: 'codex', envVar: 'PRLL_CODEX_BIN' },
|
|
8
|
+
openclaw: { command: 'openclaw', envVar: 'PRLL_OPENCLAW_BIN' },
|
|
9
|
+
};
|
|
10
|
+
const RESOLUTION_CACHE_TTL_MS = 5 * 60_000;
|
|
11
|
+
const resolutionCache = new Map();
|
|
12
|
+
export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log) {
|
|
13
|
+
const env = { ...baseEnv };
|
|
14
|
+
const originalPath = env.PATH;
|
|
15
|
+
const pathPlan = candidatePathPlan(env);
|
|
16
|
+
const primaryPath = mergePath(pathPlan.primaryDirs, originalPath);
|
|
17
|
+
env.PATH = mergePath([...pathPlan.primaryDirs, ...pathPlan.versionedFallbackDirs], originalPath);
|
|
18
|
+
const spec = RUNTIME_BINARIES[runtimeType];
|
|
19
|
+
if (!spec)
|
|
20
|
+
return env;
|
|
21
|
+
const configured = env[spec.envVar]?.trim();
|
|
22
|
+
if (configured) {
|
|
23
|
+
const resolved = resolveRuntimeCommand(configured, env, originalPath, primaryPath, env.PATH);
|
|
24
|
+
if (resolved) {
|
|
25
|
+
env[spec.envVar] = resolved.binaryPath;
|
|
26
|
+
env.PATH = anchorResolvedPath(env.PATH, resolved);
|
|
27
|
+
log.info(`runtime ${runtimeType}: using ${spec.envVar}=${resolved.binaryPath}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
log.warn(`runtime ${runtimeType}: configured ${spec.envVar}=${configured} is not executable`);
|
|
31
|
+
}
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
const resolved = resolveRuntimeCommand(spec.command, env, originalPath, primaryPath, env.PATH);
|
|
35
|
+
if (resolved) {
|
|
36
|
+
env[spec.envVar] = resolved.binaryPath;
|
|
37
|
+
env.PATH = anchorResolvedPath(env.PATH, resolved);
|
|
38
|
+
log.info(`runtime ${runtimeType}: resolved ${spec.command} at ${resolved.binaryPath}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log.warn(`runtime ${runtimeType}: could not resolve ${spec.command}; child will rely on PATH ` +
|
|
42
|
+
`(${env.PATH ?? '<empty>'})`);
|
|
43
|
+
}
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
function resolveRuntimeCommand(command, env, inheritedPath, primaryPath, fallbackPath) {
|
|
47
|
+
const cacheKey = `runtime\0${command}\0${primaryPath}\0${fallbackPath}\0${env.SHELL ?? ''}\0${env.HOME ?? ''}`;
|
|
48
|
+
const cached = getCachedResolution(cacheKey);
|
|
49
|
+
if (cached)
|
|
50
|
+
return cached;
|
|
51
|
+
const direct = resolveDirectPath(command);
|
|
52
|
+
if (direct) {
|
|
53
|
+
const resolved = { binaryPath: direct };
|
|
54
|
+
setCachedResolution(cacheKey, resolved);
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
const fromInheritedPath = resolveFromPath(command, inheritedPath, env);
|
|
58
|
+
if (fromInheritedPath) {
|
|
59
|
+
setCachedResolution(cacheKey, fromInheritedPath);
|
|
60
|
+
return fromInheritedPath;
|
|
61
|
+
}
|
|
62
|
+
const fromShell = resolveFromLoginShell(command, { ...env, PATH: inheritedPath });
|
|
63
|
+
if (fromShell) {
|
|
64
|
+
setCachedResolution(cacheKey, fromShell);
|
|
65
|
+
return fromShell;
|
|
66
|
+
}
|
|
67
|
+
const fromPrimaryPath = resolveFromPath(command, primaryPath, env);
|
|
68
|
+
if (fromPrimaryPath) {
|
|
69
|
+
setCachedResolution(cacheKey, fromPrimaryPath);
|
|
70
|
+
return fromPrimaryPath;
|
|
71
|
+
}
|
|
72
|
+
const fromFallbackPath = fallbackPath === primaryPath ? null : resolveFromPath(command, fallbackPath, env);
|
|
73
|
+
if (fromFallbackPath) {
|
|
74
|
+
setCachedResolution(cacheKey, fromFallbackPath);
|
|
75
|
+
return fromFallbackPath;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function resolveDirectPath(command) {
|
|
80
|
+
if (!command.includes('/') && !command.includes('\\'))
|
|
81
|
+
return null;
|
|
82
|
+
const abs = path.isAbsolute(command) ? command : path.resolve(process.cwd(), command);
|
|
83
|
+
return isExecutable(abs) ? abs : null;
|
|
84
|
+
}
|
|
85
|
+
function resolveFromPath(command, pathValue, env) {
|
|
86
|
+
if (!pathValue || command.includes('/') || command.includes('\\'))
|
|
87
|
+
return null;
|
|
88
|
+
const dirs = pathValue.split(path.delimiter).filter(Boolean);
|
|
89
|
+
for (const dir of dirs) {
|
|
90
|
+
for (const file of commandCandidates(command, env)) {
|
|
91
|
+
const candidate = path.join(dir, file);
|
|
92
|
+
if (isExecutable(candidate))
|
|
93
|
+
return { binaryPath: candidate, pathValue };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function resolveFromLoginShell(command, env) {
|
|
99
|
+
if (command.includes('/') || command.includes('\\'))
|
|
100
|
+
return null;
|
|
101
|
+
const shells = unique([
|
|
102
|
+
env.SHELL?.trim(),
|
|
103
|
+
process.platform === 'darwin' ? '/bin/zsh' : undefined,
|
|
104
|
+
'/bin/bash',
|
|
105
|
+
'/bin/sh',
|
|
106
|
+
]);
|
|
107
|
+
for (const shell of shells) {
|
|
108
|
+
if (!shell || !isExecutable(shell))
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
const out = execFileSync(shell, [
|
|
112
|
+
'-lic',
|
|
113
|
+
`resolved=$(command -v ${shellQuote(command)}) || exit $?; printf '__PRLL_BIN__%s\n__PRLL_PATH__%s\n' "$resolved" "$PATH"`,
|
|
114
|
+
], {
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
env,
|
|
117
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
|
+
timeout: 2_500,
|
|
119
|
+
});
|
|
120
|
+
let binaryPath = '';
|
|
121
|
+
let pathValue = '';
|
|
122
|
+
for (const line of out.split(/\r?\n/)) {
|
|
123
|
+
if (line.startsWith('__PRLL_BIN__'))
|
|
124
|
+
binaryPath = line.slice('__PRLL_BIN__'.length).trim();
|
|
125
|
+
if (line.startsWith('__PRLL_PATH__'))
|
|
126
|
+
pathValue = line.slice('__PRLL_PATH__'.length);
|
|
127
|
+
}
|
|
128
|
+
if (path.isAbsolute(binaryPath) && isExecutable(binaryPath)) {
|
|
129
|
+
return { binaryPath, pathValue: pathValue || undefined };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Shell startup files may be noisy or slow. Keep resolution best-effort.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function candidatePathPlan(env) {
|
|
139
|
+
const home = env.HOME || os.homedir();
|
|
140
|
+
const primaryDirs = [
|
|
141
|
+
...splitPath(env.PRLL_DAEMON_RUNTIME_PATH),
|
|
142
|
+
...splitPath(env.PRLL_DAEMON_EXTRA_PATH),
|
|
143
|
+
path.dirname(process.execPath),
|
|
144
|
+
path.join(path.dirname(process.execPath), 'bin'),
|
|
145
|
+
path.resolve(path.dirname(process.execPath), '..', 'Resources', 'bin'),
|
|
146
|
+
path.join(home, '.local', 'bin'),
|
|
147
|
+
path.join(home, 'bin'),
|
|
148
|
+
path.join(home, '.npm-global', 'bin'),
|
|
149
|
+
path.join(home, 'Library', 'pnpm'),
|
|
150
|
+
path.join(home, '.local', 'share', 'pnpm'),
|
|
151
|
+
path.join(home, '.volta', 'bin'),
|
|
152
|
+
path.join(home, '.bun', 'bin'),
|
|
153
|
+
path.join(home, '.asdf', 'shims'),
|
|
154
|
+
path.join(home, '.local', 'share', 'mise', 'shims'),
|
|
155
|
+
path.join(home, '.mise', 'shims'),
|
|
156
|
+
path.join(home, '.fnm', 'aliases', 'default', 'bin'),
|
|
157
|
+
path.join(home, 'Library', 'Application Support', 'fnm', 'aliases', 'default', 'bin'),
|
|
158
|
+
'/opt/homebrew/bin',
|
|
159
|
+
'/usr/local/bin',
|
|
160
|
+
'/usr/bin',
|
|
161
|
+
'/bin',
|
|
162
|
+
'/usr/sbin',
|
|
163
|
+
'/sbin',
|
|
164
|
+
'/snap/bin',
|
|
165
|
+
];
|
|
166
|
+
const versionedFallbackDirs = [
|
|
167
|
+
...nvmVersionBinDirs(home),
|
|
168
|
+
...fnmVersionBinDirs(home),
|
|
169
|
+
];
|
|
170
|
+
return {
|
|
171
|
+
primaryDirs: unique(primaryDirs).filter((dir) => !!dir && isDirectory(dir)),
|
|
172
|
+
versionedFallbackDirs: unique(versionedFallbackDirs).filter((dir) => !!dir && isDirectory(dir)),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function nvmVersionBinDirs(home) {
|
|
176
|
+
const root = path.join(home, '.nvm', 'versions', 'node');
|
|
177
|
+
let versions;
|
|
178
|
+
try {
|
|
179
|
+
versions = fs.readdirSync(root);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return sortVersionNamesDesc(versions).map((version) => path.join(root, version, 'bin'));
|
|
185
|
+
}
|
|
186
|
+
function fnmVersionBinDirs(home) {
|
|
187
|
+
const roots = [
|
|
188
|
+
path.join(home, '.fnm', 'node-versions'),
|
|
189
|
+
path.join(home, 'Library', 'Application Support', 'fnm', 'node-versions'),
|
|
190
|
+
];
|
|
191
|
+
const dirs = [];
|
|
192
|
+
for (const root of roots) {
|
|
193
|
+
let versions;
|
|
194
|
+
try {
|
|
195
|
+
versions = fs.readdirSync(root);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
dirs.push(...sortVersionNamesDesc(versions).map((version) => path.join(root, version, 'installation', 'bin')));
|
|
201
|
+
}
|
|
202
|
+
return dirs;
|
|
203
|
+
}
|
|
204
|
+
function sortVersionNamesDesc(values) {
|
|
205
|
+
return [...values].sort((a, b) => compareVersionName(b, a));
|
|
206
|
+
}
|
|
207
|
+
function compareVersionName(a, b) {
|
|
208
|
+
const av = parseVersionName(a);
|
|
209
|
+
const bv = parseVersionName(b);
|
|
210
|
+
const max = Math.max(av.length, bv.length);
|
|
211
|
+
for (let i = 0; i < max; i += 1) {
|
|
212
|
+
const diff = (av[i] ?? 0) - (bv[i] ?? 0);
|
|
213
|
+
if (diff !== 0)
|
|
214
|
+
return diff;
|
|
215
|
+
}
|
|
216
|
+
return a.localeCompare(b);
|
|
217
|
+
}
|
|
218
|
+
function parseVersionName(value) {
|
|
219
|
+
return value
|
|
220
|
+
.replace(/^v/i, '')
|
|
221
|
+
.split('.')
|
|
222
|
+
.map((part) => Number.parseInt(part, 10))
|
|
223
|
+
.filter((part) => Number.isFinite(part));
|
|
224
|
+
}
|
|
225
|
+
function splitPath(value) {
|
|
226
|
+
return value?.split(path.delimiter).filter(Boolean) ?? [];
|
|
227
|
+
}
|
|
228
|
+
function mergePath(prependDirs, existing) {
|
|
229
|
+
return unique([...prependDirs, ...splitPath(existing)]).join(path.delimiter);
|
|
230
|
+
}
|
|
231
|
+
function anchorResolvedPath(pathValue, resolution) {
|
|
232
|
+
return mergePath([path.dirname(resolution.binaryPath), ...splitPath(resolution.pathValue)], pathValue);
|
|
233
|
+
}
|
|
234
|
+
function commandCandidates(command, env) {
|
|
235
|
+
if (process.platform !== 'win32')
|
|
236
|
+
return [command];
|
|
237
|
+
const hasExt = /\.[^\\/]+$/.test(command);
|
|
238
|
+
if (hasExt)
|
|
239
|
+
return [command];
|
|
240
|
+
const exts = (env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
241
|
+
.split(';')
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.map((ext) => ext.toLowerCase());
|
|
244
|
+
return [command, ...exts.map((ext) => `${command}${ext}`)];
|
|
245
|
+
}
|
|
246
|
+
function getCachedResolution(cacheKey) {
|
|
247
|
+
const entry = resolutionCache.get(cacheKey);
|
|
248
|
+
if (!entry)
|
|
249
|
+
return null;
|
|
250
|
+
if (entry.expiresAt <= Date.now()) {
|
|
251
|
+
resolutionCache.delete(cacheKey);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return entry.value;
|
|
255
|
+
}
|
|
256
|
+
function setCachedResolution(cacheKey, value) {
|
|
257
|
+
resolutionCache.set(cacheKey, { value, expiresAt: Date.now() + RESOLUTION_CACHE_TTL_MS });
|
|
258
|
+
}
|
|
259
|
+
function isExecutable(file) {
|
|
260
|
+
try {
|
|
261
|
+
const stat = fs.statSync(file);
|
|
262
|
+
if (!stat.isFile())
|
|
263
|
+
return false;
|
|
264
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function isDirectory(dir) {
|
|
272
|
+
try {
|
|
273
|
+
return fs.statSync(dir).isDirectory();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function unique(values) {
|
|
280
|
+
const seen = new Set();
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const value of values) {
|
|
283
|
+
if (!value || seen.has(value))
|
|
284
|
+
continue;
|
|
285
|
+
seen.add(value);
|
|
286
|
+
out.push(value);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function shellQuote(value) {
|
|
291
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
292
|
+
}
|