@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.
@@ -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
- for (const key of PTY_ENV_ALLOWLIST) {
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
- /** Get the unix socket path for the PTY server. */
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 path.join(getPtyDir(), SOCKET_NAME);
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
- process.kill(pid, 0);
113
- return true;
171
+ if (isAlive(pid))
172
+ return true;
114
173
  }
115
174
  catch {
116
- try {
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 slot.
225
- if (fs.existsSync(socketPath)) {
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 || process.env.SHELL || 'zsh';
290
- const cwd = req.params?.cwd || process.env.HOME || '/';
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
- session.pty.write(`${command}; echo "${SENTINEL}:$?"\n`);
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
- fs.chmodSync(agentsDir, 0o700);
531
- // umask covers any inherited group/other bits while listen() is creating
532
- // the socket inode it only matters for the unobservable instant before
533
- // we can chmod the inode itself.
534
- process.umask(0o077);
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
- fs.chmodSync(socketPath, 0o600);
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
- try {
553
- fs.unlinkSync(socketPath);
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
  }
@@ -212,8 +212,8 @@ export async function refresh(options = {}) {
212
212
  if (!isShimsInPath()) {
213
213
  const pathResult = addShimsToPath();
214
214
  if (pathResult.success && !pathResult.alreadyPresent) {
215
- console.log(chalk.green(`\nAdded shims to ~/${pathResult.rcFile}`));
216
- console.log(chalk.gray('Restart your shell or run: source ~/' + pathResult.rcFile));
215
+ console.log(chalk.green(`\nAdded shims to ${pathResult.location}`));
216
+ console.log(chalk.gray(pathResult.reloadHint));
217
217
  }
218
218
  else if (!pathResult.success) {
219
219
  console.log(chalk.yellow('\nCould not auto-add shims to PATH:'));
@@ -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
- const bySuffix = artifacts.filter(a => a.path.endsWith('/' + name) || a.path === name);
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;
@@ -248,20 +248,25 @@ export declare function isShimsInPath(): boolean;
248
248
  * Get shell configuration instructions for adding shims to PATH.
249
249
  */
250
250
  export declare function getPathSetupInstructions(): string;
251
+ interface ShimPathResult {
252
+ success: boolean;
253
+ alreadyPresent?: boolean;
254
+ rcFile?: string;
255
+ /** Human label of where the entry landed, e.g. `~/.zshrc` or `your user PATH`. */
256
+ location?: string;
257
+ /** Per-platform "how to pick it up" hint, e.g. `source ~/.zshrc` / open a new terminal. */
258
+ reloadHint?: string;
259
+ error?: string;
260
+ }
251
261
  /**
252
- * Add shims directory to shell PATH configuration.
253
- * Returns true if added, false if already present or failed.
262
+ * Add the shims directory to PATH: edits the shell rc file on POSIX, or registers
263
+ * it on the Windows User PATH (registry + WM_SETTINGCHANGE). Idempotent.
254
264
  */
255
265
  export declare function addShimsToPath(overrides?: {
256
266
  homeDir?: string;
257
267
  shell?: string;
258
268
  shimsDir?: string;
259
- }): {
260
- success: boolean;
261
- alreadyPresent?: boolean;
262
- rcFile?: string;
263
- error?: string;
264
- };
269
+ }): ShimPathResult;
265
270
  export declare function listAgentsWithInstalledVersions(): AgentId[];
266
271
  /**
267
272
  * Resource diff between two versions. Each field lists resources present in
package/dist/lib/shims.js CHANGED
@@ -11,8 +11,10 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import * as os from 'os';
14
+ import { execFileSync } from 'child_process';
14
15
  import { fileURLToPath } from 'url';
15
16
  import { confirm, select } from '@inquirer/prompts';
17
+ import { IS_WINDOWS } from './platform/index.js';
16
18
  import { getShimsDir, getVersionsDir, getBackupsDir, ensureAgentsDir } from './state.js';
17
19
  export { getShimsDir };
18
20
  import { AGENTS } from './agents.js';
@@ -521,8 +523,28 @@ export function createShim(agent) {
521
523
  const shimPath = path.join(shimsDir, agentConfig.cliCommand);
522
524
  const script = generateShimScript(agent);
523
525
  fs.writeFileSync(shimPath, script, { mode: 0o755 });
526
+ // Windows can't execute the bash shim directly. Drop a `.cmd` companion next
527
+ // to it that delegates to the node-side transparent resolver (`agents __shim`),
528
+ // so the version resolution stays single-sourced instead of reimplemented in batch.
529
+ if (IS_WINDOWS) {
530
+ writeWindowsCmdShim(shimPath + '.cmd', agentConfig.cliCommand);
531
+ }
524
532
  return shimPath;
525
533
  }
534
+ /**
535
+ * Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
536
+ * `spec` is the agent's cliCommand for the default-version shim, or
537
+ * `cliCommand@version` for a versioned alias. node + the dist entrypoint are
538
+ * resolved at generation time so the launcher does not depend on `agents`
539
+ * already being on PATH.
540
+ */
541
+ function writeWindowsCmdShim(cmdPath, spec) {
542
+ const indexJs = getAgentsBinForGeneratedShim();
543
+ const content = `@echo off\r\n` +
544
+ `rem Auto-generated by agents-cli - do not edit\r\n` +
545
+ `node "${indexJs}" __shim ${spec} %*\r\n`;
546
+ fs.writeFileSync(cmdPath, content);
547
+ }
526
548
  /**
527
549
  * Remove the shim for an agent.
528
550
  */
@@ -532,6 +554,12 @@ export function removeShim(agent) {
532
554
  const shimPath = path.join(shimsDir, agentConfig.cliCommand);
533
555
  if (fs.existsSync(shimPath)) {
534
556
  fs.unlinkSync(shimPath);
557
+ if (IS_WINDOWS) {
558
+ try {
559
+ fs.unlinkSync(shimPath + '.cmd');
560
+ }
561
+ catch { }
562
+ }
535
563
  return true;
536
564
  }
537
565
  return false;
@@ -682,6 +710,9 @@ export function createVersionedAlias(agent, version) {
682
710
  const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
683
711
  const script = generateVersionedAliasScript(agent, version);
684
712
  fs.writeFileSync(aliasPath, script, { mode: 0o755 });
713
+ if (IS_WINDOWS) {
714
+ writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`);
715
+ }
685
716
  return aliasPath;
686
717
  }
687
718
  /**
@@ -693,6 +724,12 @@ export function removeVersionedAlias(agent, version) {
693
724
  const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
694
725
  if (fs.existsSync(aliasPath)) {
695
726
  fs.unlinkSync(aliasPath);
727
+ if (IS_WINDOWS) {
728
+ try {
729
+ fs.unlinkSync(aliasPath + '.cmd');
730
+ }
731
+ catch { }
732
+ }
696
733
  return true;
697
734
  }
698
735
  return false;
@@ -1484,10 +1521,16 @@ Then restart your shell or run:
1484
1521
  source ~/${rcFile}`;
1485
1522
  }
1486
1523
  /**
1487
- * Add shims directory to shell PATH configuration.
1488
- * Returns true if added, false if already present or failed.
1524
+ * Add the shims directory to PATH: edits the shell rc file on POSIX, or registers
1525
+ * it on the Windows User PATH (registry + WM_SETTINGCHANGE). Idempotent.
1489
1526
  */
1490
1527
  export function addShimsToPath(overrides) {
1528
+ // Windows has no shell rc file to edit. Register the shims dir on the User PATH
1529
+ // via the platform-native mechanism instead. (The `shell` override is the test
1530
+ // hook for exercising the POSIX path, so it bypasses this branch.)
1531
+ if (IS_WINDOWS && !overrides?.shell) {
1532
+ return addShimsToWindowsUserPath(overrides?.shimsDir || getShimsDir());
1533
+ }
1491
1534
  const shimsDir = overrides?.shimsDir || getShimsDir();
1492
1535
  const { rcFile, rcPath, shell } = getShellRcFile(overrides);
1493
1536
  // Read current rc file content
@@ -1520,16 +1563,53 @@ export function addShimsToPath(overrides) {
1520
1563
  const separator = contentWithoutShimLines.length > 0 && !contentWithoutShimLines.endsWith('\n') ? '\n' : '';
1521
1564
  let newContent = contentWithoutShimLines + separator + exportBlock;
1522
1565
  newContent = newContent.replace(/\n{2,}$/g, '\n');
1566
+ const location = `~/${rcFile}`;
1567
+ const reloadHint = `Restart your shell or run: source ~/${rcFile}`;
1523
1568
  if (newContent === content) {
1524
- return { success: true, alreadyPresent: true, rcFile };
1569
+ return { success: true, alreadyPresent: true, rcFile, location, reloadHint };
1525
1570
  }
1526
1571
  fs.writeFileSync(rcPath, newContent, 'utf-8');
1527
- return { success: true, rcFile };
1572
+ return { success: true, rcFile, location, reloadHint };
1528
1573
  }
1529
1574
  catch (err) {
1530
1575
  return { success: false, error: `Could not write ${rcFile}: ${err.message}` };
1531
1576
  }
1532
1577
  }
1578
+ /**
1579
+ * Register the shims dir on the Windows User PATH via the .NET environment API,
1580
+ * which writes the registry AND broadcasts WM_SETTINGCHANGE — the correct analog
1581
+ * of editing a shell rc file (no `setx` truncation, no manual step). Idempotent:
1582
+ * a no-op when the dir is already present. The shims dir is passed via an env var
1583
+ * so it is never interpolated into the PowerShell script text.
1584
+ */
1585
+ function addShimsToWindowsUserPath(shimsDir) {
1586
+ const script = [
1587
+ '$d = $env:AGENTS_SHIMS_DIR',
1588
+ "$u = [Environment]::GetEnvironmentVariable('Path','User')",
1589
+ "if ($null -eq $u) { $u = '' }",
1590
+ "$parts = @($u -split ';' | Where-Object { $_ -ne '' })",
1591
+ "if ($parts -contains $d) { 'present' } else {",
1592
+ " [Environment]::SetEnvironmentVariable('Path', (($parts + $d) -join ';'), 'User')",
1593
+ " 'added'",
1594
+ '}',
1595
+ ].join('\n');
1596
+ try {
1597
+ const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
1598
+ encoding: 'utf-8',
1599
+ env: { ...process.env, AGENTS_SHIMS_DIR: shimsDir },
1600
+ stdio: ['ignore', 'pipe', 'pipe'],
1601
+ }).trim();
1602
+ return {
1603
+ success: true,
1604
+ alreadyPresent: out.includes('present'),
1605
+ location: 'your user PATH',
1606
+ reloadHint: 'Open a new terminal for the change to take effect.',
1607
+ };
1608
+ }
1609
+ catch (err) {
1610
+ return { success: false, error: `Could not update the Windows user PATH: ${err.message}` };
1611
+ }
1612
+ }
1533
1613
  export function listAgentsWithInstalledVersions() {
1534
1614
  const versionsDir = getVersionsDir();
1535
1615
  if (!fs.existsSync(versionsDir)) {
@@ -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
- try {
304
- const whichPath = execFileSync('which', [executable], { encoding: 'utf-8' }).trim();
305
- return [true, whichPath];
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.6",
3
+ "version": "1.20.8",
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",
@@ -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 here; the
148
+ // shims dir gets registered on the User PATH by `agents setup` (via the native
149
+ // registry API), so we point the user there instead of mutating PATH from an
150
+ // npm lifecycle script. The primary `agents` command is already on PATH via
151
+ // npm's global bin and works immediately.
152
+ if (process.platform === 'win32') {
153
+ console.log(`\nagents-cli installed.`);
154
+ const written = writeAliasShims();
155
+ console.log(` Installed shorthands: ${written.join(', ')}`);
156
+ console.log(`\nNext: run agents setup — it finishes setup and adds the shims dir to your PATH`);
157
+ console.log(`(so the bare shorthands ${ALIASES.join(', ')} and versioned aliases work in 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`;