hacksh 0.11.0 → 0.13.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
@@ -1,7 +1,8 @@
1
1
  # hacksh
2
2
 
3
3
  Define a type-safe command-line interface with a Zod 4 object schema. The same schema parses and
4
- validates arguments, infers the value passed to `run`, and generates help.
4
+ validates arguments, infers the value passed to `run`, and generates help. Run external commands
5
+ with the context’s `exec`, and use its `logger` for progress and interactive prompts.
5
6
 
6
7
  The name comes from 拍手 (_hakushu_), the Japanese word for "clap", as a nod to
7
8
  [clap](https://github.com/clap-rs/clap), the command-line argument parser for Rust. It is spelled
@@ -30,21 +31,22 @@ import { z } from "zod";
30
31
  import { defineCommand, runMain } from "hacksh";
31
32
 
32
33
  const main = defineCommand({
33
- name: "gh-pr-commits",
34
+ name: "git-summary",
34
35
  version: "0.1.0",
35
- description: "List commits in a GitHub pull request",
36
+ description: "Show a commit summary",
36
37
  args: z.object({
37
- _: z.tuple([
38
- z.coerce.number().int().optional().describe("Pull request number").meta({ title: "PR" }),
39
- ]),
40
- repo: z.string().optional().describe("Repository in OWNER/REPO format"),
41
- verbose: z.boolean().default(false).describe("Show details"),
38
+ _: z.tuple([z.string().default("HEAD").describe("Commit to inspect").meta({ title: "REF" })]),
39
+ repo: z.string().optional().describe("Repository directory"),
42
40
  }),
43
- argOptions: { repo: { alias: "R" }, verbose: { alias: ["v", "V"] } },
44
- async run({ args, rawArgs, extraArgs, passthrough, logger }) {
45
- // args is { _: [number?]; repo?: string; verbose: boolean }
46
- logger.info("Loading commits for {repo}", { repo: args.repo ?? "the current repository" });
47
- logger.debug("Invocation arguments", { rawArgs, extraArgs, passthrough });
41
+ argOptions: { repo: { alias: "C" } },
42
+ async run({ args, exec }) {
43
+ const [ref] = args._;
44
+ const { stdout } = await exec(
45
+ "git",
46
+ ["show", "--no-patch", "--format=%h %s", "--end-of-options", ref],
47
+ { cwd: args.repo },
48
+ );
49
+ console.log(stdout);
48
50
  },
49
51
  });
50
52
 
@@ -52,11 +54,22 @@ await runMain(main);
52
54
  ```
53
55
 
54
56
  ```sh
55
- gh-pr-commits 42 -R owner/repo --verbose
56
- gh-pr-commits --help
57
- gh-pr-commits --version
57
+ git-summary
58
+ git-summary HEAD~1 -C ./my-repo
59
+ git-summary --help
60
+ git-summary --version
58
61
  ```
59
62
 
63
+ Use an `async run` handler and its `exec` for external commands. It captures output, removes one
64
+ final newline, throws on failure, and observes invocation cancellation. Pass arguments as an array;
65
+ there is no command string to quote or split. You do not need a `node:child_process` import or a
66
+ custom process wrapper for these behaviors. See [External commands](#external-commands) for expected
67
+ nonzero exits, stdin, and terminal access.
68
+
69
+ `console.log` above writes the command’s data to stdout. Direct stdout writes are not converted
70
+ to JSON by hacksh’s output mode. Logger events go through the printer; their stream depends on
71
+ the level, so informational logs also share stdout by default.
72
+
60
73
  `--version` is a root-only builtin. It prints the root command's `version`. When the root has
61
74
  none, `runMain` resolves the executable (`process.argv[1]`) through symlinks and prints the
62
75
  `version` of the nearest ancestor `package.json` that declares one, so a published CLI reports its
@@ -67,8 +80,8 @@ are accepted (`--dryRun` and `--dry-run`). Every boolean flag also accepts a `--
67
80
  that sets it to `false`, such as `--no-verbose`, which is how a default-on flag is disabled.
68
81
  Boolean flags also accept the explicit forms `--verbose=true` and
69
82
  `--verbose=false`; other inline values and values on a negated flag (such as
70
- `--no-verbose=false`) are validation errors. Value options accept `--repo owner/repo` and
71
- `--repo=owner/repo`; values such as `false` remain strings for non-boolean fields.
83
+ `--no-verbose=false`) are validation errors. Value options accept `--repo ./my-repo` and
84
+ `--repo=./my-repo`; values such as `false` remain strings for non-boolean fields.
72
85
 
73
86
  ## Positional arguments
74
87
 
@@ -98,8 +111,58 @@ copy out a.txt b.txt # { _: ["out", "a.txt", "b.txt"] }
98
111
  Help and validation errors label each item with its `.meta({ title })`, such as `<DESTINATION>` and
99
112
  `[FILES...]`. Untitled items are labeled by position (`ARG1`, `ARG2`, ...) and an untitled rest is
100
113
  labeled `ARGS`. Use `z.array` instead of a tuple to accept only variadic values. Without a rest,
101
- additional positional arguments are kept in `extraArgs`. Unknown options are also kept in
102
- `extraArgs`; arguments after `--` are kept in `passthrough`.
114
+ additional positional arguments are rejected by default, as are unknown options.
115
+
116
+ ### Unknown arguments and forwarding
117
+
118
+ A command rejects unknown options and excess positional arguments before its `run` handler or
119
+ schema transforms execute. For ordinary commands, declare the accepted arguments in `args`;
120
+ there is no need to inspect `extraArgs` manually. Errors are `HackshValidationError` instances,
121
+ with each rejected token in `issues[].arg`.
122
+
123
+ Everything after `--` is always collected unchanged in `passthrough`, independently of this policy.
124
+ Use that explicit boundary when forwarding arguments to another executable:
125
+
126
+ ```ts
127
+ const forward = defineCommand({
128
+ name: "git-forward",
129
+ async run({ exec, passthrough }) {
130
+ const { stdout } = await exec("git", passthrough);
131
+ console.log(stdout);
132
+ },
133
+ });
134
+ // git-forward -- status --short
135
+ ```
136
+
137
+ For a wrapper that also accepts undeclared arguments before `--`, set `allowUnknownArgs: true`:
138
+
139
+ ```ts
140
+ const wrapper = defineCommand({
141
+ allowUnknownArgs: true,
142
+ async run({ extraArgs, passthrough, exec }) {
143
+ await exec("some-tool", [...extraArgs, ...passthrough]);
144
+ },
145
+ });
146
+ ```
147
+
148
+ `extraArgs` preserves the order of unknown option tokens and excess positional values. Unknown
149
+ options do not consume a following value: if the schema has an unfilled positional slot, that
150
+ value fills it. Known options are still parsed and validated. Use `--` when a child's arguments
151
+ must not be interpreted by this command. A handler that does not use `passthrough` ignores those
152
+ arguments; commands that require rejecting them can check `passthrough.length` explicitly.
153
+
154
+ `allowUnknownArgs` defaults to `false`, inherits down the selected subcommand route, and can be
155
+ overridden by each child. A child's setting does not change validation of its parent's arguments.
156
+ Definitions are not mutated, so reusing a child under another root does not retain inherited settings.
157
+ Standalone `parseArgs` uses the supplied command's setting and does not resolve a parent route.
158
+
159
+ Once routing succeeds, help and root version output take precedence over argument validation,
160
+ including unknown arguments. Routing errors such as an unknown subcommand still fail. A child's
161
+ undeclared `--version` is an unknown argument, since the version builtin belongs to the root.
162
+
163
+ **Migrating from 0.11.0:** unknown input was previously accepted by default. Add
164
+ `allowUnknownArgs: true` to commands that intentionally use `extraArgs`, or to their shared parent.
165
+ No migration is needed for `--` forwarding.
103
166
 
104
167
  ## Argument options
105
168
 
@@ -325,21 +388,27 @@ positive `columns` value, the renderer conservatively counts explicit lines only
325
388
  Every `run` context has an `exec` function that starts an executable with an argument array. It
326
389
  never uses a shell, so spaces, quotes, `$`, `;`, `|`, and `*` in arguments reach the child
327
390
  unchanged. By default it resolves with the captured output and rejects with `HackshExecError`
328
- when the command fails. Pass `reject: false` to receive failures as results instead.
391
+ when the command fails. Pass `reject: false` to receive failures as results instead. Prefer this
392
+ context API to `spawnSync` or a custom subprocess wrapper inside a command handler so execution
393
+ uses the invocation’s environment, cancellation, and printer.
329
394
 
330
395
  ```ts
331
- const sync = defineCommand({
332
- name: "sync",
396
+ const check = defineCommand({
397
+ name: "check-head",
333
398
  async run({ exec, logger }) {
334
- const { stdout: branch } = await exec("git", ["branch", "--show-current"]);
335
- const upstream = await exec("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], {
399
+ const head = await exec("git", ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], {
336
400
  reject: false,
337
401
  });
338
- if (upstream.status === "failed") {
339
- logger.warn("{branch} has no upstream", { branch });
340
- return;
402
+ if (head.status === "failed") {
403
+ // Only this specific exit means that the revision was not found.
404
+ if (head.reason === "exit-code" && head.exitCode === 1) {
405
+ logger.warn("The repository has no HEAD commit");
406
+ return;
407
+ }
408
+ // Missing executables, cancellation, and other failures must still fail the command.
409
+ throw new Error("Could not inspect HEAD", { cause: head });
341
410
  }
342
- await exec("git", ["pull", "--ff-only"], { cwd: "/path/to/repo", timeout: 60_000 });
411
+ logger.info("HEAD is {commit}", { commit: head.stdout });
343
412
  },
344
413
  });
345
414
  ```
@@ -352,10 +421,24 @@ UTF-8 `stdout` and `stderr` with one final newline removed. A failure's `reason`
352
421
  gives `ExecSuccess`, while `false` or a `boolean` variable gives `ExecResult`, which must be
353
422
  narrowed before reading `exitCode`.
354
423
 
424
+ ### Working with captured output
425
+
426
+ Use `stdout` directly for a single value such as a commit ID or repository path. Do not call
427
+ `.trim()` on paths: surrounding whitespace can be part of the name. `exec` already removes one
428
+ final LF or CRLF; it preserves other whitespace and NUL separators.
429
+
430
+ Interpret structured output according to the child command’s format. If it offers JSON, request
431
+ that format and parse it with `JSON.parse` (and a Zod schema when validation is needed). For Git’s
432
+ `--porcelain -z` output, keep the NUL delimiters and parse the records in a named, application-specific
433
+ helper. Splitting that format on newlines would break paths containing newlines. hacksh does not
434
+ parse Git records or infer whether stdout is text, JSON, or a list.
435
+
436
+ ### Execution options
437
+
355
438
  Options are `cwd`, `env`, `input`, `reject`, `timeout` (milliseconds), `signal` (an
356
- `AbortSignal`), `maxBuffer` (bytes per stream, default 100,000,000), and `forceKillAfterDelay`
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.
439
+ `AbortSignal`), `maxBuffer` (bytes per stream, default 100,000,000), `forceKillAfterDelay`
440
+ (milliseconds, default 5,000), and `dryRun`. Every call also stops with `aborted` when the
441
+ invocation's `signal` aborts. `input` is written to stdin and closed; otherwise stdin is empty.
359
442
  Output beyond `maxBuffer` is truncated to that many bytes. On timeout, abort, or output limit,
360
443
  hacksh sends `SIGTERM` to the child it started and `SIGKILL` after `forceKillAfterDelay`; it does
361
444
  not stop grandchildren, and it does not wait for them to close inherited pipes. Invalid arguments
@@ -375,6 +458,18 @@ JSON event has `phase: "start"`, `command` (`file`, `args`, and `cwd` when set),
375
458
  in the event, the result's `command`, the `HackshExecError` message, and a `spawn` failure's
376
459
  `error`. The child still receives the real values, and its captured output is returned unchanged.
377
460
 
461
+ `dryRun: true` does not start the child and does not write `input`. The result is
462
+ `{ status: "skipped", command }` with the redacted command and no `stdout`, `stderr`, or `exitCode`.
463
+ The `exec` event uses `phase: "skip"` and the same command, stdio, and `interactive` flag.
464
+ Pretty output marks that line with `#` instead of `$`. Omit `dryRun`, or pass `false`, to keep
465
+ today's success or failure result. A boolean `dryRun` returns the union of a skip and the executed
466
+ result, so narrow `status` before reading output. `reject` does not apply to a skip. An invocation
467
+ signal that is already aborted still fails as `aborted` before any skip event, and `reject` applies
468
+ to that failure. Option checks still run, including `input` together with `stdin: "inherit"`.
469
+ JSON mode, a non-interactive environment, and a busy terminal still show the plan and return
470
+ a skip. `display: false` hides the skip event. `dryRun` is not read from CLI flags or
471
+ `context.args`.
472
+
378
473
  ```ts
379
474
  await exec("git", ["worktree", "add", path, branch], { cwd: repository });
380
475
  await exec("gh", ["auth", "login", "--with-token"], { input: token, display: false });
package/dist/index.d.ts CHANGED
@@ -242,7 +242,7 @@ interface PromptPrintEvent {
242
242
  }
243
243
  interface ExecPrintEvent {
244
244
  type: "exec";
245
- phase: "start";
245
+ phase: "start" | "skip";
246
246
  command: Omit<ExecCommand, "cwd"> & {
247
247
  cwd?: string;
248
248
  };
@@ -308,6 +308,7 @@ interface ExecOptions {
308
308
  interactive?: boolean;
309
309
  display?: boolean;
310
310
  redact?: readonly string[];
311
+ dryRun?: boolean;
311
312
  }
312
313
  interface ExecCommand {
313
314
  file: string;
@@ -342,7 +343,22 @@ type ExecFailure = {
342
343
  });
343
344
  type ExecFailureReason = ExecFailure["reason"];
344
345
  type ExecResult = ExecSuccess | ExecFailure;
345
- type ExecReturn<O extends ExecOptions> = "reject" extends keyof O ? false extends O["reject"] ? ExecResult : ExecSuccess : ExecSuccess;
346
+ interface ExecSkipped {
347
+ status: "skipped";
348
+ command: ExecCommand;
349
+ }
350
+ type ExecOutcome<O extends ExecOptions> = "reject" extends keyof O ? false extends O["reject"] ? ExecResult : ExecSuccess : ExecSuccess;
351
+ type SpecifiedDryRun<O> = "dryRun" extends keyof O ? O["dryRun"] : false;
352
+ type ReturnsFailure<O extends ExecOptions> = "reject" extends keyof O ? false extends O["reject"] ? true : false : false;
353
+ type AbortedFailure = {
354
+ status: "failed";
355
+ command: ExecCommand;
356
+ stdout: string;
357
+ stderr: string;
358
+ reason: "aborted";
359
+ };
360
+ type DefiniteSkip<O extends ExecOptions> = ReturnsFailure<O> extends true ? ExecSkipped | AbortedFailure : ExecSkipped;
361
+ type ExecReturn<O extends ExecOptions> = true extends SpecifiedDryRun<O> ? false extends SpecifiedDryRun<O> ? ExecSkipped | ExecOutcome<O> : undefined extends SpecifiedDryRun<O> ? ExecSkipped | ExecOutcome<O> : DefiniteSkip<O> : ExecOutcome<O>;
346
362
  type RejectInheritedInput<O> = O extends {
347
363
  input: string;
348
364
  stdin: "inherit";
@@ -379,6 +395,8 @@ interface CommandDef<S extends ArgsSchema = EmptyArgs> {
379
395
  version?: string;
380
396
  description?: string;
381
397
  hidden?: boolean;
398
+ /** Keep unknown options and excess positionals in extraArgs. Inherits from the parent; defaults to false. */
399
+ allowUnknownArgs?: boolean;
382
400
  args?: S;
383
401
  argOptions?: ArgOptionsMap<S>;
384
402
  subCommands?: Record<string, CommandDef<ArgsSchema>>;
@@ -422,5 +440,5 @@ interface RunMainOptions extends OutputOptions {
422
440
  declare function runMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): Promise<void>;
423
441
  declare function createMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): (argv?: string[]) => Promise<void>;
424
442
  //#endregion
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 };
443
+ 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 ExecSkipped, 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 };
426
444
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -108,6 +108,7 @@ function schemaShape(schema) {
108
108
  //#endregion
109
109
  //#region src/command.ts
110
110
  function defineCommand(def) {
111
+ if (def.allowUnknownArgs !== void 0 && typeof def.allowUnknownArgs !== "boolean") throw new TypeError("allowUnknownArgs must be a boolean");
111
112
  const shape = def.args ? schemaShape(def.args) : {};
112
113
  const positionals = readPositionals(shape);
113
114
  if (positionals) {
@@ -611,6 +612,10 @@ async function parseCommandArgs(command, rawArgs, tokenized = tokenizeCommandArg
611
612
  else if (token.kind === "unknown-option") extraArgs.push(token.value);
612
613
  else if (positionals && (positionals.rest || positionalValues.length < positionals.items.length)) positionalValues.push(token.value);
613
614
  else extraArgs.push(token.value);
615
+ if (!command.allowUnknownArgs && extraArgs.length > 0) throw new HackshValidationError(command, extraArgs.map((arg) => ({
616
+ arg,
617
+ message: "unexpected argument"
618
+ })));
614
619
  const envSources = /* @__PURE__ */ new Map();
615
620
  if (env && command.args) {
616
621
  const shape = schemaShape(command.args);
@@ -689,7 +694,8 @@ const OPTION_KEYS = /* @__PURE__ */ new Set([
689
694
  "stderr",
690
695
  "interactive",
691
696
  "display",
692
- "redact"
697
+ "redact",
698
+ "dryRun"
693
699
  ]);
694
700
  const MAX_DELAY = 2147483647;
695
701
  const REFUSAL_MESSAGE = {
@@ -809,7 +815,8 @@ function parsePlan(file, args, options, context) {
809
815
  stderr,
810
816
  interactive: raw.interactive === void 0 ? false : readBoolean(raw.interactive, "interactive"),
811
817
  display: raw.display === void 0 ? true : readBoolean(raw.display, "display"),
812
- redact
818
+ redact,
819
+ dryRun: raw.dryRun === void 0 ? false : readBoolean(raw.dryRun, "dryRun")
813
820
  };
814
821
  }
815
822
  function stripFinalNewline(text) {
@@ -877,6 +884,23 @@ function toResult(command, outcome, stdout, stderr) {
877
884
  function isSpawnErrno(error) {
878
885
  return error instanceof Error && "syscall" in error;
879
886
  }
887
+ function execEvent(plan, phase) {
888
+ const { file, args, cwd } = plan.reported;
889
+ return {
890
+ type: "exec",
891
+ phase,
892
+ command: cwd === void 0 ? {
893
+ file,
894
+ args
895
+ } : {
896
+ file,
897
+ args,
898
+ cwd
899
+ },
900
+ stdio: commandStdio(plan),
901
+ interactive: plan.interactive
902
+ };
903
+ }
880
904
  function commandStdio(plan) {
881
905
  return {
882
906
  stdin: plan.stdin === "inherit" ? "inherit" : plan.input === void 0 ? "ignore" : "input",
@@ -1015,29 +1039,20 @@ function blockedReason(plan, context, stdio) {
1015
1039
  async function execute(file, args, options, context) {
1016
1040
  const plan = parsePlan(file, args, options, context);
1017
1041
  if (plan.signal.aborted) return complete(plan, emptyFailure(plan, "aborted"));
1042
+ if (plan.dryRun) {
1043
+ if (plan.display) await context.print(execEvent(plan, "skip"));
1044
+ return {
1045
+ status: "skipped",
1046
+ command: plan.reported
1047
+ };
1048
+ }
1018
1049
  const stdio = commandStdio(plan);
1019
1050
  const blocked = blockedReason(plan, context, stdio);
1020
1051
  if (blocked) return complete(plan, emptyFailure(plan, blocked));
1021
1052
  const holdTerminal = usesTerminal(plan, stdio);
1022
1053
  if (holdTerminal && !context.terminal.acquire("exec")) return complete(plan, emptyFailure(plan, "overlapping"));
1023
1054
  try {
1024
- if (plan.display) {
1025
- const { file, args, cwd } = plan.reported;
1026
- await context.print({
1027
- type: "exec",
1028
- phase: "start",
1029
- command: cwd === void 0 ? {
1030
- file,
1031
- args
1032
- } : {
1033
- file,
1034
- args,
1035
- cwd
1036
- },
1037
- stdio,
1038
- interactive: plan.interactive
1039
- });
1040
- }
1055
+ if (plan.display) await context.print(execEvent(plan, "start"));
1041
1056
  return complete(plan, await runPlan(plan));
1042
1057
  } finally {
1043
1058
  if (holdTerminal) context.terminal.release();
@@ -1187,7 +1202,7 @@ function formatTextEvent(event) {
1187
1202
  case "error": return event.error.kind === "usage" || event.error.kind === "validation" ? event.error.message : `Error: ${event.error.message}`;
1188
1203
  case "log": return event.record.message;
1189
1204
  case "prompt": return event.prompt.message;
1190
- case "exec": return `$ ${formatExecCommand(event.command)}`;
1205
+ case "exec": return `${event.phase === "skip" ? "#" : "$"} ${formatExecCommand(event.command)}`;
1191
1206
  }
1192
1207
  }
1193
1208
  /** Undecorated console compatibility adapter; automatic output uses the selected format printer. */
@@ -1278,7 +1293,8 @@ const unicodeSymbols = {
1278
1293
  version: "●",
1279
1294
  errorEvent: "■",
1280
1295
  prompt: "?",
1281
- exec: "$"
1296
+ exec: "$",
1297
+ execSkip: "#"
1282
1298
  }
1283
1299
  };
1284
1300
  const asciiSymbols = {
@@ -1296,7 +1312,8 @@ const asciiSymbols = {
1296
1312
  version: "i",
1297
1313
  errorEvent: "x",
1298
1314
  prompt: "?",
1299
- exec: "$"
1315
+ exec: "$",
1316
+ execSkip: "#"
1300
1317
  }
1301
1318
  };
1302
1319
  const markStyles = {
@@ -1316,7 +1333,8 @@ const markStyles = {
1316
1333
  version: "blue",
1317
1334
  errorEvent: "red",
1318
1335
  prompt: "cyan",
1319
- exec: "gray"
1336
+ exec: "gray",
1337
+ execSkip: "gray"
1320
1338
  };
1321
1339
  function safelyInspect(value) {
1322
1340
  if (typeof value === "string") return value;
@@ -1363,7 +1381,7 @@ function eventMark(event) {
1363
1381
  if (event.type === "version") return "version";
1364
1382
  if (event.type === "error") return "errorEvent";
1365
1383
  if (event.type === "prompt") return event.prompt.status === "submitted" ? "success" : "prompt";
1366
- if (event.type === "exec") return "exec";
1384
+ if (event.type === "exec") return event.phase === "skip" ? "execSkip" : "exec";
1367
1385
  return event.record.kind === "message" ? event.record.level : event.record.kind;
1368
1386
  }
1369
1387
  function eventLines(event) {
@@ -2067,7 +2085,8 @@ function resolveCommand(root, rawArgs) {
2067
2085
  const parentPath = commandEnvPath(command);
2068
2086
  command = {
2069
2087
  ...child,
2070
- name: child.name ?? key
2088
+ name: child.name ?? key,
2089
+ allowUnknownArgs: child.allowUnknownArgs ?? command.allowUnknownArgs
2071
2090
  };
2072
2091
  registerCommandPath(command, parentPath === void 0 ? void 0 : [...parentPath, key]);
2073
2092
  remaining = remaining.slice(index + 1);