@procedures/shell 0.54.1 → 0.55.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
@@ -1,5 +1,22 @@
1
1
  # @procedures/shell
2
2
 
3
+ ## 0.55.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @procedures/client@0.55.1
8
+
9
+ ## 0.55.0
10
+
11
+ ### Patch Changes
12
+
13
+ - 2918e97: Two security fixes:
14
+
15
+ - `@procedures/shell`: on Windows, a program name with no folder starts only from a folder of `PATH`, never from the working folder of the call. Before, a call such as "run `git` in this cloned repository" started a `git.exe` from the top folder of the repository. When no folder of `PATH` has the program, the start fails with `ENOENT`, as for a missing program, and the error names the program as before. `taskkill` starts by its full path in the system folder, and `shell.which` gives a match from `PATH` only.
16
+ - `@procedures/fs`: under a root, a link to a missing target is resolved against the real folder of the link. Before, the check resolved a relative target against the text of the path. When an earlier part of the path was a link to a folder, the check said "inside the root" for a write that went outside it. A `..` in a relative target follows the rule of the system: POSIX goes up from the real folder of the name before it, and Windows removes that name as text. The archive procedures and the other users of `@procedures/fs/root` get the same fix.
17
+ - Updated dependencies [fd9c6d4]
18
+ - @procedures/client@0.55.0
19
+
3
20
  ## 0.54.1
4
21
 
5
22
  ### Patch Changes
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The file of a program: the one place that answers which file a command starts.
3
+ *
4
+ * On Windows, Node (libuv) looks for a program name with no folder first in the working folder of
5
+ * the child, and only then in the folders of `PATH`. So "run `git` in this cloned repository"
6
+ * started a `git.exe` from the top folder of the repository. The helpers give `spawn` the
7
+ * absolute path from `startFile()`, which searches only the folders of `PATH`. The functions take
8
+ * the operating system, the environment and the file test as inputs, so each system tests them.
9
+ */
10
+ /** The options of `programPath`. Each one has a default from the host, so the tests can set it. */
11
+ export interface ProgramPathOptions {
12
+ /** The operating system (default: `process.platform`). */
13
+ platform?: NodeJS.Platform | undefined;
14
+ /** The environment of the host, for a child environment with no `PATH` (default: `process.env`). */
15
+ hostEnv?: Readonly<Record<string, string | undefined>> | undefined;
16
+ /** The function that tells if a path is a file (default: `fs.statSync`). */
17
+ isFile?: ((path: string) => boolean) | undefined;
18
+ }
19
+ /**
20
+ * The value of `PATH` in an environment that Node gives to a child on Windows. The names of the
21
+ * variables have no fixed case there. Node keeps the first name in sorted order (`PATH` before
22
+ * `Path`), and leaves out a variable with no value.
23
+ */
24
+ export declare function windowsPathValue(env: Readonly<Record<string, string | undefined>>): string | undefined;
25
+ /**
26
+ * The folders of a `PATH` value on Windows, in order, as libuv reads them: a `;` separates two
27
+ * folders, and quotes around a folder are not part of it. The list leaves out an empty folder and
28
+ * a folder that is not fully qualified, because both lead to the working folder.
29
+ */
30
+ export declare function windowsPathFolders(value: string): string[];
31
+ /**
32
+ * This function gives the file that `command` starts, for `spawn`:
33
+ * - On POSIX, and for a command with a folder part, it gives `command`. The system finds it as before.
34
+ * - On Windows, for a name with no folder, it gives the absolute path of the first match in the
35
+ * folders of the `PATH` of `env`, the environment of the child. It never looks in the working
36
+ * folder. It tries the names that libuv tries: the exact name when it has an extension, then
37
+ * the name with `.com`, then the name with `.exe`.
38
+ * - It gives `undefined` when no folder of `PATH` has the program.
39
+ *
40
+ * When `env` has no `PATH`, libuv uses the `PATH` of the host. This function does the same.
41
+ */
42
+ export declare function programPath(command: string, env: Readonly<Record<string, string | undefined>>, options?: ProgramPathOptions): string | undefined;
43
+ /**
44
+ * This function gives the file that `startChild` gives to `spawn`. It is the result of
45
+ * `programPath()` when there is one. When no folder of `PATH` has the program, it gives a fully
46
+ * qualified path that does not exist, in the system folder. So the start fails with ENOENT, as for
47
+ * a missing program, and the system does not look in the working folder.
48
+ */
49
+ export declare function startFile(command: string, env: Readonly<Record<string, string | undefined>>, options?: ProgramPathOptions): string;
50
+ /**
51
+ * When `file` is not `command`, this function changes each start error of `child` to name
52
+ * `command`, as Node names it for a start by name: the `message` (`spawn git ENOENT`), the
53
+ * `stack`, the `syscall` and the `path`. So the error of a call is the same as before the lookup.
54
+ * A child with no other listener of `error` still throws the error, as Node does.
55
+ */
56
+ export declare function nameStartErrors(child: NodeJS.EventEmitter, command: string, file: string): void;
57
+ /**
58
+ * The program and the arguments of `shell.which` for a command. On Windows, `where name` gives a
59
+ * match in the working folder first. `where $PATH:name` looks only in the folders of `PATH`, as a
60
+ * start of a program by its name does. A command with a folder part goes to `where` as it is.
61
+ */
62
+ export declare function whichCall(command: string, platform?: NodeJS.Platform): {
63
+ program: string;
64
+ args: string[];
65
+ };
66
+ /**
67
+ * The path of a program of the system folder of Windows, for example `taskkill.exe`. The folder
68
+ * comes from `SystemRoot`, then `windir`, then `C:\Windows`. A value that is not fully qualified
69
+ * is not used.
70
+ */
71
+ export declare function systemProgram(name: string, env?: Readonly<Record<string, string | undefined>>): string;
72
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/command/program.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,mGAAmG;AACnG,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,GAAG,SAAS,CAAC;IACvC,oGAAoG;IACpG,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IACnE,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CAClD;AAeD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,MAAM,GAAG,SAAS,CAGtG;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB1D;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACjD,OAAO,GAAE,kBAAuB,GAC/B,MAAM,GAAG,SAAS,CAgBpB;AAQD;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACjD,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CAGR;AAUD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAa/F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAG5H;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAAG,MAAM,CAGnH"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The file of a program: the one place that answers which file a command starts.
3
+ *
4
+ * On Windows, Node (libuv) looks for a program name with no folder first in the working folder of
5
+ * the child, and only then in the folders of `PATH`. So "run `git` in this cloned repository"
6
+ * started a `git.exe` from the top folder of the repository. The helpers give `spawn` the
7
+ * absolute path from `startFile()`, which searches only the folders of `PATH`. The functions take
8
+ * the operating system, the environment and the file test as inputs, so each system tests them.
9
+ */
10
+ import { statSync } from "node:fs";
11
+ import { win32 } from "node:path";
12
+ function isFile(path) {
13
+ try {
14
+ return statSync(path).isFile();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /** True for a path with a drive and a separator (`C:\x`), or a UNC path (`\\server\share`). */
21
+ function fullyQualified(path) {
22
+ return /^(?:[a-zA-Z]:[\\/]|[\\/]{2}[^\\/])/.test(path);
23
+ }
24
+ /**
25
+ * The value of `PATH` in an environment that Node gives to a child on Windows. The names of the
26
+ * variables have no fixed case there. Node keeps the first name in sorted order (`PATH` before
27
+ * `Path`), and leaves out a variable with no value.
28
+ */
29
+ export function windowsPathValue(env) {
30
+ const key = Object.keys(env).sort().find((name) => name.toUpperCase() === "PATH");
31
+ return key === undefined ? undefined : env[key];
32
+ }
33
+ /**
34
+ * The folders of a `PATH` value on Windows, in order, as libuv reads them: a `;` separates two
35
+ * folders, and quotes around a folder are not part of it. The list leaves out an empty folder and
36
+ * a folder that is not fully qualified, because both lead to the working folder.
37
+ */
38
+ export function windowsPathFolders(value) {
39
+ const folders = [];
40
+ let start = 0;
41
+ while (start <= value.length) {
42
+ const quote = value[start];
43
+ let from = start;
44
+ if (quote === '"' || quote === "'") {
45
+ const end = value.indexOf(quote, start + 1);
46
+ from = end === -1 ? value.length : end;
47
+ }
48
+ const semicolon = value.indexOf(";", from);
49
+ const end = semicolon === -1 ? value.length : semicolon;
50
+ const folder = value.slice(start, end).replace(/^["']/, "").replace(/["']$/, "");
51
+ if (fullyQualified(folder))
52
+ folders.push(folder);
53
+ start = end + 1;
54
+ }
55
+ return folders;
56
+ }
57
+ /** True when a command has a folder part, for example `.\x`, `bin/x` or `C:x`. */
58
+ function hasFolder(command) {
59
+ return /[\\/:]/.test(command);
60
+ }
61
+ /**
62
+ * This function gives the file that `command` starts, for `spawn`:
63
+ * - On POSIX, and for a command with a folder part, it gives `command`. The system finds it as before.
64
+ * - On Windows, for a name with no folder, it gives the absolute path of the first match in the
65
+ * folders of the `PATH` of `env`, the environment of the child. It never looks in the working
66
+ * folder. It tries the names that libuv tries: the exact name when it has an extension, then
67
+ * the name with `.com`, then the name with `.exe`.
68
+ * - It gives `undefined` when no folder of `PATH` has the program.
69
+ *
70
+ * When `env` has no `PATH`, libuv uses the `PATH` of the host. This function does the same.
71
+ */
72
+ export function programPath(command, env, options = {}) {
73
+ if ((options.platform ?? process.platform) !== "win32")
74
+ return command;
75
+ // An empty name stays as it is: spawn gives its own error for it
76
+ if (command === "" || hasFolder(command))
77
+ return command;
78
+ if (command === ".")
79
+ return undefined;
80
+ const dot = command.indexOf(".");
81
+ const names = [...(dot !== -1 && dot < command.length - 1 ? [command] : []), `${command}.com`, `${command}.exe`];
82
+ const value = windowsPathValue(env) ?? windowsPathValue(options.hostEnv ?? process.env) ?? "";
83
+ const test = options.isFile ?? isFile;
84
+ for (const folder of windowsPathFolders(value)) {
85
+ for (const name of names) {
86
+ const path = win32.join(folder, name);
87
+ if (test(path))
88
+ return path;
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+ /**
94
+ * A folder name that no file system of Windows allows: NTFS refuses `<` and `>` in a name. A path
95
+ * through it does not exist, so a start of it fails with ENOENT and searches no other folder.
96
+ */
97
+ const NO_FOLDER = "<not in PATH>";
98
+ /**
99
+ * This function gives the file that `startChild` gives to `spawn`. It is the result of
100
+ * `programPath()` when there is one. When no folder of `PATH` has the program, it gives a fully
101
+ * qualified path that does not exist, in the system folder. So the start fails with ENOENT, as for
102
+ * a missing program, and the system does not look in the working folder.
103
+ */
104
+ export function startFile(command, env, options = {}) {
105
+ // No join: a join would remove a name such as `..`, and the path would be a folder that exists
106
+ return programPath(command, env, options) ?? `${systemProgram(NO_FOLDER, options.hostEnv ?? process.env)}\\${command}`;
107
+ }
108
+ /**
109
+ * When `file` is not `command`, this function changes each start error of `child` to name
110
+ * `command`, as Node names it for a start by name: the `message` (`spawn git ENOENT`), the
111
+ * `stack`, the `syscall` and the `path`. So the error of a call is the same as before the lookup.
112
+ * A child with no other listener of `error` still throws the error, as Node does.
113
+ */
114
+ export function nameStartErrors(child, command, file) {
115
+ if (file === command)
116
+ return;
117
+ const fileCall = `spawn ${file}`;
118
+ const commandCall = `spawn ${command}`;
119
+ child.prependListener("error", (error) => {
120
+ if (error.syscall === fileCall) {
121
+ error.syscall = commandCall;
122
+ if (error.path === file)
123
+ error.path = command;
124
+ if (error.message.startsWith(fileCall))
125
+ error.message = commandCall + error.message.slice(fileCall.length);
126
+ if (error.stack !== undefined)
127
+ error.stack = error.stack.replace(fileCall, commandCall);
128
+ }
129
+ if (child.listenerCount("error") === 1)
130
+ throw error;
131
+ });
132
+ }
133
+ /**
134
+ * The program and the arguments of `shell.which` for a command. On Windows, `where name` gives a
135
+ * match in the working folder first. `where $PATH:name` looks only in the folders of `PATH`, as a
136
+ * start of a program by its name does. A command with a folder part goes to `where` as it is.
137
+ */
138
+ export function whichCall(command, platform = process.platform) {
139
+ if (platform !== "win32")
140
+ return { program: "which", args: [command] };
141
+ return { program: "where", args: [hasFolder(command) ? command : `$PATH:${command}`] };
142
+ }
143
+ /**
144
+ * The path of a program of the system folder of Windows, for example `taskkill.exe`. The folder
145
+ * comes from `SystemRoot`, then `windir`, then `C:\Windows`. A value that is not fully qualified
146
+ * is not used.
147
+ */
148
+ export function systemProgram(name, env = process.env) {
149
+ const root = [env["SystemRoot"], env["windir"]].find((value) => value !== undefined && fullyQualified(value));
150
+ return win32.join(root ?? "C:\\Windows", "System32", name);
151
+ }
152
+ //# sourceMappingURL=program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../../src/command/program.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,10 @@ export interface SpawnOptions {
19
19
  * This function starts `command` with `shell: false` and `windowsHide`. On POSIX the child gets
20
20
  * its own process group, so `killTree` reaches its descendants. The child is recorded until it
21
21
  * ends, so the end of the host kills it.
22
+ *
23
+ * On Windows, a command with no folder starts from a folder of `PATH` only (`startFile`), never
24
+ * from the working folder. When no folder of `PATH` has it, the start fails with ENOENT, as for
25
+ * a missing program. The error names the command, as before.
22
26
  */
23
27
  export declare function startChild(command: string, options: SpawnOptions, stdin: "pipe" | "ignore"): ChildProcess;
24
28
  /** The text of an error that kept a program from starting, for example `spawn x ENOENT`. */
@@ -1 +1 @@
1
- {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../src/command/spawn.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9D,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrC,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,YAAY,CAYzG;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD"}
1
+ {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../src/command/spawn.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAyC,MAAM,oBAAoB,CAAC;AAIrG,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrC,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,YAAY,CAmBzG;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD"}
@@ -2,22 +2,34 @@
2
2
  * Start a child process the one way the helpers allow: an argument list, no shell, no window.
3
3
  */
4
4
  import { spawn } from "node:child_process";
5
+ import { nameStartErrors, startFile } from "./program.js";
5
6
  import { OWN_PROCESS_GROUP, track } from "./tree-kill.js";
6
7
  /**
7
8
  * This function starts `command` with `shell: false` and `windowsHide`. On POSIX the child gets
8
9
  * its own process group, so `killTree` reaches its descendants. The child is recorded until it
9
10
  * ends, so the end of the host kills it.
11
+ *
12
+ * On Windows, a command with no folder starts from a folder of `PATH` only (`startFile`), never
13
+ * from the working folder. When no folder of `PATH` has it, the start fails with ENOENT, as for
14
+ * a missing program. The error names the command, as before.
10
15
  */
11
16
  export function startChild(command, options, stdin) {
12
- const child = spawn(command, [...(options.args ?? [])], {
17
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
18
+ const file = startFile(command, env);
19
+ const spawnOptions = {
13
20
  cwd: options.cwd,
14
- env: options.env ? { ...process.env, ...options.env } : process.env,
21
+ env,
15
22
  shell: false,
16
23
  windowsHide: true,
17
24
  detached: OWN_PROCESS_GROUP,
18
25
  stdio: [stdin, "pipe", "pipe"],
26
+ // The program gets the name of the call as its first argument, as before
27
+ argv0: command,
19
28
  ...(options.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
20
- });
29
+ };
30
+ const args = [...(options.args ?? [])];
31
+ const child = spawn(file, args, spawnOptions);
32
+ nameStartErrors(child, command, file);
21
33
  track(child);
22
34
  return child;
23
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../src/command/spawn.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAgB1D;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,OAAqB,EAAE,KAAwB;IACzF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE;QACtD,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG;QACnE,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,iBAAiB;QAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;QAC9B,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAC,CAAC;IACH,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
1
+ {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../src/command/spawn.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAA4D,MAAM,oBAAoB,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAgB1D;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,OAAqB,EAAE,KAAwB;IACzF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAC3E,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACrC,MAAM,YAAY,GAAqB;QACrC,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG;QACH,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,iBAAiB;QAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;QAC9B,yEAAyE;QACzE,KAAK,EAAE,OAAO;QACd,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;IAC9C,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACtC,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tree-kill.d.ts","sourceRoot":"","sources":["../../src/command/tree-kill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI5E;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAAkB,CAAC;AAMnD,iGAAiG;AACjG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,GAAE,MAAM,CAAC,OAAmB,GAAG,IAAI,CAgBtF;AAsCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAOrD;AAgBD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAU/C"}
1
+ {"version":3,"file":"tree-kill.d.ts","sourceRoot":"","sources":["../../src/command/tree-kill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAW5E;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAAkB,CAAC;AAMnD,iGAAiG;AACjG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,GAAE,MAAM,CAAC,OAAmB,GAAG,IAAI,CAgBtF;AAsCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAOrD;AAgBD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAU/C"}
@@ -16,7 +16,13 @@
16
16
  * still runs at the exit.
17
17
  */
18
18
  import { execFile, spawnSync } from "node:child_process";
19
+ import { systemProgram } from "./program.js";
19
20
  const WINDOWS = process.platform === "win32";
21
+ /**
22
+ * The full path of `taskkill.exe` in the system folder. A bare name started a `taskkill.exe` of
23
+ * the working folder of the host first.
24
+ */
25
+ const TASKKILL = WINDOWS ? systemProgram("taskkill.exe") : "taskkill";
20
26
  /**
21
27
  * True when a child must start with `detached: true`. On POSIX, `detached` puts the child in a
22
28
  * new process group, which `killTree` needs. On Windows, `detached` opens a new console.
@@ -31,7 +37,7 @@ export function killTree(child, signal = "SIGTERM") {
31
37
  return;
32
38
  const pid = child.pid;
33
39
  if (WINDOWS) {
34
- execFile("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true }, (error) => {
40
+ execFile(TASKKILL, ["/T", "/F", "/PID", String(pid)], { windowsHide: true }, (error) => {
35
41
  // taskkill fails when the process ended in the meantime, or when taskkill is missing
36
42
  if (error && running(child))
37
43
  child.kill(signal);
@@ -53,7 +59,7 @@ function killTreeSync(child) {
53
59
  const pid = child.pid;
54
60
  try {
55
61
  if (WINDOWS) {
56
- spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true, stdio: "ignore" });
62
+ spawnSync(TASKKILL, ["/T", "/F", "/PID", String(pid)], { windowsHide: true, stdio: "ignore" });
57
63
  }
58
64
  else {
59
65
  process.kill(-pid, "SIGKILL");
@@ -1 +1 @@
1
- {"version":3,"file":"tree-kill.js","sourceRoot":"","sources":["../../src/command/tree-kill.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tree-kill.js","sourceRoot":"","sources":["../../src/command/tree-kill.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"which.d.ts","sourceRoot":"","sources":["../../../src/procedures/shell/which.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAExE,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,GAAG,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,GACzC,OAAO,CAAC,gBAAgB,CAAC,CAa3B"}
1
+ {"version":3,"file":"which.d.ts","sourceRoot":"","sources":["../../../src/procedures/shell/which.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAExE,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,GAAG,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,GACzC,OAAO,CAAC,gBAAgB,CAAC,CAc3B"}
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Find the full path to a command.
5
5
  */
6
+ import { whichCall } from "../../command/program.js";
6
7
  import { runCommand } from "../../command/run.js";
7
8
  export async function shellWhich(input, ctx) {
8
9
  // The name is one argument of where/which, with no shell. (Before, `where ${command}` ran in a
@@ -10,8 +11,9 @@ export async function shellWhich(input, ctx) {
10
11
  if (input.command.startsWith("-")) {
11
12
  return { path: null, found: false };
12
13
  }
13
- const program = process.platform === "win32" ? "where" : "which";
14
- const result = await runCommand(program, { args: [input.command], signal: ctx?.signal, timeout: 30_000 });
14
+ // On Windows, only the folders of PATH, as the start of a program by its name
15
+ const { program, args } = whichCall(input.command);
16
+ const result = await runCommand(program, { args, signal: ctx?.signal, timeout: 30_000 });
15
17
  if (!result.success) {
16
18
  return { path: null, found: false };
17
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"which.js","sourceRoot":"","sources":["../../../src/procedures/shell/which.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGlD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAsB,EACtB,GAA0C;IAE1C,+FAA+F;IAC/F,8EAA8E;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACjE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1G,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAClE,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChF,CAAC"}
1
+ {"version":3,"file":"which.js","sourceRoot":"","sources":["../../../src/procedures/shell/which.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGlD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAsB,EACtB,GAA0C;IAE1C,+FAA+F;IAC/F,8EAA8E;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,8EAA8E;IAC9E,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACzF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAClE,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChF,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@procedures/shell",
4
- "version": "0.54.1",
4
+ "version": "0.55.1",
5
5
  "description": "Safe command execution for TypeScript: shell.run with argv and no shell, shell.stream line streaming with backpressure, process-tree kill, timeouts and output caps.",
6
6
  "keywords": [
7
7
  "shell",
@@ -72,13 +72,13 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@mark1russell7/cue": "^2.1.1",
75
- "@procedures/fs": "0.54.1",
75
+ "@procedures/fs": "0.55.1",
76
76
  "@types/node": "^22.0.0",
77
77
  "typescript": "^7.0.2",
78
78
  "vitest": "^5.0.3"
79
79
  },
80
80
  "dependencies": {
81
- "@procedures/client": "0.54.1",
81
+ "@procedures/client": "0.55.1",
82
82
  "zod": "^4.6.5"
83
83
  },
84
84
  "client": {
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The file of a program: the one place that answers which file a command starts.
3
+ *
4
+ * On Windows, Node (libuv) looks for a program name with no folder first in the working folder of
5
+ * the child, and only then in the folders of `PATH`. So "run `git` in this cloned repository"
6
+ * started a `git.exe` from the top folder of the repository. The helpers give `spawn` the
7
+ * absolute path from `startFile()`, which searches only the folders of `PATH`. The functions take
8
+ * the operating system, the environment and the file test as inputs, so each system tests them.
9
+ */
10
+
11
+ import { statSync } from "node:fs";
12
+ import { win32 } from "node:path";
13
+
14
+ /** The options of `programPath`. Each one has a default from the host, so the tests can set it. */
15
+ export interface ProgramPathOptions {
16
+ /** The operating system (default: `process.platform`). */
17
+ platform?: NodeJS.Platform | undefined;
18
+ /** The environment of the host, for a child environment with no `PATH` (default: `process.env`). */
19
+ hostEnv?: Readonly<Record<string, string | undefined>> | undefined;
20
+ /** The function that tells if a path is a file (default: `fs.statSync`). */
21
+ isFile?: ((path: string) => boolean) | undefined;
22
+ }
23
+
24
+ function isFile(path: string): boolean {
25
+ try {
26
+ return statSync(path).isFile();
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /** True for a path with a drive and a separator (`C:\x`), or a UNC path (`\\server\share`). */
33
+ function fullyQualified(path: string): boolean {
34
+ return /^(?:[a-zA-Z]:[\\/]|[\\/]{2}[^\\/])/.test(path);
35
+ }
36
+
37
+ /**
38
+ * The value of `PATH` in an environment that Node gives to a child on Windows. The names of the
39
+ * variables have no fixed case there. Node keeps the first name in sorted order (`PATH` before
40
+ * `Path`), and leaves out a variable with no value.
41
+ */
42
+ export function windowsPathValue(env: Readonly<Record<string, string | undefined>>): string | undefined {
43
+ const key = Object.keys(env).sort().find((name) => name.toUpperCase() === "PATH");
44
+ return key === undefined ? undefined : env[key];
45
+ }
46
+
47
+ /**
48
+ * The folders of a `PATH` value on Windows, in order, as libuv reads them: a `;` separates two
49
+ * folders, and quotes around a folder are not part of it. The list leaves out an empty folder and
50
+ * a folder that is not fully qualified, because both lead to the working folder.
51
+ */
52
+ export function windowsPathFolders(value: string): string[] {
53
+ const folders: string[] = [];
54
+ let start = 0;
55
+ while (start <= value.length) {
56
+ const quote = value[start];
57
+ let from = start;
58
+ if (quote === '"' || quote === "'") {
59
+ const end = value.indexOf(quote, start + 1);
60
+ from = end === -1 ? value.length : end;
61
+ }
62
+ const semicolon = value.indexOf(";", from);
63
+ const end = semicolon === -1 ? value.length : semicolon;
64
+ const folder = value.slice(start, end).replace(/^["']/, "").replace(/["']$/, "");
65
+ if (fullyQualified(folder)) folders.push(folder);
66
+ start = end + 1;
67
+ }
68
+ return folders;
69
+ }
70
+
71
+ /** True when a command has a folder part, for example `.\x`, `bin/x` or `C:x`. */
72
+ function hasFolder(command: string): boolean {
73
+ return /[\\/:]/.test(command);
74
+ }
75
+
76
+ /**
77
+ * This function gives the file that `command` starts, for `spawn`:
78
+ * - On POSIX, and for a command with a folder part, it gives `command`. The system finds it as before.
79
+ * - On Windows, for a name with no folder, it gives the absolute path of the first match in the
80
+ * folders of the `PATH` of `env`, the environment of the child. It never looks in the working
81
+ * folder. It tries the names that libuv tries: the exact name when it has an extension, then
82
+ * the name with `.com`, then the name with `.exe`.
83
+ * - It gives `undefined` when no folder of `PATH` has the program.
84
+ *
85
+ * When `env` has no `PATH`, libuv uses the `PATH` of the host. This function does the same.
86
+ */
87
+ export function programPath(
88
+ command: string,
89
+ env: Readonly<Record<string, string | undefined>>,
90
+ options: ProgramPathOptions = {},
91
+ ): string | undefined {
92
+ if ((options.platform ?? process.platform) !== "win32") return command;
93
+ // An empty name stays as it is: spawn gives its own error for it
94
+ if (command === "" || hasFolder(command)) return command;
95
+ if (command === ".") return undefined;
96
+ const dot = command.indexOf(".");
97
+ const names = [...(dot !== -1 && dot < command.length - 1 ? [command] : []), `${command}.com`, `${command}.exe`];
98
+ const value = windowsPathValue(env) ?? windowsPathValue(options.hostEnv ?? process.env) ?? "";
99
+ const test = options.isFile ?? isFile;
100
+ for (const folder of windowsPathFolders(value)) {
101
+ for (const name of names) {
102
+ const path = win32.join(folder, name);
103
+ if (test(path)) return path;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ /**
110
+ * A folder name that no file system of Windows allows: NTFS refuses `<` and `>` in a name. A path
111
+ * through it does not exist, so a start of it fails with ENOENT and searches no other folder.
112
+ */
113
+ const NO_FOLDER = "<not in PATH>";
114
+
115
+ /**
116
+ * This function gives the file that `startChild` gives to `spawn`. It is the result of
117
+ * `programPath()` when there is one. When no folder of `PATH` has the program, it gives a fully
118
+ * qualified path that does not exist, in the system folder. So the start fails with ENOENT, as for
119
+ * a missing program, and the system does not look in the working folder.
120
+ */
121
+ export function startFile(
122
+ command: string,
123
+ env: Readonly<Record<string, string | undefined>>,
124
+ options: ProgramPathOptions = {},
125
+ ): string {
126
+ // No join: a join would remove a name such as `..`, and the path would be a folder that exists
127
+ return programPath(command, env, options) ?? `${systemProgram(NO_FOLDER, options.hostEnv ?? process.env)}\\${command}`;
128
+ }
129
+
130
+ /** The part of an error of Node that names the file of a start. */
131
+ interface StartError {
132
+ message: string;
133
+ stack?: string | undefined;
134
+ syscall?: string | undefined;
135
+ path?: string | undefined;
136
+ }
137
+
138
+ /**
139
+ * When `file` is not `command`, this function changes each start error of `child` to name
140
+ * `command`, as Node names it for a start by name: the `message` (`spawn git ENOENT`), the
141
+ * `stack`, the `syscall` and the `path`. So the error of a call is the same as before the lookup.
142
+ * A child with no other listener of `error` still throws the error, as Node does.
143
+ */
144
+ export function nameStartErrors(child: NodeJS.EventEmitter, command: string, file: string): void {
145
+ if (file === command) return;
146
+ const fileCall = `spawn ${file}`;
147
+ const commandCall = `spawn ${command}`;
148
+ child.prependListener("error", (error: StartError) => {
149
+ if (error.syscall === fileCall) {
150
+ error.syscall = commandCall;
151
+ if (error.path === file) error.path = command;
152
+ if (error.message.startsWith(fileCall)) error.message = commandCall + error.message.slice(fileCall.length);
153
+ if (error.stack !== undefined) error.stack = error.stack.replace(fileCall, commandCall);
154
+ }
155
+ if (child.listenerCount("error") === 1) throw error;
156
+ });
157
+ }
158
+
159
+ /**
160
+ * The program and the arguments of `shell.which` for a command. On Windows, `where name` gives a
161
+ * match in the working folder first. `where $PATH:name` looks only in the folders of `PATH`, as a
162
+ * start of a program by its name does. A command with a folder part goes to `where` as it is.
163
+ */
164
+ export function whichCall(command: string, platform: NodeJS.Platform = process.platform): { program: string; args: string[] } {
165
+ if (platform !== "win32") return { program: "which", args: [command] };
166
+ return { program: "where", args: [hasFolder(command) ? command : `$PATH:${command}`] };
167
+ }
168
+
169
+ /**
170
+ * The path of a program of the system folder of Windows, for example `taskkill.exe`. The folder
171
+ * comes from `SystemRoot`, then `windir`, then `C:\Windows`. A value that is not fully qualified
172
+ * is not used.
173
+ */
174
+ export function systemProgram(name: string, env: Readonly<Record<string, string | undefined>> = process.env): string {
175
+ const root = [env["SystemRoot"], env["windir"]].find((value): value is string => value !== undefined && fullyQualified(value));
176
+ return win32.join(root ?? "C:\\Windows", "System32", name);
177
+ }
@@ -2,7 +2,8 @@
2
2
  * Start a child process the one way the helpers allow: an argument list, no shell, no window.
3
3
  */
4
4
 
5
- import { spawn, type ChildProcess } from "node:child_process";
5
+ import { spawn, type ChildProcess, type SpawnOptions as NodeSpawnOptions } from "node:child_process";
6
+ import { nameStartErrors, startFile } from "./program.js";
6
7
  import { OWN_PROCESS_GROUP, track } from "./tree-kill.js";
7
8
 
8
9
  export interface SpawnOptions {
@@ -23,17 +24,28 @@ export interface SpawnOptions {
23
24
  * This function starts `command` with `shell: false` and `windowsHide`. On POSIX the child gets
24
25
  * its own process group, so `killTree` reaches its descendants. The child is recorded until it
25
26
  * ends, so the end of the host kills it.
27
+ *
28
+ * On Windows, a command with no folder starts from a folder of `PATH` only (`startFile`), never
29
+ * from the working folder. When no folder of `PATH` has it, the start fails with ENOENT, as for
30
+ * a missing program. The error names the command, as before.
26
31
  */
27
32
  export function startChild(command: string, options: SpawnOptions, stdin: "pipe" | "ignore"): ChildProcess {
28
- const child = spawn(command, [...(options.args ?? [])], {
33
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
34
+ const file = startFile(command, env);
35
+ const spawnOptions: NodeSpawnOptions = {
29
36
  cwd: options.cwd,
30
- env: options.env ? { ...process.env, ...options.env } : process.env,
37
+ env,
31
38
  shell: false,
32
39
  windowsHide: true,
33
40
  detached: OWN_PROCESS_GROUP,
34
41
  stdio: [stdin, "pipe", "pipe"],
42
+ // The program gets the name of the call as its first argument, as before
43
+ argv0: command,
35
44
  ...(options.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
36
- });
45
+ };
46
+ const args = [...(options.args ?? [])];
47
+ const child = spawn(file, args, spawnOptions);
48
+ nameStartErrors(child, command, file);
37
49
  track(child);
38
50
  return child;
39
51
  }
@@ -17,9 +17,16 @@
17
17
  */
18
18
 
19
19
  import { execFile, spawnSync, type ChildProcess } from "node:child_process";
20
+ import { systemProgram } from "./program.js";
20
21
 
21
22
  const WINDOWS = process.platform === "win32";
22
23
 
24
+ /**
25
+ * The full path of `taskkill.exe` in the system folder. A bare name started a `taskkill.exe` of
26
+ * the working folder of the host first.
27
+ */
28
+ const TASKKILL = WINDOWS ? systemProgram("taskkill.exe") : "taskkill";
29
+
23
30
  /**
24
31
  * True when a child must start with `detached: true`. On POSIX, `detached` puts the child in a
25
32
  * new process group, which `killTree` needs. On Windows, `detached` opens a new console.
@@ -35,7 +42,7 @@ export function killTree(child: ChildProcess, signal: NodeJS.Signals = "SIGTERM"
35
42
  if (!running(child)) return;
36
43
  const pid = child.pid!;
37
44
  if (WINDOWS) {
38
- execFile("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true }, (error) => {
45
+ execFile(TASKKILL, ["/T", "/F", "/PID", String(pid)], { windowsHide: true }, (error) => {
39
46
  // taskkill fails when the process ended in the meantime, or when taskkill is missing
40
47
  if (error && running(child)) child.kill(signal);
41
48
  });
@@ -55,7 +62,7 @@ function killTreeSync(child: ChildProcess): void {
55
62
  const pid = child.pid!;
56
63
  try {
57
64
  if (WINDOWS) {
58
- spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true, stdio: "ignore" });
65
+ spawnSync(TASKKILL, ["/T", "/F", "/PID", String(pid)], { windowsHide: true, stdio: "ignore" });
59
66
  } else {
60
67
  process.kill(-pid, "SIGKILL");
61
68
  }
@@ -4,6 +4,7 @@
4
4
  * Find the full path to a command.
5
5
  */
6
6
 
7
+ import { whichCall } from "../../command/program.js";
7
8
  import { runCommand } from "../../command/run.js";
8
9
  import type { ShellWhichInput, ShellWhichOutput } from "../../types.js";
9
10
 
@@ -16,8 +17,9 @@ export async function shellWhich(
16
17
  if (input.command.startsWith("-")) {
17
18
  return { path: null, found: false };
18
19
  }
19
- const program = process.platform === "win32" ? "where" : "which";
20
- const result = await runCommand(program, { args: [input.command], signal: ctx?.signal, timeout: 30_000 });
20
+ // On Windows, only the folders of PATH, as the start of a program by its name
21
+ const { program, args } = whichCall(input.command);
22
+ const result = await runCommand(program, { args, signal: ctx?.signal, timeout: 30_000 });
21
23
  if (!result.success) {
22
24
  return { path: null, found: false };
23
25
  }