hacksh 0.5.4 → 0.7.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
@@ -311,6 +311,94 @@ Physical rows are calculated from explicit newlines, ANSI-free Unicode display w
311
311
  `stdout.columns`, including terminal wrapping. When a custom output writer does not expose a
312
312
  positive `columns` value, the renderer conservatively counts explicit lines only.
313
313
 
314
+ ## External commands
315
+
316
+ Every `run` context has an `exec` function that starts an executable with an argument array. It
317
+ never uses a shell, so spaces, quotes, `$`, `;`, `|`, and `*` in arguments reach the child
318
+ unchanged. By default it resolves with the captured output and rejects with `HackshExecError`
319
+ when the command fails. Pass `reject: false` to receive failures as results instead.
320
+
321
+ ```ts
322
+ const sync = defineCommand({
323
+ name: "sync",
324
+ async run({ exec, logger }) {
325
+ const { stdout: branch } = await exec("git", ["branch", "--show-current"]);
326
+ const upstream = await exec("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], {
327
+ reject: false,
328
+ });
329
+ if (upstream.status === "failed") {
330
+ logger.warn("{branch} has no upstream", { branch });
331
+ return;
332
+ }
333
+ await exec("git", ["pull", "--ff-only"], { cwd: "/path/to/repo", timeout: 60_000 });
334
+ },
335
+ });
336
+ ```
337
+
338
+ Results have `status` `"succeeded"` or `"failed"`, the `command` (`file`, `args`, `cwd`), and
339
+ UTF-8 `stdout` and `stderr` with one final newline removed. A failure's `reason` is `exit-code`
340
+ (with `exitCode`), `signal` (a signal the child received from elsewhere, with `signal`), `spawn`
341
+ (the executable or cwd could not be used, with `error`), or one of `timeout`, `aborted`, and
342
+ `output-limit` when hacksh stopped the child. The return type follows `reject`: omitted or `true`
343
+ gives `ExecSuccess`, while `false` or a `boolean` variable gives `ExecResult`, which must be
344
+ narrowed before reading `exitCode`.
345
+
346
+ Options are `cwd`, `env`, `input`, `reject`, `timeout` (milliseconds), `signal` (an
347
+ `AbortSignal`), `maxBuffer` (bytes per stream, default 100,000,000), and `forceKillAfterDelay`
348
+ (milliseconds, default 5,000). `input` is written to stdin and closed; otherwise stdin is empty.
349
+ Output beyond `maxBuffer` is truncated to that many bytes. On timeout, abort, or output limit,
350
+ hacksh sends `SIGTERM` to the child it started and `SIGKILL` after `forceKillAfterDelay`; it does
351
+ not stop grandchildren, and it does not wait for them to close inherited pipes. Invalid arguments
352
+ or options reject with `TypeError` without starting anything, even with `reject: false`.
353
+
354
+ The child's environment starts from the invocation's `env` option on `runCommand` or `runMain`, or
355
+ from `process.env` when that is absent. The `env` option on `exec` sets variables and removes those
356
+ set to `undefined`. Concurrent invocations with different `env` options do not share settings.
357
+ `HackshExecError` messages include the command and the reason but never the child's output; read
358
+ `error.result` for the full failure, which error summaries and JSON error events omit.
359
+
360
+ Before starting a child, `exec` prints an `exec` event through the invocation's printer, on stderr
361
+ in both formats. Pretty output shows `$` followed by the command line and, when set, its `cwd`. The
362
+ JSON event has `phase: "start"`, `command` (`file`, `args`, and `cwd` when set), `stdio`, and
363
+ `interactive`; it never includes `env` or `input`. Pass `display: false` to hide one call. Pass
364
+ `redact` with secret values to replace every occurrence in the file, arguments, and cwd with `***`
365
+ in the event, the result's `command`, the `HackshExecError` message, and a `spawn` failure's
366
+ `error`. The child still receives the real values, and its captured output is returned unchanged.
367
+
368
+ ```ts
369
+ await exec("git", ["worktree", "add", path, branch], { cwd: repository });
370
+ await exec("gh", ["auth", "login", "--with-token"], { input: token, display: false });
371
+ await exec("curl", ["-H", `Authorization: Bearer ${token}`, url], { redact: [token] });
372
+ ```
373
+
374
+ Child output is captured by default and never written to the terminal by hacksh. Set `stdin`,
375
+ `stdout`, or `stderr` to `"inherit"` to hand that stream of the parent process to the child; an
376
+ inherited stream bypasses the printer and any `stdout`/`stderr` writers given to `runMain` or
377
+ `runCommand`, and its result field is `""`. `input` cannot be combined with `stdin: "inherit"`.
378
+ Set `interactive: true` when the child talks to the user through the terminal, including tools
379
+ that open `/dev/tty` themselves; hacksh cannot detect that, and it does not create a PTY.
380
+
381
+ ```ts
382
+ // Select a row: the table goes to stdin, the selection comes back on stdout.
383
+ const { stdout: picked } = await exec("fzf", [], { input: table, interactive: true });
384
+ // Hand the whole terminal to a long-running command.
385
+ await exec("gh", ["run", "watch", id], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
386
+ ```
387
+
388
+ These calls fail before starting the child, without an `exec` event, and follow `reject` like any
389
+ other failure: `json-output` when a stream is inherited in JSON mode, `non-interactive` when
390
+ `interactive: true` is used outside an interactive mode (the same rule as prompts), and
391
+ `overlapping` when a call that uses the terminal (interactive or inheriting a stream) starts while
392
+ a prompt or another such call is active. A prompt started during such a call fails with
393
+ `HackshPromptError` reason `overlapping`. Captured calls can run alongside prompts. Signals such
394
+ as Ctrl-C reach the whole foreground process group; hacksh does not forward them or manage jobs.
395
+
396
+ On Deno, `exec` needs `--allow-run` and unscoped `--allow-env`, because Deno's `node:child_process`
397
+ reads the whole environment to start a process.
398
+
399
+ To test a command without starting processes, call its `run` with a context whose `exec` is your own
400
+ function of type `Exec`.
401
+
314
402
  ## Output formats
315
403
 
316
404
  `runMain` and `runCommand` resolve one output mode before command routing, parsing, or execution.
@@ -338,7 +426,8 @@ capabilities. A stream without those readable operations fails with an `input-er
338
426
  is attempted.
339
427
 
340
428
  JSON output contains exactly one `PrintEvent` object per line. Help and version events and
341
- trace/debug/info logs go to stdout; errors and warning/error/fatal logs go to stderr. Usage attached
429
+ trace/debug/info logs go to stdout; errors, warning/error/fatal logs, and `exec` events go to
430
+ stderr. Usage attached
342
431
  to an error retains its `stderr` stream. Every log level is emitted by default. JSON strings have
343
432
  terminal control sequences removed, and the printer never serializes executable command or Zod
344
433
  schema objects.
package/dist/index.d.ts CHANGED
@@ -26,6 +26,12 @@ declare class HackshPromptError extends Error {
26
26
  readonly prompt: PromptErrorDetails;
27
27
  constructor(reason: HackshPromptErrorReason, prompt: PromptErrorDetails, message: string, options?: ErrorOptions);
28
28
  }
29
+ /** An external command that did not succeed; `result` is not enumerable so error summaries omit child output. */
30
+ declare class HackshExecError extends Error {
31
+ #private;
32
+ constructor(message: string, result: ExecFailure, options?: ErrorOptions);
33
+ get result(): ExecFailure;
34
+ }
29
35
  type HackshErrorKind = "error" | "prompt" | "usage" | "validation" | "thrown";
30
36
  interface ValidationIssueSummary {
31
37
  arg: string;
@@ -45,6 +51,60 @@ interface HackshErrorSummary {
45
51
  value?: unknown;
46
52
  }
47
53
  //#endregion
54
+ //#region src/prompts.d.ts
55
+ interface SelectOption<T extends string> {
56
+ value: T;
57
+ label?: string;
58
+ hint?: string;
59
+ }
60
+ interface SelectOptions<T extends string> {
61
+ message: string;
62
+ options: readonly SelectOption<T>[];
63
+ initialValue?: T;
64
+ }
65
+ interface ConfirmOptions {
66
+ message: string;
67
+ initialValue?: boolean;
68
+ }
69
+ interface TextOptions {
70
+ message: string;
71
+ placeholder?: string;
72
+ validate?(value: string): string | undefined;
73
+ }
74
+ interface Prompter {
75
+ select<const T extends string>(options: SelectOptions<T>): Promise<T>;
76
+ confirm(options: ConfirmOptions): Promise<boolean>;
77
+ text(options: TextOptions): Promise<string>;
78
+ }
79
+ //#endregion
80
+ //#region src/logger.d.ts
81
+ type LogLevel = "trace" | "debug" | "info" | "warning" | "error" | "fatal";
82
+ type LogKind = "message" | "step" | "success";
83
+ type LogProperties = Record<string, unknown>;
84
+ /** A structured message emitted by a command's logger. */
85
+ interface LogRecord {
86
+ level: LogLevel;
87
+ kind: LogKind;
88
+ category: readonly string[];
89
+ message: string;
90
+ rawMessage: string;
91
+ properties: LogProperties;
92
+ timestamp: number;
93
+ }
94
+ interface Logger extends Prompter {
95
+ readonly category: readonly string[];
96
+ trace(message: string, properties?: LogProperties): void;
97
+ debug(message: string, properties?: LogProperties): void;
98
+ info(message: string, properties?: LogProperties): void;
99
+ warn(message: string, properties?: LogProperties): void;
100
+ error(message: string | Error, properties?: LogProperties): void;
101
+ fatal(message: string | Error, properties?: LogProperties): void;
102
+ step(message: string, properties?: LogProperties): void;
103
+ success(message: string, properties?: LogProperties): void;
104
+ with(properties: LogProperties): Logger;
105
+ getChild(category: string): Logger;
106
+ }
107
+ //#endregion
48
108
  //#region src/output.d.ts
49
109
  interface OutputOptions {
50
110
  output?: OutputPreference;
@@ -175,8 +235,21 @@ interface PromptPrintEvent {
175
235
  type: "prompt";
176
236
  prompt: PromptState;
177
237
  }
238
+ interface ExecPrintEvent {
239
+ type: "exec";
240
+ phase: "start";
241
+ command: Omit<ExecCommand, "cwd"> & {
242
+ cwd?: string;
243
+ };
244
+ stdio: {
245
+ stdin: "ignore" | "input" | "inherit";
246
+ stdout: "pipe" | "inherit";
247
+ stderr: "pipe" | "inherit";
248
+ };
249
+ interactive: boolean;
250
+ }
178
251
  /** A semantic, command- and schema-free output value emitted by hacksh. */
179
- type PrintEvent = UsagePrintEvent | VersionPrintEvent | ErrorPrintEvent | LogPrintEvent | PromptPrintEvent;
252
+ type PrintEvent = UsagePrintEvent | VersionPrintEvent | ErrorPrintEvent | LogPrintEvent | PromptPrintEvent | ExecPrintEvent;
180
253
  /** Receives every output event produced by one command invocation. */
181
254
  interface Printer {
182
255
  print(event: PrintEvent): void | Promise<void>;
@@ -213,59 +286,63 @@ declare function createStreamPrinter(options: StreamPrinterOptions): Printer;
213
286
  /** Create a one-semantic-event-per-line JSON printer. */
214
287
  declare function createJsonPrinter(options?: JsonPrinterOptions): Printer;
215
288
  //#endregion
216
- //#region src/prompts.d.ts
217
- interface SelectOption<T extends string> {
218
- value: T;
219
- label?: string;
220
- hint?: string;
221
- }
222
- interface SelectOptions<T extends string> {
223
- message: string;
224
- options: readonly SelectOption<T>[];
225
- initialValue?: T;
226
- }
227
- interface ConfirmOptions {
228
- message: string;
229
- initialValue?: boolean;
230
- }
231
- interface TextOptions {
232
- message: string;
233
- placeholder?: string;
234
- validate?(value: string): string | undefined;
235
- }
236
- interface Prompter {
237
- select<const T extends string>(options: SelectOptions<T>): Promise<T>;
238
- confirm(options: ConfirmOptions): Promise<boolean>;
239
- text(options: TextOptions): Promise<string>;
240
- }
241
- //#endregion
242
- //#region src/logger.d.ts
243
- type LogLevel = "trace" | "debug" | "info" | "warning" | "error" | "fatal";
244
- type LogKind = "message" | "step" | "success";
245
- type LogProperties = Record<string, unknown>;
246
- /** A structured message emitted by a command's logger. */
247
- interface LogRecord {
248
- level: LogLevel;
249
- kind: LogKind;
250
- category: readonly string[];
251
- message: string;
252
- rawMessage: string;
253
- properties: LogProperties;
254
- timestamp: number;
255
- }
256
- interface Logger extends Prompter {
257
- readonly category: readonly string[];
258
- trace(message: string, properties?: LogProperties): void;
259
- debug(message: string, properties?: LogProperties): void;
260
- info(message: string, properties?: LogProperties): void;
261
- warn(message: string, properties?: LogProperties): void;
262
- error(message: string | Error, properties?: LogProperties): void;
263
- fatal(message: string | Error, properties?: LogProperties): void;
264
- step(message: string, properties?: LogProperties): void;
265
- success(message: string, properties?: LogProperties): void;
266
- with(properties: LogProperties): Logger;
267
- getChild(category: string): Logger;
268
- }
289
+ //#region src/exec.d.ts
290
+ type Environment = Readonly<Record<string, string | undefined>>;
291
+ interface ExecOptions {
292
+ cwd?: string;
293
+ env?: Environment;
294
+ input?: string;
295
+ reject?: boolean;
296
+ timeout?: number;
297
+ signal?: AbortSignal;
298
+ maxBuffer?: number;
299
+ forceKillAfterDelay?: number;
300
+ stdin?: "pipe" | "inherit";
301
+ stdout?: "pipe" | "inherit";
302
+ stderr?: "pipe" | "inherit";
303
+ interactive?: boolean;
304
+ display?: boolean;
305
+ redact?: readonly string[];
306
+ }
307
+ interface ExecCommand {
308
+ file: string;
309
+ args: readonly string[];
310
+ cwd: string | undefined;
311
+ }
312
+ interface ExecSuccess {
313
+ status: "succeeded";
314
+ command: ExecCommand;
315
+ stdout: string;
316
+ stderr: string;
317
+ exitCode: 0;
318
+ }
319
+ type ExecFailure = {
320
+ status: "failed";
321
+ command: ExecCommand;
322
+ stdout: string;
323
+ stderr: string;
324
+ } & ({
325
+ reason: "exit-code";
326
+ exitCode: number;
327
+ } | {
328
+ reason: "signal";
329
+ signal: string;
330
+ } | {
331
+ reason: "spawn";
332
+ error: Error;
333
+ } | {
334
+ reason: "timeout" | "aborted" | "output-limit";
335
+ } | {
336
+ reason: "json-output" | "non-interactive" | "overlapping";
337
+ });
338
+ type ExecFailureReason = ExecFailure["reason"];
339
+ type ExecResult = ExecSuccess | ExecFailure;
340
+ type ExecReturn<O extends ExecOptions> = "reject" extends keyof O ? false extends O["reject"] ? ExecResult : ExecSuccess : ExecSuccess;
341
+ type RejectInheritedInput<O> = O extends {
342
+ input: string;
343
+ stdin: "inherit";
344
+ } ? never : unknown;
345
+ type Exec = <const O extends ExecOptions = {}>(file: string, args?: readonly string[], options?: O & { [K in Exclude<keyof O, keyof ExecOptions>]: never; } & RejectInheritedInput<O>) => Promise<ExecReturn<O>>;
269
346
  //#endregion
270
347
  //#region src/schema.d.ts
271
348
  type ArgsSchema = $ZodObject;
@@ -289,6 +366,7 @@ interface CommandContext<S extends ArgsSchema> {
289
366
  passthrough: string[];
290
367
  extraArgs: string[];
291
368
  logger: Logger;
369
+ exec: Exec;
292
370
  }
293
371
  interface CommandDef<S extends ArgsSchema = EmptyArgs> {
294
372
  name?: string;
@@ -337,5 +415,5 @@ interface RunMainOptions extends OutputOptions {
337
415
  declare function runMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): Promise<void>;
338
416
  declare function createMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): (argv?: string[]) => Promise<void>;
339
417
  //#endregion
340
- export { type ArgOptions, type ArgOptionsMap, type CommandContext, type CommandDef, type ConfirmOptions, type ConfirmPromptState, type ErrorPrintEvent, type HackshErrorKind, type HackshErrorSummary, 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 };
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 };
341
419
  //# sourceMappingURL=index.d.ts.map