printable-shell-command 1.1.0 → 1.1.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/src/index.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import type {
2
- ChildProcess as NodeChildProcess,
3
- SpawnOptions as NodeSpawnOptions,
4
- SpawnOptionsWithStdioTuple as NodeSpawnOptionsWithStdioTuple,
5
- SpawnOptionsWithoutStdio as NodeSpawnOptionsWithoutStdio,
6
- StdioNull as NodeStdioNull,
7
- StdioPipe as NodeStdioPipe,
2
+ ChildProcess as NodeChildProcess,
3
+ SpawnOptions as NodeSpawnOptions,
4
+ SpawnOptionsWithoutStdio as NodeSpawnOptionsWithoutStdio,
5
+ SpawnOptionsWithStdioTuple as NodeSpawnOptionsWithStdioTuple,
6
+ StdioNull as NodeStdioNull,
7
+ StdioPipe as NodeStdioPipe,
8
8
  } from "node:child_process";
9
+ import { styleText } from "node:util";
9
10
  import type {
10
- SpawnOptions as BunSpawnOptions,
11
- Subprocess as BunSubprocess,
11
+ SpawnOptions as BunSpawnOptions,
12
+ Subprocess as BunSubprocess,
12
13
  } from "bun";
13
14
 
14
15
  const DEFAULT_MAIN_INDENTATION = "";
@@ -20,7 +21,7 @@ const LINE_WRAP_LINE_END = " \\\n";
20
21
 
21
22
  // biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.
22
23
  function isString(s: any): s is string {
23
- return typeof s === "string";
24
+ return typeof s === "string";
24
25
  }
25
26
 
26
27
  // TODO: allow `.toString()`ables?
@@ -30,417 +31,449 @@ type ArgsEntry = SingleArgument | FlagArgumentPair;
30
31
  type Args = ArgsEntry[];
31
32
 
32
33
  export interface PrintOptions {
33
- mainIndentation?: string; // Defaults to ""
34
- argIndentation?: string; // Defaults to " "
35
- // - "auto": Quote only arguments that need it for safety. This tries to be
36
- // portable and safe across shells, but true safety and portability is hard
37
- // to guarantee.
38
- // - "extra-safe": Quote all arguments, even ones that don't need it. This is
39
- // more likely to be safe under all circumstances.
40
- quoting?: "auto" | "extra-safe";
41
- // Line wrapping to use between arguments. Defaults to `"by-entry"`.
42
- argumentLineWrapping?:
43
- | "by-entry"
44
- | "nested-by-entry"
45
- | "by-argument"
46
- | "inline";
34
+ /** Defaults to "" */
35
+ mainIndentation?: string;
36
+ /** Defaults to " " */
37
+ argIndentation?: string;
38
+ /**
39
+ * - `"auto"`: Quote only arguments that need it for safety. This tries to be
40
+ * portable and safe across shells, but true safety and portability is hard
41
+ * to guarantee.
42
+ * - `"extra-safe"`: Quote all arguments, even ones that don't need it. This is
43
+ * more likely to be safe under all circumstances.
44
+ */
45
+ quoting?: "auto" | "extra-safe";
46
+ /** Line wrapping to use between arguments. Defaults to `"by-entry"`. */
47
+ argumentLineWrapping?:
48
+ | "by-entry"
49
+ | "nested-by-entry"
50
+ | "by-argument"
51
+ | "inline";
52
+ /**
53
+ * Style text using `node`'s [`styleText(…)`](https://nodejs.org/api/util.html#utilstyletextformat-text-options)
54
+ *
55
+ * Example usage:
56
+ *
57
+ * ```
58
+ * new PrintableShellCommand("echo", ["hi"]).print({
59
+ * styleTextFormat: ["gray", "bold"],
60
+ * });
61
+ * */
62
+ styleTextFormat?: Parameters<typeof styleText>[0];
47
63
  }
48
64
 
49
65
  // https://mywiki.wooledge.org/BashGuide/SpecialCharacters
50
66
  const SPECIAL_SHELL_CHARACTERS = new Set([
51
- " ",
52
- '"',
53
- "'",
54
- "`",
55
- "|",
56
- "$",
57
- "*",
58
- "?",
59
- ">",
60
- "<",
61
- "(",
62
- ")",
63
- "[",
64
- "]",
65
- "{",
66
- "}",
67
- "&",
68
- "\\",
69
- ";",
67
+ " ",
68
+ '"',
69
+ "'",
70
+ "`",
71
+ "|",
72
+ "$",
73
+ "*",
74
+ "?",
75
+ ">",
76
+ "<",
77
+ "(",
78
+ ")",
79
+ "[",
80
+ "]",
81
+ "{",
82
+ "}",
83
+ "&",
84
+ "\\",
85
+ ";",
70
86
  ]);
71
87
 
72
88
  // https://mywiki.wooledge.org/BashGuide/SpecialCharacters
73
89
  const SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND =
74
- // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)
75
- (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set(["="]));
90
+ // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)
91
+ (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set(["="]));
76
92
 
77
93
  export class PrintableShellCommand {
78
- #commandName: string;
79
- constructor(
80
- commandName: string,
81
- private args: Args = [],
82
- ) {
83
- if (!isString(commandName)) {
84
- // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.
85
- throw new Error("Command name is not a string:", commandName as any);
86
- }
87
- this.#commandName = commandName;
88
- if (typeof args === "undefined") {
89
- return;
90
- }
91
- if (!Array.isArray(args)) {
92
- throw new Error("Command arguments are not an array");
93
- }
94
- for (let i = 0; i < args.length; i++) {
95
- const argEntry = args[i];
96
- if (typeof argEntry === "string") {
97
- continue;
98
- }
99
- if (
100
- Array.isArray(argEntry) &&
101
- argEntry.length === 2 &&
102
- isString(argEntry[0]) &&
103
- isString(argEntry[1])
104
- ) {
105
- continue;
106
- }
107
- throw new Error(`Invalid arg entry at index: ${i}`);
108
- }
109
- }
110
-
111
- get commandName(): string {
112
- return this.#commandName;
113
- }
114
-
115
- // For use with `bun`.
116
- //
117
- // Usage example:
118
- //
119
- // import { PrintableShellCommand } from "printable-shell-command";
120
- // import { spawn } from "bun";
121
- //
122
- // const command = new PrintableShellCommand(/* … */);
123
- // await spawn(command.toFlatCommand()).exited;
124
- //
125
- public toFlatCommand(): string[] {
126
- return [this.commandName, ...this.args.flat()];
127
- }
128
-
129
- // Convenient alias for `toFlatCommand()`.
130
- //
131
- // Usage example:
132
- //
133
- // import { PrintableShellCommand } from "printable-shell-command";
134
- // import { spawn } from "bun";
135
- //
136
- // const command = new PrintableShellCommand(/* … */);
137
- // await spawn(command.forBun()).exited;
138
- //
139
- public forBun(): string[] {
140
- return this.toFlatCommand();
141
- }
142
-
143
- // For use with `node:child_process`
144
- //
145
- // Usage example:
146
- //
147
- // import { PrintableShellCommand } from "printable-shell-command";
148
- // import { spawn } from "node:child_process";
149
- //
150
- // const command = new PrintableShellCommand(/* … */);
151
- // const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`
152
- //
153
- public toCommandWithFlatArgs(): [string, string[]] {
154
- return [this.commandName, this.args.flat()];
155
- }
156
-
157
- // For use with `node:child_process`
158
- //
159
- // Usage example:
160
- //
161
- // import { PrintableShellCommand } from "printable-shell-command";
162
- // import { spawn } from "node:child_process";
163
- //
164
- // const command = new PrintableShellCommand(/* … */);
165
- // const child_process = spawn(...command.forNode()); // Note the `...`
166
- //
167
- // Convenient alias for `toCommandWithFlatArgs()`.
168
- public forNode(): [string, string[]] {
169
- return this.toCommandWithFlatArgs();
170
- }
171
-
172
- #escapeArg(
173
- arg: string,
174
- isMainCommand: boolean,
175
- options: PrintOptions,
176
- ): string {
177
- const argCharacters = new Set(arg);
178
- const specialShellCharacters = isMainCommand
179
- ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND
180
- : SPECIAL_SHELL_CHARACTERS;
181
- if (
182
- options?.quoting === "extra-safe" ||
183
- // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)
184
- (argCharacters as unknown as any).intersection(specialShellCharacters)
185
- .size > 0
186
- ) {
187
- // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).
188
- const escaped = arg.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
189
- return `'${escaped}'`;
190
- }
191
- return arg;
192
- }
193
-
194
- #mainIndentation(options: PrintOptions): string {
195
- return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;
196
- }
197
-
198
- #argIndentation(options: PrintOptions): string {
199
- return (
200
- this.#mainIndentation(options) +
201
- (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)
202
- );
203
- }
204
-
205
- #lineWrapSeparator(options: PrintOptions): string {
206
- return LINE_WRAP_LINE_END + this.#argIndentation(options);
207
- }
208
-
209
- #argPairSeparator(options: PrintOptions): string {
210
- switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
211
- case "by-entry": {
212
- return INLINE_SEPARATOR;
213
- }
214
- case "nested-by-entry": {
215
- return this.#lineWrapSeparator(options) + this.#argIndentation(options);
216
- }
217
- case "by-argument": {
218
- return this.#lineWrapSeparator(options);
219
- }
220
- case "inline": {
221
- return INLINE_SEPARATOR;
222
- }
223
- default:
224
- throw new Error("Invalid argument line wrapping argument.");
225
- }
226
- }
227
-
228
- #entrySeparator(options: PrintOptions): string {
229
- switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
230
- case "by-entry": {
231
- return LINE_WRAP_LINE_END + this.#argIndentation(options);
232
- }
233
- case "nested-by-entry": {
234
- return LINE_WRAP_LINE_END + this.#argIndentation(options);
235
- }
236
- case "by-argument": {
237
- return LINE_WRAP_LINE_END + this.#argIndentation(options);
238
- }
239
- case "inline": {
240
- return INLINE_SEPARATOR;
241
- }
242
- default:
243
- throw new Error("Invalid argument line wrapping argument.");
244
- }
245
- }
246
-
247
- public getPrintableCommand(options?: PrintOptions): string {
248
- // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???
249
- // biome-ignore lint/style/noParameterAssign: We want a default assignment without affecting the signature.
250
- options ??= {};
251
- const serializedEntries: string[] = [];
252
-
253
- serializedEntries.push(
254
- this.#mainIndentation(options) +
255
- this.#escapeArg(this.commandName, true, options),
256
- );
257
-
258
- for (let i = 0; i < this.args.length; i++) {
259
- const argsEntry = this.args[i];
260
-
261
- if (isString(argsEntry)) {
262
- serializedEntries.push(this.#escapeArg(argsEntry, false, options));
263
- } else {
264
- const [part1, part2] = argsEntry;
265
- serializedEntries.push(
266
- this.#escapeArg(part1, false, options) +
267
- this.#argPairSeparator(options) +
268
- this.#escapeArg(part2, false, options),
269
- );
270
- }
271
- }
272
-
273
- return serializedEntries.join(this.#entrySeparator(options));
274
- }
275
-
276
- public print(options?: PrintOptions): PrintableShellCommand {
277
- console.log(this.getPrintableCommand(options));
278
- return this;
279
- }
280
-
281
- /**
282
- * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313
283
- */
284
- public spawnNode<
285
- Stdin extends NodeStdioNull | NodeStdioPipe,
286
- Stdout extends NodeStdioNull | NodeStdioPipe,
287
- Stderr extends NodeStdioNull | NodeStdioPipe,
288
- >(
289
- options?:
290
- | NodeSpawnOptions
291
- | NodeSpawnOptionsWithoutStdio
292
- | NodeSpawnOptionsWithStdioTuple<Stdin, Stdout, Stderr>,
293
- ): // TODO: figure out how to return `ChildProcessByStdio<…>` without duplicating fragile boilerplate.
294
- NodeChildProcess & { success: Promise<void> } {
295
- const { spawn } = process.getBuiltinModule("node:child_process");
296
- // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.
297
- const subprocess = spawn(...this.forNode(), options) as NodeChildProcess & {
298
- success: Promise<void>;
299
- };
300
- Object.defineProperty(subprocess, "success", {
301
- get() {
302
- return new Promise<void>((resolve, reject) =>
303
- this.addListener(
304
- "exit",
305
- (exitCode: number /* we only use the first arg */) => {
306
- if (exitCode === 0) {
307
- resolve();
308
- } else {
309
- reject(`Command failed with non-zero exit code: ${exitCode}`);
310
- }
311
- },
312
- ),
313
- );
314
- },
315
- enumerable: false,
316
- });
317
- return subprocess;
318
- }
319
-
320
- /** A wrapper for `.spawnNode(…)` that sets stdio to `"inherit"` (common for
321
- * invoking commands from scripts whose output and interaction should be
322
- * surfaced to the user). */
323
- public spawnNodeInherit(
324
- options?: Omit<NodeSpawnOptions, "stdio">,
325
- ): NodeChildProcess & { success: Promise<void> } {
326
- if (options && "stdio" in options) {
327
- throw new Error("Unexpected `stdio` field.");
328
- }
329
- return this.spawnNode({ ...options, stdio: "inherit" });
330
- }
331
-
332
- /** Equivalent to:
333
- *
334
- * ```
335
- * await this.print().spawnNodeInherit().success;
336
- * ```
337
- */
338
- public async shellOutNode(
339
- options?: Omit<NodeSpawnOptions, "stdio">,
340
- ): Promise<void> {
341
- await this.print().spawnNodeInherit(options).success;
342
- }
343
-
344
- /**
345
- * The returned subprocess includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313
346
- */
347
- public spawnBun<
348
- const In extends BunSpawnOptions.Writable = "ignore",
349
- const Out extends BunSpawnOptions.Readable = "pipe",
350
- const Err extends BunSpawnOptions.Readable = "inherit",
351
- >(
352
- options?: Omit<BunSpawnOptions.OptionsObject<In, Out, Err>, "cmd">,
353
- ): BunSubprocess<In, Out, Err> & { success: Promise<void> } {
354
- if (options && "cmd" in options) {
355
- throw new Error("Unexpected `cmd` field.");
356
- }
357
- const { spawn } = process.getBuiltinModule("bun") as typeof import("bun");
358
- const subprocess = spawn({
359
- ...options,
360
- cmd: this.forBun(),
361
- }) as BunSubprocess<In, Out, Err> & { success: Promise<void> };
362
- Object.defineProperty(subprocess, "success", {
363
- get() {
364
- return new Promise<void>((resolve, reject) =>
365
- this.exited
366
- .then((exitCode: number) => {
367
- if (exitCode === 0) {
368
- resolve();
369
- } else {
370
- reject(
371
- new Error(
372
- `Command failed with non-zero exit code: ${exitCode}`,
373
- ),
374
- );
375
- }
376
- })
377
- .catch(reject),
378
- );
379
- },
380
- enumerable: false,
381
- });
382
- return subprocess;
383
- }
384
-
385
- /**
386
- * A wrapper for `.spawnBunInherit(…)` that sets stdio to `"inherit"` (common
387
- * for invoking commands from scripts whose output and interaction should be
388
- * surfaced to the user).
389
- */
390
- public spawnBunInherit(
391
- options?: Omit<
392
- Omit<
393
- BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
394
- "cmd"
395
- >,
396
- "stdio"
397
- >,
398
- ): BunSubprocess<"inherit", "inherit", "inherit"> & {
399
- success: Promise<void>;
400
- } {
401
- if (options && "stdio" in options) {
402
- throw new Error("Unexpected `stdio` field.");
403
- }
404
- return this.spawnBun({
405
- ...options,
406
- stdio: ["inherit", "inherit", "inherit"],
407
- });
408
- }
409
-
410
- /** Equivalent to:
411
- *
412
- * ```
413
- * new Response(this.spawnBun(options).stdout);
414
- * ```
415
- */
416
- public spawnBunStdout(
417
- options?: Omit<
418
- Omit<
419
- BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
420
- "cmd"
421
- >,
422
- "stdio"
423
- >,
424
- ): Response {
425
- // biome-ignore lint/suspicious/noExplicitAny: Avoid breaking the lib check when used without `@types/bun`.
426
- return new Response((this.spawnBun(options) as any).stdout);
427
- }
428
-
429
- /** Equivalent to:
430
- *
431
- * ```
432
- * await this.print().spawnBunInherit().success;
433
- * ```
434
- */
435
- public async shellOutBun(
436
- options?: Omit<
437
- Omit<
438
- BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
439
- "cmd"
440
- >,
441
- "stdio"
442
- >,
443
- ): Promise<void> {
444
- await this.print().spawnBunInherit(options).success;
445
- }
94
+ #commandName: string;
95
+ constructor(
96
+ commandName: string,
97
+ private args: Args = [],
98
+ ) {
99
+ if (!isString(commandName)) {
100
+ // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.
101
+ throw new Error("Command name is not a string:", commandName as any);
102
+ }
103
+ this.#commandName = commandName;
104
+ if (typeof args === "undefined") {
105
+ return;
106
+ }
107
+ if (!Array.isArray(args)) {
108
+ throw new Error("Command arguments are not an array");
109
+ }
110
+ for (let i = 0; i < args.length; i++) {
111
+ const argEntry = args[i];
112
+ if (typeof argEntry === "string") {
113
+ continue;
114
+ }
115
+ if (
116
+ Array.isArray(argEntry) &&
117
+ argEntry.length === 2 &&
118
+ isString(argEntry[0]) &&
119
+ isString(argEntry[1])
120
+ ) {
121
+ continue;
122
+ }
123
+ throw new Error(`Invalid arg entry at index: ${i}`);
124
+ }
125
+ }
126
+
127
+ get commandName(): string {
128
+ return this.#commandName;
129
+ }
130
+
131
+ /** For use with `bun`.
132
+ *
133
+ * Usage example:
134
+ *
135
+ * ```
136
+ * import { PrintableShellCommand } from "printable-shell-command";
137
+ * import { spawn } from "bun";
138
+ *
139
+ * const command = new PrintableShellCommand();
140
+ * await spawn(command.toFlatCommand()).exited;
141
+ * ```
142
+ */
143
+ public toFlatCommand(): string[] {
144
+ return [this.commandName, ...this.args.flat()];
145
+ }
146
+
147
+ /**
148
+ * Convenient alias for `toFlatCommand()`.
149
+ *
150
+ * Usage example:
151
+ *
152
+ * ```
153
+ * import { PrintableShellCommand } from "printable-shell-command";
154
+ * import { spawn } from "bun";
155
+ *
156
+ * const command = new PrintableShellCommand();
157
+ * await spawn(command.forBun()).exited;
158
+ * ```
159
+ *
160
+ * */
161
+ public forBun(): string[] {
162
+ return this.toFlatCommand();
163
+ }
164
+
165
+ /**
166
+ * For use with `node:child_process`
167
+ *
168
+ * Usage example:
169
+ *
170
+ * ```
171
+ * import { PrintableShellCommand } from "printable-shell-command";
172
+ * import { spawn } from "node:child_process";
173
+ *
174
+ * const command = new PrintableShellCommand( … );
175
+ * const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`
176
+ * ```
177
+ *
178
+ */
179
+ public toCommandWithFlatArgs(): [string, string[]] {
180
+ return [this.commandName, this.args.flat()];
181
+ }
182
+
183
+ /**
184
+ * For use with `node:child_process`
185
+ *
186
+ * Usage example:
187
+ *
188
+ * ```
189
+ * import { PrintableShellCommand } from "printable-shell-command";
190
+ * import { spawn } from "node:child_process";
191
+ *
192
+ * const command = new PrintableShellCommand( … );
193
+ * const child_process = spawn(...command.forNode()); // Note the `...`
194
+ * ```
195
+ *
196
+ * Convenient alias for `toCommandWithFlatArgs()`.
197
+ */
198
+ public forNode(): [string, string[]] {
199
+ return this.toCommandWithFlatArgs();
200
+ }
201
+
202
+ #escapeArg(
203
+ arg: string,
204
+ isMainCommand: boolean,
205
+ options: PrintOptions,
206
+ ): string {
207
+ const argCharacters = new Set(arg);
208
+ const specialShellCharacters = isMainCommand
209
+ ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND
210
+ : SPECIAL_SHELL_CHARACTERS;
211
+ if (
212
+ options?.quoting === "extra-safe" ||
213
+ // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)
214
+ (argCharacters as unknown as any).intersection(specialShellCharacters)
215
+ .size > 0
216
+ ) {
217
+ // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).
218
+ const escaped = arg.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
219
+ return `'${escaped}'`;
220
+ }
221
+ return arg;
222
+ }
223
+
224
+ #mainIndentation(options: PrintOptions): string {
225
+ return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;
226
+ }
227
+
228
+ #argIndentation(options: PrintOptions): string {
229
+ return (
230
+ this.#mainIndentation(options) +
231
+ (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)
232
+ );
233
+ }
234
+
235
+ #lineWrapSeparator(options: PrintOptions): string {
236
+ return LINE_WRAP_LINE_END + this.#argIndentation(options);
237
+ }
238
+
239
+ #argPairSeparator(options: PrintOptions): string {
240
+ switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
241
+ case "by-entry": {
242
+ return INLINE_SEPARATOR;
243
+ }
244
+ case "nested-by-entry": {
245
+ return this.#lineWrapSeparator(options) + this.#argIndentation(options);
246
+ }
247
+ case "by-argument": {
248
+ return this.#lineWrapSeparator(options);
249
+ }
250
+ case "inline": {
251
+ return INLINE_SEPARATOR;
252
+ }
253
+ default:
254
+ throw new Error("Invalid argument line wrapping argument.");
255
+ }
256
+ }
257
+
258
+ #entrySeparator(options: PrintOptions): string {
259
+ switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
260
+ case "by-entry": {
261
+ return LINE_WRAP_LINE_END + this.#argIndentation(options);
262
+ }
263
+ case "nested-by-entry": {
264
+ return LINE_WRAP_LINE_END + this.#argIndentation(options);
265
+ }
266
+ case "by-argument": {
267
+ return LINE_WRAP_LINE_END + this.#argIndentation(options);
268
+ }
269
+ case "inline": {
270
+ return INLINE_SEPARATOR;
271
+ }
272
+ default:
273
+ throw new Error("Invalid argument line wrapping argument.");
274
+ }
275
+ }
276
+
277
+ public getPrintableCommand(options?: PrintOptions): string {
278
+ // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???
279
+ options ??= {};
280
+ const serializedEntries: string[] = [];
281
+
282
+ serializedEntries.push(
283
+ this.#mainIndentation(options) +
284
+ this.#escapeArg(this.commandName, true, options),
285
+ );
286
+
287
+ for (let i = 0; i < this.args.length; i++) {
288
+ const argsEntry = this.args[i];
289
+
290
+ if (isString(argsEntry)) {
291
+ serializedEntries.push(this.#escapeArg(argsEntry, false, options));
292
+ } else {
293
+ const [part1, part2] = argsEntry;
294
+ serializedEntries.push(
295
+ this.#escapeArg(part1, false, options) +
296
+ this.#argPairSeparator(options) +
297
+ this.#escapeArg(part2, false, options),
298
+ );
299
+ }
300
+ }
301
+
302
+ let text = serializedEntries.join(this.#entrySeparator(options));
303
+ if (options?.styleTextFormat) {
304
+ text = styleText(options.styleTextFormat, text);
305
+ }
306
+ return text;
307
+ }
308
+
309
+ public print(options?: PrintOptions): PrintableShellCommand {
310
+ console.log(this.getPrintableCommand(options));
311
+ return this;
312
+ }
313
+
314
+ /**
315
+ * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313
316
+ */
317
+ public spawnNode<
318
+ Stdin extends NodeStdioNull | NodeStdioPipe,
319
+ Stdout extends NodeStdioNull | NodeStdioPipe,
320
+ Stderr extends NodeStdioNull | NodeStdioPipe,
321
+ >(
322
+ options?:
323
+ | NodeSpawnOptions
324
+ | NodeSpawnOptionsWithoutStdio
325
+ | NodeSpawnOptionsWithStdioTuple<Stdin, Stdout, Stderr>,
326
+ ): // TODO: figure out how to return `ChildProcessByStdio<…>` without duplicating fragile boilerplate.
327
+ NodeChildProcess & { success: Promise<void> } {
328
+ const { spawn } = process.getBuiltinModule("node:child_process");
329
+ // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.
330
+ const subprocess = spawn(...this.forNode(), options) as NodeChildProcess & {
331
+ success: Promise<void>;
332
+ };
333
+ Object.defineProperty(subprocess, "success", {
334
+ get() {
335
+ return new Promise<void>((resolve, reject) =>
336
+ this.addListener(
337
+ "exit",
338
+ (exitCode: number /* we only use the first arg */) => {
339
+ if (exitCode === 0) {
340
+ resolve();
341
+ } else {
342
+ reject(`Command failed with non-zero exit code: ${exitCode}`);
343
+ }
344
+ },
345
+ ),
346
+ );
347
+ },
348
+ enumerable: false,
349
+ });
350
+ return subprocess;
351
+ }
352
+
353
+ /** A wrapper for `.spawnNode(…)` that sets stdio to `"inherit"` (common for
354
+ * invoking commands from scripts whose output and interaction should be
355
+ * surfaced to the user). */
356
+ public spawnNodeInherit(
357
+ options?: Omit<NodeSpawnOptions, "stdio">,
358
+ ): NodeChildProcess & { success: Promise<void> } {
359
+ if (options && "stdio" in options) {
360
+ throw new Error("Unexpected `stdio` field.");
361
+ }
362
+ return this.spawnNode({ ...options, stdio: "inherit" });
363
+ }
364
+
365
+ /** Equivalent to:
366
+ *
367
+ * ```
368
+ * await this.print().spawnNodeInherit().success;
369
+ * ```
370
+ */
371
+ public async shellOutNode(
372
+ options?: Omit<NodeSpawnOptions, "stdio">,
373
+ ): Promise<void> {
374
+ await this.print().spawnNodeInherit(options).success;
375
+ }
376
+
377
+ /**
378
+ * The returned subprocess includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313
379
+ */
380
+ public spawnBun<
381
+ const In extends BunSpawnOptions.Writable = "ignore",
382
+ const Out extends BunSpawnOptions.Readable = "pipe",
383
+ const Err extends BunSpawnOptions.Readable = "inherit",
384
+ >(
385
+ options?: Omit<BunSpawnOptions.OptionsObject<In, Out, Err>, "cmd">,
386
+ ): BunSubprocess<In, Out, Err> & { success: Promise<void> } {
387
+ if (options && "cmd" in options) {
388
+ throw new Error("Unexpected `cmd` field.");
389
+ }
390
+ const { spawn } = process.getBuiltinModule("bun") as typeof import("bun");
391
+ const subprocess = spawn({
392
+ ...options,
393
+ cmd: this.forBun(),
394
+ }) as BunSubprocess<In, Out, Err> & { success: Promise<void> };
395
+ Object.defineProperty(subprocess, "success", {
396
+ get() {
397
+ return new Promise<void>((resolve, reject) =>
398
+ this.exited
399
+ .then((exitCode: number) => {
400
+ if (exitCode === 0) {
401
+ resolve();
402
+ } else {
403
+ reject(
404
+ new Error(
405
+ `Command failed with non-zero exit code: ${exitCode}`,
406
+ ),
407
+ );
408
+ }
409
+ })
410
+ .catch(reject),
411
+ );
412
+ },
413
+ enumerable: false,
414
+ });
415
+ return subprocess;
416
+ }
417
+
418
+ /**
419
+ * A wrapper for `.spawnBunInherit(…)` that sets stdio to `"inherit"` (common
420
+ * for invoking commands from scripts whose output and interaction should be
421
+ * surfaced to the user).
422
+ */
423
+ public spawnBunInherit(
424
+ options?: Omit<
425
+ Omit<
426
+ BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
427
+ "cmd"
428
+ >,
429
+ "stdio"
430
+ >,
431
+ ): BunSubprocess<"inherit", "inherit", "inherit"> & {
432
+ success: Promise<void>;
433
+ } {
434
+ if (options && "stdio" in options) {
435
+ throw new Error("Unexpected `stdio` field.");
436
+ }
437
+ return this.spawnBun({
438
+ ...options,
439
+ stdio: ["inherit", "inherit", "inherit"],
440
+ });
441
+ }
442
+
443
+ /** Equivalent to:
444
+ *
445
+ * ```
446
+ * new Response(this.spawnBun(options).stdout);
447
+ * ```
448
+ */
449
+ public spawnBunStdout(
450
+ options?: Omit<
451
+ Omit<
452
+ BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
453
+ "cmd"
454
+ >,
455
+ "stdio"
456
+ >,
457
+ ): Response {
458
+ // biome-ignore lint/suspicious/noExplicitAny: Avoid breaking the lib check when used without `@types/bun`.
459
+ return new Response((this.spawnBun(options) as any).stdout);
460
+ }
461
+
462
+ /** Equivalent to:
463
+ *
464
+ * ```
465
+ * await this.print().spawnBunInherit().success;
466
+ * ```
467
+ */
468
+ public async shellOutBun(
469
+ options?: Omit<
470
+ Omit<
471
+ BunSpawnOptions.OptionsObject<"inherit", "inherit", "inherit">,
472
+ "cmd"
473
+ >,
474
+ "stdio"
475
+ >,
476
+ ): Promise<void> {
477
+ await this.print().spawnBunInherit(options).success;
478
+ }
446
479
  }