@dmsdc-ai/aigentry-telepty 0.4.0 → 0.4.1

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 CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
4
 
5
+ ## [0.4.1] - 2026-05-17
6
+
7
+ ### Fixed
8
+
9
+ - **#25** — Windows PATHEXT resolution for `telepty allow`. npm-global CLIs
10
+ (`claude`, `codex`, `gemini`) now spawn correctly with bare names on
11
+ Windows. Previously `telepty allow … claude` failed with
12
+ `Cannot create process, error code: 2` (ERROR_FILE_NOT_FOUND) because
13
+ node-pty's `CreateProcessW` does not walk `%PATHEXT%` the way `cmd.exe`
14
+ does, so the npm-global `claude.cmd` shim was unreachable from the bare
15
+ name. New: `src/win-resolve-executable.js` resolver (Windows-only branch
16
+ walks `PATH` × `PATHEXT`; POSIX no-op) + 14 unit tests. macOS/Linux
17
+ behavior unchanged.
18
+
19
+ ### Notes
20
+
21
+ - **Snyk SAST scan on changed files** — `src/win-resolve-executable.js`
22
+ + `test/win-resolve-executable.test.js` = **0 findings** (At-Inception
23
+ clean). `cli.js` shows **5 pre-existing findings** (2 Medium Command
24
+ Injection at `execSync` L469 + `pty.spawn` L1075, 3 Low Path Traversal
25
+ at L2287/L2289/L2598) verified identical fingerprint vs HEAD~1 — out
26
+ of #25 surgical scope. Tracked in **dmsdc-ai/aigentry-telepty#26** for
27
+ follow-up PR.
28
+
5
29
  ## [0.4.0] — 2026-05-15
6
30
 
7
31
  ### Added — Phase 1 sidecar supervisor spike (M1–M5)
package/cli.js CHANGED
@@ -17,6 +17,7 @@ const { getRuntimeInfo } = require('./runtime-info');
17
17
  const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./session-routing');
18
18
  const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
19
19
  const { runInteractiveSkillInstaller } = require('./skill-installer');
20
+ const { resolveWindowsExecutable } = require('./src/win-resolve-executable');
20
21
  const crossMachine = require('./cross-machine');
21
22
  const { parseHostSpec, buildDaemonUrl, buildDaemonWsUrl } = require('./host-spec');
22
23
  const { FileMailbox } = require('./src/mailbox/index');
@@ -1068,7 +1069,10 @@ async function main() {
1068
1069
  }
1069
1070
 
1070
1071
  function spawnChild() {
1071
- child = pty.spawn(command, cmdArgs, {
1072
+ // Windows: walk %PATHEXT% so bare names (`claude`, `codex`, `gemini`)
1073
+ // resolve to their npm-global `.cmd`/`.ps1` shims. POSIX: no-op. (#25)
1074
+ const resolvedCommand = resolveWindowsExecutable(command, process.env);
1075
+ child = pty.spawn(resolvedCommand, cmdArgs, {
1072
1076
  name: 'xterm-256color',
1073
1077
  cols: process.stdout.columns || 80,
1074
1078
  rows: process.stdout.rows || 30,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -33,9 +33,9 @@
33
33
  "CHANGELOG.md"
34
34
  ],
35
35
  "scripts": {
36
- "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
37
- "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js",
38
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
36
+ "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
37
+ "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js",
38
+ "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
39
  "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
40
40
  },
41
41
  "keywords": [
@@ -0,0 +1,87 @@
1
+ // src/win-resolve-executable.js — Windows PATHEXT-aware executable resolver
2
+ //
3
+ // Fixes #25: `telepty allow ... <bare-command>` on Windows fails with
4
+ // `ERROR_FILE_NOT_FOUND` because node-pty's `CreateProcessW` does not walk
5
+ // `%PATHEXT%` the way cmd.exe shell does. npm-global CLIs install as
6
+ // `<cmd>.cmd` / `<cmd>.ps1`, so the bare name `claude` resolves to nothing.
7
+ //
8
+ // On POSIX this resolver is a no-op (execve handles PATH lookup natively).
9
+ //
10
+ // Exports:
11
+ // resolveWindowsExecutable(command, env = process.env, opts = {})
12
+ // → resolved absolute path on Windows when bare command is found via
13
+ // PATH × PATHEXT walk, the original `command` on POSIX, or throws.
14
+ //
15
+ // Constraints honored:
16
+ // - Constitution §1 lightweight: ≤80 lines, fs + path + process only.
17
+ // - Constitution §2 cross-platform: POSIX behavior unchanged.
18
+ // - Constitution §17 무의존: no new dependencies.
19
+
20
+ 'use strict';
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.JS';
26
+
27
+ function resolveWindowsExecutable(command, env, opts) {
28
+ if (typeof command !== 'string' || command.length === 0) {
29
+ throw new Error('telepty: resolveWindowsExecutable requires a non-empty command');
30
+ }
31
+ const e = env || process.env;
32
+ const o = opts || {};
33
+ const platform = o.platform || process.platform;
34
+ const existsSync = o.existsSync || fs.existsSync;
35
+
36
+ if (platform !== 'win32') return command;
37
+
38
+ // Always use win32 path semantics in the Windows branch so behavior is
39
+ // identical when the resolver runs on a Windows host AND when tests mock
40
+ // `platform: 'win32'` on a POSIX host.
41
+ const p = path.win32;
42
+
43
+ // Already extension-bearing absolute path → trust if it exists.
44
+ if (p.isAbsolute(command)) {
45
+ return existsSync(command) ? command : tryWithExt(command, e, existsSync, command);
46
+ }
47
+
48
+ // Contains a separator → resolve relative to cwd, then try with extensions.
49
+ if (command.includes('\\') || command.includes('/')) {
50
+ const abs = p.resolve(command);
51
+ return existsSync(abs) ? abs : tryWithExt(abs, e, existsSync, command);
52
+ }
53
+
54
+ // Bare name → walk PATH × PATHEXT.
55
+ const exts = parseExts(e.PATHEXT || DEFAULT_PATHEXT);
56
+ const dirs = (e.PATH || '').split(';').filter(Boolean);
57
+ for (const dir of dirs) {
58
+ for (const ext of exts) {
59
+ const candidate = p.join(dir, command + ext);
60
+ if (existsSync(candidate)) return candidate;
61
+ }
62
+ }
63
+ throw new Error(
64
+ `telepty: cannot find executable "${command}" on PATH (Windows PATHEXT walk failed)`
65
+ );
66
+ }
67
+
68
+ function tryWithExt(absPath, env, existsSync, originalCommand) {
69
+ const exts = parseExts(env.PATHEXT || DEFAULT_PATHEXT);
70
+ for (const ext of exts) {
71
+ if (ext === '') continue;
72
+ const candidate = absPath + ext;
73
+ if (existsSync(candidate)) return candidate;
74
+ }
75
+ throw new Error(
76
+ `telepty: cannot find executable "${originalCommand}" (Windows PATHEXT walk failed)`
77
+ );
78
+ }
79
+
80
+ function parseExts(pathext) {
81
+ // Always include empty string first so an already-extension-bearing command
82
+ // matches before the PATHEXT-suffixed candidates.
83
+ const list = pathext.split(';').map((s) => s.trim()).filter(Boolean);
84
+ return ['', ...list];
85
+ }
86
+
87
+ module.exports = { resolveWindowsExecutable };