@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/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 <agent>[@version] Deep details for one agent+version — paths, capabilities, resources, drill into any kind
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;
@@ -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
- this.waiters.set(id, resolve);
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
  }
@@ -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
- try {
118
- process.kill(pid, 0);
118
+ if (isAlive(pid))
119
119
  return true;
120
- }
121
- catch {
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) {
@@ -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
- const versionedName = `${cmd[0]}@${options.version}`;
372
- const absPath = path.join(getShimsDir(), versionedName);
373
- cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
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: false,
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
+ }
@@ -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 socket to appear
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 agentsBin = execSync('which agents', { encoding: 'utf-8' }).trim();
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
- if (!fs.existsSync(socketPath)) {
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
  }
@@ -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
- /** Get the unix socket path for the PTY server. */
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;