@dmsdc-ai/aigentry-telepty 0.4.0 → 0.4.2
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 +58 -0
- package/cli.js +27 -2
- package/cross-machine.js +47 -8
- package/package.json +4 -4
- package/session-routing.js +6 -1
- package/src/win-resolve-executable.js +87 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.2] - 2026-05-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **#28** — SSH-peer routing for `telepty inject` / `list` / `enter`
|
|
10
|
+
cross-machine: file-backed `peers.json` fallback resolves the prior
|
|
11
|
+
`fetch failed` against SSH peers in fresh CLI subprocesses. Previously
|
|
12
|
+
`cross-machine.js` consulted only the in-memory `activePeers` Map, which
|
|
13
|
+
is process-local and empty for every CLI subprocess spawned after
|
|
14
|
+
`telepty connect`. New: `listSshPeers` + `getSshPeerHandle` helpers
|
|
15
|
+
(`cross-machine.js`) make SSH-peer discovery/inject symmetric with the
|
|
16
|
+
existing HTTP-peer path; `pickSessionTarget` (`session-routing.js`)
|
|
17
|
+
matches `<id>@<peerName>` against the peer alias; `resolveSessionTarget`
|
|
18
|
+
(`cli.js`) enriches synthetic targets with `peerName` when the host
|
|
19
|
+
matches a known SSH peer. 7 new unit tests
|
|
20
|
+
(`test/cross-machine-ssh-routing.test.js`) + 1 new peer-alias test
|
|
21
|
+
(`test/session-routing.test.js`). Scope: `inject` / `list` / `enter`;
|
|
22
|
+
`attach` / `read-screen` / `rename` / `destroy` / `state` /
|
|
23
|
+
`session info` share the same gap but are deferred to v0.4.3+.
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
|
|
27
|
+
- **Snyk SAST scan on changed files** — `cross-machine.js` +
|
|
28
|
+
`session-routing.js` + `test/cross-machine-ssh-routing.test.js` +
|
|
29
|
+
`test/session-routing.test.js` = **0 findings** (At-Inception clean).
|
|
30
|
+
`cli.js` shows **5 pre-existing findings** (2 Medium Command Injection
|
|
31
|
+
at `execSync` L469 + `pty.spawn` L1096, 3 Low Path Traversal at
|
|
32
|
+
L2308/L2310/L2619) with **identical fingerprints** vs HEAD~1 (5/5
|
|
33
|
+
verified). Line numbers for sinks below L543 shifted +21 from the
|
|
34
|
+
`resolveSessionTarget` enrichment block (cli.js L543–L566) — logical
|
|
35
|
+
source→sink unchanged; no new sink call sites added. Out of #28
|
|
36
|
+
surgical scope. Tracked in **dmsdc-ai/aigentry-telepty#26** for
|
|
37
|
+
follow-up PR.
|
|
38
|
+
|
|
39
|
+
## [0.4.1] - 2026-05-17
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **#25** — Windows PATHEXT resolution for `telepty allow`. npm-global CLIs
|
|
44
|
+
(`claude`, `codex`, `gemini`) now spawn correctly with bare names on
|
|
45
|
+
Windows. Previously `telepty allow … claude` failed with
|
|
46
|
+
`Cannot create process, error code: 2` (ERROR_FILE_NOT_FOUND) because
|
|
47
|
+
node-pty's `CreateProcessW` does not walk `%PATHEXT%` the way `cmd.exe`
|
|
48
|
+
does, so the npm-global `claude.cmd` shim was unreachable from the bare
|
|
49
|
+
name. New: `src/win-resolve-executable.js` resolver (Windows-only branch
|
|
50
|
+
walks `PATH` × `PATHEXT`; POSIX no-op) + 14 unit tests. macOS/Linux
|
|
51
|
+
behavior unchanged.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- **Snyk SAST scan on changed files** — `src/win-resolve-executable.js`
|
|
56
|
+
+ `test/win-resolve-executable.test.js` = **0 findings** (At-Inception
|
|
57
|
+
clean). `cli.js` shows **5 pre-existing findings** (2 Medium Command
|
|
58
|
+
Injection at `execSync` L469 + `pty.spawn` L1075, 3 Low Path Traversal
|
|
59
|
+
at L2287/L2289/L2598) verified identical fingerprint vs HEAD~1 — out
|
|
60
|
+
of #25 surgical scope. Tracked in **dmsdc-ai/aigentry-telepty#26** for
|
|
61
|
+
follow-up PR.
|
|
62
|
+
|
|
5
63
|
## [0.4.0] — 2026-05-15
|
|
6
64
|
|
|
7
65
|
### 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');
|
|
@@ -541,7 +542,28 @@ function isRemoteSession(session) {
|
|
|
541
542
|
|
|
542
543
|
async function resolveSessionTarget(sessionRef, options = {}) {
|
|
543
544
|
const sessions = options.sessions || await discoverSessions({ silent: true });
|
|
544
|
-
|
|
545
|
+
const target = pickSessionTarget(sessionRef, sessions, REMOTE_HOST);
|
|
546
|
+
// When <id>@<peerName> uses an SSH peer alias (e.g. `winserver`) and the
|
|
547
|
+
// session is not in `sessions` (discovery missed it, ControlMaster expired,
|
|
548
|
+
// or remote has no such session), pickSessionTarget returns a synthetic
|
|
549
|
+
// target with no peerName/remote flag. Detect SSH peer alias here so the
|
|
550
|
+
// caller routes through cross-machine.remoteInject (SSH path) rather than
|
|
551
|
+
// falling into the HTTP fetch path with `http://winserver:3848/...`. #411
|
|
552
|
+
if (
|
|
553
|
+
target &&
|
|
554
|
+
!target.peerName &&
|
|
555
|
+
target.host &&
|
|
556
|
+
target.host !== '127.0.0.1' &&
|
|
557
|
+
!target.host.includes(':') &&
|
|
558
|
+
!target.host.includes('@')
|
|
559
|
+
) {
|
|
560
|
+
const sshPeer = crossMachine.getSshPeerHandle(target.host);
|
|
561
|
+
if (sshPeer) {
|
|
562
|
+
target.peerName = target.host;
|
|
563
|
+
target.remote = true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return target;
|
|
545
567
|
}
|
|
546
568
|
|
|
547
569
|
async function ensureDaemonRunning(options = {}) {
|
|
@@ -1068,7 +1090,10 @@ async function main() {
|
|
|
1068
1090
|
}
|
|
1069
1091
|
|
|
1070
1092
|
function spawnChild() {
|
|
1071
|
-
|
|
1093
|
+
// Windows: walk %PATHEXT% so bare names (`claude`, `codex`, `gemini`)
|
|
1094
|
+
// resolve to their npm-global `.cmd`/`.ps1` shims. POSIX: no-op. (#25)
|
|
1095
|
+
const resolvedCommand = resolveWindowsExecutable(command, process.env);
|
|
1096
|
+
child = pty.spawn(resolvedCommand, cmdArgs, {
|
|
1072
1097
|
name: 'xterm-256color',
|
|
1073
1098
|
cols: process.stdout.columns || 80,
|
|
1074
1099
|
rows: process.stdout.rows || 30,
|
package/cross-machine.js
CHANGED
|
@@ -37,6 +37,37 @@ function savePeers(data) {
|
|
|
37
37
|
// In-memory active peers
|
|
38
38
|
const activePeers = new Map(); // name -> { target, controlSocket, connectedAt, machineId }
|
|
39
39
|
|
|
40
|
+
// File-backed SSH peer enumeration. Required because CLI subprocesses (fresh
|
|
41
|
+
// node procs) start with an empty `activePeers` Map — only the process that
|
|
42
|
+
// called connect() has it populated. peers.json is the cross-process source of
|
|
43
|
+
// truth. controlPath(target) is deterministic, so any process can reuse the
|
|
44
|
+
// ControlMaster socket established by an earlier connect() as long as
|
|
45
|
+
// ControlPersist hasn't expired. See #411.
|
|
46
|
+
function listSshPeers() {
|
|
47
|
+
const peers = loadPeers().peers || {};
|
|
48
|
+
return Object.entries(peers)
|
|
49
|
+
.filter(([, entry]) => getPeerTransport(entry) === 'ssh' && entry && entry.target)
|
|
50
|
+
.map(([name, entry]) => ({
|
|
51
|
+
name,
|
|
52
|
+
target: entry.target,
|
|
53
|
+
machineId: entry.machineId || name,
|
|
54
|
+
lastConnected: entry.lastConnected
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSshPeerHandle(name) {
|
|
59
|
+
if (activePeers.has(name)) return activePeers.get(name);
|
|
60
|
+
const peers = loadPeers().peers || {};
|
|
61
|
+
const entry = peers[name];
|
|
62
|
+
if (!entry || getPeerTransport(entry) !== 'ssh' || !entry.target) return null;
|
|
63
|
+
return {
|
|
64
|
+
target: entry.target,
|
|
65
|
+
controlSocket: controlPath(entry.target),
|
|
66
|
+
name,
|
|
67
|
+
machineId: entry.machineId || name
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
function shellQuote(value) {
|
|
41
72
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
42
73
|
}
|
|
@@ -175,14 +206,11 @@ function disconnectAll() {
|
|
|
175
206
|
* @returns {Array} sessions with host info
|
|
176
207
|
*/
|
|
177
208
|
function listRemoteSessions(name) {
|
|
178
|
-
const peer =
|
|
209
|
+
const peer = getSshPeerHandle(name);
|
|
179
210
|
if (!peer) return [];
|
|
180
211
|
|
|
181
212
|
try {
|
|
182
|
-
const output =
|
|
183
|
-
`ssh -o ControlPath=${peer.controlSocket} ${peer.target} "telepty list --json"`,
|
|
184
|
-
{ timeout: 10000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
185
|
-
);
|
|
213
|
+
const output = runRemoteCommand(peer, 'telepty list --json', { timeout: 10000 });
|
|
186
214
|
const sessions = JSON.parse(output);
|
|
187
215
|
return sessions.map(s => ({ ...s, host: peer.target, peerName: name, remote: true }));
|
|
188
216
|
} catch {
|
|
@@ -191,13 +219,21 @@ function listRemoteSessions(name) {
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
/**
|
|
194
|
-
* Discover sessions across all connected peers
|
|
222
|
+
* Discover sessions across all connected peers, including SSH peers that are
|
|
223
|
+
* persisted in peers.json but not in this process's activePeers Map. Fresh
|
|
224
|
+
* CLI subprocesses depend on the file-backed path — #411.
|
|
195
225
|
* @returns {Array} all remote sessions
|
|
196
226
|
*/
|
|
197
227
|
function discoverAllRemoteSessions() {
|
|
198
228
|
const allSessions = [];
|
|
229
|
+
const seen = new Set();
|
|
199
230
|
for (const [name] of activePeers) {
|
|
200
231
|
allSessions.push(...listRemoteSessions(name));
|
|
232
|
+
seen.add(name);
|
|
233
|
+
}
|
|
234
|
+
for (const peer of listSshPeers()) {
|
|
235
|
+
if (seen.has(peer.name)) continue;
|
|
236
|
+
allSessions.push(...listRemoteSessions(peer.name));
|
|
201
237
|
}
|
|
202
238
|
return allSessions;
|
|
203
239
|
}
|
|
@@ -206,7 +242,7 @@ function discoverAllRemoteSessions() {
|
|
|
206
242
|
* Inject text into a remote session via SSH.
|
|
207
243
|
*/
|
|
208
244
|
function remoteInject(name, sessionId, prompt, options = {}) {
|
|
209
|
-
const peer =
|
|
245
|
+
const peer = getSshPeerHandle(name);
|
|
210
246
|
if (!peer) return { success: false, error: `Not connected to ${name}` };
|
|
211
247
|
|
|
212
248
|
try {
|
|
@@ -227,7 +263,7 @@ function remoteInject(name, sessionId, prompt, options = {}) {
|
|
|
227
263
|
}
|
|
228
264
|
|
|
229
265
|
function remoteEnsureSharedContext(name, descriptor) {
|
|
230
|
-
const peer =
|
|
266
|
+
const peer = getSshPeerHandle(name);
|
|
231
267
|
if (!peer) return { success: false, error: `Not connected to ${name}` };
|
|
232
268
|
|
|
233
269
|
try {
|
|
@@ -454,6 +490,9 @@ module.exports = {
|
|
|
454
490
|
listHttpPeers,
|
|
455
491
|
listHttpRemoteSessions,
|
|
456
492
|
discoverHttpRemoteSessions,
|
|
493
|
+
// File-backed SSH peer enumeration (cross-process — #411)
|
|
494
|
+
listSshPeers,
|
|
495
|
+
getSshPeerHandle,
|
|
457
496
|
getPeerTransport,
|
|
458
497
|
PEERS_PATH
|
|
459
498
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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/cross-machine-ssh-routing.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/cross-machine-ssh-routing.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/cross-machine-ssh-routing.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": [
|
package/session-routing.js
CHANGED
|
@@ -38,7 +38,12 @@ function pickSessionTarget(sessionRef, sessions, defaultHost = '127.0.0.1') {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (parsed.host) {
|
|
41
|
-
|
|
41
|
+
// Match by host or by peerName alias (e.g. `sid@winserver` where
|
|
42
|
+
// winserver is the SSH peer name and session.host is `user@FQDN`). #411
|
|
43
|
+
const exactMatch = sessions.find((session) =>
|
|
44
|
+
session.id === parsed.id &&
|
|
45
|
+
(session.host === parsed.host || session.peerName === parsed.host)
|
|
46
|
+
);
|
|
42
47
|
return exactMatch || { id: parsed.id, host: parsed.host };
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -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 };
|