dsh-ccgui 1.0.0 → 1.0.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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Turns a configured command into one Windows can start without a shell. The harness spawns
3
+ * with CreateProcess and never through `cmd.exe`, which finds only `.exe` and `.com` files by
4
+ * bare name and, since Node's fix for CVE-2024-27980, refuses `.cmd` and `.bat` outright. Yet
5
+ * `npx`, `codebuddy`, `opencode` and most CLIs installed through a package manager are `.cmd`
6
+ * shims on Windows.
7
+ *
8
+ * Wrapping the shim in `cmd.exe /c` is not an option: the spawn seam has no verbatim-argument
9
+ * switch, so Node's quoting of the command line would break cmd's own parsing. Instead, the
10
+ * shim is read for the script it starts, and that script is run with the shim's interpreter
11
+ * directly, the way the shim would have run it. The interpreter is often not node: npm writes
12
+ * `bun` into the shim of a package whose bin has a bun shebang, such as OMP CLI.
13
+ * @module dsh-ccgui/windows-command
14
+ */
15
+ import { readFileSync, statSync } from 'node:fs';
16
+ import { win32 } from 'node:path';
17
+ /** What Windows itself falls back to when PATHEXT is unset. */
18
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
19
+ const EXECUTABLE_EXTENSIONS = new Set(['.exe', '.com']);
20
+ const BATCH_EXTENSIONS = new Set(['.cmd', '.bat']);
21
+ const EXE = '.exe';
22
+ /** What npm's shim runs when it names no other interpreter. */
23
+ const DEFAULT_INTERPRETER = 'node';
24
+ /** A shim's interpreter may itself be a shim (bun installed through npm); cycles end here. */
25
+ const MAX_SHIM_DEPTH = 4;
26
+ /** The variable npm's shim keeps its interpreter in. */
27
+ const NPM_PROG = '%_prog%';
28
+ /**
29
+ * The line npm and pnpm shims start their target on: an optional interpreter, then the
30
+ * target relative to the shim's own folder, then all arguments. Spaces only between the
31
+ * parts, so the interpreter never comes from the line before.
32
+ */
33
+ const SHIM_LAUNCH = /(?:("[^"]*"|[^\s"]+)[ \t]+)?"%~?dp0%?\\([^"]*)"[ \t]+%\*/g;
34
+ const NPM_PROG_VALUE = /SET "_prog=([^"]+)"/g;
35
+ const unquote = (text) => text.replace(/^"(.*)"$/, '$1');
36
+ function extensions(pathExt) {
37
+ return (pathExt ?? DEFAULT_PATHEXT)
38
+ .split(';')
39
+ .map((ext) => ext.trim().toLowerCase())
40
+ .filter((ext) => ext.startsWith('.'));
41
+ }
42
+ function searchDirs(name, path) {
43
+ if (win32.basename(name) !== name)
44
+ return [''];
45
+ return (path ?? '')
46
+ .split(';')
47
+ .map((dir) => unquote(dir.trim()))
48
+ .filter((dir) => dir !== '');
49
+ }
50
+ /**
51
+ * Find the file Windows would run for `name`. A file without one of the PATHEXT extensions is
52
+ * skipped even under its exact name: next to each `.cmd`, npm also leaves a `sh` script with
53
+ * no extension, which CreateProcess cannot run.
54
+ */
55
+ function locate(name, { path, pathExt, files }) {
56
+ const exts = extensions(pathExt);
57
+ const named = exts.includes(win32.extname(name).toLowerCase());
58
+ const candidates = named ? [name] : exts.map((ext) => `${name}${ext}`);
59
+ for (const dir of searchDirs(name, path)) {
60
+ const found = candidates.map((file) => win32.join(dir, file)).find((file) => files.isFile(file));
61
+ if (found !== undefined)
62
+ return found;
63
+ }
64
+ return undefined;
65
+ }
66
+ function readLaunch(shim) {
67
+ const [last] = [...shim.matchAll(SHIM_LAUNCH)].slice(-1);
68
+ const target = last?.[2];
69
+ if (last === undefined || target === undefined)
70
+ return undefined;
71
+ const interpreter = last[1] === undefined ? undefined : unquote(last[1]);
72
+ if (interpreter?.toLowerCase() !== NPM_PROG)
73
+ return { interpreter, target };
74
+ // npm picks `<prog>.exe` beside the shim when there is one, otherwise the value without dp0.
75
+ const fallback = [...shim.matchAll(NPM_PROG_VALUE)].map((match) => match[1]).find((value) => !value?.includes('dp0'));
76
+ return { interpreter: fallback ?? DEFAULT_INTERPRETER, target };
77
+ }
78
+ function unsupported(shim) {
79
+ return new Error(`无法在不经过命令行的情况下启动 ${shim}。请在配置里把这个 CLI 的 command 改成 .exe 或它的 node 脚本的完整路径。`);
80
+ }
81
+ function missingInterpreter(interpreter, shim) {
82
+ return new Error(`找不到 ${interpreter}:启动 ${shim} 需要它。请先安装 ${interpreter} 并加入 PATH,或在配置里把这个 CLI 的 command 改成 .exe 的完整路径。`);
83
+ }
84
+ /**
85
+ * The argv prefix that starts a shim's interpreter. npm's shims prefer `<name>.exe` beside
86
+ * themselves, then fall back to PATH, where the interpreter may in turn be a shim.
87
+ */
88
+ function interpreterArgv(interpreter, shim, lookup, depth) {
89
+ const shimDir = win32.dirname(shim);
90
+ const bare = win32.basename(interpreter) === interpreter;
91
+ const beside = win32.join(shimDir, win32.extname(interpreter) === '' ? `${interpreter}${EXE}` : interpreter);
92
+ if (bare && lookup.files.isFile(beside))
93
+ return [beside];
94
+ const found = locate(interpreter, lookup);
95
+ if (found === undefined)
96
+ throw missingInterpreter(interpreter, shim);
97
+ const ext = win32.extname(found).toLowerCase();
98
+ if (EXECUTABLE_EXTENSIONS.has(ext))
99
+ return [found];
100
+ if (BATCH_EXTENSIONS.has(ext) && depth < MAX_SHIM_DEPTH)
101
+ return argvForShim(found, [], lookup, depth + 1);
102
+ throw unsupported(shim);
103
+ }
104
+ function argvForShim(shim, args, lookup, depth) {
105
+ const text = lookup.files.read(shim);
106
+ const launch = text === undefined ? undefined : readLaunch(text);
107
+ if (launch === undefined)
108
+ throw unsupported(shim);
109
+ const shimDir = win32.dirname(shim);
110
+ const target = win32.resolve(shimDir, launch.target);
111
+ if (launch.interpreter === undefined) {
112
+ if (EXECUTABLE_EXTENSIONS.has(win32.extname(target).toLowerCase()))
113
+ return [target, ...args];
114
+ throw unsupported(shim);
115
+ }
116
+ return [...interpreterArgv(launch.interpreter, shim, lookup, depth), target, ...args];
117
+ }
118
+ /**
119
+ * Resolve a command the way a Windows shell would, without the shell.
120
+ * @param command - the command as configured.
121
+ * @param lookup - where to look.
122
+ * @returns the command with a program CreateProcess can start; unchanged when the program is
123
+ * not found, so the spawn reports it as missing.
124
+ * @throws Error when the program is a batch file that only a shell could run.
125
+ */
126
+ export function resolveWindowsCommand(command, lookup) {
127
+ const [program, ...args] = command.argv;
128
+ if (program === undefined)
129
+ return command;
130
+ // The child runs with this PATH, so the shim's interpreter is looked up in it as well.
131
+ const effective = { ...lookup, path: command.env.PATH ?? lookup.path };
132
+ const found = locate(program, effective);
133
+ if (found === undefined)
134
+ return command;
135
+ const ext = win32.extname(found).toLowerCase();
136
+ if (BATCH_EXTENSIONS.has(ext))
137
+ return { ...command, argv: argvForShim(found, args, effective, 0) };
138
+ return { ...command, argv: [found, ...args] };
139
+ }
140
+ const diskFiles = {
141
+ isFile: (path) => statSync(path, { throwIfNoEntry: false })?.isFile() ?? false,
142
+ read: (path) => {
143
+ try {
144
+ return readFileSync(path, 'utf8');
145
+ }
146
+ catch {
147
+ // An unreadable shim is reported by the caller, with the path, as one it cannot run.
148
+ return undefined;
149
+ }
150
+ },
151
+ };
152
+ /**
153
+ * On Windows, resolve the command against the real file system; elsewhere, leave it alone.
154
+ * @param command - the command as configured.
155
+ * @param platform - the current platform.
156
+ * @param env - the process environment, for PATH and PATHEXT.
157
+ * @returns the command to spawn.
158
+ */
159
+ export function startOnWindows(command, platform, env) {
160
+ if (platform !== 'win32')
161
+ return command;
162
+ return resolveWindowsCommand(command, { path: env.PATH, pathExt: env.PATHEXT, files: diskFiles });
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ccgui",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "在 DeepSeek Harness 里直接使用 Claude Code、Codex、PI、OMP 等 Agent CLI",
5
5
  "icon": "./assets/logo.png",
6
6
  "license": "MIT",