printable-shell-command 5.0.6 → 5.0.7

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.
@@ -4,6 +4,7 @@ import type { WriteStream } from "node:tty";
4
4
  import { styleText } from "node:util";
5
5
  import { Path } from "path-class";
6
6
  import type { NodeWithCwd, spawnType, WithSuccess } from "./spawn";
7
+ import { type TrailingNewlineOptions } from "./trimTrailingNewlines";
7
8
  type StyleTextFormat = Parameters<typeof styleText>[0];
8
9
  type SingleArgument = string | Path;
9
10
  type ArgsEntry = SingleArgument | SingleArgument[];
@@ -125,8 +126,12 @@ export declare class PrintableShellCommand {
125
126
  *
126
127
  */
127
128
  spawnDetached(options?: NodeWithCwd<Omit<Omit<NodeSpawnOptions, "stdio">, "detached">>): void;
128
- stdout(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>): Response;
129
- stderr(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>): Response;
129
+ stdout(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>): Response & {
130
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
131
+ };
132
+ stderr(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>): Response & {
133
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
134
+ };
130
135
  /**
131
136
  * Convenience function for:
132
137
  *
@@ -134,7 +139,9 @@ export declare class PrintableShellCommand {
134
139
  *
135
140
  * This can make some simple invocations easier to read and/or fit on a single line.
136
141
  */
137
- text(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>): Promise<string>;
142
+ text(options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">> & {
143
+ trimTrailingNewlines: "single-required" | "single-if-present" | "never";
144
+ }): Promise<string>;
138
145
  /**
139
146
  * Convenience function for:
140
147
  *
@@ -5,6 +5,50 @@ import { stderr } from "node:process";
5
5
  import { Readable, Writable } from "node:stream";
6
6
  import { styleText } from "node:util";
7
7
  import { Path, stringifyIfPath } from "path-class";
8
+
9
+ // src/trimTrailingNewlines.ts
10
+ var DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR = "never";
11
+ async function handleTrailingNewlines(text, options) {
12
+ const awaitedText = await text;
13
+ switch (options?.trimTrailingNewlines ?? DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR) {
14
+ case "single-required": {
15
+ if (!awaitedText.endsWith("\n")) {
16
+ throw new Error("Trailing newline required, but not present.");
17
+ }
18
+ return awaitedText.slice(0, -1);
19
+ }
20
+ case "single-if-present": {
21
+ if (awaitedText.endsWith("\n")) {
22
+ return awaitedText.slice(0, -1);
23
+ } else {
24
+ return awaitedText;
25
+ }
26
+ }
27
+ case "never": {
28
+ return awaitedText;
29
+ }
30
+ default:
31
+ throw new Error("Invalid value for `trimTrailingNewlines`.");
32
+ }
33
+ }
34
+ function wrapHandleTrailingNewlines(fn) {
35
+ return (options) => handleTrailingNewlines(fn(), options);
36
+ }
37
+ function wrapHandleTrailingNewlinesForText(v) {
38
+ const originalTextFn = v.text.bind(v);
39
+ return wrapHandleTrailingNewlines(originalTextFn);
40
+ }
41
+ function wrapHandleTrailingNewlinesForResponse(response) {
42
+ const textFn = wrapHandleTrailingNewlinesForText(response);
43
+ Object.defineProperty(response, "text", {
44
+ get: () => {
45
+ return textFn;
46
+ }
47
+ });
48
+ return response;
49
+ }
50
+
51
+ // src/PrintableShellCommand.ts
8
52
  var DEFAULT_MAIN_INDENTATION = "";
9
53
  var DEFAULT_ARG_INDENTATION = " ";
10
54
  var DEFAULT_ARGUMENT_LINE_WRAPPING = "by-entry";
@@ -261,11 +305,11 @@ var PrintableShellCommand = class {
261
305
  let cachedResponse;
262
306
  s.stdout.response = () => (
263
307
  // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592
264
- cachedResponse ??= new Response(
265
- Readable.from(this.#generator(s.stdout, s.success))
308
+ cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
309
+ new Response(Readable.from(this.#generator(s.stdout, s.success)))
266
310
  )
267
311
  );
268
- s.stdout.text = () => s.stdout.response().text();
312
+ s.stdout.text = wrapHandleTrailingNewlinesForText(s.stdout.response());
269
313
  const thisCached = this;
270
314
  s.stdout.text0 = async function* () {
271
315
  yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));
@@ -277,11 +321,11 @@ var PrintableShellCommand = class {
277
321
  let cachedResponse;
278
322
  s.stderr.response = () => (
279
323
  // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592
280
- cachedResponse ??= new Response(
281
- Readable.from(this.#generator(s.stderr, s.success))
324
+ cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
325
+ new Response(Readable.from(this.#generator(s.stderr, s.success)))
282
326
  )
283
327
  );
284
- s.stderr.text = () => s.stderr.response().text();
328
+ s.stderr.text = wrapHandleTrailingNewlinesForText(s.stderr.response());
285
329
  const thisCached = this;
286
330
  s.stderr.text0 = async function* () {
287
331
  yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));
@@ -370,7 +414,9 @@ var PrintableShellCommand = class {
370
414
  return this.#generator(subprocess.stdout, subprocess.success);
371
415
  }
372
416
  stdout(options) {
373
- return new Response(Readable.from(this.#stdoutSpawnGenerator(options)));
417
+ return wrapHandleTrailingNewlinesForResponse(
418
+ new Response(Readable.from(this.#stdoutSpawnGenerator(options)))
419
+ );
374
420
  }
375
421
  #stderrSpawnGenerator(options) {
376
422
  if (options && "stdio" in options) {
@@ -383,7 +429,9 @@ var PrintableShellCommand = class {
383
429
  return this.#generator(subprocess.stderr, subprocess.success);
384
430
  }
385
431
  stderr(options) {
386
- return new Response(Readable.from(this.#stderrSpawnGenerator(options)));
432
+ return wrapHandleTrailingNewlinesForResponse(
433
+ new Response(Readable.from(this.#stderrSpawnGenerator(options)))
434
+ );
387
435
  }
388
436
  async *#split0(generator) {
389
437
  let pending = "";
@@ -406,8 +454,14 @@ var PrintableShellCommand = class {
406
454
  *
407
455
  * This can make some simple invocations easier to read and/or fit on a single line.
408
456
  */
409
- text(options) {
410
- return this.stdout(options).text();
457
+ async text(options) {
458
+ const {
459
+ trimTrailingNewlines: trimTrailingNewlinesOption,
460
+ ...spawnOptions
461
+ } = options ?? {};
462
+ return handleTrailingNewlines(this.stdout(spawnOptions).text(), {
463
+ trimTrailingNewlines: trimTrailingNewlinesOption
464
+ });
411
465
  }
412
466
  /**
413
467
  * Convenience function for:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/PrintableShellCommand.ts"],
4
- "sourcesContent": ["import assert from \"node:assert\";\nimport type {\n ChildProcessByStdio,\n ChildProcess as NodeChildProcess,\n SpawnOptions as NodeSpawnOptions,\n} from \"node:child_process\";\nimport { createReadStream } from \"node:fs\";\nimport { stderr } from \"node:process\";\nimport { Readable, Writable } from \"node:stream\";\nimport type { WriteStream } from \"node:tty\";\nimport { styleText } from \"node:util\";\nimport { Path, stringifyIfPath } from \"path-class\";\nimport type {\n NodeWithCwd,\n spawnType,\n WithStderrResponse,\n WithStdoutResponse,\n WithSuccess,\n} from \"./spawn\";\n\n// TODO: does this import work?\n/**\n * @import { stdout } from \"node:process\"\n */\n\nconst DEFAULT_MAIN_INDENTATION = \"\";\nconst DEFAULT_ARG_INDENTATION = \" \";\nconst DEFAULT_ARGUMENT_LINE_WRAPPING = \"by-entry\";\n\nconst INLINE_SEPARATOR = \" \";\nconst LINE_WRAP_LINE_END = \" \\\\\\n\";\n\ntype StyleTextFormat = Parameters<typeof styleText>[0];\n\nconst TTY_AUTO_STYLE: StyleTextFormat = [\"gray\", \"bold\"];\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.\nfunction isString(s: any): s is string {\n return typeof s === \"string\";\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type here.\nfunction isValidArgsEntryArray(entries: any[]): entries is SingleArgument[] {\n for (const entry of entries) {\n if (isString(entry)) {\n continue;\n }\n if (entry instanceof Path) {\n continue;\n }\n return false;\n }\n return true;\n}\n\n// TODO: allow `.toString()`ables?\ntype SingleArgument = string | Path;\ntype ArgsEntry = SingleArgument | SingleArgument[];\ntype Args = ArgsEntry[];\n\nconst ARGUMENT_LINE_WRAPPING_VALUES = [\n \"by-entry\",\n \"nested-by-entry\",\n \"by-argument\",\n \"inline\",\n] as const;\ntype ArgumentLineWrapping = (typeof ARGUMENT_LINE_WRAPPING_VALUES)[number];\n\nexport interface PrintOptions {\n /** Defaults to \"\" */\n mainIndentation?: string;\n /** Defaults to \" \" */\n argIndentation?: string;\n /**\n * - `\"auto\"`: Quote only arguments that need it for safety. This tries to be\n * portable and safe across shells, but true safety and portability is hard\n * to guarantee.\n * - `\"extra-safe\"`: Quote all arguments, even ones that don't need it. This is\n * more likely to be safe under all circumstances.\n */\n quoting?: \"auto\" | \"extra-safe\";\n /** Line wrapping to use between arguments. Defaults to `\"by-entry\"`. */\n argumentLineWrapping?: ArgumentLineWrapping;\n /** Include the first arg (or first arg group) on the same line as the command, regardless of the `argumentLineWrapping` setting. */\n skipLineWrapBeforeFirstArg?: true | false;\n /**\n * Style text using `node`'s {@link styleText | `styleText(\u2026)`}.\n *\n * Example usage:\n *\n * ```\n * new PrintableShellCommand(\"echo\", [\"hi\"]).print({\n * style: [\"green\", \"underline\"],\n * });\n * */\n style?: StyleTextFormat;\n}\n\nexport interface StreamPrintOptions extends PrintOptions {\n /**\n * Auto-style the text when:\n *\n * - the output stream is detected to be a TTY\n * - `styleTextFormat` is not specified.\n *\n * The current auto style is: `[\"gray\", \"bold\"]`\n */\n autoStyle?: \"tty\" | \"never\";\n // This would be a `WritableStream` (open web standard), but `WriteStream` allows us to query `.isTTY`.\n stream?: WriteStream | Writable;\n}\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS = new Set([\n \" \",\n '\"',\n \"'\",\n \"`\",\n \"|\",\n \"$\",\n \"*\",\n \"?\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"{\",\n \"}\",\n \"&\",\n \"\\\\\",\n \";\",\n \"#\",\n]);\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND =\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set([\"=\"]));\n\n// TODO: Is there an idiomatic ways to check that all potential fields of\n// `StdinSource` satisfy `(typeof STDIN_SOURCE_KEYS)[number]`, without adding\n// extra indirection for type wrangling?\nconst STDIN_SOURCE_KEYS = [\"text\", \"json\", \"path\", \"stream\"] as const;\nexport type StdinSource =\n | { text: string }\n // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON data.\n | { json: any }\n | { path: string | Path }\n | { stream: Readable | ReadableStream };\n\nexport class PrintableShellCommand {\n #commandName: string | Path;\n constructor(\n commandName: string | Path,\n private args: Args = [],\n ) {\n if (!isString(commandName) && !(commandName instanceof Path)) {\n // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.\n throw new Error(\"Command name is not a string:\", commandName as any);\n }\n this.#commandName = commandName;\n if (typeof args === \"undefined\") {\n return;\n }\n if (!Array.isArray(args)) {\n throw new Error(\"Command arguments are not an array\");\n }\n for (let i = 0; i < args.length; i++) {\n const argEntry = args[i];\n if (typeof argEntry === \"string\") {\n continue;\n }\n if (argEntry instanceof Path) {\n continue;\n }\n if (Array.isArray(argEntry) && isValidArgsEntryArray(argEntry)) {\n continue;\n }\n throw new Error(`Invalid arg entry at index: ${i}`);\n }\n }\n\n get commandName(): string {\n return stringifyIfPath(this.#commandName);\n }\n\n /**\n * For use with `node:child_process`\n *\n * Usage example:\n *\n * ```\n * import { PrintableShellCommand } from \"printable-shell-command\";\n * import { spawn } from \"node:child_process\";\n *\n * const command = new PrintableShellCommand( \u2026 );\n * const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`\n * ```\n *\n */\n public toCommandWithFlatArgs(): [string, string[]] {\n return [this.commandName, this.args.flat().map(stringifyIfPath)];\n }\n\n #mainIndentation(options: PrintOptions): string {\n return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;\n }\n\n #argIndentation(options: PrintOptions): string {\n return (\n this.#mainIndentation(options) +\n (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)\n );\n }\n\n #lineWrapSeparator(options: PrintOptions): string {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n\n #argPairSeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\": {\n return INLINE_SEPARATOR;\n }\n case \"nested-by-entry\": {\n return this.#lineWrapSeparator(options) + this.#argIndentation(options);\n }\n case \"by-argument\": {\n return this.#lineWrapSeparator(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #intraEntrySeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\":\n case \"nested-by-entry\":\n case \"by-argument\": {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #separatorAfterCommand(\n options: PrintOptions,\n numFollowingEntries: number,\n ): string {\n if (numFollowingEntries === 0) {\n return \"\";\n }\n if (options.skipLineWrapBeforeFirstArg ?? false) {\n return INLINE_SEPARATOR;\n }\n return this.#intraEntrySeparator(options);\n }\n\n public getPrintableCommand(options?: PrintOptions): string {\n // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???\n options ??= {};\n const serializedEntries: string[] = [];\n\n for (let i = 0; i < this.args.length; i++) {\n const argsEntry = stringifyIfPath(this.args[i]);\n\n if (isString(argsEntry)) {\n serializedEntries.push(escapeArg(argsEntry, false, options));\n } else {\n serializedEntries.push(\n argsEntry\n .map((part) => escapeArg(stringifyIfPath(part), false, options))\n .join(this.#argPairSeparator(options)),\n );\n }\n }\n\n let text =\n this.#mainIndentation(options) +\n escapeArg(this.commandName, true, options) +\n this.#separatorAfterCommand(options, serializedEntries.length) +\n serializedEntries.join(this.#intraEntrySeparator(options));\n if (options?.style) {\n text = styleText(options.style, text);\n }\n return text;\n }\n\n /**\n * Print the shell command to {@link stderr} (default) or a specified stream.\n *\n * By default, this will be auto-styled (as bold gray) when `.isTTY` is true\n * for the stream. `.isTTY` is populated for the {@link stderr} and\n * {@link stdout} objects. Pass `\"autoStyle\": \"never\"` or an explicit\n * `style` to disable this.\n *\n */\n public print(options?: StreamPrintOptions): PrintableShellCommand {\n const stream = options?.stream ?? stderr;\n // Note: we only need to modify top-level fields, so `structuredClone(\u2026)`\n // would be overkill and can only cause performance issues.\n const optionsCopy = { ...options };\n optionsCopy.style ??=\n options?.autoStyle !== \"never\" &&\n (stream as { isTTY?: boolean }).isTTY === true\n ? TTY_AUTO_STYLE\n : undefined;\n const writable =\n stream instanceof Writable ? stream : Writable.fromWeb(stream);\n writable.write(this.getPrintableCommand(optionsCopy));\n writable.write(\"\\n\");\n return this;\n }\n\n #stdinSource: StdinSource | undefined;\n /**\n * Send data to `stdin` of the subprocess.\n *\n * Note that this will overwrite:\n *\n * - Any previous value set using {@link PrintableShellCommand.stdin | `.stdin(\u2026)`}.\n * - Any value set for `stdin` using the `\"stdio\"` field of {@link PrintableShellCommand.spawn | `.spawn(\u2026)`}.\n */\n stdin(source: StdinSource): PrintableShellCommand {\n const [key, ...moreKeys] = Object.keys(source);\n assert.equal(moreKeys.length, 0);\n // TODO: validate values?\n assert((STDIN_SOURCE_KEYS as unknown as string[]).includes(key));\n\n this.#stdinSource = source;\n return this;\n }\n\n /**\n * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313\n */\n public spawn: typeof spawnType = ((\n options?: Parameters<typeof spawnType>[0],\n ) => {\n const { spawn } = process.getBuiltinModule(\"node:child_process\");\n const cwd = stringifyIfPath(options?.cwd);\n options = { ...options };\n if (this.#stdinSource) {\n options ??= {};\n if (typeof options.stdio === \"undefined\") {\n options.stdio = \"pipe\";\n }\n if (typeof options.stdio === \"string\") {\n options.stdio = new Array(3).fill(options.stdio);\n }\n options.stdio = [\"pipe\", ...options.stdio.slice(1)];\n }\n // biome-ignore lint/suspicious/noTsIgnore: We don't want linting to depend on *broken* type checking.\n // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.\n const subprocess = spawn(...this.toCommandWithFlatArgs(), {\n ...(options as object),\n cwd,\n }) as NodeChildProcess & {\n success: Promise<void>;\n };\n // TODO: define properties on prototypes instead.\n Object.defineProperty(subprocess, \"success\", {\n get() {\n return new Promise<void>((resolve, reject) => {\n function handle(exitCode: number) {\n if (exitCode === 0) {\n resolve();\n } else {\n reject(`Command failed with non-zero exit code: ${exitCode}`);\n }\n }\n if (subprocess.exitCode !== null) {\n handle(subprocess.exitCode);\n }\n this.addListener(\n \"exit\",\n /* we only use the first arg */\n handle,\n );\n // biome-ignore lint/suspicious/noExplicitAny: We don't have the type available.\n this.addListener(\"error\", (err: any) => {\n reject(err);\n });\n });\n },\n enumerable: false,\n });\n if (subprocess.stdout) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStdoutResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stdout.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= new Response(\n Readable.from(this.#generator(s.stdout, s.success)),\n ));\n s.stdout.text = () => s.stdout.response().text();\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stdout.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));\n };\n s.stdout.json = <T>() => s.stdout.response().json() as Promise<T>;\n }\n if (subprocess.stderr) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStderrResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stderr.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= new Response(\n Readable.from(this.#generator(s.stderr, s.success)),\n ));\n s.stderr.text = () => s.stderr.response().text();\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stderr.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));\n };\n s.stderr.json = <T>() => s.stderr.response().json() as Promise<T>;\n }\n if (this.#stdinSource) {\n const { stdin } = subprocess;\n assert(stdin);\n if (\"text\" in this.#stdinSource) {\n stdin.write(this.#stdinSource.text);\n stdin.end();\n } else if (\"json\" in this.#stdinSource) {\n stdin.write(JSON.stringify(this.#stdinSource.json));\n stdin.end();\n } else if (\"path\" in this.#stdinSource) {\n createReadStream(stringifyIfPath(this.#stdinSource.path)).pipe(stdin);\n } else if (\"stream\" in this.#stdinSource) {\n const stream = (() => {\n const { stream } = this.#stdinSource;\n return stream instanceof Readable ? stream : Readable.fromWeb(stream);\n })();\n stream.pipe(stdin);\n } else {\n throw new Error(\"Invalid `.stdin(\u2026)` source?\");\n }\n }\n return subprocess;\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling\n }) as any;\n\n /** A wrapper for `.spawn(\u2026)` that sets stdio to `\"inherit\"` (common for\n * invoking commands from scripts whose output and interaction should be\n * surfaced to the user).\n *\n * If there is no other interaction with the shell from the calling process,\n * then it acts \"transparent\" and allows user to interact with the subprocess\n * in its stead.\n */\n public spawnTransparently(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): ChildProcessByStdio<null, null, null> & WithSuccess {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling.\n return this.spawn({ ...options, stdio: \"inherit\" }) as any;\n }\n\n /**\n * A wrapper for {@link PrintableShellCommand.spawn | `.spawn(\u2026)`} that:\n *\n * - sets `detached` to `true`,\n * - sets stdio to `\"inherit\"`,\n * - calls `.unref()`, and\n * - does not wait for the process to exit.\n *\n * This is similar to starting a command in the background and disowning it (in a shell).\n *\n */\n public spawnDetached(\n options?: NodeWithCwd<Omit<Omit<NodeSpawnOptions, \"stdio\">, \"detached\">>,\n ): void {\n if (options) {\n for (const field of [\"stdio\", \"detached\"]) {\n if (field in options) {\n throw new Error(`Unexpected \\`${field}\\` field.`);\n }\n }\n }\n const childProcess = this.spawn({\n stdio: \"ignore\",\n ...options,\n detached: true,\n });\n childProcess.unref();\n }\n\n #generator(\n readable: Readable,\n successPromise: Promise<void>,\n ): AsyncGenerator<string> {\n // TODO: we'd make this a `ReadableStream`, but `ReadableStream.from(\u2026)` is\n // not implemented in `bun`: https://github.com/oven-sh/bun/issues/3700\n return (async function* () {\n for await (const chunk of readable) {\n yield chunk;\n }\n await successPromise;\n })();\n }\n\n #stdoutSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"pipe\", \"inherit\"],\n });\n return this.#generator(subprocess.stdout, subprocess.success);\n }\n\n public stdout(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return new Response(Readable.from(this.#stdoutSpawnGenerator(options)));\n }\n\n #stderrSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"inherit\", \"pipe\"],\n });\n return this.#generator(subprocess.stderr, subprocess.success);\n }\n\n public stderr(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return new Response(Readable.from(this.#stderrSpawnGenerator(options)));\n }\n\n async *#split0(generator: AsyncGenerator<string>): AsyncGenerator<string> {\n let pending = \"\";\n for await (const chunk of generator) {\n pending += chunk;\n const newChunks = pending.split(\"\\x00\");\n pending = newChunks.splice(-1)[0];\n yield* newChunks;\n }\n if (pending !== \"\") {\n throw new Error(\n \"Missing a trailing NUL character at the end of a NUL-delimited stream.\",\n );\n }\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).text()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public text(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Promise<string> {\n return this.stdout(options).text();\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).json()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public json<T>(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Promise<T> {\n return this.stdout(options).json() as Promise<T>;\n }\n\n /**\n * Parse `stdout` into a generator of string values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *text0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n yield* this.#split0(this.#stdoutSpawnGenerator(options));\n }\n\n /**\n * Parse `stdout` into a generator of JSON values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *json0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON\n AsyncGenerator<any> {\n for await (const part of this.#split0(\n this.#stdoutSpawnGenerator(options),\n )) {\n yield JSON.parse(part);\n }\n }\n\n /** Equivalent to:\n *\n * ```\n * await this.print(\u2026).spawnTransparently(\u2026).success;\n * ```\n */\n public async shellOut(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n print?: StreamPrintOptions | ArgumentLineWrapping | boolean;\n },\n ): Promise<void> {\n const { print: printOptions, ...spawnOptions } = options ?? {};\n\n if (typeof printOptions === \"string\") {\n assert(ARGUMENT_LINE_WRAPPING_VALUES.includes(printOptions));\n this.print({ argumentLineWrapping: printOptions });\n } else if (printOptions === true) {\n this.print();\n } else if (printOptions === false) {\n // no-op\n } else {\n this.print(printOptions);\n }\n await this.spawnTransparently(spawnOptions).success;\n }\n}\n\nexport function escapeArg(\n arg: string,\n isMainCommand: boolean,\n options: PrintOptions,\n): string {\n const argCharacters = new Set(arg);\n const specialShellCharacters = isMainCommand\n ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND\n : SPECIAL_SHELL_CHARACTERS;\n if (\n options?.quoting === \"extra-safe\" ||\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (argCharacters as unknown as any).intersection(specialShellCharacters)\n .size > 0\n ) {\n // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).\n const escaped = arg.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"'\", \"\\\\'\");\n return `'${escaped}'`;\n }\n return arg;\n}\n"],
5
- "mappings": ";AAAA,OAAO,YAAY;AAMnB,SAAS,wBAAwB;AACjC,SAAS,cAAc;AACvB,SAAS,UAAU,gBAAgB;AAEnC,SAAS,iBAAiB;AAC1B,SAAS,MAAM,uBAAuB;AActC,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;AAChC,IAAM,iCAAiC;AAEvC,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAI3B,IAAM,iBAAkC,CAAC,QAAQ,MAAM;AAGvD,SAAS,SAAS,GAAqB;AACrC,SAAO,OAAO,MAAM;AACtB;AAGA,SAAS,sBAAsB,SAA6C;AAC1E,aAAW,SAAS,SAAS;AAC3B,QAAI,SAAS,KAAK,GAAG;AACnB;AAAA,IACF;AACA,QAAI,iBAAiB,MAAM;AACzB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOA,IAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgDA,IAAM,2BAA2B,oBAAI,IAAI;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM;AAAA;AAAA,EAEH,yBAA4C,MAAM,oBAAI,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA;AAKnE,IAAM,oBAAoB,CAAC,QAAQ,QAAQ,QAAQ,QAAQ;AAQpD,IAAM,wBAAN,MAA4B;AAAA,EAEjC,YACE,aACQ,OAAa,CAAC,GACtB;AADQ;AAER,QAAI,CAAC,SAAS,WAAW,KAAK,EAAE,uBAAuB,OAAO;AAE5D,YAAM,IAAI,MAAM,iCAAiC,WAAkB;AAAA,IACrE;AACA,SAAK,eAAe;AACpB,QAAI,OAAO,SAAS,aAAa;AAC/B;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,IAAI,GAAG;AACxB,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AACA,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,WAAW,KAAK,CAAC;AACvB,UAAI,OAAO,aAAa,UAAU;AAChC;AAAA,MACF;AACA,UAAI,oBAAoB,MAAM;AAC5B;AAAA,MACF;AACA,UAAI,MAAM,QAAQ,QAAQ,KAAK,sBAAsB,QAAQ,GAAG;AAC9D;AAAA,MACF;AACA,YAAM,IAAI,MAAM,+BAA+B,CAAC,EAAE;AAAA,IACpD;AAAA,EACF;AAAA,EA7BA;AAAA,EA+BA,IAAI,cAAsB;AACxB,WAAO,gBAAgB,KAAK,YAAY;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBO,wBAA4C;AACjD,WAAO,CAAC,KAAK,aAAa,KAAK,KAAK,KAAK,EAAE,IAAI,eAAe,CAAC;AAAA,EACjE;AAAA,EAEA,iBAAiB,SAA+B;AAC9C,WAAO,SAAS,mBAAmB;AAAA,EACrC;AAAA,EAEA,gBAAgB,SAA+B;AAC7C,WACE,KAAK,iBAAiB,OAAO,KAC5B,SAAS,kBAAkB;AAAA,EAEhC;AAAA,EAEA,mBAAmB,SAA+B;AAChD,WAAO,qBAAqB,KAAK,gBAAgB,OAAO;AAAA,EAC1D;AAAA,EAEA,kBAAkB,SAA+B;AAC/C,YAAQ,SAAS,wBAAwB,gCAAgC;AAAA,MACvE,KAAK,YAAY;AACf,eAAO;AAAA,MACT;AAAA,MACA,KAAK,mBAAmB;AACtB,eAAO,KAAK,mBAAmB,OAAO,IAAI,KAAK,gBAAgB,OAAO;AAAA,MACxE;AAAA,MACA,KAAK,eAAe;AAClB,eAAO,KAAK,mBAAmB,OAAO;AAAA,MACxC;AAAA,MACA,KAAK,UAAU;AACb,eAAO;AAAA,MACT;AAAA,MACA;AACE,cAAM,IAAI,MAAM,0CAA0C;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,qBAAqB,SAA+B;AAClD,YAAQ,SAAS,wBAAwB,gCAAgC;AAAA,MACvE,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,eAAe;AAClB,eAAO,qBAAqB,KAAK,gBAAgB,OAAO;AAAA,MAC1D;AAAA,MACA,KAAK,UAAU;AACb,eAAO;AAAA,MACT;AAAA,MACA;AACE,cAAM,IAAI,MAAM,0CAA0C;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,uBACE,SACA,qBACQ;AACR,QAAI,wBAAwB,GAAG;AAC7B,aAAO;AAAA,IACT;AACA,QAAI,QAAQ,8BAA8B,OAAO;AAC/C,aAAO;AAAA,IACT;AACA,WAAO,KAAK,qBAAqB,OAAO;AAAA,EAC1C;AAAA,EAEO,oBAAoB,SAAgC;AAEzD,gBAAY,CAAC;AACb,UAAM,oBAA8B,CAAC;AAErC,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK,QAAQ,KAAK;AACzC,YAAM,YAAY,gBAAgB,KAAK,KAAK,CAAC,CAAC;AAE9C,UAAI,SAAS,SAAS,GAAG;AACvB,0BAAkB,KAAK,UAAU,WAAW,OAAO,OAAO,CAAC;AAAA,MAC7D,OAAO;AACL,0BAAkB;AAAA,UAChB,UACG,IAAI,CAAC,SAAS,UAAU,gBAAgB,IAAI,GAAG,OAAO,OAAO,CAAC,EAC9D,KAAK,KAAK,kBAAkB,OAAO,CAAC;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,OACF,KAAK,iBAAiB,OAAO,IAC7B,UAAU,KAAK,aAAa,MAAM,OAAO,IACzC,KAAK,uBAAuB,SAAS,kBAAkB,MAAM,IAC7D,kBAAkB,KAAK,KAAK,qBAAqB,OAAO,CAAC;AAC3D,QAAI,SAAS,OAAO;AAClB,aAAO,UAAU,QAAQ,OAAO,IAAI;AAAA,IACtC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWO,MAAM,SAAqD;AAChE,UAAM,SAAS,SAAS,UAAU;AAGlC,UAAM,cAAc,EAAE,GAAG,QAAQ;AACjC,gBAAY,UACV,SAAS,cAAc,WACtB,OAA+B,UAAU,OACtC,iBACA;AACN,UAAM,WACJ,kBAAkB,WAAW,SAAS,SAAS,QAAQ,MAAM;AAC/D,aAAS,MAAM,KAAK,oBAAoB,WAAW,CAAC;AACpD,aAAS,MAAM,IAAI;AACnB,WAAO;AAAA,EACT;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,QAA4C;AAChD,UAAM,CAAC,KAAK,GAAG,QAAQ,IAAI,OAAO,KAAK,MAAM;AAC7C,WAAO,MAAM,SAAS,QAAQ,CAAC;AAE/B,WAAQ,kBAA0C,SAAS,GAAG,CAAC;AAE/D,SAAK,eAAe;AACpB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKO,SAA2B,CAChC,YACG;AACH,UAAM,EAAE,MAAM,IAAI,QAAQ,iBAAiB,oBAAoB;AAC/D,UAAM,MAAM,gBAAgB,SAAS,GAAG;AACxC,cAAU,EAAE,GAAG,QAAQ;AACvB,QAAI,KAAK,cAAc;AACrB,kBAAY,CAAC;AACb,UAAI,OAAO,QAAQ,UAAU,aAAa;AACxC,gBAAQ,QAAQ;AAAA,MAClB;AACA,UAAI,OAAO,QAAQ,UAAU,UAAU;AACrC,gBAAQ,QAAQ,IAAI,MAAM,CAAC,EAAE,KAAK,QAAQ,KAAK;AAAA,MACjD;AACA,cAAQ,QAAQ,CAAC,QAAQ,GAAG,QAAQ,MAAM,MAAM,CAAC,CAAC;AAAA,IACpD;AAGA,UAAM,aAAa,MAAM,GAAG,KAAK,sBAAsB,GAAG;AAAA,MACxD,GAAI;AAAA,MACJ;AAAA,IACF,CAAC;AAID,WAAO,eAAe,YAAY,WAAW;AAAA,MAC3C,MAAM;AACJ,eAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,mBAAS,OAAO,UAAkB;AAChC,gBAAI,aAAa,GAAG;AAClB,sBAAQ;AAAA,YACV,OAAO;AACL,qBAAO,2CAA2C,QAAQ,EAAE;AAAA,YAC9D;AAAA,UACF;AACA,cAAI,WAAW,aAAa,MAAM;AAChC,mBAAO,WAAW,QAAQ;AAAA,UAC5B;AACA,eAAK;AAAA,YACH;AAAA;AAAA,YAEA;AAAA,UACF;AAEA,eAAK,YAAY,SAAS,CAAC,QAAa;AACtC,mBAAO,GAAG;AAAA,UACZ,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AACD,QAAI,WAAW,QAAQ;AAErB,YAAM,IAAI;AAGV,UAAI;AACJ,QAAE,OAAO,WAAW;AAAA;AAAA,QAEjB,mBAAmB,IAAI;AAAA,UACtB,SAAS,KAAK,KAAK,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC;AAAA,QACpD;AAAA;AACF,QAAE,OAAO,OAAO,MAAM,EAAE,OAAO,SAAS,EAAE,KAAK;AAC/C,YAAM,aAAa;AACnB,QAAE,OAAO,QAAQ,mBAAmB;AAClC,eAAO,WAAW,QAAQ,WAAW,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC;AAAA,MACtE;AACA,QAAE,OAAO,OAAO,MAAS,EAAE,OAAO,SAAS,EAAE,KAAK;AAAA,IACpD;AACA,QAAI,WAAW,QAAQ;AAErB,YAAM,IAAI;AAGV,UAAI;AACJ,QAAE,OAAO,WAAW;AAAA;AAAA,QAEjB,mBAAmB,IAAI;AAAA,UACtB,SAAS,KAAK,KAAK,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC;AAAA,QACpD;AAAA;AACF,QAAE,OAAO,OAAO,MAAM,EAAE,OAAO,SAAS,EAAE,KAAK;AAC/C,YAAM,aAAa;AACnB,QAAE,OAAO,QAAQ,mBAAmB;AAClC,eAAO,WAAW,QAAQ,WAAW,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC;AAAA,MACtE;AACA,QAAE,OAAO,OAAO,MAAS,EAAE,OAAO,SAAS,EAAE,KAAK;AAAA,IACpD;AACA,QAAI,KAAK,cAAc;AACrB,YAAM,EAAE,MAAM,IAAI;AAClB,aAAO,KAAK;AACZ,UAAI,UAAU,KAAK,cAAc;AAC/B,cAAM,MAAM,KAAK,aAAa,IAAI;AAClC,cAAM,IAAI;AAAA,MACZ,WAAW,UAAU,KAAK,cAAc;AACtC,cAAM,MAAM,KAAK,UAAU,KAAK,aAAa,IAAI,CAAC;AAClD,cAAM,IAAI;AAAA,MACZ,WAAW,UAAU,KAAK,cAAc;AACtC,yBAAiB,gBAAgB,KAAK,aAAa,IAAI,CAAC,EAAE,KAAK,KAAK;AAAA,MACtE,WAAW,YAAY,KAAK,cAAc;AACxC,cAAM,UAAU,MAAM;AACpB,gBAAM,EAAE,QAAAA,QAAO,IAAI,KAAK;AACxB,iBAAOA,mBAAkB,WAAWA,UAAS,SAAS,QAAQA,OAAM;AAAA,QACtE,GAAG;AACH,eAAO,KAAK,KAAK;AAAA,MACnB,OAAO;AACL,cAAM,IAAI,MAAM,kCAA6B;AAAA,MAC/C;AAAA,IACF;AACA,WAAO;AAAA,EAET;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUO,mBACL,SACqD;AACrD,QAAI,WAAW,WAAW,SAAS;AACjC,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAGA,WAAO,KAAK,MAAM,EAAE,GAAG,SAAS,OAAO,UAAU,CAAC;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaO,cACL,SACM;AACN,QAAI,SAAS;AACX,iBAAW,SAAS,CAAC,SAAS,UAAU,GAAG;AACzC,YAAI,SAAS,SAAS;AACpB,gBAAM,IAAI,MAAM,gBAAgB,KAAK,WAAW;AAAA,QAClD;AAAA,MACF;AAAA,IACF;AACA,UAAM,eAAe,KAAK,MAAM;AAAA,MAC9B,OAAO;AAAA,MACP,GAAG;AAAA,MACH,UAAU;AAAA,IACZ,CAAC;AACD,iBAAa,MAAM;AAAA,EACrB;AAAA,EAEA,WACE,UACA,gBACwB;AAGxB,YAAQ,mBAAmB;AACzB,uBAAiB,SAAS,UAAU;AAClC,cAAM;AAAA,MACR;AACA,YAAM;AAAA,IACR,GAAG;AAAA,EACL;AAAA,EAEA,sBACE,SACwB;AACxB,QAAI,WAAW,WAAW,SAAS;AACjC,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM,aAAa,KAAK,MAAM;AAAA,MAC5B,GAAG;AAAA,MACH,OAAO,CAAC,UAAU,QAAQ,SAAS;AAAA,IACrC,CAAC;AACD,WAAO,KAAK,WAAW,WAAW,QAAQ,WAAW,OAAO;AAAA,EAC9D;AAAA,EAEO,OACL,SACU;AAEV,WAAO,IAAI,SAAS,SAAS,KAAK,KAAK,sBAAsB,OAAO,CAAC,CAAC;AAAA,EACxE;AAAA,EAEA,sBACE,SACwB;AACxB,QAAI,WAAW,WAAW,SAAS;AACjC,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM,aAAa,KAAK,MAAM;AAAA,MAC5B,GAAG;AAAA,MACH,OAAO,CAAC,UAAU,WAAW,MAAM;AAAA,IACrC,CAAC;AACD,WAAO,KAAK,WAAW,WAAW,QAAQ,WAAW,OAAO;AAAA,EAC9D;AAAA,EAEO,OACL,SACU;AAEV,WAAO,IAAI,SAAS,SAAS,KAAK,KAAK,sBAAsB,OAAO,CAAC,CAAC;AAAA,EACxE;AAAA,EAEA,OAAO,QAAQ,WAA2D;AACxE,QAAI,UAAU;AACd,qBAAiB,SAAS,WAAW;AACnC,iBAAW;AACX,YAAM,YAAY,QAAQ,MAAM,IAAM;AACtC,gBAAU,UAAU,OAAO,EAAE,EAAE,CAAC;AAChC,aAAO;AAAA,IACT;AACA,QAAI,YAAY,IAAI;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASO,KACL,SACiB;AACjB,WAAO,KAAK,OAAO,OAAO,EAAE,KAAK;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASO,KACL,SACY;AACZ,WAAO,KAAK,OAAO,OAAO,EAAE,KAAK;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAc,MACZ,SACwB;AACxB,WAAO,KAAK,QAAQ,KAAK,sBAAsB,OAAO,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAc,MACZ,SAEkB;AAClB,qBAAiB,QAAQ,KAAK;AAAA,MAC5B,KAAK,sBAAsB,OAAO;AAAA,IACpC,GAAG;AACD,YAAM,KAAK,MAAM,IAAI;AAAA,IACvB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAa,SACX,SAGe;AACf,UAAM,EAAE,OAAO,cAAc,GAAG,aAAa,IAAI,WAAW,CAAC;AAE7D,QAAI,OAAO,iBAAiB,UAAU;AACpC,aAAO,8BAA8B,SAAS,YAAY,CAAC;AAC3D,WAAK,MAAM,EAAE,sBAAsB,aAAa,CAAC;AAAA,IACnD,WAAW,iBAAiB,MAAM;AAChC,WAAK,MAAM;AAAA,IACb,WAAW,iBAAiB,OAAO;AAAA,IAEnC,OAAO;AACL,WAAK,MAAM,YAAY;AAAA,IACzB;AACA,UAAM,KAAK,mBAAmB,YAAY,EAAE;AAAA,EAC9C;AACF;AAEO,SAAS,UACd,KACA,eACA,SACQ;AACR,QAAM,gBAAgB,IAAI,IAAI,GAAG;AACjC,QAAM,yBAAyB,gBAC3B,4CACA;AACJ,MACE,SAAS,YAAY;AAAA,EAEpB,cAAiC,aAAa,sBAAsB,EAClE,OAAO,GACV;AAEA,UAAM,UAAU,IAAI,WAAW,MAAM,MAAM,EAAE,WAAW,KAAK,KAAK;AAClE,WAAO,IAAI,OAAO;AAAA,EACpB;AACA,SAAO;AACT;",
3
+ "sources": ["../../../src/PrintableShellCommand.ts", "../../../src/trimTrailingNewlines.ts"],
4
+ "sourcesContent": ["import assert from \"node:assert\";\nimport type {\n ChildProcessByStdio,\n ChildProcess as NodeChildProcess,\n SpawnOptions as NodeSpawnOptions,\n} from \"node:child_process\";\nimport { createReadStream } from \"node:fs\";\nimport { stderr } from \"node:process\";\nimport { Readable, Writable } from \"node:stream\";\nimport type { WriteStream } from \"node:tty\";\nimport { styleText } from \"node:util\";\nimport { Path, stringifyIfPath } from \"path-class\";\nimport type {\n NodeWithCwd,\n spawnType,\n WithStderrResponse,\n WithStdoutResponse,\n WithSuccess,\n} from \"./spawn\";\nimport {\n handleTrailingNewlines,\n type TrailingNewlineOptions,\n wrapHandleTrailingNewlinesForResponse,\n wrapHandleTrailingNewlinesForText,\n} from \"./trimTrailingNewlines\";\n\n// TODO: does this import work?\n/**\n * @import { stdout } from \"node:process\"\n */\n\nconst DEFAULT_MAIN_INDENTATION = \"\";\nconst DEFAULT_ARG_INDENTATION = \" \";\nconst DEFAULT_ARGUMENT_LINE_WRAPPING = \"by-entry\";\n\nconst INLINE_SEPARATOR = \" \";\nconst LINE_WRAP_LINE_END = \" \\\\\\n\";\n\ntype StyleTextFormat = Parameters<typeof styleText>[0];\n\nconst TTY_AUTO_STYLE: StyleTextFormat = [\"gray\", \"bold\"];\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.\nfunction isString(s: any): s is string {\n return typeof s === \"string\";\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type here.\nfunction isValidArgsEntryArray(entries: any[]): entries is SingleArgument[] {\n for (const entry of entries) {\n if (isString(entry)) {\n continue;\n }\n if (entry instanceof Path) {\n continue;\n }\n return false;\n }\n return true;\n}\n\n// TODO: allow `.toString()`ables?\ntype SingleArgument = string | Path;\ntype ArgsEntry = SingleArgument | SingleArgument[];\ntype Args = ArgsEntry[];\n\nconst ARGUMENT_LINE_WRAPPING_VALUES = [\n \"by-entry\",\n \"nested-by-entry\",\n \"by-argument\",\n \"inline\",\n] as const;\ntype ArgumentLineWrapping = (typeof ARGUMENT_LINE_WRAPPING_VALUES)[number];\n\nexport interface PrintOptions {\n /** Defaults to \"\" */\n mainIndentation?: string;\n /** Defaults to \" \" */\n argIndentation?: string;\n /**\n * - `\"auto\"`: Quote only arguments that need it for safety. This tries to be\n * portable and safe across shells, but true safety and portability is hard\n * to guarantee.\n * - `\"extra-safe\"`: Quote all arguments, even ones that don't need it. This is\n * more likely to be safe under all circumstances.\n */\n quoting?: \"auto\" | \"extra-safe\";\n /** Line wrapping to use between arguments. Defaults to `\"by-entry\"`. */\n argumentLineWrapping?: ArgumentLineWrapping;\n /** Include the first arg (or first arg group) on the same line as the command, regardless of the `argumentLineWrapping` setting. */\n skipLineWrapBeforeFirstArg?: true | false;\n /**\n * Style text using `node`'s {@link styleText | `styleText(\u2026)`}.\n *\n * Example usage:\n *\n * ```\n * new PrintableShellCommand(\"echo\", [\"hi\"]).print({\n * style: [\"green\", \"underline\"],\n * });\n * */\n style?: StyleTextFormat;\n}\n\nexport interface StreamPrintOptions extends PrintOptions {\n /**\n * Auto-style the text when:\n *\n * - the output stream is detected to be a TTY\n * - `styleTextFormat` is not specified.\n *\n * The current auto style is: `[\"gray\", \"bold\"]`\n */\n autoStyle?: \"tty\" | \"never\";\n // This would be a `WritableStream` (open web standard), but `WriteStream` allows us to query `.isTTY`.\n stream?: WriteStream | Writable;\n}\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS = new Set([\n \" \",\n '\"',\n \"'\",\n \"`\",\n \"|\",\n \"$\",\n \"*\",\n \"?\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"{\",\n \"}\",\n \"&\",\n \"\\\\\",\n \";\",\n \"#\",\n]);\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND =\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set([\"=\"]));\n\n// TODO: Is there an idiomatic ways to check that all potential fields of\n// `StdinSource` satisfy `(typeof STDIN_SOURCE_KEYS)[number]`, without adding\n// extra indirection for type wrangling?\nconst STDIN_SOURCE_KEYS = [\"text\", \"json\", \"path\", \"stream\"] as const;\nexport type StdinSource =\n | { text: string }\n // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON data.\n | { json: any }\n | { path: string | Path }\n | { stream: Readable | ReadableStream };\n\nexport class PrintableShellCommand {\n #commandName: string | Path;\n constructor(\n commandName: string | Path,\n private args: Args = [],\n ) {\n if (!isString(commandName) && !(commandName instanceof Path)) {\n // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.\n throw new Error(\"Command name is not a string:\", commandName as any);\n }\n this.#commandName = commandName;\n if (typeof args === \"undefined\") {\n return;\n }\n if (!Array.isArray(args)) {\n throw new Error(\"Command arguments are not an array\");\n }\n for (let i = 0; i < args.length; i++) {\n const argEntry = args[i];\n if (typeof argEntry === \"string\") {\n continue;\n }\n if (argEntry instanceof Path) {\n continue;\n }\n if (Array.isArray(argEntry) && isValidArgsEntryArray(argEntry)) {\n continue;\n }\n throw new Error(`Invalid arg entry at index: ${i}`);\n }\n }\n\n get commandName(): string {\n return stringifyIfPath(this.#commandName);\n }\n\n /**\n * For use with `node:child_process`\n *\n * Usage example:\n *\n * ```\n * import { PrintableShellCommand } from \"printable-shell-command\";\n * import { spawn } from \"node:child_process\";\n *\n * const command = new PrintableShellCommand( \u2026 );\n * const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`\n * ```\n *\n */\n public toCommandWithFlatArgs(): [string, string[]] {\n return [this.commandName, this.args.flat().map(stringifyIfPath)];\n }\n\n #mainIndentation(options: PrintOptions): string {\n return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;\n }\n\n #argIndentation(options: PrintOptions): string {\n return (\n this.#mainIndentation(options) +\n (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)\n );\n }\n\n #lineWrapSeparator(options: PrintOptions): string {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n\n #argPairSeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\": {\n return INLINE_SEPARATOR;\n }\n case \"nested-by-entry\": {\n return this.#lineWrapSeparator(options) + this.#argIndentation(options);\n }\n case \"by-argument\": {\n return this.#lineWrapSeparator(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #intraEntrySeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\":\n case \"nested-by-entry\":\n case \"by-argument\": {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #separatorAfterCommand(\n options: PrintOptions,\n numFollowingEntries: number,\n ): string {\n if (numFollowingEntries === 0) {\n return \"\";\n }\n if (options.skipLineWrapBeforeFirstArg ?? false) {\n return INLINE_SEPARATOR;\n }\n return this.#intraEntrySeparator(options);\n }\n\n public getPrintableCommand(options?: PrintOptions): string {\n // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???\n options ??= {};\n const serializedEntries: string[] = [];\n\n for (let i = 0; i < this.args.length; i++) {\n const argsEntry = stringifyIfPath(this.args[i]);\n\n if (isString(argsEntry)) {\n serializedEntries.push(escapeArg(argsEntry, false, options));\n } else {\n serializedEntries.push(\n argsEntry\n .map((part) => escapeArg(stringifyIfPath(part), false, options))\n .join(this.#argPairSeparator(options)),\n );\n }\n }\n\n let text =\n this.#mainIndentation(options) +\n escapeArg(this.commandName, true, options) +\n this.#separatorAfterCommand(options, serializedEntries.length) +\n serializedEntries.join(this.#intraEntrySeparator(options));\n if (options?.style) {\n text = styleText(options.style, text);\n }\n return text;\n }\n\n /**\n * Print the shell command to {@link stderr} (default) or a specified stream.\n *\n * By default, this will be auto-styled (as bold gray) when `.isTTY` is true\n * for the stream. `.isTTY` is populated for the {@link stderr} and\n * {@link stdout} objects. Pass `\"autoStyle\": \"never\"` or an explicit\n * `style` to disable this.\n *\n */\n public print(options?: StreamPrintOptions): PrintableShellCommand {\n const stream = options?.stream ?? stderr;\n // Note: we only need to modify top-level fields, so `structuredClone(\u2026)`\n // would be overkill and can only cause performance issues.\n const optionsCopy = { ...options };\n optionsCopy.style ??=\n options?.autoStyle !== \"never\" &&\n (stream as { isTTY?: boolean }).isTTY === true\n ? TTY_AUTO_STYLE\n : undefined;\n const writable =\n stream instanceof Writable ? stream : Writable.fromWeb(stream);\n writable.write(this.getPrintableCommand(optionsCopy));\n writable.write(\"\\n\");\n return this;\n }\n\n #stdinSource: StdinSource | undefined;\n /**\n * Send data to `stdin` of the subprocess.\n *\n * Note that this will overwrite:\n *\n * - Any previous value set using {@link PrintableShellCommand.stdin | `.stdin(\u2026)`}.\n * - Any value set for `stdin` using the `\"stdio\"` field of {@link PrintableShellCommand.spawn | `.spawn(\u2026)`}.\n */\n stdin(source: StdinSource): PrintableShellCommand {\n const [key, ...moreKeys] = Object.keys(source);\n assert.equal(moreKeys.length, 0);\n // TODO: validate values?\n assert((STDIN_SOURCE_KEYS as unknown as string[]).includes(key));\n\n this.#stdinSource = source;\n return this;\n }\n\n /**\n * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313\n */\n public spawn: typeof spawnType = ((\n options?: Parameters<typeof spawnType>[0],\n ) => {\n const { spawn } = process.getBuiltinModule(\"node:child_process\");\n const cwd = stringifyIfPath(options?.cwd);\n options = { ...options };\n if (this.#stdinSource) {\n options ??= {};\n if (typeof options.stdio === \"undefined\") {\n options.stdio = \"pipe\";\n }\n if (typeof options.stdio === \"string\") {\n options.stdio = new Array(3).fill(options.stdio);\n }\n options.stdio = [\"pipe\", ...options.stdio.slice(1)];\n }\n // biome-ignore lint/suspicious/noTsIgnore: We don't want linting to depend on *broken* type checking.\n // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.\n const subprocess = spawn(...this.toCommandWithFlatArgs(), {\n ...(options as object),\n cwd,\n }) as NodeChildProcess & {\n success: Promise<void>;\n };\n // TODO: define properties on prototypes instead.\n Object.defineProperty(subprocess, \"success\", {\n get() {\n return new Promise<void>((resolve, reject) => {\n function handle(exitCode: number) {\n if (exitCode === 0) {\n resolve();\n } else {\n reject(`Command failed with non-zero exit code: ${exitCode}`);\n }\n }\n if (subprocess.exitCode !== null) {\n handle(subprocess.exitCode);\n }\n this.addListener(\n \"exit\",\n /* we only use the first arg */\n handle,\n );\n // biome-ignore lint/suspicious/noExplicitAny: We don't have the type available.\n this.addListener(\"error\", (err: any) => {\n reject(err);\n });\n });\n },\n enumerable: false,\n });\n if (subprocess.stdout) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStdoutResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stdout.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stdout, s.success))),\n ));\n s.stdout.text = wrapHandleTrailingNewlinesForText(s.stdout.response());\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stdout.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));\n };\n s.stdout.json = <T>() => s.stdout.response().json() as Promise<T>;\n }\n if (subprocess.stderr) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStderrResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stderr.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stderr, s.success))),\n ));\n s.stderr.text = wrapHandleTrailingNewlinesForText(s.stderr.response());\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stderr.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));\n };\n s.stderr.json = <T>() => s.stderr.response().json() as Promise<T>;\n }\n if (this.#stdinSource) {\n const { stdin } = subprocess;\n assert(stdin);\n if (\"text\" in this.#stdinSource) {\n stdin.write(this.#stdinSource.text);\n stdin.end();\n } else if (\"json\" in this.#stdinSource) {\n stdin.write(JSON.stringify(this.#stdinSource.json));\n stdin.end();\n } else if (\"path\" in this.#stdinSource) {\n createReadStream(stringifyIfPath(this.#stdinSource.path)).pipe(stdin);\n } else if (\"stream\" in this.#stdinSource) {\n const stream = (() => {\n const { stream } = this.#stdinSource;\n return stream instanceof Readable ? stream : Readable.fromWeb(stream);\n })();\n stream.pipe(stdin);\n } else {\n throw new Error(\"Invalid `.stdin(\u2026)` source?\");\n }\n }\n return subprocess;\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling\n }) as any;\n\n /** A wrapper for `.spawn(\u2026)` that sets stdio to `\"inherit\"` (common for\n * invoking commands from scripts whose output and interaction should be\n * surfaced to the user).\n *\n * If there is no other interaction with the shell from the calling process,\n * then it acts \"transparent\" and allows user to interact with the subprocess\n * in its stead.\n */\n public spawnTransparently(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): ChildProcessByStdio<null, null, null> & WithSuccess {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling.\n return this.spawn({ ...options, stdio: \"inherit\" }) as any;\n }\n\n /**\n * A wrapper for {@link PrintableShellCommand.spawn | `.spawn(\u2026)`} that:\n *\n * - sets `detached` to `true`,\n * - sets stdio to `\"inherit\"`,\n * - calls `.unref()`, and\n * - does not wait for the process to exit.\n *\n * This is similar to starting a command in the background and disowning it (in a shell).\n *\n */\n public spawnDetached(\n options?: NodeWithCwd<Omit<Omit<NodeSpawnOptions, \"stdio\">, \"detached\">>,\n ): void {\n if (options) {\n for (const field of [\"stdio\", \"detached\"]) {\n if (field in options) {\n throw new Error(`Unexpected \\`${field}\\` field.`);\n }\n }\n }\n const childProcess = this.spawn({\n stdio: \"ignore\",\n ...options,\n detached: true,\n });\n childProcess.unref();\n }\n\n #generator(\n readable: Readable,\n successPromise: Promise<void>,\n ): AsyncGenerator<string> {\n // TODO: we'd make this a `ReadableStream`, but `ReadableStream.from(\u2026)` is\n // not implemented in `bun`: https://github.com/oven-sh/bun/issues/3700\n return (async function* () {\n for await (const chunk of readable) {\n yield chunk;\n }\n await successPromise;\n })();\n }\n\n #stdoutSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"pipe\", \"inherit\"],\n });\n return this.#generator(subprocess.stdout, subprocess.success);\n }\n\n public stdout(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stdoutSpawnGenerator(options))),\n );\n }\n\n #stderrSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"inherit\", \"pipe\"],\n });\n return this.#generator(subprocess.stderr, subprocess.success);\n }\n\n public stderr(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stderrSpawnGenerator(options))),\n );\n }\n\n async *#split0(generator: AsyncGenerator<string>): AsyncGenerator<string> {\n let pending = \"\";\n for await (const chunk of generator) {\n pending += chunk;\n const newChunks = pending.split(\"\\x00\");\n pending = newChunks.splice(-1)[0];\n yield* newChunks;\n }\n if (pending !== \"\") {\n throw new Error(\n \"Missing a trailing NUL character at the end of a NUL-delimited stream.\",\n );\n }\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).text()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public async text(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n trimTrailingNewlines: \"single-required\" | \"single-if-present\" | \"never\";\n },\n ): Promise<string> {\n const {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n ...spawnOptions\n } = options ?? {};\n return handleTrailingNewlines(this.stdout(spawnOptions).text(), {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n });\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).json()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public json<T>(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Promise<T> {\n return this.stdout(options).json() as Promise<T>;\n }\n\n /**\n * Parse `stdout` into a generator of string values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *text0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n yield* this.#split0(this.#stdoutSpawnGenerator(options));\n }\n\n /**\n * Parse `stdout` into a generator of JSON values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *json0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON\n AsyncGenerator<any> {\n for await (const part of this.#split0(\n this.#stdoutSpawnGenerator(options),\n )) {\n yield JSON.parse(part);\n }\n }\n\n /** Equivalent to:\n *\n * ```\n * await this.print(\u2026).spawnTransparently(\u2026).success;\n * ```\n */\n public async shellOut(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n print?: StreamPrintOptions | ArgumentLineWrapping | boolean;\n },\n ): Promise<void> {\n const { print: printOptions, ...spawnOptions } = options ?? {};\n\n if (typeof printOptions === \"string\") {\n assert(ARGUMENT_LINE_WRAPPING_VALUES.includes(printOptions));\n this.print({ argumentLineWrapping: printOptions });\n } else if (printOptions === true) {\n this.print();\n } else if (printOptions === false) {\n // no-op\n } else {\n this.print(printOptions);\n }\n await this.spawnTransparently(spawnOptions).success;\n }\n}\n\nexport function escapeArg(\n arg: string,\n isMainCommand: boolean,\n options: PrintOptions,\n): string {\n const argCharacters = new Set(arg);\n const specialShellCharacters = isMainCommand\n ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND\n : SPECIAL_SHELL_CHARACTERS;\n if (\n options?.quoting === \"extra-safe\" ||\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (argCharacters as unknown as any).intersection(specialShellCharacters)\n .size > 0\n ) {\n // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).\n const escaped = arg.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"'\", \"\\\\'\");\n return `'${escaped}'`;\n }\n return arg;\n}\n", "const DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR = \"never\";\nexport interface TrailingNewlineOptions {\n trimTrailingNewlines?: \"single-required\" | \"single-if-present\" | \"never\";\n}\n\n// TODO: Handle carriage return?\nexport async function handleTrailingNewlines(\n text: Promise<string>,\n options?: TrailingNewlineOptions,\n): Promise<string> {\n const awaitedText = await text;\n switch (\n options?.trimTrailingNewlines ??\n DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR\n ) {\n case \"single-required\": {\n if (!awaitedText.endsWith(\"\\n\")) {\n throw new Error(\"Trailing newline required, but not present.\");\n }\n return awaitedText.slice(0, -1);\n }\n case \"single-if-present\": {\n if (awaitedText.endsWith(\"\\n\")) {\n return awaitedText.slice(0, -1);\n } else {\n return awaitedText;\n }\n }\n case \"never\": {\n return awaitedText;\n }\n default:\n throw new Error(\"Invalid value for `trimTrailingNewlines`.\");\n }\n}\n\nfunction wrapHandleTrailingNewlines(\n fn: () => Promise<string>,\n): (options?: TrailingNewlineOptions) => Promise<string> {\n return (options?: TrailingNewlineOptions) =>\n handleTrailingNewlines(fn(), options);\n}\n\nexport function wrapHandleTrailingNewlinesForText(v: {\n text: () => Promise<string>;\n}): (options?: TrailingNewlineOptions) => Promise<string> {\n const originalTextFn = v.text.bind(v);\n return wrapHandleTrailingNewlines(originalTextFn);\n}\n\nexport function wrapHandleTrailingNewlinesForResponse(\n response: Response,\n): Response & { text: (options?: TrailingNewlineOptions) => Promise<string> } {\n const textFn = wrapHandleTrailingNewlinesForText(response);\n Object.defineProperty(response, \"text\", {\n get: () => {\n return textFn;\n },\n });\n return response;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["stream"]
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import type { ChildProcess, ChildProcessByStdio, ChildProcessWithoutNullStreams, ProcessEnvOptions, SpawnOptions, SpawnOptionsWithoutStdio, SpawnOptionsWithStdioTuple, StdioNull, StdioPipe } from "node:child_process";
2
2
  import type { Readable, Writable } from "node:stream";
3
3
  import type { Path } from "path-class";
4
+ import type { TrailingNewlineOptions } from "./trimTrailingNewlines";
4
5
  export type NodeCwd = ProcessEnvOptions["cwd"] | Path;
5
6
  export type NodeWithCwd<T extends {
6
7
  cwd?: ProcessEnvOptions["cwd"];
@@ -12,7 +13,7 @@ export interface WithSuccess {
12
13
  }
13
14
  export interface WithResponse {
14
15
  response: () => Response;
15
- text: () => Promise<string>;
16
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
16
17
  text0: () => AsyncGenerator<string>;
17
18
  json: <T>() => Promise<T>;
18
19
  }
@@ -0,0 +1,10 @@
1
+ export interface TrailingNewlineOptions {
2
+ trimTrailingNewlines?: "single-required" | "single-if-present" | "never";
3
+ }
4
+ export declare function handleTrailingNewlines(text: Promise<string>, options?: TrailingNewlineOptions): Promise<string>;
5
+ export declare function wrapHandleTrailingNewlinesForText(v: {
6
+ text: () => Promise<string>;
7
+ }): (options?: TrailingNewlineOptions) => Promise<string>;
8
+ export declare function wrapHandleTrailingNewlinesForResponse(response: Response): Response & {
9
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "printable-shell-command",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "description": "A helper class to construct shell commands in a way that allows printing them.",
5
5
  "author": {
6
6
  "name": "Lucas Garron",
@@ -28,6 +28,7 @@
28
28
  "@cubing/dev-config": ">=0.8.3",
29
29
  "@types/bun": "^1.3.5",
30
30
  "bun-dx": "^0.1.4",
31
+ "clipboardy": "^5.0.2",
31
32
  "esbuild": "^0.27.2",
32
33
  "readme-cli-help": "^0.4.10",
33
34
  "typescript": "^5.9.3"
@@ -332,6 +332,60 @@ test(".stdout(…)", async () => {
332
332
  ).toEqual("");
333
333
  });
334
334
 
335
+ test(".stdout().text({ trimTrailingNewlines: … })", async () => {
336
+ expect(() =>
337
+ new PrintableShellCommand("echo", ["-n", "hi"]).stdout().text({
338
+ trimTrailingNewlines: "single-required",
339
+ }),
340
+ ).toThrow("Trailing newline required, but not present.");
341
+ expect(
342
+ await new PrintableShellCommand("echo", ["hi"]).stdout().text({
343
+ trimTrailingNewlines: "single-required",
344
+ }),
345
+ ).toEqual("hi");
346
+ expect(
347
+ await new PrintableShellCommand("echo", ["hi"]).stdout().text({
348
+ trimTrailingNewlines: "single-if-present",
349
+ }),
350
+ ).toEqual("hi");
351
+ expect(
352
+ await new PrintableShellCommand("echo", ["hi"]).stdout().text({
353
+ trimTrailingNewlines: "never",
354
+ }),
355
+ ).toEqual("hi\n");
356
+ });
357
+
358
+ test(".stdout.text({ trimTrailingNewlines: … })", async () => {
359
+ expect(() =>
360
+ new PrintableShellCommand("echo", ["-n", "hi"])
361
+ .spawn({ stdio: ["ignore", "pipe", "ignore"] })
362
+ .stdout.text({
363
+ trimTrailingNewlines: "single-required",
364
+ }),
365
+ ).toThrow("Trailing newline required, but not present.");
366
+ expect(
367
+ await new PrintableShellCommand("echo", ["hi"])
368
+ .spawn({ stdio: ["ignore", "pipe", "ignore"] })
369
+ .stdout.text({
370
+ trimTrailingNewlines: "single-required",
371
+ }),
372
+ ).toEqual("hi");
373
+ expect(
374
+ await new PrintableShellCommand("echo", ["hi"])
375
+ .spawn({ stdio: ["ignore", "pipe", "ignore"] })
376
+ .stdout.text({
377
+ trimTrailingNewlines: "single-if-present",
378
+ }),
379
+ ).toEqual("hi");
380
+ expect(
381
+ await new PrintableShellCommand("echo", ["hi"])
382
+ .spawn({ stdio: ["ignore", "pipe", "ignore"] })
383
+ .stdout.text({
384
+ trimTrailingNewlines: "never",
385
+ }),
386
+ ).toEqual("hi\n");
387
+ });
388
+
335
389
  test(".stderr(…)", async () => {
336
390
  expect(
337
391
  await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
@@ -345,6 +399,66 @@ test(".stderr(…)", async () => {
345
399
  ).toEqual("hi\n");
346
400
  });
347
401
 
402
+ test(".stderr().text({ trimTrailingNewlines: … })", async () => {
403
+ expect(() =>
404
+ new PrintableShellCommand("bash", ["-c", "echo -n hi 1>&2"]).stderr().text({
405
+ trimTrailingNewlines: "single-required",
406
+ }),
407
+ ).toThrow("Trailing newline required, but not present.");
408
+ expect(
409
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
410
+ .stderr()
411
+ .text({
412
+ trimTrailingNewlines: "single-required",
413
+ }),
414
+ ).toEqual("hi");
415
+ expect(
416
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
417
+ .stderr()
418
+ .text({
419
+ trimTrailingNewlines: "single-if-present",
420
+ }),
421
+ ).toEqual("hi");
422
+ expect(
423
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
424
+ .stderr()
425
+ .text({
426
+ trimTrailingNewlines: "never",
427
+ }),
428
+ ).toEqual("hi\n");
429
+ });
430
+
431
+ test(".stderr.text({ trimTrailingNewlines: … })", async () => {
432
+ expect(() =>
433
+ new PrintableShellCommand("bash", ["-c", "echo -n hi 1>&2"])
434
+ .spawn({ stdio: ["ignore", "ignore", "pipe"] })
435
+ .stderr.text({
436
+ trimTrailingNewlines: "single-required",
437
+ }),
438
+ ).toThrow("Trailing newline required, but not present.");
439
+ expect(
440
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
441
+ .spawn({ stdio: ["ignore", "ignore", "pipe"] })
442
+ .stderr.text({
443
+ trimTrailingNewlines: "single-required",
444
+ }),
445
+ ).toEqual("hi");
446
+ expect(
447
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
448
+ .spawn({ stdio: ["ignore", "ignore", "pipe"] })
449
+ .stderr.text({
450
+ trimTrailingNewlines: "single-if-present",
451
+ }),
452
+ ).toEqual("hi");
453
+ expect(
454
+ await new PrintableShellCommand("bash", ["-c", "echo hi 1>&2"])
455
+ .spawn({ stdio: ["ignore", "ignore", "pipe"] })
456
+ .stderr.text({
457
+ trimTrailingNewlines: "never",
458
+ }),
459
+ ).toEqual("hi\n");
460
+ });
461
+
348
462
  test(".stdout() and .stderr() — multiple success Promise awaiters", async () => {
349
463
  const { stdout, stderr } = new PrintableShellCommand("echo", ["hi"]).spawn({
350
464
  stdio: ["ignore", "pipe", "pipe"],
@@ -361,6 +475,29 @@ test(".text()", async () => {
361
475
  );
362
476
  });
363
477
 
478
+ test(".text({ trimTrailingNewlines: … })", async () => {
479
+ expect(() =>
480
+ new PrintableShellCommand("echo", ["-n", "hi"]).text({
481
+ trimTrailingNewlines: "single-required",
482
+ }),
483
+ ).toThrow("Trailing newline required, but not present.");
484
+ expect(
485
+ await new PrintableShellCommand("echo", ["hi"]).text({
486
+ trimTrailingNewlines: "single-required",
487
+ }),
488
+ ).toEqual("hi");
489
+ expect(
490
+ await new PrintableShellCommand("echo", ["hi"]).text({
491
+ trimTrailingNewlines: "single-if-present",
492
+ }),
493
+ ).toEqual("hi");
494
+ expect(
495
+ await new PrintableShellCommand("echo", ["hi"]).text({
496
+ trimTrailingNewlines: "never",
497
+ }),
498
+ ).toEqual("hi\n");
499
+ });
500
+
364
501
  test(".text()", async () => {
365
502
  const bogusBinaryPath = (await Path.makeTempDir()).join("bogus-bin");
366
503
  expect(() => new PrintableShellCommand(bogusBinaryPath).text()).toThrow(
@@ -17,6 +17,12 @@ import type {
17
17
  WithStdoutResponse,
18
18
  WithSuccess,
19
19
  } from "./spawn";
20
+ import {
21
+ handleTrailingNewlines,
22
+ type TrailingNewlineOptions,
23
+ wrapHandleTrailingNewlinesForResponse,
24
+ wrapHandleTrailingNewlinesForText,
25
+ } from "./trimTrailingNewlines";
20
26
 
21
27
  // TODO: does this import work?
22
28
  /**
@@ -403,10 +409,10 @@ export class PrintableShellCommand {
403
409
  let cachedResponse: Response | undefined;
404
410
  s.stdout.response = () =>
405
411
  // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592
406
- (cachedResponse ??= new Response(
407
- Readable.from(this.#generator(s.stdout, s.success)),
412
+ (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
413
+ new Response(Readable.from(this.#generator(s.stdout, s.success))),
408
414
  ));
409
- s.stdout.text = () => s.stdout.response().text();
415
+ s.stdout.text = wrapHandleTrailingNewlinesForText(s.stdout.response());
410
416
  const thisCached = this; // TODO: make this type-check using `.bind(…)`
411
417
  s.stdout.text0 = async function* () {
412
418
  yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));
@@ -421,10 +427,10 @@ export class PrintableShellCommand {
421
427
  let cachedResponse: Response | undefined;
422
428
  s.stderr.response = () =>
423
429
  // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592
424
- (cachedResponse ??= new Response(
425
- Readable.from(this.#generator(s.stderr, s.success)),
430
+ (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
431
+ new Response(Readable.from(this.#generator(s.stderr, s.success))),
426
432
  ));
427
- s.stderr.text = () => s.stderr.response().text();
433
+ s.stderr.text = wrapHandleTrailingNewlinesForText(s.stderr.response());
428
434
  const thisCached = this; // TODO: make this type-check using `.bind(…)`
429
435
  s.stderr.text0 = async function* () {
430
436
  yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));
@@ -533,9 +539,13 @@ export class PrintableShellCommand {
533
539
 
534
540
  public stdout(
535
541
  options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>,
536
- ): Response {
542
+ ): Response & {
543
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
544
+ } {
537
545
  // TODO: Use `ReadableStream.from(…)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269
538
- return new Response(Readable.from(this.#stdoutSpawnGenerator(options)));
546
+ return wrapHandleTrailingNewlinesForResponse(
547
+ new Response(Readable.from(this.#stdoutSpawnGenerator(options))),
548
+ );
539
549
  }
540
550
 
541
551
  #stderrSpawnGenerator(
@@ -553,9 +563,13 @@ export class PrintableShellCommand {
553
563
 
554
564
  public stderr(
555
565
  options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>,
556
- ): Response {
566
+ ): Response & {
567
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
568
+ } {
557
569
  // TODO: Use `ReadableStream.from(…)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269
558
- return new Response(Readable.from(this.#stderrSpawnGenerator(options)));
570
+ return wrapHandleTrailingNewlinesForResponse(
571
+ new Response(Readable.from(this.#stderrSpawnGenerator(options))),
572
+ );
559
573
  }
560
574
 
561
575
  async *#split0(generator: AsyncGenerator<string>): AsyncGenerator<string> {
@@ -580,10 +594,18 @@ export class PrintableShellCommand {
580
594
  *
581
595
  * This can make some simple invocations easier to read and/or fit on a single line.
582
596
  */
583
- public text(
584
- options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">>,
597
+ public async text(
598
+ options?: NodeWithCwd<Omit<NodeSpawnOptions, "stdio">> & {
599
+ trimTrailingNewlines: "single-required" | "single-if-present" | "never";
600
+ },
585
601
  ): Promise<string> {
586
- return this.stdout(options).text();
602
+ const {
603
+ trimTrailingNewlines: trimTrailingNewlinesOption,
604
+ ...spawnOptions
605
+ } = options ?? {};
606
+ return handleTrailingNewlines(this.stdout(spawnOptions).text(), {
607
+ trimTrailingNewlines: trimTrailingNewlinesOption,
608
+ });
587
609
  }
588
610
 
589
611
  /**
package/src/spawn.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  } from "node:child_process";
12
12
  import type { Readable, Writable } from "node:stream";
13
13
  import type { Path } from "path-class";
14
+ import type { TrailingNewlineOptions } from "./trimTrailingNewlines";
14
15
 
15
16
  export type NodeCwd = ProcessEnvOptions["cwd"] | Path;
16
17
  export type NodeWithCwd<T extends { cwd?: ProcessEnvOptions["cwd"] }> = Omit<
@@ -24,7 +25,7 @@ export interface WithSuccess {
24
25
 
25
26
  export interface WithResponse {
26
27
  response: () => Response;
27
- text: () => Promise<string>;
28
+ text: (options?: TrailingNewlineOptions) => Promise<string>;
28
29
  text0: () => AsyncGenerator<string>;
29
30
  json: <T>() => Promise<T>;
30
31
  }
@@ -0,0 +1,61 @@
1
+ const DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR = "never";
2
+ export interface TrailingNewlineOptions {
3
+ trimTrailingNewlines?: "single-required" | "single-if-present" | "never";
4
+ }
5
+
6
+ // TODO: Handle carriage return?
7
+ export async function handleTrailingNewlines(
8
+ text: Promise<string>,
9
+ options?: TrailingNewlineOptions,
10
+ ): Promise<string> {
11
+ const awaitedText = await text;
12
+ switch (
13
+ options?.trimTrailingNewlines ??
14
+ DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR
15
+ ) {
16
+ case "single-required": {
17
+ if (!awaitedText.endsWith("\n")) {
18
+ throw new Error("Trailing newline required, but not present.");
19
+ }
20
+ return awaitedText.slice(0, -1);
21
+ }
22
+ case "single-if-present": {
23
+ if (awaitedText.endsWith("\n")) {
24
+ return awaitedText.slice(0, -1);
25
+ } else {
26
+ return awaitedText;
27
+ }
28
+ }
29
+ case "never": {
30
+ return awaitedText;
31
+ }
32
+ default:
33
+ throw new Error("Invalid value for `trimTrailingNewlines`.");
34
+ }
35
+ }
36
+
37
+ function wrapHandleTrailingNewlines(
38
+ fn: () => Promise<string>,
39
+ ): (options?: TrailingNewlineOptions) => Promise<string> {
40
+ return (options?: TrailingNewlineOptions) =>
41
+ handleTrailingNewlines(fn(), options);
42
+ }
43
+
44
+ export function wrapHandleTrailingNewlinesForText(v: {
45
+ text: () => Promise<string>;
46
+ }): (options?: TrailingNewlineOptions) => Promise<string> {
47
+ const originalTextFn = v.text.bind(v);
48
+ return wrapHandleTrailingNewlines(originalTextFn);
49
+ }
50
+
51
+ export function wrapHandleTrailingNewlinesForResponse(
52
+ response: Response,
53
+ ): Response & { text: (options?: TrailingNewlineOptions) => Promise<string> } {
54
+ const textFn = wrapHandleTrailingNewlinesForText(response);
55
+ Object.defineProperty(response, "text", {
56
+ get: () => {
57
+ return textFn;
58
+ },
59
+ });
60
+ return response;
61
+ }