@phnx-labs/agents-cli 1.20.6 → 1.20.8
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/CHANGELOG.md +13 -0
- package/dist/commands/computer-actions.d.ts +55 -0
- package/dist/commands/computer-actions.js +486 -0
- package/dist/commands/computer.js +67 -56
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/sessions.js +9 -12
- package/dist/commands/setup.js +2 -2
- package/dist/commands/versions.js +2 -2
- package/dist/index.js +23 -1
- package/dist/lib/computer-rpc.d.ts +2 -0
- package/dist/lib/computer-rpc.js +21 -1
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +61 -5
- package/dist/lib/platform/exec.d.ts +9 -0
- package/dist/lib/platform/exec.js +24 -0
- package/dist/lib/platform/index.d.ts +20 -0
- package/dist/lib/platform/index.js +20 -0
- package/dist/lib/platform/paths.d.ts +22 -0
- package/dist/lib/platform/paths.js +49 -0
- package/dist/lib/platform/process.d.ts +12 -0
- package/dist/lib/platform/process.js +22 -0
- package/dist/lib/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +102 -25
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.d.ts +13 -8
- package/dist/lib/shims.js +84 -4
- package/dist/lib/teams/agents.js +5 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +18 -1
package/dist/index.js
CHANGED
|
@@ -103,6 +103,21 @@ import { isInteractiveTerminal, isPromptCancelled } from './commands/utils.js';
|
|
|
103
103
|
import { AGENTS } from './lib/agents.js';
|
|
104
104
|
import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
|
|
105
105
|
import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
106
|
+
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
107
|
+
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
108
|
+
// `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
|
|
109
|
+
// parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
|
|
110
|
+
// through completely untouched and we skip registering the full command tree.
|
|
111
|
+
if (process.argv[2] === '__shim') {
|
|
112
|
+
const spec = process.argv[3] || '';
|
|
113
|
+
const rawArgs = process.argv.slice(4);
|
|
114
|
+
const atIndex = spec.indexOf('@');
|
|
115
|
+
const agent = atIndex === -1 ? spec : spec.slice(0, atIndex);
|
|
116
|
+
const pinned = atIndex === -1 ? undefined : spec.slice(atIndex + 1);
|
|
117
|
+
const { execShimPassthrough } = await import('./lib/exec.js');
|
|
118
|
+
const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
|
|
119
|
+
process.exit(code);
|
|
120
|
+
}
|
|
106
121
|
const program = new Command();
|
|
107
122
|
program
|
|
108
123
|
.name('agents')
|
|
@@ -134,7 +149,7 @@ Agent versions:
|
|
|
134
149
|
prune cleanup [target] Remove orphan resources and older duplicate version installs
|
|
135
150
|
trash Inspect and restore soft-deleted version directories
|
|
136
151
|
view [agent[@version]] List versions, or inspect one in detail
|
|
137
|
-
inspect <
|
|
152
|
+
inspect <target> Deep details for one agent+version, or a DotAgents repo (user|system|project|alias|path)
|
|
138
153
|
|
|
139
154
|
Agent configuration (synced across versions):
|
|
140
155
|
rules Instructions given to agents (CLAUDE.md, etc.)
|
|
@@ -461,6 +476,13 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
461
476
|
for (const agent of installedAgents) {
|
|
462
477
|
removeLegacyUserShim(agent);
|
|
463
478
|
}
|
|
479
|
+
// The remaining flow is rc-file PATH repair, which is POSIX-only. On Windows
|
|
480
|
+
// the shims were just regenerated (incl. `.cmd` companions) above; PATH setup
|
|
481
|
+
// is covered by the install-time guidance, so stop here rather than printing
|
|
482
|
+
// shell-rc instructions that don't apply.
|
|
483
|
+
if (IS_WINDOWS) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
464
486
|
const defaultAgents = installedAgents.filter((agent) => getGlobalDefault(agent));
|
|
465
487
|
const shadowed = defaultAgents
|
|
466
488
|
.map((agent) => ({ agent, shadowedBy: getPathShadowingExecutable(agent) }))
|
|
@@ -21,6 +21,8 @@ export declare function writeComputerPeers(allowedExecPaths: string[]): void;
|
|
|
21
21
|
export declare function resolveHelperExec(): string | null;
|
|
22
22
|
export declare function resolveHelperApp(): string | null;
|
|
23
23
|
export declare function openComputerClient(): ComputerClient;
|
|
24
|
+
export declare const RPC_TIMEOUT_MS = 30000;
|
|
25
|
+
export declare function resolveRpcTimeoutMs(env: string | undefined): number;
|
|
24
26
|
export declare function describeTransport(): {
|
|
25
27
|
kind: 'socket' | 'stdio' | 'none';
|
|
26
28
|
path: string | null;
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -200,6 +200,15 @@ export function openComputerClient() {
|
|
|
200
200
|
}
|
|
201
201
|
return new StdioClient(helperExec);
|
|
202
202
|
}
|
|
203
|
+
// Per-call RPC timeout. Without it a hung daemon (deadlocked connection
|
|
204
|
+
// queue, stopped process) hangs the CLI forever — the waiter map never
|
|
205
|
+
// settles. 30s clears every daemon-side ceiling (wait caps at 30s,
|
|
206
|
+
// launch_app at 10s, screenshot at 5s). Overridable for slower flows.
|
|
207
|
+
export const RPC_TIMEOUT_MS = 30_000;
|
|
208
|
+
export function resolveRpcTimeoutMs(env) {
|
|
209
|
+
const n = Number(env);
|
|
210
|
+
return Number.isFinite(n) && n > 0 ? n : RPC_TIMEOUT_MS;
|
|
211
|
+
}
|
|
203
212
|
// Shared waiter map + line parser. Both transports plug their reader into
|
|
204
213
|
// `handleChunk` and their writer into `send`.
|
|
205
214
|
class BaseClient {
|
|
@@ -241,8 +250,19 @@ class BaseClient {
|
|
|
241
250
|
}
|
|
242
251
|
const id = this.nextId++;
|
|
243
252
|
const payload = JSON.stringify({ id, method, params: params ?? {} }) + '\n';
|
|
253
|
+
const timeoutMs = resolveRpcTimeoutMs(process.env.COMPUTER_HELPER_RPC_TIMEOUT_MS);
|
|
244
254
|
return new Promise((resolve) => {
|
|
245
|
-
|
|
255
|
+
const timer = setTimeout(() => {
|
|
256
|
+
if (this.waiters.delete(id)) {
|
|
257
|
+
resolve({ id, error: { code: 'rpc_timeout', message: `helper did not respond within ${timeoutMs}ms` } });
|
|
258
|
+
}
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
// Resolve as an error (never reject) so callers flow through unwrap()
|
|
261
|
+
// uniformly, matching failPending's contract.
|
|
262
|
+
this.waiters.set(id, (r) => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
resolve(r);
|
|
265
|
+
});
|
|
246
266
|
this.send(payload);
|
|
247
267
|
});
|
|
248
268
|
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
14
|
+
import { isAlive } from './platform/index.js';
|
|
14
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
15
16
|
import { JobScheduler } from './scheduler.js';
|
|
16
17
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
@@ -114,14 +115,10 @@ export function isDaemonRunning() {
|
|
|
114
115
|
const pid = readDaemonPid();
|
|
115
116
|
if (!pid)
|
|
116
117
|
return false;
|
|
117
|
-
|
|
118
|
-
process.kill(pid, 0);
|
|
118
|
+
if (isAlive(pid))
|
|
119
119
|
return true;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
removeDaemonPid();
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
120
|
+
removeDaemonPid();
|
|
121
|
+
return false;
|
|
125
122
|
}
|
|
126
123
|
/** Redact values that look like tokens or credentials in a log message. */
|
|
127
124
|
function redactSecrets(message) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -95,6 +95,15 @@ export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
|
|
|
95
95
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
96
96
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
|
97
97
|
export declare function execAgent(options: ExecOptions): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
100
|
+
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
101
|
+
* project/default) and execs the real binary with the user's RAW args and the
|
|
102
|
+
* per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
|
|
103
|
+
* mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
|
|
104
|
+
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
105
|
+
*/
|
|
106
|
+
export declare function execShimPassthrough(agent: AgentId, rawArgs: string[], cwd: string, pinnedVersion?: string): Promise<number>;
|
|
98
107
|
/**
|
|
99
108
|
* Patterns that indicate a rate/usage limit. Matching is intentionally broad
|
|
100
109
|
* because providers phrase these differently -- Anthropic uses "5-hour limit"
|
package/dist/lib/exec.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import { ALL_MODES } from './types.js';
|
|
12
12
|
import { AGENTS } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './routines.js';
|
|
14
|
-
import { getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
14
|
+
import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
@@ -367,10 +367,27 @@ export function buildExecCommand(options) {
|
|
|
367
367
|
// Resolve to the absolute path of the shim so spawn doesn't depend on PATH —
|
|
368
368
|
// on Linux installs where the shims dir isn't on PATH, spawning the bare
|
|
369
369
|
// versioned name fails with ENOENT even though `agents view` shows the agent.
|
|
370
|
+
//
|
|
371
|
+
// On Windows, shims are bash scripts and cannot be executed by spawn() directly.
|
|
372
|
+
// buildExecEnv() already sets the isolation env vars (CLAUDE_CONFIG_DIR, CODEX_HOME,
|
|
373
|
+
// etc.) that the bash shim would set, so we can skip the shim entirely and resolve
|
|
374
|
+
// straight to the real binary via getBinaryPath.
|
|
370
375
|
if (options.version && cmd.length > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
if (process.platform === 'win32') {
|
|
377
|
+
const binaryPath = getBinaryPath(options.agent, options.version);
|
|
378
|
+
const binaryPathCmd = binaryPath + '.cmd';
|
|
379
|
+
if (fs.existsSync(binaryPathCmd)) {
|
|
380
|
+
cmd[0] = binaryPathCmd;
|
|
381
|
+
}
|
|
382
|
+
else if (fs.existsSync(binaryPath)) {
|
|
383
|
+
cmd[0] = binaryPath;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const versionedName = `${cmd[0]}@${options.version}`;
|
|
388
|
+
const absPath = path.join(getShimsDir(), versionedName);
|
|
389
|
+
cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
|
|
390
|
+
}
|
|
374
391
|
}
|
|
375
392
|
// Add reasoning effort flags (before mode flags for codex -c positioning)
|
|
376
393
|
// For codex, -c must come before 'exec' subcommand, so we insert at position 1
|
|
@@ -458,6 +475,42 @@ export async function execAgent(options) {
|
|
|
458
475
|
const { exitCode } = await spawnAgent(options);
|
|
459
476
|
return exitCode;
|
|
460
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
480
|
+
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
481
|
+
* project/default) and execs the real binary with the user's RAW args and the
|
|
482
|
+
* per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
|
|
483
|
+
* mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
|
|
484
|
+
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
485
|
+
*/
|
|
486
|
+
export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
487
|
+
const version = pinnedVersion ?? resolveVersion(agent, cwd) ?? undefined;
|
|
488
|
+
if (!version || !isVersionInstalled(agent, version)) {
|
|
489
|
+
process.stderr.write(`agents: no installed default for ${agent}. Set one with: agents use ${agent} <version>\n`);
|
|
490
|
+
return 127;
|
|
491
|
+
}
|
|
492
|
+
let binary = getBinaryPath(agent, version);
|
|
493
|
+
if (process.platform === 'win32') {
|
|
494
|
+
// npm ships <cmd>.cmd alongside the bare script on Windows; that's the runnable form.
|
|
495
|
+
const cmdPath = binary + '.cmd';
|
|
496
|
+
if (fs.existsSync(cmdPath))
|
|
497
|
+
binary = cmdPath;
|
|
498
|
+
}
|
|
499
|
+
// The only flag the bash shim injects (codex); everything else is transparent.
|
|
500
|
+
const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
|
|
501
|
+
// mode/effort are required by ExecOptions but unused by buildExecEnv (which only
|
|
502
|
+
// derives the per-version config-dir env); pass the agent's default to satisfy the type.
|
|
503
|
+
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
|
|
504
|
+
const useShell = process.platform === 'win32' && (!path.isAbsolute(binary) || binary.endsWith('.cmd'));
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
const child = spawn(binary, [...launchArgs, ...rawArgs], { cwd, stdio: 'inherit', env, shell: useShell });
|
|
507
|
+
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
508
|
+
child.on('error', (err) => {
|
|
509
|
+
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
510
|
+
resolve(127);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
461
514
|
/**
|
|
462
515
|
* Spawn an agent process and return its exit code plus a tee'd copy of stderr.
|
|
463
516
|
*
|
|
@@ -495,11 +548,14 @@ async function spawnAgent(options) {
|
|
|
495
548
|
const stdio = interactive
|
|
496
549
|
? ['inherit', 'inherit', 'inherit']
|
|
497
550
|
: ['inherit', piped ? 'pipe' : 'inherit', 'pipe'];
|
|
551
|
+
// On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
|
|
552
|
+
// whether addressed by name or absolute path.
|
|
553
|
+
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
498
554
|
const child = spawn(executable, args, {
|
|
499
555
|
cwd: options.cwd || process.cwd(),
|
|
500
556
|
stdio,
|
|
501
557
|
env: buildExecEnv(options),
|
|
502
|
-
shell:
|
|
558
|
+
shell: useShell,
|
|
503
559
|
});
|
|
504
560
|
// Mark startup time (time from function call to process spawn)
|
|
505
561
|
timer.mark('startup');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
2
|
+
export declare function whichCommand(platform?: NodeJS.Platform): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
5
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
6
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
7
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executable resolution, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
6
|
+
export function whichCommand(platform = process.platform) {
|
|
7
|
+
return platform === 'win32' ? 'where' : 'which';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
11
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
12
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
13
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
14
|
+
*/
|
|
15
|
+
export function findExecutable(name, platform = process.platform) {
|
|
16
|
+
try {
|
|
17
|
+
const out = execFileSync(whichCommand(platform), [name], { encoding: 'utf-8' });
|
|
18
|
+
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
19
|
+
return first || null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const IS_WINDOWS: boolean;
|
|
16
|
+
export declare const IS_MACOS: boolean;
|
|
17
|
+
export declare const IS_LINUX: boolean;
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export const IS_WINDOWS = process.platform === 'win32';
|
|
16
|
+
export const IS_MACOS = process.platform === 'darwin';
|
|
17
|
+
export const IS_LINUX = process.platform === 'linux';
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this positional argument look like a filesystem path (vs a search term)?
|
|
3
|
+
*
|
|
4
|
+
* POSIX markers (`.`, `./`, `../`, `/`, `~`) are recognized on every platform —
|
|
5
|
+
* identical to the long-standing behavior. Windows-only shapes (drive-letter
|
|
6
|
+
* `C:\…`, UNC `\\…`, backslash-relative `.\` / `..\`) are recognized ONLY on
|
|
7
|
+
* win32, so a literal `C:\repo` typed on macOS/Linux still resolves as a search
|
|
8
|
+
* term — i.e. no behavior change off Windows.
|
|
9
|
+
*/
|
|
10
|
+
export declare function looksLikePath(query: string, platform?: NodeJS.Platform): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a path for comparison/prefix-matching: backslashes folded to forward
|
|
13
|
+
* slashes and lowercased on Windows (its filesystem is case-insensitive). On
|
|
14
|
+
* POSIX the input is returned unchanged, so callers behave exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toComparablePath(p: string, platform?: NodeJS.Platform): string;
|
|
17
|
+
/**
|
|
18
|
+
* Canonical home directory. Use this instead of `process.env.HOME`, which is
|
|
19
|
+
* unset on Windows (where the home is `USERPROFILE`); `os.homedir()` resolves
|
|
20
|
+
* correctly on all three platforms.
|
|
21
|
+
*/
|
|
22
|
+
export declare function homeDir(): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path classification + normalization, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
/** Windows drive-letter absolute path: `C:\` or `C:/`. */
|
|
6
|
+
const WIN_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
|
7
|
+
/**
|
|
8
|
+
* Does this positional argument look like a filesystem path (vs a search term)?
|
|
9
|
+
*
|
|
10
|
+
* POSIX markers (`.`, `./`, `../`, `/`, `~`) are recognized on every platform —
|
|
11
|
+
* identical to the long-standing behavior. Windows-only shapes (drive-letter
|
|
12
|
+
* `C:\…`, UNC `\\…`, backslash-relative `.\` / `..\`) are recognized ONLY on
|
|
13
|
+
* win32, so a literal `C:\repo` typed on macOS/Linux still resolves as a search
|
|
14
|
+
* term — i.e. no behavior change off Windows.
|
|
15
|
+
*/
|
|
16
|
+
export function looksLikePath(query, platform = process.platform) {
|
|
17
|
+
if (query === '.' ||
|
|
18
|
+
query.startsWith('./') ||
|
|
19
|
+
query.startsWith('../') ||
|
|
20
|
+
query.startsWith('/') ||
|
|
21
|
+
query.startsWith('~')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
return (WIN_DRIVE_RE.test(query) ||
|
|
26
|
+
query.startsWith('\\\\') ||
|
|
27
|
+
query.startsWith('.\\') ||
|
|
28
|
+
query.startsWith('..\\'));
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a path for comparison/prefix-matching: backslashes folded to forward
|
|
34
|
+
* slashes and lowercased on Windows (its filesystem is case-insensitive). On
|
|
35
|
+
* POSIX the input is returned unchanged, so callers behave exactly as before.
|
|
36
|
+
*/
|
|
37
|
+
export function toComparablePath(p, platform = process.platform) {
|
|
38
|
+
if (platform === 'win32')
|
|
39
|
+
return p.replace(/\\/g, '/').toLowerCase();
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Canonical home directory. Use this instead of `process.env.HOME`, which is
|
|
44
|
+
* unset on Windows (where the home is `USERPROFILE`); `os.homedir()` resolves
|
|
45
|
+
* correctly on all three platforms.
|
|
46
|
+
*/
|
|
47
|
+
export function homeDir() {
|
|
48
|
+
return os.homedir();
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process liveness / control, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Is a process with this PID currently alive?
|
|
6
|
+
*
|
|
7
|
+
* Uses the signal-0 probe, which is cross-platform in Node (Windows included —
|
|
8
|
+
* it maps to OpenProcess). Returns false on any error (no such process, or no
|
|
9
|
+
* permission to signal it), matching the long-standing call sites that treat a
|
|
10
|
+
* throw from `process.kill(pid, 0)` as "not running".
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAlive(pid: number): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process liveness / control, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Is a process with this PID currently alive?
|
|
6
|
+
*
|
|
7
|
+
* Uses the signal-0 probe, which is cross-platform in Node (Windows included —
|
|
8
|
+
* it maps to OpenProcess). Returns false on any error (no such process, or no
|
|
9
|
+
* permission to signal it), matching the long-standing call sites that treat a
|
|
10
|
+
* throw from `process.kill(pid, 0)` as "not running".
|
|
11
|
+
*/
|
|
12
|
+
export function isAlive(pid) {
|
|
13
|
+
if (!pid || pid <= 0)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/lib/pty-client.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
|
|
13
13
|
const CONNECT_TIMEOUT_MS = 5000;
|
|
14
14
|
const RESPONSE_TIMEOUT_MS = 30000;
|
|
15
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
15
16
|
/**
|
|
16
17
|
* Send a request to the PTY server and return the response.
|
|
17
18
|
* Auto-starts the server if not running.
|
|
@@ -41,11 +42,13 @@ async function ensureServer() {
|
|
|
41
42
|
});
|
|
42
43
|
child.unref();
|
|
43
44
|
fs.closeSync(logFd);
|
|
44
|
-
// Wait for
|
|
45
|
+
// Wait for the server to become reachable. On Unix the socket file appearing is
|
|
46
|
+
// a cheap readiness signal; on Windows the named pipe is not a filesystem object
|
|
47
|
+
// (fs.existsSync always returns false), so we just attempt the ping directly.
|
|
45
48
|
const socketPath = getSocketPath();
|
|
46
49
|
const deadline = Date.now() + 5000;
|
|
47
50
|
while (Date.now() < deadline) {
|
|
48
|
-
if (fs.existsSync(socketPath)) {
|
|
51
|
+
if (IS_WINDOWS || fs.existsSync(socketPath)) {
|
|
49
52
|
// Verify we can connect
|
|
50
53
|
try {
|
|
51
54
|
await sendRequest({ action: 'ping' });
|
|
@@ -70,9 +73,11 @@ function getServerSpawnArgs() {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
catch { }
|
|
73
|
-
// Fallback: use the globally installed agents command
|
|
76
|
+
// Fallback: use the globally installed agents command. `which` is Unix-only;
|
|
77
|
+
// Windows uses `where`, which can return multiple lines — take the first.
|
|
74
78
|
try {
|
|
75
|
-
const
|
|
79
|
+
const lookup = IS_WINDOWS ? 'where agents' : 'which agents';
|
|
80
|
+
const agentsBin = execSync(lookup, { encoding: 'utf-8' }).split(/\r?\n/)[0].trim();
|
|
76
81
|
if (agentsBin) {
|
|
77
82
|
return { bin: agentsBin, args: ['pty', '_server'] };
|
|
78
83
|
}
|
|
@@ -86,7 +91,10 @@ function getServerSpawnArgs() {
|
|
|
86
91
|
function sendRequest(req) {
|
|
87
92
|
return new Promise((resolve, reject) => {
|
|
88
93
|
const socketPath = getSocketPath();
|
|
89
|
-
|
|
94
|
+
// On Unix a missing socket file means the server isn't up — fail fast with a
|
|
95
|
+
// clear message. On Windows the named pipe isn't a filesystem object, so we
|
|
96
|
+
// skip the probe and let createConnection surface ENOENT/connection errors.
|
|
97
|
+
if (!IS_WINDOWS && !fs.existsSync(socketPath)) {
|
|
90
98
|
reject(new Error('PTY server socket not found. Is the server running?'));
|
|
91
99
|
return;
|
|
92
100
|
}
|
package/dist/lib/pty-server.d.ts
CHANGED
|
@@ -18,7 +18,30 @@
|
|
|
18
18
|
* Returns null on any error so callers can skip the guard rather than crash.
|
|
19
19
|
*/
|
|
20
20
|
export declare function captureProcessStartTime(pid: number): string | null;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
|
|
23
|
+
* finishes — that line drives completion detection in the exec/read flow.
|
|
24
|
+
* The separator and exit-code variable are shell-family specific:
|
|
25
|
+
* POSIX sh/zsh/bash : `cmd; echo "S:$?"`
|
|
26
|
+
* PowerShell : `cmd; echo "S:$LASTEXITCODE"`
|
|
27
|
+
* cmd.exe : `cmd & echo S:%errorlevel%` (`&` always runs the echo)
|
|
28
|
+
* Only the completion marker matters; the numeric exit code is informational
|
|
29
|
+
* (the authoritative code comes from node-pty's onExit).
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildSentinelCommand(shell: string, command: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the IPC endpoint for a given platform + PTY scratch dir. Pure so both
|
|
34
|
+
* branches are testable without stubbing process.platform.
|
|
35
|
+
*
|
|
36
|
+
* Unix: an AF_UNIX socket file inside the scratch dir.
|
|
37
|
+
* Windows: a named pipe (`\\.\pipe\…`). Named pipes are NOT filesystem objects,
|
|
38
|
+
* so the name is derived from a hash of the (per-user) scratch dir to keep it
|
|
39
|
+
* stable across invocations and isolated per user — and callers must never probe
|
|
40
|
+
* it with fs.existsSync (it always reports false). Both forms are accepted by
|
|
41
|
+
* net.createServer/createConnection.
|
|
42
|
+
*/
|
|
43
|
+
export declare function derivePtyEndpoint(platform: NodeJS.Platform, ptyDir: string): string;
|
|
44
|
+
/** Get the IPC endpoint the PTY server listens on / clients connect to. */
|
|
22
45
|
export declare function getSocketPath(): string;
|
|
23
46
|
/** Get the path to the PTY server PID file. */
|
|
24
47
|
export declare function getPtyPidPath(): string;
|