hacksh 0.9.0 → 0.11.0

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/README.md CHANGED
@@ -304,10 +304,12 @@ multiselect prompt.
304
304
  Prompts are allowed only when the resolved mode is interactive: pretty output outside CI with TTY
305
305
  stdin and stdout. JSON, CI, and non-TTY invocations throw `HackshPromptError` immediately even if a
306
306
  default is available. Its `reason` and `prompt` fields identify why input failed and the prompt's
307
- `kind`, `message`, and select `choices`. Reasons distinguish `non-interactive`, `cancelled`, `eof`,
308
- `overlapping`, `invalid-options`, `input-error`, and `printer-error`. Ctrl-C and Escape cancel;
309
- `runMain` reports cancellation and exits with status 130. Only one prompt may be active in an
310
- invocation. Input listeners and raw terminal mode are restored after every outcome.
307
+ `kind`, `message`, and select `choices`. Reasons distinguish `non-interactive`, `cancelled`,
308
+ `aborted`, `eof`, `overlapping`, `invalid-options`, `input-error`, and `printer-error`. Ctrl-C and
309
+ Escape cancel; `runMain` reports cancellation and exits with status 130. A prompt that starts or is
310
+ waiting after the invocation's `signal` aborts fails with `aborted` and the signal's reason as its
311
+ `cause`. Only one prompt may be active in an invocation. Input listeners and raw terminal mode are
312
+ restored after every outcome.
311
313
  Failed prompts emit a non-visible `closed` prompt state through the same printer before rejecting,
312
314
  so callers that catch cancellation, EOF, input, or printer errors do not leave stale terminal UI.
313
315
 
@@ -352,7 +354,8 @@ narrowed before reading `exitCode`.
352
354
 
353
355
  Options are `cwd`, `env`, `input`, `reject`, `timeout` (milliseconds), `signal` (an
354
356
  `AbortSignal`), `maxBuffer` (bytes per stream, default 100,000,000), and `forceKillAfterDelay`
355
- (milliseconds, default 5,000). `input` is written to stdin and closed; otherwise stdin is empty.
357
+ (milliseconds, default 5,000). Every call also stops with `aborted` when the invocation's `signal`
358
+ aborts. `input` is written to stdin and closed; otherwise stdin is empty.
356
359
  Output beyond `maxBuffer` is truncated to that many bytes. On timeout, abort, or output limit,
357
360
  hacksh sends `SIGTERM` to the child it started and `SIGKILL` after `forceKillAfterDelay`; it does
358
361
  not stop grandchildren, and it does not wait for them to close inherited pipes. Invalid arguments
@@ -571,6 +574,42 @@ Usage failures are `HackshUsageError` instances. Argument failures are
571
574
  `HackshValidationError` instances whose `issues` identify CLI names such as `--repo` or `<PR>`.
572
575
  Prompt failures are `HackshPromptError` instances with structured `reason` and `prompt` fields.
573
576
 
577
+ ## Cancellation and exit status
578
+
579
+ Every `run` context has a `signal`, an `AbortSignal` for that invocation. `exec` calls and prompts
580
+ already follow it; pass it to `fetch` and other APIs that accept one. Cancellation is cooperative:
581
+ hacksh does not stop code that ignores the signal.
582
+
583
+ ```ts
584
+ import { defineCommand, HackshExitError, runCommand } from "hacksh";
585
+
586
+ const check = defineCommand({
587
+ name: "check",
588
+ async run({ signal }) {
589
+ const response = await fetch("https://example.com/health", { signal });
590
+ if (!response.ok) throw new HackshExitError(2, `Health check returned ${response.status}`);
591
+ },
592
+ });
593
+
594
+ const controller = new AbortController();
595
+ await runCommand(check, [], { signal: controller.signal });
596
+ ```
597
+
598
+ `runCommand` accepts a `signal` option and never listens for process signals or exits. If that
599
+ signal is already aborted, `runCommand` rejects with its reason without calling `run`; aborting it
600
+ later aborts the context's `signal`.
601
+
602
+ `runMain` listens for `SIGINT` and `SIGTERM` while it runs and removes those listeners before it
603
+ returns or exits. The first signal aborts the invocation with an `AbortError` named after the
604
+ signal; once `run` settles, output is drained and the process exits with 130 for `SIGINT` or 143 for
605
+ `SIGTERM`, even if `run` threw. A second signal while `run` is still pending exits immediately with
606
+ that signal's status.
607
+
608
+ Throw `HackshExitError(exitCode, message?)` to choose the exit status, an integer from 0 to 255.
609
+ `runMain` prints a non-empty message as an ordinary error and prints nothing when the message is
610
+ omitted; `onError` still receives the error. `runCommand` rethrows it, so library callers read
611
+ `exitCode` from the caught error. Other failures exit with 1, or 130 for a cancelled prompt.
612
+
574
613
  ## Development
575
614
 
576
615
  | Command | Description |
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ declare class HackshValidationError extends HackshUsageError {
14
14
  constructor(command: AnyCommandDef, issues: ReadonlyArray<HackshIssue>);
15
15
  }
16
16
  type PromptKind = "select" | "confirm" | "text";
17
- type HackshPromptErrorReason = "non-interactive" | "cancelled" | "eof" | "overlapping" | "invalid-options" | "input-error" | "printer-error";
17
+ type HackshPromptErrorReason = "non-interactive" | "cancelled" | "aborted" | "eof" | "overlapping" | "invalid-options" | "input-error" | "printer-error";
18
18
  interface PromptErrorDetails {
19
19
  kind: PromptKind;
20
20
  message: string;
@@ -32,6 +32,11 @@ declare class HackshExecError extends Error {
32
32
  constructor(message: string, result: ExecFailure, options?: ErrorOptions);
33
33
  get result(): ExecFailure;
34
34
  }
35
+ /** A requested process exit status; `runMain` exits with it and reports only a non-empty message. */
36
+ declare class HackshExitError extends Error {
37
+ readonly exitCode: number;
38
+ constructor(exitCode: number, message?: string, options?: ErrorOptions);
39
+ }
35
40
  type HackshErrorKind = "error" | "prompt" | "usage" | "validation" | "thrown";
36
41
  interface ValidationIssueSummary {
37
42
  arg: string;
@@ -367,6 +372,7 @@ interface CommandContext<S extends ArgsSchema> {
367
372
  extraArgs: string[];
368
373
  logger: Logger;
369
374
  exec: Exec;
375
+ signal: AbortSignal;
370
376
  }
371
377
  interface CommandDef<S extends ArgsSchema = EmptyArgs> {
372
378
  name?: string;
@@ -403,6 +409,7 @@ declare function createPrettyPrinter(options: PrettyPrinterOptions): Printer;
403
409
  //#region src/run.d.ts
404
410
  interface RunCommandOptions extends OutputOptions {
405
411
  printer?: Printer;
412
+ signal?: AbortSignal;
406
413
  showUsage?: ShowUsageCallback;
407
414
  }
408
415
  declare function runCommand<S extends ArgsSchema>(root: CommandDef<S>, argv: string[], options?: RunCommandOptions): Promise<unknown>;
@@ -415,5 +422,5 @@ interface RunMainOptions extends OutputOptions {
415
422
  declare function runMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): Promise<void>;
416
423
  declare function createMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): (argv?: string[]) => Promise<void>;
417
424
  //#endregion
418
- export { type ArgOptions, type ArgOptionsMap, type CommandContext, type CommandDef, type ConfirmOptions, type ConfirmPromptState, type ErrorPrintEvent, type Exec, type ExecCommand, type ExecFailure, type ExecFailureReason, type ExecOptions, type ExecPrintEvent, type ExecResult, type ExecReturn, type ExecSuccess, type HackshErrorKind, type HackshErrorSummary, HackshExecError, type HackshIssue, HackshPromptError, type HackshPromptErrorReason, HackshUsageError, HackshValidationError, type JsonPrinterOptions, type LogKind, type LogLevel, type LogPrintEvent, type LogProperties, type LogRecord, type Logger, type OutputFormat, type OutputMode, type OutputOptions, type OutputPreference, type OutputWriter, type PrettyPrinterOptions, type PrintEvent, type PrintStream, type Printer, type PromptChoiceState, type PromptErrorDetails, type PromptKind, type PromptPrintEvent, type PromptState, type PromptStatus, type Prompter, type ResolveOutputModeOptions, type RunCommandOptions, type RunMainOptions, type SelectOption, type SelectOptions, type SelectPromptState, type ShowUsageCallback, type ShowUsageOptions, type StreamPrinterOptions, type TerminalInput, type TerminalOutput, type TextOptions, type TextPromptState, type UsageArgumentSummary, type UsageCommandSummary, type UsageOptionSummary, type UsageOutput, type UsagePrintEvent, type UsageSummary, type UsageWriter, type ValidationIssueSummary, type VersionPrintEvent, consolePrinter, createJsonPrinter, createMain, createPrettyPrinter, createStreamPrinter, defineCommand, parseArgs, renderUsage, resolveOutputMode, runCommand, runMain, showUsage, summarizeUsage };
425
+ export { type ArgOptions, type ArgOptionsMap, type CommandContext, type CommandDef, type ConfirmOptions, type ConfirmPromptState, type ErrorPrintEvent, type Exec, type ExecCommand, type ExecFailure, type ExecFailureReason, type ExecOptions, type ExecPrintEvent, type ExecResult, type ExecReturn, type ExecSuccess, type HackshErrorKind, type HackshErrorSummary, HackshExecError, HackshExitError, type HackshIssue, HackshPromptError, type HackshPromptErrorReason, HackshUsageError, HackshValidationError, type JsonPrinterOptions, type LogKind, type LogLevel, type LogPrintEvent, type LogProperties, type LogRecord, type Logger, type OutputFormat, type OutputMode, type OutputOptions, type OutputPreference, type OutputWriter, type PrettyPrinterOptions, type PrintEvent, type PrintStream, type Printer, type PromptChoiceState, type PromptErrorDetails, type PromptKind, type PromptPrintEvent, type PromptState, type PromptStatus, type Prompter, type ResolveOutputModeOptions, type RunCommandOptions, type RunMainOptions, type SelectOption, type SelectOptions, type SelectPromptState, type ShowUsageCallback, type ShowUsageOptions, type StreamPrinterOptions, type TerminalInput, type TerminalOutput, type TextOptions, type TextPromptState, type UsageArgumentSummary, type UsageCommandSummary, type UsageOptionSummary, type UsageOutput, type UsagePrintEvent, type UsageSummary, type UsageWriter, type ValidationIssueSummary, type VersionPrintEvent, consolePrinter, createJsonPrinter, createMain, createPrettyPrinter, createStreamPrinter, defineCommand, parseArgs, renderUsage, resolveOutputMode, runCommand, runMain, showUsage, summarizeUsage };
419
426
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -185,6 +185,16 @@ var HackshExecError = class extends Error {
185
185
  return this.#result;
186
186
  }
187
187
  };
188
+ /** A requested process exit status; `runMain` exits with it and reports only a non-empty message. */
189
+ var HackshExitError = class extends Error {
190
+ exitCode;
191
+ constructor(exitCode, message, options) {
192
+ if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) throw new TypeError("Exit code must be an integer from 0 to 255");
193
+ super(message, options);
194
+ this.name = "HackshExitError";
195
+ this.exitCode = exitCode;
196
+ }
197
+ };
188
198
  function safeString$1(value, fallback) {
189
199
  try {
190
200
  return String(value);
@@ -757,7 +767,7 @@ function redactError(error, secrets) {
757
767
  function observedError(plan, error) {
758
768
  return plan.redact.length === 0 ? error : redactError(error, plan.redact);
759
769
  }
760
- function parsePlan(file, args, options, baseEnv) {
770
+ function parsePlan(file, args, options, context) {
761
771
  if (typeof file !== "string" || file === "") throw new TypeError("Exec file must be a non-empty string");
762
772
  assertNoNul(file, "Exec file");
763
773
  const argv = args === void 0 ? [] : args;
@@ -787,11 +797,11 @@ function parsePlan(file, args, options, baseEnv) {
787
797
  return {
788
798
  command,
789
799
  reported: redactCommand(command, redact),
790
- env: mergeEnvironment(baseEnv, raw.env),
800
+ env: mergeEnvironment(context.env, raw.env),
791
801
  input,
792
802
  reject: reject ?? true,
793
803
  timeout: timeout === void 0 ? void 0 : readInteger(timeout, "Exec option `timeout`", 1, MAX_DELAY),
794
- signal,
804
+ signal: signal === void 0 ? context.signal : AbortSignal.any([context.signal, signal]),
795
805
  maxBuffer: maxBuffer === void 0 ? 1e8 : readInteger(maxBuffer, "Exec option `maxBuffer`", 1, Number.MAX_SAFE_INTEGER),
796
806
  forceKillAfterDelay: forceKillAfterDelay === void 0 ? 5e3 : readInteger(forceKillAfterDelay, "Exec option `forceKillAfterDelay`", 0, MAX_DELAY),
797
807
  stdin,
@@ -902,7 +912,7 @@ function runPlan(plan) {
902
912
  spawnError: void 0,
903
913
  exit: void 0
904
914
  };
905
- if (plan.signal?.aborted) {
915
+ if (plan.signal.aborted) {
906
916
  outcome.cause = "aborted";
907
917
  return Promise.resolve(toResult(plan.reported, outcome, "", ""));
908
918
  }
@@ -921,7 +931,7 @@ function runPlan(plan) {
921
931
  settled = true;
922
932
  clearTimeout(timeoutTimer);
923
933
  clearTimeout(killTimer);
924
- plan.signal?.removeEventListener("abort", onAbort);
934
+ plan.signal.removeEventListener("abort", onAbort);
925
935
  resolve(toResult(plan.reported, outcome, readStdout(), readStderr()));
926
936
  };
927
937
  const release = () => {
@@ -943,7 +953,7 @@ function runPlan(plan) {
943
953
  const readStdout = stdout ? collect(stdout, plan.maxBuffer, () => terminate("output-limit")) : () => "";
944
954
  const readStderr = stderr ? collect(stderr, plan.maxBuffer, () => terminate("output-limit")) : () => "";
945
955
  const timeoutTimer = plan.timeout === void 0 ? void 0 : setTimeout(() => terminate("timeout"), plan.timeout);
946
- plan.signal?.addEventListener("abort", onAbort, { once: true });
956
+ plan.signal.addEventListener("abort", onAbort, { once: true });
947
957
  child.on("error", (error) => {
948
958
  if (child.pid !== void 0) return;
949
959
  outcome.spawnError = observedError(plan, error);
@@ -1003,8 +1013,8 @@ function blockedReason(plan, context, stdio) {
1003
1013
  if (plan.interactive && !context.mode.interactive) return "non-interactive";
1004
1014
  }
1005
1015
  async function execute(file, args, options, context) {
1006
- const plan = parsePlan(file, args, options, context.env);
1007
- if (plan.signal?.aborted) return complete(plan, emptyFailure(plan, "aborted"));
1016
+ const plan = parsePlan(file, args, options, context);
1017
+ if (plan.signal.aborted) return complete(plan, emptyFailure(plan, "aborted"));
1008
1018
  const stdio = commandStdio(plan);
1009
1019
  const blocked = blockedReason(plan, context, stdio);
1010
1020
  if (blocked) return complete(plan, emptyFailure(plan, blocked));
@@ -1493,11 +1503,13 @@ var PromptController = class {
1493
1503
  #printEvent;
1494
1504
  #bufferedKeys = [];
1495
1505
  #terminal;
1506
+ #signal;
1496
1507
  constructor(options) {
1497
1508
  this.#interactive = options.interactive;
1498
1509
  this.#inputValue = options.input;
1499
1510
  this.#printEvent = (event) => options.print(event);
1500
1511
  this.#terminal = options.terminal;
1512
+ this.#signal = options.signal;
1501
1513
  }
1502
1514
  async #print(state, prompt) {
1503
1515
  try {
@@ -1511,6 +1523,7 @@ var PromptController = class {
1511
1523
  }
1512
1524
  async #run(prompt, initial, accept, update, submitted, closed) {
1513
1525
  if (!this.#interactive) throw promptError("non-interactive", prompt, `Cannot request ${prompt.kind} input in non-interactive mode: ${prompt.message}`);
1526
+ if (this.#signal.aborted) throw promptError("aborted", prompt, "Prompt aborted", this.#signal.reason);
1514
1527
  const input = asInput(this.#inputValue, prompt);
1515
1528
  if (!this.#terminal.acquire("prompt")) throw promptError("overlapping", prompt, this.#terminal.owner === "exec" ? "A command is using the terminal" : "Another prompt is already active");
1516
1529
  let originallyFlowing = false;
@@ -1597,6 +1610,7 @@ var PromptController = class {
1597
1610
  };
1598
1611
  const onEnd = () => fail("eof", "Prompt input ended before a value was submitted");
1599
1612
  const onError = (cause) => fail("input-error", "Prompt input failed", cause);
1613
+ const onAbort = () => fail("aborted", "Prompt aborted", this.#signal.reason);
1600
1614
  const onData = (chunk) => {
1601
1615
  if (typeof chunk === "string" || chunk instanceof Uint8Array) keypressInput.write(chunk);
1602
1616
  else fail("input-error", "Prompt input produced a non-byte value");
@@ -1608,12 +1622,15 @@ var PromptController = class {
1608
1622
  input.removeListener("end", onEnd);
1609
1623
  input.removeListener("close", onEnd);
1610
1624
  input.removeListener("error", onError);
1625
+ this.#signal.removeEventListener("abort", onAbort);
1611
1626
  };
1612
1627
  keypressInput.on("keypress", onKeypress);
1613
1628
  input.on("data", onData);
1614
1629
  input.on("end", onEnd);
1615
1630
  input.on("close", onEnd);
1616
1631
  input.on("error", onError);
1632
+ this.#signal.addEventListener("abort", onAbort, { once: true });
1633
+ if (this.#signal.aborted) onAbort();
1617
1634
  input.resume();
1618
1635
  drainKeys().catch((cause) => fail("input-error", "Prompt input could not be processed", cause));
1619
1636
  })
@@ -1798,7 +1815,8 @@ function createLogger(printer, category, options) {
1798
1815
  interactive: options.interactive,
1799
1816
  input: options.input,
1800
1817
  print: (event) => options.printPrompt(event),
1801
- terminal: options.terminal
1818
+ terminal: options.terminal,
1819
+ signal: options.signal
1802
1820
  });
1803
1821
  return createBoundLogger(printer, [...category], {}, prompts);
1804
1822
  }
@@ -2055,9 +2073,10 @@ function resolveCommand(root, rawArgs) {
2055
2073
  remaining = remaining.slice(index + 1);
2056
2074
  }
2057
2075
  }
2058
- async function executeCommand(root, argv, displayUsage, printer, loggerOptions, execContext, fallbackVersion = async () => void 0) {
2076
+ async function executeCommand(root, argv, displayUsage, output, env, signal, fallbackVersion = async () => void 0) {
2059
2077
  if (root.args && "version" in schemaShape(root.args)) throw new TypeError("Root option `version` conflicts with the builtin --version");
2060
- const env = execContext.env ?? process.env;
2078
+ const { printer } = output;
2079
+ const argEnv = env ?? process.env;
2061
2080
  const resolved = resolveCommand(root, argv);
2062
2081
  if (resolved.segments[0]?.tokenized.version) {
2063
2082
  const version = root.version ?? await fallbackVersion() ?? "";
@@ -2071,16 +2090,33 @@ async function executeCommand(root, argv, displayUsage, printer, loggerOptions,
2071
2090
  await printUsage(displayUsage, resolved.command, printer, "stdout");
2072
2091
  return;
2073
2092
  }
2074
- for (const parent of resolved.parents) await parseCommandArgs(parent.command, parent.rawArgs, parent.tokenized, env, commandEnvPath(parent.command));
2075
- const parsed = await parseCommandArgs(resolved.command, resolved.rawArgs, resolved.tokenized, env, commandEnvPath(resolved.command));
2093
+ for (const parent of resolved.parents) await parseCommandArgs(parent.command, parent.rawArgs, parent.tokenized, argEnv, commandEnvPath(parent.command));
2094
+ const parsed = await parseCommandArgs(resolved.command, resolved.rawArgs, resolved.tokenized, argEnv, commandEnvPath(resolved.command));
2076
2095
  if (!resolved.command.run) throw new HackshUsageError("No command specified", resolved.command);
2096
+ signal.throwIfAborted();
2077
2097
  const category = resolved.segments.flatMap(({ command }) => command.name === void 0 ? [] : [command.name]);
2078
2098
  return resolved.command.run({
2079
2099
  ...parsed,
2080
2100
  cmd: resolved.command,
2081
2101
  rawArgs: resolved.rawArgs,
2082
- logger: createLogger(printer, category, loggerOptions),
2083
- exec: createExec(execContext)
2102
+ logger: createLogger(printer, category, {
2103
+ interactive: output.interactive,
2104
+ input: output.input,
2105
+ printPrompt: (event) => output.printChecked(event),
2106
+ terminal: output.terminal,
2107
+ signal
2108
+ }),
2109
+ exec: createExec({
2110
+ env,
2111
+ mode: {
2112
+ format: output.format,
2113
+ interactive: output.interactive
2114
+ },
2115
+ print: (event) => printer.print(event),
2116
+ terminal: output.terminal,
2117
+ signal
2118
+ }),
2119
+ signal
2084
2120
  });
2085
2121
  }
2086
2122
  function printUsage(displayUsage, command, printer, stream) {
@@ -2107,26 +2143,10 @@ function prepareOutput(options, env) {
2107
2143
  terminal: new TerminalLock()
2108
2144
  };
2109
2145
  }
2110
- function invocationExec(output, env) {
2111
- return {
2112
- env,
2113
- mode: {
2114
- format: output.format,
2115
- interactive: output.interactive
2116
- },
2117
- print: (event) => output.printer.print(event),
2118
- terminal: output.terminal
2119
- };
2120
- }
2121
2146
  async function runCommand(root, argv, options = {}) {
2122
2147
  const output = prepareOutput(options, options.env ?? process.env);
2123
2148
  try {
2124
- return await executeCommand(root, argv, options.showUsage ?? showUsage, output.printer, {
2125
- interactive: output.interactive,
2126
- input: output.input,
2127
- printPrompt: (event) => output.printChecked(event),
2128
- terminal: output.terminal
2129
- }, invocationExec(output, options.env));
2149
+ return await executeCommand(root, argv, options.showUsage ?? showUsage, output, options.env, options.signal ?? new AbortController().signal);
2130
2150
  } finally {
2131
2151
  await output.drain();
2132
2152
  }
@@ -2139,8 +2159,33 @@ async function drainAndExit(output, code = 1) {
2139
2159
  }
2140
2160
  }
2141
2161
  function commandErrorExitCode(error) {
2162
+ if (error instanceof HackshExitError) return error.exitCode;
2142
2163
  return error instanceof HackshPromptError && error.reason === "cancelled" ? 130 : 1;
2143
2164
  }
2165
+ const SIGNAL_EXIT_CODES = {
2166
+ SIGINT: 130,
2167
+ SIGTERM: 143
2168
+ };
2169
+ function listenForExitSignals() {
2170
+ const controller = new AbortController();
2171
+ let exitCode;
2172
+ const listeners = Object.entries(SIGNAL_EXIT_CODES).map(([signal, code]) => {
2173
+ const listener = () => {
2174
+ if (exitCode !== void 0) process.exit(code);
2175
+ exitCode = code;
2176
+ controller.abort(new DOMException(`Received ${signal}`, "AbortError"));
2177
+ };
2178
+ process.on(signal, listener);
2179
+ return [signal, listener];
2180
+ });
2181
+ return {
2182
+ signal: controller.signal,
2183
+ exitCode: () => exitCode,
2184
+ release() {
2185
+ for (const [signal, listener] of listeners) process.removeListener(signal, listener);
2186
+ }
2187
+ };
2188
+ }
2144
2189
  async function reportOutputSetupError(error, options) {
2145
2190
  if (options.onError) {
2146
2191
  await options.onError(error);
@@ -2156,6 +2201,14 @@ async function reportOutputSetupError(error, options) {
2156
2201
  });
2157
2202
  return drainAndExit(output);
2158
2203
  }
2204
+ async function reportCommandError(error, output, displayUsage) {
2205
+ if (error instanceof HackshExitError && error.message === "") return;
2206
+ await output.printer.print({
2207
+ type: "error",
2208
+ error: summarizeError(error)
2209
+ });
2210
+ if (error instanceof HackshUsageError) await printUsage(displayUsage, error.command, output.printer, "stderr");
2211
+ }
2159
2212
  async function runMain(command, options = {}) {
2160
2213
  const displayUsage = options.showUsage ?? showUsage;
2161
2214
  let output;
@@ -2164,33 +2217,28 @@ async function runMain(command, options = {}) {
2164
2217
  } catch (error) {
2165
2218
  return reportOutputSetupError(error, options);
2166
2219
  }
2220
+ const signals = listenForExitSignals();
2221
+ const exit = (code) => {
2222
+ signals.release();
2223
+ return drainAndExit(output, signals.exitCode() ?? code);
2224
+ };
2167
2225
  try {
2168
- await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage, output.printer, {
2169
- interactive: output.interactive,
2170
- input: output.input,
2171
- printPrompt: (event) => output.printChecked(event),
2172
- terminal: output.terminal
2173
- }, invocationExec(output, options.env), () => findPackageVersion(process.argv[1]));
2226
+ await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage, output, options.env, signals.signal, () => findPackageVersion(process.argv[1]));
2174
2227
  } catch (error) {
2175
2228
  if (options.onError) {
2176
2229
  await options.onError(error);
2177
- return drainAndExit(output, commandErrorExitCode(error));
2230
+ return exit(commandErrorExitCode(error));
2178
2231
  }
2179
2232
  try {
2180
- if (error instanceof HackshUsageError) {
2181
- await output.printer.print({
2182
- type: "error",
2183
- error: summarizeError(error)
2184
- });
2185
- await printUsage(displayUsage, error.command, output.printer, "stderr");
2186
- } else await output.printer.print({
2187
- type: "error",
2188
- error: summarizeError(error)
2189
- });
2233
+ await reportCommandError(error, output, displayUsage);
2190
2234
  } finally {
2191
- await drainAndExit(output, commandErrorExitCode(error));
2235
+ await exit(commandErrorExitCode(error));
2192
2236
  }
2237
+ } finally {
2238
+ signals.release();
2193
2239
  }
2240
+ const signalExitCode = signals.exitCode();
2241
+ if (signalExitCode !== void 0) return drainAndExit(output, signalExitCode);
2194
2242
  await output.drain();
2195
2243
  }
2196
2244
  function createMain(command, options = {}) {
@@ -2200,6 +2248,6 @@ function createMain(command, options = {}) {
2200
2248
  });
2201
2249
  }
2202
2250
  //#endregion
2203
- export { HackshExecError, HackshPromptError, HackshUsageError, HackshValidationError, consolePrinter, createJsonPrinter, createMain, createPrettyPrinter, createStreamPrinter, defineCommand, parseArgs, renderUsage, resolveOutputMode, runCommand, runMain, showUsage, summarizeUsage };
2251
+ export { HackshExecError, HackshExitError, HackshPromptError, HackshUsageError, HackshValidationError, consolePrinter, createJsonPrinter, createMain, createPrettyPrinter, createStreamPrinter, defineCommand, parseArgs, renderUsage, resolveOutputMode, runCommand, runMain, showUsage, summarizeUsage };
2204
2252
 
2205
2253
  //# sourceMappingURL=index.js.map