@phnx-labs/agents-cli 1.20.6 → 1.20.7
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 +36 -0
- package/dist/commands/computer-actions.js +328 -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/index.js +23 -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/session/artifacts.js +8 -2
- package/dist/lib/shims.js +45 -0
- package/dist/lib/teams/agents.js +5 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +18 -1
|
@@ -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;
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as crypto from 'crypto';
|
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { getPtyDir as getPtyDirRoot } from './state.js';
|
|
18
|
+
import { isAlive } from './platform/index.js';
|
|
18
19
|
/**
|
|
19
20
|
* Capture a stable identifier for a process at the moment it was started.
|
|
20
21
|
* Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
|
|
@@ -52,6 +53,7 @@ export function captureProcessStartTime(pid) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
// --- Constants ---
|
|
56
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
55
57
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
58
|
const SOCKET_NAME = 'pty.sock';
|
|
57
59
|
const PID_FILE = 'pty.pid';
|
|
@@ -72,24 +74,81 @@ const PTY_ENV_ALLOWLIST = [
|
|
|
72
74
|
'EDITOR', 'VISUAL', 'PAGER', 'LESS',
|
|
73
75
|
'NO_COLOR', 'FORCE_COLOR',
|
|
74
76
|
];
|
|
77
|
+
/**
|
|
78
|
+
* Windows allowlist. cmd.exe / PowerShell refuse to start (or misbehave) without
|
|
79
|
+
* SystemRoot, ComSpec, PATHEXT and the USERPROFILE/APPDATA family, so a Unix-style
|
|
80
|
+
* allowlist would spawn a broken shell. PATH/TERM/color/NODE vars are shared with
|
|
81
|
+
* the Unix list; the rest are Windows-specific.
|
|
82
|
+
*/
|
|
83
|
+
const PTY_ENV_ALLOWLIST_WIN = [
|
|
84
|
+
'SystemRoot', 'SystemDrive', 'windir', 'ComSpec', 'PATH', 'PATHEXT',
|
|
85
|
+
'TEMP', 'TMP', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'HOME',
|
|
86
|
+
'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMDATA',
|
|
87
|
+
'USERNAME', 'USERDOMAIN', 'COMPUTERNAME', 'OS',
|
|
88
|
+
'PROCESSOR_ARCHITECTURE', 'NUMBER_OF_PROCESSORS',
|
|
89
|
+
'TERM', 'COLORTERM', 'NO_COLOR', 'FORCE_COLOR',
|
|
90
|
+
'NODE_PATH', 'BUN_INSTALL',
|
|
91
|
+
];
|
|
75
92
|
function buildPtyEnv() {
|
|
76
93
|
const env = {};
|
|
77
|
-
|
|
94
|
+
const allowlist = IS_WINDOWS ? PTY_ENV_ALLOWLIST_WIN : PTY_ENV_ALLOWLIST;
|
|
95
|
+
for (const key of allowlist) {
|
|
78
96
|
const v = process.env[key];
|
|
79
97
|
if (v !== undefined)
|
|
80
98
|
env[key] = v;
|
|
81
99
|
}
|
|
82
100
|
return env;
|
|
83
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
|
|
104
|
+
* finishes — that line drives completion detection in the exec/read flow.
|
|
105
|
+
* The separator and exit-code variable are shell-family specific:
|
|
106
|
+
* POSIX sh/zsh/bash : `cmd; echo "S:$?"`
|
|
107
|
+
* PowerShell : `cmd; echo "S:$LASTEXITCODE"`
|
|
108
|
+
* cmd.exe : `cmd & echo S:%errorlevel%` (`&` always runs the echo)
|
|
109
|
+
* Only the completion marker matters; the numeric exit code is informational
|
|
110
|
+
* (the authoritative code comes from node-pty's onExit).
|
|
111
|
+
*/
|
|
112
|
+
export function buildSentinelCommand(shell, command) {
|
|
113
|
+
// Split on both separators: a Windows shell path (`C:\…\cmd.exe`) must be
|
|
114
|
+
// recognized even when this code runs under POSIX path.basename, which does
|
|
115
|
+
// not treat `\` as a separator.
|
|
116
|
+
const name = (shell.split(/[\\/]/).pop() || shell).toLowerCase();
|
|
117
|
+
if (name === 'cmd.exe' || name === 'cmd') {
|
|
118
|
+
return `${command} & echo ${SENTINEL}:%errorlevel%`;
|
|
119
|
+
}
|
|
120
|
+
if (name === 'powershell.exe' || name === 'powershell' || name === 'pwsh.exe' || name === 'pwsh') {
|
|
121
|
+
return `${command}; echo "${SENTINEL}:$LASTEXITCODE"`;
|
|
122
|
+
}
|
|
123
|
+
return `${command}; echo "${SENTINEL}:$?"`;
|
|
124
|
+
}
|
|
84
125
|
/** Get the PTY helper directory, creating it if needed. */
|
|
85
126
|
function getPtyDir() {
|
|
86
127
|
const dir = getPtyDirRoot();
|
|
87
128
|
fs.mkdirSync(dir, { recursive: true });
|
|
88
129
|
return dir;
|
|
89
130
|
}
|
|
90
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the IPC endpoint for a given platform + PTY scratch dir. Pure so both
|
|
133
|
+
* branches are testable without stubbing process.platform.
|
|
134
|
+
*
|
|
135
|
+
* Unix: an AF_UNIX socket file inside the scratch dir.
|
|
136
|
+
* Windows: a named pipe (`\\.\pipe\…`). Named pipes are NOT filesystem objects,
|
|
137
|
+
* so the name is derived from a hash of the (per-user) scratch dir to keep it
|
|
138
|
+
* stable across invocations and isolated per user — and callers must never probe
|
|
139
|
+
* it with fs.existsSync (it always reports false). Both forms are accepted by
|
|
140
|
+
* net.createServer/createConnection.
|
|
141
|
+
*/
|
|
142
|
+
export function derivePtyEndpoint(platform, ptyDir) {
|
|
143
|
+
if (platform === 'win32') {
|
|
144
|
+
const hash = crypto.createHash('sha1').update(ptyDir).digest('hex').slice(0, 16);
|
|
145
|
+
return `\\\\.\\pipe\\agents-pty-${hash}`;
|
|
146
|
+
}
|
|
147
|
+
return path.join(ptyDir, SOCKET_NAME);
|
|
148
|
+
}
|
|
149
|
+
/** Get the IPC endpoint the PTY server listens on / clients connect to. */
|
|
91
150
|
export function getSocketPath() {
|
|
92
|
-
return
|
|
151
|
+
return derivePtyEndpoint(process.platform, getPtyDir());
|
|
93
152
|
}
|
|
94
153
|
/** Get the path to the PTY server PID file. */
|
|
95
154
|
export function getPtyPidPath() {
|
|
@@ -109,16 +168,17 @@ export function isPtyServerRunning() {
|
|
|
109
168
|
const pid = parseInt(fs.readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
110
169
|
if (isNaN(pid))
|
|
111
170
|
return false;
|
|
112
|
-
|
|
113
|
-
|
|
171
|
+
if (isAlive(pid))
|
|
172
|
+
return true;
|
|
114
173
|
}
|
|
115
174
|
catch {
|
|
116
|
-
|
|
117
|
-
fs.unlinkSync(pidPath);
|
|
118
|
-
}
|
|
119
|
-
catch { }
|
|
120
|
-
return false;
|
|
175
|
+
// read failed — fall through and treat the pid file as stale
|
|
121
176
|
}
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(pidPath);
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
return false;
|
|
122
182
|
}
|
|
123
183
|
// --- Logging ---
|
|
124
184
|
function rotateLogsIfNeeded(logPath) {
|
|
@@ -221,8 +281,10 @@ export async function runPtyServer() {
|
|
|
221
281
|
fs.unlinkSync(pidPath);
|
|
222
282
|
}
|
|
223
283
|
catch { } });
|
|
224
|
-
// Remove stale socket from a prior crashed server. Safe now that we hold the PID
|
|
225
|
-
|
|
284
|
+
// Remove stale socket from a prior crashed server. Safe now that we hold the PID
|
|
285
|
+
// slot. Windows named pipes are not filesystem inodes — they vanish with their
|
|
286
|
+
// owning process, so there's nothing to unlink (and existsSync always reports false).
|
|
287
|
+
if (!IS_WINDOWS && fs.existsSync(socketPath)) {
|
|
226
288
|
try {
|
|
227
289
|
fs.unlinkSync(socketPath);
|
|
228
290
|
}
|
|
@@ -286,8 +348,10 @@ export async function runPtyServer() {
|
|
|
286
348
|
case 'start': {
|
|
287
349
|
const rows = req.params?.rows || 24;
|
|
288
350
|
const cols = req.params?.cols || 120;
|
|
289
|
-
const shell = req.params?.shell
|
|
290
|
-
|
|
351
|
+
const shell = req.params?.shell
|
|
352
|
+
|| (IS_WINDOWS ? (process.env.ComSpec || 'powershell.exe') : (process.env.SHELL || 'zsh'));
|
|
353
|
+
const cwd = req.params?.cwd
|
|
354
|
+
|| (IS_WINDOWS ? (process.env.USERPROFILE || process.env.HOME || process.cwd()) : (process.env.HOME || '/'));
|
|
291
355
|
const id = generateId();
|
|
292
356
|
let ptyProcess;
|
|
293
357
|
try {
|
|
@@ -358,7 +422,9 @@ export async function runPtyServer() {
|
|
|
358
422
|
session.appActive = true;
|
|
359
423
|
session.activeCommand = command;
|
|
360
424
|
session.pendingOutput = '';
|
|
361
|
-
|
|
425
|
+
// Windows conpty submits on CR; POSIX line discipline expects LF.
|
|
426
|
+
const submit = IS_WINDOWS ? '\r' : '\n';
|
|
427
|
+
session.pty.write(`${buildSentinelCommand(session.shell, command)}${submit}`);
|
|
362
428
|
session.lastActivity = Date.now();
|
|
363
429
|
return { ok: true, submitted: true };
|
|
364
430
|
}
|
|
@@ -525,20 +591,28 @@ export async function runPtyServer() {
|
|
|
525
591
|
// any local user with execute on the parent dir could connect to the socket
|
|
526
592
|
// during the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
527
593
|
// socket mode advisory only, so the parent dir is the real boundary.
|
|
594
|
+
//
|
|
595
|
+
// On Windows the transport is a named pipe, not a filesystem inode: chmod/umask
|
|
596
|
+
// are no-ops (and umask throws in some Node builds), and pipe ACLs default to
|
|
597
|
+
// the creating user. So we skip the Unix hardening entirely there.
|
|
528
598
|
const agentsDir = getPtyDirRoot();
|
|
529
599
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
600
|
+
if (!IS_WINDOWS) {
|
|
601
|
+
fs.chmodSync(agentsDir, 0o700);
|
|
602
|
+
// umask covers any inherited group/other bits while listen() is creating
|
|
603
|
+
// the socket inode — it only matters for the unobservable instant before
|
|
604
|
+
// we can chmod the inode itself.
|
|
605
|
+
process.umask(0o077);
|
|
606
|
+
}
|
|
535
607
|
await new Promise((resolve) => {
|
|
536
608
|
server.listen(socketPath, () => resolve());
|
|
537
609
|
});
|
|
538
610
|
// Surface chmod failures: a 0o600 socket is a load-bearing security
|
|
539
611
|
// assumption, not a nice-to-have. If we can't lock it down, refuse to
|
|
540
|
-
// start so the caller learns immediately.
|
|
541
|
-
|
|
612
|
+
// start so the caller learns immediately. (No-op on Windows named pipes.)
|
|
613
|
+
if (!IS_WINDOWS) {
|
|
614
|
+
fs.chmodSync(socketPath, 0o600);
|
|
615
|
+
}
|
|
542
616
|
log('INFO', `PTY server started (PID: ${process.pid}, socket: ${socketPath})`);
|
|
543
617
|
// Shutdown handler
|
|
544
618
|
function shutdown() {
|
|
@@ -549,10 +623,13 @@ export async function runPtyServer() {
|
|
|
549
623
|
sessions.clear();
|
|
550
624
|
clearInterval(cleanupInterval);
|
|
551
625
|
server.close();
|
|
552
|
-
|
|
553
|
-
|
|
626
|
+
// Named pipes are reclaimed by the OS on close; only Unix sockets leave a file.
|
|
627
|
+
if (!IS_WINDOWS) {
|
|
628
|
+
try {
|
|
629
|
+
fs.unlinkSync(socketPath);
|
|
630
|
+
}
|
|
631
|
+
catch { }
|
|
554
632
|
}
|
|
555
|
-
catch { }
|
|
556
633
|
try {
|
|
557
634
|
fs.unlinkSync(getPtyPidPath());
|
|
558
635
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { parseSession } from './parse.js';
|
|
11
|
+
import { toComparablePath } from '../platform/index.js';
|
|
11
12
|
/** Tool names that produce file artifacts (writes, edits, patches). */
|
|
12
13
|
const WRITE_TOOLS = new Set([
|
|
13
14
|
'Write', 'Edit', 'write_file', 'edit_file', 'create_file', 'replace', 'patch',
|
|
@@ -87,8 +88,13 @@ export function resolveArtifact(artifacts, name) {
|
|
|
87
88
|
return byBase[0];
|
|
88
89
|
if (byBase.length > 1)
|
|
89
90
|
return byBase[0];
|
|
90
|
-
// Path suffix match (e.g. "src/foo.ts")
|
|
91
|
-
|
|
91
|
+
// Path suffix match (e.g. "src/foo.ts"). Normalize so Windows `\` paths match
|
|
92
|
+
// a forward-slash query too; on POSIX this is identical to the old comparison.
|
|
93
|
+
const target = toComparablePath(name);
|
|
94
|
+
const bySuffix = artifacts.filter(a => {
|
|
95
|
+
const ap = toComparablePath(a.path);
|
|
96
|
+
return ap.endsWith('/' + target) || ap === target;
|
|
97
|
+
});
|
|
92
98
|
if (bySuffix.length >= 1)
|
|
93
99
|
return bySuffix[0];
|
|
94
100
|
return null;
|
package/dist/lib/shims.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { confirm, select } from '@inquirer/prompts';
|
|
16
|
+
import { IS_WINDOWS } from './platform/index.js';
|
|
16
17
|
import { getShimsDir, getVersionsDir, getBackupsDir, ensureAgentsDir } from './state.js';
|
|
17
18
|
export { getShimsDir };
|
|
18
19
|
import { AGENTS } from './agents.js';
|
|
@@ -521,8 +522,28 @@ export function createShim(agent) {
|
|
|
521
522
|
const shimPath = path.join(shimsDir, agentConfig.cliCommand);
|
|
522
523
|
const script = generateShimScript(agent);
|
|
523
524
|
fs.writeFileSync(shimPath, script, { mode: 0o755 });
|
|
525
|
+
// Windows can't execute the bash shim directly. Drop a `.cmd` companion next
|
|
526
|
+
// to it that delegates to the node-side transparent resolver (`agents __shim`),
|
|
527
|
+
// so the version resolution stays single-sourced instead of reimplemented in batch.
|
|
528
|
+
if (IS_WINDOWS) {
|
|
529
|
+
writeWindowsCmdShim(shimPath + '.cmd', agentConfig.cliCommand);
|
|
530
|
+
}
|
|
524
531
|
return shimPath;
|
|
525
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
|
|
535
|
+
* `spec` is the agent's cliCommand for the default-version shim, or
|
|
536
|
+
* `cliCommand@version` for a versioned alias. node + the dist entrypoint are
|
|
537
|
+
* resolved at generation time so the launcher does not depend on `agents`
|
|
538
|
+
* already being on PATH.
|
|
539
|
+
*/
|
|
540
|
+
function writeWindowsCmdShim(cmdPath, spec) {
|
|
541
|
+
const indexJs = getAgentsBinForGeneratedShim();
|
|
542
|
+
const content = `@echo off\r\n` +
|
|
543
|
+
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
544
|
+
`node "${indexJs}" __shim ${spec} %*\r\n`;
|
|
545
|
+
fs.writeFileSync(cmdPath, content);
|
|
546
|
+
}
|
|
526
547
|
/**
|
|
527
548
|
* Remove the shim for an agent.
|
|
528
549
|
*/
|
|
@@ -532,6 +553,12 @@ export function removeShim(agent) {
|
|
|
532
553
|
const shimPath = path.join(shimsDir, agentConfig.cliCommand);
|
|
533
554
|
if (fs.existsSync(shimPath)) {
|
|
534
555
|
fs.unlinkSync(shimPath);
|
|
556
|
+
if (IS_WINDOWS) {
|
|
557
|
+
try {
|
|
558
|
+
fs.unlinkSync(shimPath + '.cmd');
|
|
559
|
+
}
|
|
560
|
+
catch { }
|
|
561
|
+
}
|
|
535
562
|
return true;
|
|
536
563
|
}
|
|
537
564
|
return false;
|
|
@@ -682,6 +709,9 @@ export function createVersionedAlias(agent, version) {
|
|
|
682
709
|
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
683
710
|
const script = generateVersionedAliasScript(agent, version);
|
|
684
711
|
fs.writeFileSync(aliasPath, script, { mode: 0o755 });
|
|
712
|
+
if (IS_WINDOWS) {
|
|
713
|
+
writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`);
|
|
714
|
+
}
|
|
685
715
|
return aliasPath;
|
|
686
716
|
}
|
|
687
717
|
/**
|
|
@@ -693,6 +723,12 @@ export function removeVersionedAlias(agent, version) {
|
|
|
693
723
|
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
694
724
|
if (fs.existsSync(aliasPath)) {
|
|
695
725
|
fs.unlinkSync(aliasPath);
|
|
726
|
+
if (IS_WINDOWS) {
|
|
727
|
+
try {
|
|
728
|
+
fs.unlinkSync(aliasPath + '.cmd');
|
|
729
|
+
}
|
|
730
|
+
catch { }
|
|
731
|
+
}
|
|
696
732
|
return true;
|
|
697
733
|
}
|
|
698
734
|
return false;
|
|
@@ -1488,6 +1524,15 @@ Then restart your shell or run:
|
|
|
1488
1524
|
* Returns true if added, false if already present or failed.
|
|
1489
1525
|
*/
|
|
1490
1526
|
export function addShimsToPath(overrides) {
|
|
1527
|
+
// Windows has no shell rc file to edit: the primary `agents` command is already
|
|
1528
|
+
// on PATH via npm's global bin, and bare shorthands / versioned aliases are
|
|
1529
|
+
// handled by the `.cmd` shims plus the PATH guidance printed at install time.
|
|
1530
|
+
// Report "already present" so callers don't emit a misleading "added to
|
|
1531
|
+
// ~/.bashrc / source ~/.bashrc" message. (The `shell` override is the test hook
|
|
1532
|
+
// for exercising the POSIX path, so it bypasses this short-circuit.)
|
|
1533
|
+
if (IS_WINDOWS && !overrides?.shell) {
|
|
1534
|
+
return { success: true, alreadyPresent: true };
|
|
1535
|
+
}
|
|
1491
1536
|
const shimsDir = overrides?.shimsDir || getShimsDir();
|
|
1492
1537
|
const { rcFile, rcPath, shell } = getShellRcFile(overrides);
|
|
1493
1538
|
// Read current rc file content
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import { randomUUID } from 'crypto';
|
|
16
16
|
import { resolveAgentsDir } from './persistence.js';
|
|
17
|
+
import { findExecutable } from '../platform/index.js';
|
|
17
18
|
import { normalizeEvents } from './parsers.js';
|
|
18
19
|
import { debug } from './debug.js';
|
|
19
20
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from '../gemini-settings.js';
|
|
@@ -300,13 +301,10 @@ export function checkCliAvailable(agentType) {
|
|
|
300
301
|
if (!executable) {
|
|
301
302
|
return [false, `Unknown agent type: ${agentType}`];
|
|
302
303
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
catch {
|
|
308
|
-
return [false, `CLI tool '${executable}' not found in PATH. Install it first.`];
|
|
309
|
-
}
|
|
304
|
+
const resolved = findExecutable(executable);
|
|
305
|
+
return resolved
|
|
306
|
+
? [true, resolved]
|
|
307
|
+
: [false, `CLI tool '${executable}' not found in PATH. Install it first.`];
|
|
310
308
|
}
|
|
311
309
|
/** Check availability of all known agent CLIs. Returns a map of agent type to install status. */
|
|
312
310
|
export function checkAllClis() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.7",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -95,6 +95,11 @@ function writeAliasShims() {
|
|
|
95
95
|
const target = path.join(SHIMS_DIR, name);
|
|
96
96
|
const script = `#!/bin/sh\nAGENTS_BIN=${shellQuote(AGENTS_BIN)}\nif [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then\n echo "agents: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2\n exit 127\nfi\nexec "$AGENTS_BIN" ${name} "$@"\n`;
|
|
97
97
|
fs.writeFileSync(target, script, { mode: 0o755 });
|
|
98
|
+
// Windows can't run the POSIX shim; drop a `.cmd` companion that invokes the
|
|
99
|
+
// entrypoint via node so the bare shorthand works in a Windows shell.
|
|
100
|
+
if (process.platform === 'win32') {
|
|
101
|
+
fs.writeFileSync(target + '.cmd', `@echo off\r\nnode "${AGENTS_BIN}" ${name} %*\r\n`);
|
|
102
|
+
}
|
|
98
103
|
written.push(name);
|
|
99
104
|
}
|
|
100
105
|
return written;
|
|
@@ -139,8 +144,20 @@ function isAlreadyConfigured(rcFile) {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
async function main() {
|
|
147
|
+
// Windows has no shell rc files to edit. Write the `.cmd` shorthands and point
|
|
148
|
+
// the user at the PATH entry they can add (the primary `agents` command is
|
|
149
|
+
// already on PATH via npm's global bin, so this only affects bare shorthands
|
|
150
|
+
// and versioned aliases).
|
|
151
|
+
if (process.platform === 'win32') {
|
|
152
|
+
console.log(`\nagents-cli installed.`);
|
|
153
|
+
const written = writeAliasShims();
|
|
154
|
+
console.log(` Installed shorthands: ${written.join(', ')}`);
|
|
155
|
+
console.log(`\nTo use bare shorthands (${ALIASES.join(', ')}) and versioned aliases, add this to your PATH:`);
|
|
156
|
+
console.log(` ${SHIMS_DIR}`);
|
|
157
|
+
console.log(` PowerShell: setx PATH "$env:PATH;${SHIMS_DIR}" (then open a new terminal)`);
|
|
158
|
+
}
|
|
142
159
|
// Opt-in: AGENTS_INIT_SHELL=1 npm install -g @phnx-labs/agents-cli
|
|
143
|
-
if (process.env.AGENTS_INIT_SHELL === '1') {
|
|
160
|
+
else if (process.env.AGENTS_INIT_SHELL === '1') {
|
|
144
161
|
const rcFile = getShellRc();
|
|
145
162
|
if (!isAlreadyConfigured(rcFile)) {
|
|
146
163
|
const addition = `\n# agents-cli: version switching for AI coding agents\n${exportLine}\n`;
|