hacksh 0.14.0 → 0.16.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 +373 -286
- package/dist/{command-CfWrbHLN.d.ts → command-B-Ok_6KW.d.ts} +115 -77
- package/dist/index.d.ts +3 -3
- package/dist/index.js +27 -3
- package/dist/index.js.map +1 -1
- package/dist/{pretty-BFub611x.js → pretty-DJ2CE6TU.js} +25 -9
- package/dist/pretty-DJ2CE6TU.js.map +1 -0
- package/dist/printer-api.d.ts +4 -3
- package/dist/printer-api.js +1 -1
- package/package.json +1 -1
- package/dist/pretty-BFub611x.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Define a type-safe command-line interface with a Zod 4 object schema. The same schema parses and
|
|
4
4
|
validates arguments, infers the value passed to `run`, and generates help. Run external commands
|
|
5
|
-
with the context’s `exec`, use its `logger` for progress, and request
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
with the context’s `exec`, write results with `output`, use its `logger` for progress, and request
|
|
6
|
+
interactive input with `prompt`.
|
|
7
|
+
|
|
8
|
+
- [Install](#install)
|
|
9
|
+
- [Quick start](#quick-start)
|
|
10
|
+
- [Arguments](#arguments)
|
|
11
|
+
- [Options and flags](#options-and-flags)
|
|
12
|
+
- [Positional arguments](#positional-arguments)
|
|
13
|
+
- [Argument metadata](#argument-metadata)
|
|
14
|
+
- [Environment variables](#environment-variables)
|
|
15
|
+
- [Unknown arguments and forwarding](#unknown-arguments-and-forwarding)
|
|
16
|
+
- [Subcommands](#subcommands)
|
|
17
|
+
- [The run context](#the-run-context)
|
|
18
|
+
- [External commands](#external-commands)
|
|
19
|
+
- [Command output](#command-output)
|
|
20
|
+
- [Structured logging](#structured-logging)
|
|
21
|
+
- [Interactive input](#interactive-input)
|
|
22
|
+
- [Cancellation and exit status](#cancellation-and-exit-status)
|
|
23
|
+
- [Output formats](#output-formats)
|
|
24
|
+
- [Programmatic API and errors](#programmatic-api-and-errors)
|
|
25
|
+
- [Entry points](#entry-points)
|
|
26
|
+
- [Printers and events](#printers-and-events)
|
|
27
|
+
- [Standalone parsing and help](#standalone-parsing-and-help)
|
|
28
|
+
- [Errors](#errors)
|
|
29
|
+
- [Development](#development)
|
|
30
|
+
- [Name and acknowledgments](#name-and-acknowledgments)
|
|
16
31
|
|
|
17
32
|
## Install
|
|
18
33
|
|
|
@@ -24,7 +39,7 @@ hacksh is an ESM package and supports Node.js 22.18 or newer. Zod is a peer depe
|
|
|
24
39
|
application controls the Zod 4 version it uses. The public schema types and schema inspection use
|
|
25
40
|
the shared `zod/v4/core` API and accept schemas created with either `zod` or `zod/mini`.
|
|
26
41
|
|
|
27
|
-
##
|
|
42
|
+
## Quick start
|
|
28
43
|
|
|
29
44
|
```ts
|
|
30
45
|
import { z } from "zod";
|
|
@@ -40,14 +55,14 @@ const main = defineCommand({
|
|
|
40
55
|
]),
|
|
41
56
|
repo: z.string().optional().meta({ description: "Repository directory", alias: "C" }),
|
|
42
57
|
}),
|
|
43
|
-
async run({ args, exec }) {
|
|
58
|
+
async run({ args, exec, output }) {
|
|
44
59
|
const [ref] = args.positionals;
|
|
45
60
|
const { stdout } = await exec(
|
|
46
61
|
"git",
|
|
47
62
|
["show", "--no-patch", "--format=%h %s", "--end-of-options", ref],
|
|
48
63
|
{ cwd: args.repo },
|
|
49
64
|
);
|
|
50
|
-
|
|
65
|
+
output.println(stdout);
|
|
51
66
|
},
|
|
52
67
|
});
|
|
53
68
|
|
|
@@ -61,20 +76,15 @@ git-summary --help
|
|
|
61
76
|
git-summary --version
|
|
62
77
|
```
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
final newline, throws on failure, and observes invocation
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
`args` is inferred from the Zod schema, and `exec` runs external commands with an argument array,
|
|
80
|
+
captures output with one final newline removed, throws on failure, and observes invocation
|
|
81
|
+
cancellation. See [Arguments](#arguments) for option and positional rules,
|
|
82
|
+
[The run context](#the-run-context) for `exec`, `output`, `logger`, `prompt`, and `signal`, and
|
|
83
|
+
[Output formats](#output-formats) for how results and logs are split between stdout and stderr.
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
to JSON by hacksh’s output mode. Logger events go through the printer; their stream depends on
|
|
72
|
-
the level, so informational logs also share stdout by default.
|
|
85
|
+
## Arguments
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
none, `runMain` resolves the executable (`process.argv[1]`) through symlinks and prints the
|
|
76
|
-
`version` of the nearest ancestor `package.json` that declares one, so a published CLI reports its
|
|
77
|
-
package version without repeating it in code. `runCommand` does not search the filesystem.
|
|
87
|
+
### Options and flags
|
|
78
88
|
|
|
79
89
|
Boolean fields are flags and other fields take a value. Both a schema key and its kebab-case form
|
|
80
90
|
are accepted (`--dryRun` and `--dry-run`). Every boolean flag also accepts a `--no-` prefixed form
|
|
@@ -84,7 +94,25 @@ Boolean flags also accept the explicit forms `--verbose=true` and
|
|
|
84
94
|
`--no-verbose=false`) are validation errors. Value options accept `--repo ./my-repo` and
|
|
85
95
|
`--repo=./my-repo`; values such as `false` remain strings for non-boolean fields.
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
Boolean flags are shown in help as `--[no-]name` because they also accept the `--no-` form. The
|
|
98
|
+
`negatable` field of `UsageOptionSummary` carries the same information for machine-readable
|
|
99
|
+
help, where `flags` keeps the literal `--name` spelling.
|
|
100
|
+
|
|
101
|
+
`--help` and `--version` are built in:
|
|
102
|
+
|
|
103
|
+
- `--version` is a root-only builtin. It prints the root command's `version`. When the root has
|
|
104
|
+
none, `runMain` resolves the executable (`process.argv[1]`) through symlinks and prints the
|
|
105
|
+
`version` of the nearest ancestor `package.json` that declares one, so a published CLI reports its
|
|
106
|
+
package version without repeating it in code. `runCommand` does not search the filesystem.
|
|
107
|
+
- Without an alias of the same letter, `-h` and `-v` are shorthands for `--help` and `--version`.
|
|
108
|
+
Defining an `h` or `v` alias on an option replaces the shorthand while keeping `--help` and
|
|
109
|
+
`--version` available.
|
|
110
|
+
- Both `help` and `version` option names are reserved and throw a `TypeError` from `defineCommand`
|
|
111
|
+
on every command.
|
|
112
|
+
- Output selection does not reserve any additional command-line flags, so names such as `--json`,
|
|
113
|
+
`--output`, and `--log-format` remain available to application schemas.
|
|
114
|
+
|
|
115
|
+
### Positional arguments
|
|
88
116
|
|
|
89
117
|
Declare positional arguments as a `z.tuple` in the `positionals` field. Tuple items are assigned in order,
|
|
90
118
|
and the tuple rest collects all remaining values. Optional or defaulted items must come after
|
|
@@ -114,61 +142,11 @@ Help and validation errors label each item with its `.meta({ title })`, such as
|
|
|
114
142
|
labeled `ARGS`. Use `z.array` instead of a tuple to accept only variadic values. Without a rest,
|
|
115
143
|
additional positional arguments are rejected by default, as are unknown options.
|
|
116
144
|
|
|
117
|
-
###
|
|
118
|
-
|
|
119
|
-
A command rejects unknown options and excess positional arguments before its `run` handler or
|
|
120
|
-
schema transforms execute. For ordinary commands, declare the accepted arguments in `args`;
|
|
121
|
-
there is no need to inspect `input.unknown` manually. Errors are `HackshValidationError` instances,
|
|
122
|
-
with each rejected token in `issues[].arg`.
|
|
123
|
-
|
|
124
|
-
Everything after `--` is always collected unchanged in `input.passthrough`, independently of this policy.
|
|
125
|
-
Use that explicit boundary when forwarding arguments to another executable:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
const forward = defineCommand({
|
|
129
|
-
name: "git-forward",
|
|
130
|
-
async run({ exec, input }) {
|
|
131
|
-
const { stdout } = await exec("git", input.passthrough);
|
|
132
|
-
console.log(stdout);
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
// git-forward -- status --short
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
For a wrapper that also accepts undeclared arguments before `--`, set `allowUnknownArgs: true`:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
const wrapper = defineCommand({
|
|
142
|
-
allowUnknownArgs: true,
|
|
143
|
-
async run({ input, exec }) {
|
|
144
|
-
await exec("some-tool", [...input.unknown, ...input.passthrough]);
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
`input.unknown` preserves the order of unknown option tokens and excess positional values. Unknown
|
|
150
|
-
options do not consume a following value: if the schema has an unfilled positional slot, that
|
|
151
|
-
value fills it. Known options are still parsed and validated. Use `--` when a child's arguments
|
|
152
|
-
must not be interpreted by this command. A handler that does not use `input.passthrough` ignores those
|
|
153
|
-
arguments; commands that require rejecting them can check `input.passthrough.length` explicitly.
|
|
154
|
-
|
|
155
|
-
`allowUnknownArgs` defaults to `false`, inherits down the selected subcommand route, and can be
|
|
156
|
-
overridden by each child. A child's setting does not change validation of its parent's arguments.
|
|
157
|
-
Definitions are not mutated, so reusing a child under another root does not retain inherited settings.
|
|
158
|
-
Standalone `parseArgs` uses the supplied command's setting and does not resolve a parent route.
|
|
159
|
-
|
|
160
|
-
Once routing succeeds, help and root version output take precedence over argument validation,
|
|
161
|
-
including unknown arguments. Routing errors such as an unknown subcommand still fail. A child's
|
|
162
|
-
undeclared `--version` is an unknown argument, since the version builtin belongs to the root.
|
|
163
|
-
|
|
164
|
-
**Migrating from 0.11.0:** unknown input was previously accepted by default. Add
|
|
165
|
-
`allowUnknownArgs: true` to commands that intentionally use `input.unknown`, or to their shared parent.
|
|
166
|
-
No migration is needed for `--` forwarding.
|
|
167
|
-
|
|
168
|
-
## Argument metadata
|
|
145
|
+
### Argument metadata
|
|
169
146
|
|
|
170
147
|
Use Zod 4 `.meta({ alias, valueName, env, title, description })` on each argument schema.
|
|
171
|
-
|
|
148
|
+
Importing hacksh types `alias`, `env`, and `valueName` on Zod's global metadata; any other key stays
|
|
149
|
+
allowed. Metadata does not change schema keys, parsed output, or validation error paths. `z.enum` choices
|
|
172
150
|
and descriptions appear in generated help, including inside supported schema wrappers and tuple items.
|
|
173
151
|
|
|
174
152
|
```ts
|
|
@@ -212,7 +190,8 @@ OPTIONS
|
|
|
212
190
|
-v, -V, --[no-]verbose Show details (Default: false)
|
|
213
191
|
```
|
|
214
192
|
|
|
215
|
-
- `alias` gives a named option one or more single-character short forms.
|
|
193
|
+
- `alias` gives a named option one or more single-character short forms. Defining `v` above replaces
|
|
194
|
+
the default `-v` version shorthand, so `convert -v` sets `verbose`.
|
|
216
195
|
- `valueName` sets the value label of a named option.
|
|
217
196
|
- `env` overrides an option's automatic environment variable name, or disables it with `false`.
|
|
218
197
|
- `title` labels a positional argument; `description` supplies its help text.
|
|
@@ -221,19 +200,9 @@ Place metadata on the outer schema to override metadata inherited through wrappe
|
|
|
221
200
|
`.describe(text)` remains equivalent to description metadata; new code should use `.meta({ description: text })`.
|
|
222
201
|
With `zod/mini`, use `.check(mini.meta({ ... }))`.
|
|
223
202
|
|
|
224
|
-
Boolean flags are shown as `--[no-]name` because they also accept the `--no-` form. The
|
|
225
|
-
`negatable` field of `UsageOptionSummary` carries the same information for machine-readable
|
|
226
|
-
help, where `flags` keeps the literal `--name` spelling.
|
|
227
|
-
|
|
228
203
|
`defineCommand` validates metadata types and throws a `TypeError` for empty or multi-character
|
|
229
204
|
aliases, duplicate aliases, and option-only metadata (`alias`, `env`, `valueName`) on positional
|
|
230
|
-
schemas.
|
|
231
|
-
for `--help` and `--version`. Defining an `h` or `v` alias replaces the shorthand, so `convert -v`
|
|
232
|
-
above sets `verbose`; `--help` and `--version` remain available. Both `help` and `version` option
|
|
233
|
-
names are reserved and throw a `TypeError` from `defineCommand` on every command.
|
|
234
|
-
Output selection does not reserve
|
|
235
|
-
any additional command-line flags, so names such as `--json`, `--output`, and `--log-format` remain
|
|
236
|
-
available to application schemas.
|
|
205
|
+
schemas.
|
|
237
206
|
|
|
238
207
|
### Environment variables
|
|
239
208
|
|
|
@@ -271,6 +240,53 @@ MY_CLI_DEPLOY_TARGET=prod MY_CLI_DEPLOY_DRY_RUN=true GITHUB_TOKEN=t my-cli deplo
|
|
|
271
240
|
- Two options that resolve to the same name on one route, such as a root `deployTarget` and a
|
|
272
241
|
`deploy` subcommand's `target`, make the invocation throw a `TypeError`.
|
|
273
242
|
|
|
243
|
+
### Unknown arguments and forwarding
|
|
244
|
+
|
|
245
|
+
A command rejects unknown options and excess positional arguments before its `run` handler or
|
|
246
|
+
schema transforms execute. For ordinary commands, declare the accepted arguments in `args`;
|
|
247
|
+
there is no need to inspect `input.unknown` manually. Errors are `HackshValidationError` instances,
|
|
248
|
+
with each rejected token in `issues[].arg`.
|
|
249
|
+
|
|
250
|
+
Everything after `--` is always collected unchanged in `input.passthrough`, independently of this policy.
|
|
251
|
+
Use that explicit boundary when forwarding arguments to another executable:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const forward = defineCommand({
|
|
255
|
+
name: "git-forward",
|
|
256
|
+
async run({ exec, input, output }) {
|
|
257
|
+
const { stdout } = await exec("git", input.passthrough);
|
|
258
|
+
output.println(stdout);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
// git-forward -- status --short
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
For a wrapper that also accepts undeclared arguments before `--`, set `allowUnknownArgs: true`:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const wrapper = defineCommand({
|
|
268
|
+
allowUnknownArgs: true,
|
|
269
|
+
async run({ input, exec }) {
|
|
270
|
+
await exec("some-tool", [...input.unknown, ...input.passthrough]);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`input.unknown` preserves the order of unknown option tokens and excess positional values. Unknown
|
|
276
|
+
options do not consume a following value: if the schema has an unfilled positional slot, that
|
|
277
|
+
value fills it. Known options are still parsed and validated. Use `--` when a child's arguments
|
|
278
|
+
must not be interpreted by this command. A handler that does not use `input.passthrough` ignores those
|
|
279
|
+
arguments; commands that require rejecting them can check `input.passthrough.length` explicitly.
|
|
280
|
+
|
|
281
|
+
`allowUnknownArgs` defaults to `false`, inherits down the selected subcommand route, and can be
|
|
282
|
+
overridden by each child. A child's setting does not change validation of its parent's arguments.
|
|
283
|
+
Definitions are not mutated, so reusing a child under another root does not retain inherited settings.
|
|
284
|
+
Standalone `parseArgs` uses the supplied command's setting and does not resolve a parent route.
|
|
285
|
+
|
|
286
|
+
Once routing succeeds, help and root version output take precedence over argument validation,
|
|
287
|
+
including unknown arguments. Routing errors such as an unknown subcommand still fail. A child's
|
|
288
|
+
undeclared `--version` is an unknown argument, since the version builtin belongs to the root.
|
|
289
|
+
|
|
274
290
|
## Subcommands
|
|
275
291
|
|
|
276
292
|
```ts
|
|
@@ -304,105 +320,30 @@ listing, including usage shown for an unknown command. Hidden commands remain av
|
|
|
304
320
|
their own `--help` output remains available. If every subcommand is hidden, help keeps the generic
|
|
305
321
|
`<COMMAND>` syntax but omits the empty `COMMANDS` section.
|
|
306
322
|
|
|
307
|
-
##
|
|
308
|
-
|
|
309
|
-
Every `run` context has a `logger`. Its `trace`, `debug`, `info`, `warn`, `error`, and `fatal`
|
|
310
|
-
methods emit structured records through the invocation's printer. `step` and `success` are
|
|
311
|
-
info-level records with a distinct semantic kind for presentation. Placeholders in braces use the
|
|
312
|
-
record's properties. Every level is emitted and displayed by the default printer; there is no
|
|
313
|
-
built-in severity threshold or verbose flag.
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
const deploy = defineCommand({
|
|
317
|
-
name: "deploy",
|
|
318
|
-
async run({ logger }) {
|
|
319
|
-
const log = logger.with({ app: "web", env: "prod" });
|
|
320
|
-
log.step("Deploying {app} to {env}");
|
|
321
|
-
log.getChild("upload").debug("Uploaded {files} files", { files: 12 });
|
|
322
|
-
log.success("Deployed {app}");
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
Properties passed to an individual call override properties bound by `with`. `getChild` appends a
|
|
328
|
-
category segment; hacksh automatically starts the category with the named root and each resolved
|
|
329
|
-
subcommand. Missing placeholders, and placeholders whose values cannot be converted to text,
|
|
330
|
-
remain unchanged without discarding their properties. Passing an `Error` to `error` or `fatal`
|
|
331
|
-
uses its message and adds a canonical, schema-free error summary as `properties.error`. Error
|
|
332
|
-
values nested in other log properties are summarized the same way before custom printers receive
|
|
333
|
-
the event.
|
|
334
|
-
|
|
335
|
-
Each `log` print event contains a `record` with `level`, `kind`, `category`, interpolated `message`,
|
|
336
|
-
template `rawMessage`, merged `properties`, and a numeric `timestamp`. Logger calls do not need to
|
|
337
|
-
be awaited: they use the invocation's serialized output queue, which is drained before execution
|
|
338
|
-
finishes. A printer failure rejects the invocation without changing a successful `run` value.
|
|
323
|
+
## The run context
|
|
339
324
|
|
|
340
|
-
|
|
325
|
+
Every `run` handler receives a `CommandContext` with:
|
|
341
326
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
- `args` — validated output inferred from the command's `args` schema.
|
|
328
|
+
- `input` — the raw selected command segment (`argv`), accepted unknown tokens (`unknown`), and
|
|
329
|
+
tokens after `--` (`passthrough`). For a routed child, `input.argv` excludes parent options and
|
|
330
|
+
subcommand names.
|
|
331
|
+
- `command` — the resolved command definition. An unnamed subcommand is represented by a completed
|
|
332
|
+
copy whose `name` is its key; the original definition is not mutated.
|
|
333
|
+
- `exec` — runs external commands under the invocation's environment, printer, and cancellation.
|
|
334
|
+
- `output` — writes the command's results to stdout through the invocation's printer.
|
|
335
|
+
- `logger` — emits structured log records to stderr through the invocation's printer.
|
|
336
|
+
- `prompt` — requests interactive `select`, `confirm`, or `text` input.
|
|
337
|
+
- `signal` — an `AbortSignal` for the invocation.
|
|
345
338
|
|
|
346
|
-
|
|
347
|
-
const deploy = defineCommand({
|
|
348
|
-
name: "deploy",
|
|
349
|
-
async run({ logger, prompt }) {
|
|
350
|
-
const environment = await prompt.select({
|
|
351
|
-
message: "Environment",
|
|
352
|
-
options: [
|
|
353
|
-
{ value: "staging", hint: "safe default" },
|
|
354
|
-
{ value: "production", label: "Production" },
|
|
355
|
-
] as const,
|
|
356
|
-
initialValue: "staging",
|
|
357
|
-
}); // "staging" | "production"
|
|
358
|
-
const confirmed = await prompt.confirm({ message: `Deploy to ${environment}?` });
|
|
359
|
-
if (!confirmed) return;
|
|
360
|
-
const release = await prompt.text({
|
|
361
|
-
message: "Release name",
|
|
362
|
-
placeholder: "v1.2.3",
|
|
363
|
-
validate: (value) => (value.length === 0 ? "A release name is required" : undefined),
|
|
364
|
-
});
|
|
365
|
-
logger.success("Deploying {release}", { release });
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
Select values retain their literal union type. Arrow keys change the selected choice, Enter accepts
|
|
371
|
-
it, and `initialValue` controls the initial selection. Confirm accepts `y`, `n`, or Enter; Enter
|
|
372
|
-
uses `initialValue` when supplied and otherwise means no. Text displays its placeholder without
|
|
373
|
-
using it as input, supports editing, and repeats the prompt only when `validate` returns a nonempty
|
|
374
|
-
message; `""` means valid. Backspace removes one Unicode code point. Input pasted after a selection
|
|
375
|
-
or confirmation is retained for the next sequential prompt instead of being discarded.
|
|
376
|
-
The built-in prompt API intentionally stops at select, confirm, and text; it does not provide a
|
|
377
|
-
multiselect prompt.
|
|
378
|
-
|
|
379
|
-
Prompts are allowed only when the resolved mode is interactive: pretty output outside CI with TTY
|
|
380
|
-
stdin and stdout. JSON, CI, and non-TTY invocations throw `HackshPromptError` immediately even if a
|
|
381
|
-
default is available. Its `reason` and `prompt` fields identify why input failed and the prompt's
|
|
382
|
-
`kind`, `message`, and select `choices`. Reasons distinguish `non-interactive`, `cancelled`,
|
|
383
|
-
`aborted`, `eof`, `overlapping`, `invalid-options`, `input-error`, and `printer-error`. Ctrl-C and
|
|
384
|
-
Escape cancel; `runMain` reports cancellation and exits with status 130. A prompt that starts or is
|
|
385
|
-
waiting after the invocation's `signal` aborts fails with `aborted` and the signal's reason as its
|
|
386
|
-
`cause`. Only one prompt may be active in an invocation. Input listeners and raw terminal mode are
|
|
387
|
-
restored after every outcome.
|
|
388
|
-
Failed prompts emit a non-visible `closed` prompt state through the same printer before rejecting,
|
|
389
|
-
so callers that catch cancellation, EOF, input, or printer errors do not leave stale terminal UI.
|
|
390
|
-
|
|
391
|
-
Active pretty prompts redraw in place rather than appending a block for every key. A completed
|
|
392
|
-
prompt collapses to one result line. If a logger event arrives while a prompt is active, the
|
|
393
|
-
printer removes the prompt, renders the event, and then redraws the current prompt state.
|
|
394
|
-
Physical rows are calculated from explicit newlines, ANSI-free Unicode display width, and
|
|
395
|
-
`stdout.columns`, including terminal wrapping. When a custom output writer does not expose a
|
|
396
|
-
positive `columns` value, the renderer conservatively counts explicit lines only.
|
|
397
|
-
|
|
398
|
-
## External commands
|
|
339
|
+
### External commands
|
|
399
340
|
|
|
400
341
|
Every `run` context has an `exec` function that starts an executable with an argument array. It
|
|
401
342
|
never uses a shell, so spaces, quotes, `$`, `;`, `|`, and `*` in arguments reach the child
|
|
402
343
|
unchanged. By default it resolves with the captured output and rejects with `HackshExecError`
|
|
403
344
|
when the command fails. Pass `reject: false` to receive failures as results instead. Prefer this
|
|
404
|
-
context API to `spawnSync
|
|
405
|
-
uses the invocation’s environment, cancellation, and printer.
|
|
345
|
+
context API to `node:child_process`, `spawnSync`, or a custom subprocess wrapper inside a command
|
|
346
|
+
handler so execution uses the invocation’s environment, cancellation, and printer.
|
|
406
347
|
|
|
407
348
|
```ts
|
|
408
349
|
const check = defineCommand({
|
|
@@ -433,7 +374,7 @@ UTF-8 `stdout` and `stderr` with one final newline removed. A failure's `reason`
|
|
|
433
374
|
gives `ExecSuccess`, while `false` or a `boolean` variable gives `ExecResult`, which must be
|
|
434
375
|
narrowed before reading `exitCode`.
|
|
435
376
|
|
|
436
|
-
|
|
377
|
+
#### Working with captured output
|
|
437
378
|
|
|
438
379
|
Use `stdout` directly for a single value such as a commit ID or repository path. Do not call
|
|
439
380
|
`.trim()` on paths: surrounding whitespace can be part of the name. `exec` already removes one
|
|
@@ -445,7 +386,7 @@ that format and parse it with `JSON.parse` (and a Zod schema when validation is
|
|
|
445
386
|
helper. Splitting that format on newlines would break paths containing newlines. hacksh does not
|
|
446
387
|
parse Git records or infer whether stdout is text, JSON, or a list.
|
|
447
388
|
|
|
448
|
-
|
|
389
|
+
#### Execution options
|
|
449
390
|
|
|
450
391
|
Options are `cwd`, `env`, `input`, `reject`, `timeout` (milliseconds), `signal` (an
|
|
451
392
|
`AbortSignal`), `maxBuffer` (bytes per stream, default 100,000,000), `forceKillAfterDelay`
|
|
@@ -516,6 +457,164 @@ reads the whole environment to start a process.
|
|
|
516
457
|
To test a command without starting processes, call its `run` with a context whose `exec` is your own
|
|
517
458
|
function of type `Exec`.
|
|
518
459
|
|
|
460
|
+
### Command output
|
|
461
|
+
|
|
462
|
+
Every `run` context has an `output` for the data the command produces, kept apart from progress
|
|
463
|
+
and diagnostics on `logger`. `print(text)` writes text exactly as given, `println(text)` adds a
|
|
464
|
+
newline, and `json(value)` writes a JSON value.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
const report = defineCommand({
|
|
468
|
+
name: "report",
|
|
469
|
+
async run({ logger, output }) {
|
|
470
|
+
logger.step("Collecting commits");
|
|
471
|
+
output.print(renderTable(rows)); // already ends with a newline
|
|
472
|
+
output.println(`${rows.length} rows`);
|
|
473
|
+
output.json({ rows });
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
In pretty output, text goes to stdout unchanged and `json` values are indented with two spaces.
|
|
479
|
+
In JSON output, each call writes one line on stdout: `{"type":"output","kind":"text","text":...}`
|
|
480
|
+
holding the exact text, including the newline `println` adds, or
|
|
481
|
+
`{"type":"output","kind":"json","data":...}`. Concatenating the `text` values reproduces the
|
|
482
|
+
pretty stdout, except that terminal control sequences are removed as in every JSON string. `json`
|
|
483
|
+
values are normalized like log properties.
|
|
484
|
+
|
|
485
|
+
hacksh never prints a `run` return value; it is returned to `runCommand` callers only. Output that
|
|
486
|
+
was written before a failure stays written, and the error follows on stderr. Like logger calls,
|
|
487
|
+
`output` calls do not need to be awaited and are drained before execution finishes. Writing to
|
|
488
|
+
`process.stdout` or `console.log` directly bypasses the printer, the output mode, and any `stdout`
|
|
489
|
+
writer given to `runMain` or `runCommand`.
|
|
490
|
+
|
|
491
|
+
### Structured logging
|
|
492
|
+
|
|
493
|
+
Every `run` context has a `logger`. Its `trace`, `debug`, `info`, `warn`, `error`, and `fatal`
|
|
494
|
+
methods emit structured records through the invocation's printer. `step` and `success` are
|
|
495
|
+
info-level records with a distinct semantic kind for presentation. Placeholders in braces use the
|
|
496
|
+
record's properties. Every level is emitted and displayed by the default printer; there is no
|
|
497
|
+
built-in severity threshold or verbose flag.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const deploy = defineCommand({
|
|
501
|
+
name: "deploy",
|
|
502
|
+
async run({ logger }) {
|
|
503
|
+
const log = logger.with({ app: "web", env: "prod" });
|
|
504
|
+
log.step("Deploying {app} to {env}");
|
|
505
|
+
log.getChild("upload").debug("Uploaded {files} files", { files: 12 });
|
|
506
|
+
log.success("Deployed {app}");
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Properties passed to an individual call override properties bound by `with`. `getChild` appends a
|
|
512
|
+
category segment; hacksh automatically starts the category with the named root and each resolved
|
|
513
|
+
subcommand. Missing placeholders, and placeholders whose values cannot be converted to text,
|
|
514
|
+
remain unchanged without discarding their properties. Passing an `Error` to `error` or `fatal`
|
|
515
|
+
uses its message and adds a canonical, schema-free error summary as `properties.error`. Error
|
|
516
|
+
values nested in other log properties are summarized the same way before custom printers receive
|
|
517
|
+
the event.
|
|
518
|
+
|
|
519
|
+
Each `log` print event contains a `record` with `level`, `kind`, `category`, interpolated `message`,
|
|
520
|
+
template `rawMessage`, merged `properties`, and a numeric `timestamp`. Logger calls do not need to
|
|
521
|
+
be awaited: they use the invocation's serialized output queue, which is drained before execution
|
|
522
|
+
finishes. A printer failure rejects the invocation without changing a successful `run` value.
|
|
523
|
+
|
|
524
|
+
### Interactive input
|
|
525
|
+
|
|
526
|
+
Every `run` context has a `prompt` with `select`, `confirm`, and `text` methods. Interactive input
|
|
527
|
+
is separate from `logger`: prompts share the invocation's selected printer, serialized output queue,
|
|
528
|
+
terminal lock, output mode, and abort signal.
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const deploy = defineCommand({
|
|
532
|
+
name: "deploy",
|
|
533
|
+
async run({ logger, prompt }) {
|
|
534
|
+
const environment = await prompt.select({
|
|
535
|
+
message: "Environment",
|
|
536
|
+
options: [
|
|
537
|
+
{ value: "staging", hint: "safe default" },
|
|
538
|
+
{ value: "production", label: "Production" },
|
|
539
|
+
] as const,
|
|
540
|
+
initialValue: "staging",
|
|
541
|
+
}); // "staging" | "production"
|
|
542
|
+
const confirmed = await prompt.confirm({ message: `Deploy to ${environment}?` });
|
|
543
|
+
if (!confirmed) return;
|
|
544
|
+
const release = await prompt.text({
|
|
545
|
+
message: "Release name",
|
|
546
|
+
placeholder: "v1.2.3",
|
|
547
|
+
validate: (value) => (value.length === 0 ? "A release name is required" : undefined),
|
|
548
|
+
});
|
|
549
|
+
logger.success("Deploying {release}", { release });
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Select values retain their literal union type. Arrow keys change the selected choice, Enter accepts
|
|
555
|
+
it, and `initialValue` controls the initial selection. Confirm accepts `y`, `n`, or Enter; Enter
|
|
556
|
+
uses `initialValue` when supplied and otherwise means no. Text displays its placeholder without
|
|
557
|
+
using it as input, supports editing, and repeats the prompt only when `validate` returns a nonempty
|
|
558
|
+
message; `""` means valid. Backspace removes one Unicode code point. Input pasted after a selection
|
|
559
|
+
or confirmation is retained for the next sequential prompt instead of being discarded.
|
|
560
|
+
The built-in prompt API intentionally stops at select, confirm, and text; it does not provide a
|
|
561
|
+
multiselect prompt.
|
|
562
|
+
|
|
563
|
+
Prompts are allowed only when the resolved mode is interactive: pretty output outside CI with TTY
|
|
564
|
+
stdin and stdout. JSON, CI, and non-TTY invocations throw `HackshPromptError` immediately even if a
|
|
565
|
+
default is available. Its `reason` and `prompt` fields identify why input failed and the prompt's
|
|
566
|
+
`kind`, `message`, and select `choices`. Reasons distinguish `non-interactive`, `cancelled`,
|
|
567
|
+
`aborted`, `eof`, `overlapping`, `invalid-options`, `input-error`, and `printer-error`. Ctrl-C and
|
|
568
|
+
Escape cancel; `runMain` reports cancellation and exits with status 130. A prompt that starts or is
|
|
569
|
+
waiting after the invocation's `signal` aborts fails with `aborted` and the signal's reason as its
|
|
570
|
+
`cause`. Only one prompt may be active in an invocation. Input listeners and raw terminal mode are
|
|
571
|
+
restored after every outcome.
|
|
572
|
+
Failed prompts emit a non-visible `closed` prompt state through the same printer before rejecting,
|
|
573
|
+
so callers that catch cancellation, EOF, input, or printer errors do not leave stale terminal UI.
|
|
574
|
+
|
|
575
|
+
Active pretty prompts redraw in place rather than appending a block for every key. A completed
|
|
576
|
+
prompt collapses to one result line. If a logger or output event arrives while a prompt is active, the
|
|
577
|
+
printer removes the prompt, renders the event, and then redraws the current prompt state.
|
|
578
|
+
Physical rows are calculated from explicit newlines, ANSI-free Unicode display width, and
|
|
579
|
+
`stdout.columns`, including terminal wrapping. When a custom output writer does not expose a
|
|
580
|
+
positive `columns` value, the renderer conservatively counts explicit lines only.
|
|
581
|
+
|
|
582
|
+
### Cancellation and exit status
|
|
583
|
+
|
|
584
|
+
Every `run` context has a `signal`, an `AbortSignal` for that invocation. `exec` calls and prompts
|
|
585
|
+
already follow it; pass it to `fetch` and other APIs that accept one. Cancellation is cooperative:
|
|
586
|
+
hacksh does not stop code that ignores the signal.
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { defineCommand, HackshExitError, runCommand } from "hacksh";
|
|
590
|
+
|
|
591
|
+
const check = defineCommand({
|
|
592
|
+
name: "check",
|
|
593
|
+
async run({ signal }) {
|
|
594
|
+
const response = await fetch("https://example.com/health", { signal });
|
|
595
|
+
if (!response.ok) throw new HackshExitError(2, `Health check returned ${response.status}`);
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const controller = new AbortController();
|
|
600
|
+
await runCommand(check, [], { signal: controller.signal });
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
`runCommand` accepts a `signal` option and never listens for process signals or exits. If that
|
|
604
|
+
signal is already aborted, `runCommand` rejects with its reason without calling `run`; aborting it
|
|
605
|
+
later aborts the context's `signal`.
|
|
606
|
+
|
|
607
|
+
`runMain` listens for `SIGINT` and `SIGTERM` while it runs and removes those listeners before it
|
|
608
|
+
returns or exits. The first signal aborts the invocation with an `AbortError` named after the
|
|
609
|
+
signal; once `run` settles, output is drained and the process exits with 130 for `SIGINT` or 143 for
|
|
610
|
+
`SIGTERM`, even if `run` threw. A second signal while `run` is still pending exits immediately with
|
|
611
|
+
that signal's status.
|
|
612
|
+
|
|
613
|
+
Throw `HackshExitError(exitCode, message?)` to choose the exit status, an integer from 0 to 255.
|
|
614
|
+
`runMain` prints a non-empty message as an ordinary error and prints nothing when the message is
|
|
615
|
+
omitted; `onError` still receives the error. `runCommand` rethrows it, so library callers read
|
|
616
|
+
`exitCode` from the caught error. Other failures exit with 1, or 130 for a cancelled prompt.
|
|
617
|
+
|
|
519
618
|
## Output formats
|
|
520
619
|
|
|
521
620
|
`runMain` and `runCommand` resolve one output mode before command routing, parsing, or execution.
|
|
@@ -532,20 +631,25 @@ await runMain(main, { output: "auto" });
|
|
|
532
631
|
// HACKSH_OUTPUT=json my-cli --help overrides the configured preference.
|
|
533
632
|
```
|
|
534
633
|
|
|
634
|
+
stdout carries the command's results: `output` events, help, and version. Every log level, errors,
|
|
635
|
+
and `exec` events go to stderr, so a pipe such as `my-cli | jq` receives only results while logs
|
|
636
|
+
remain visible on the terminal. Direct writes such as `console.log` bypass the printer and are not
|
|
637
|
+
converted to JSON.
|
|
638
|
+
|
|
535
639
|
The resolved `OutputMode` has `format`, `interactive`, and `color` fields. Interaction is enabled
|
|
536
|
-
only for pretty output outside CI when both stdin and stdout are TTYs.
|
|
537
|
-
|
|
538
|
-
|
|
640
|
+
only for pretty output outside CI when both stdin and stdout are TTYs. `color` has a `stdout` and a
|
|
641
|
+
`stderr` flag; each is enabled only for pretty output when that stream is a TTY and `NO_COLOR` is
|
|
642
|
+
empty or absent, so logs stay colored on the terminal when stdout is redirected. These decisions
|
|
643
|
+
are independent: an explicit pretty format in CI can use color, but remains noninteractive. `env`,
|
|
644
|
+
`stdin`, `stdout`,
|
|
539
645
|
and `stderr` can be supplied in execution options for embedded or tested invocations.
|
|
540
646
|
Custom `stdin` values implement the readable-stream operations `on`, `removeListener`, `resume`,
|
|
541
647
|
and `pause`; `isTTY`, `isRaw`, `readableFlowing`, and `setRawMode` describe optional terminal
|
|
542
648
|
capabilities. A stream without those readable operations fails with an `input-error` if prompting
|
|
543
649
|
is attempted.
|
|
544
650
|
|
|
545
|
-
JSON output contains exactly one `PrintEvent` object per line
|
|
546
|
-
|
|
547
|
-
stderr. Usage attached
|
|
548
|
-
to an error retains its `stderr` stream. Every log level is emitted by default. JSON strings have
|
|
651
|
+
JSON output contains exactly one `PrintEvent` object per line, on the streams described above.
|
|
652
|
+
Usage attached to an error retains its `stderr` stream. Every log level is emitted by default. JSON strings have
|
|
549
653
|
terminal control sequences removed, and the printer never serializes executable command or Zod
|
|
550
654
|
schema objects.
|
|
551
655
|
|
|
@@ -574,16 +678,56 @@ block. All six log levels are displayed by default.
|
|
|
574
678
|
```
|
|
575
679
|
|
|
576
680
|
Pretty output uses Unicode symbols by default. Set `unicode: false` on `runMain` or `runCommand` for
|
|
577
|
-
ASCII marks and bars. Color is still resolved from the selected output mode:
|
|
578
|
-
|
|
681
|
+
ASCII marks and bars. Color is still resolved from the selected output mode: each event is colored
|
|
682
|
+
only when its stream is a TTY, and any non-empty `NO_COLOR` disables it. Unicode selection does not affect
|
|
579
683
|
JSON output. An invocation does not add automatic start or end messages; the renderer presents only
|
|
580
684
|
the semantic events that the command emits.
|
|
581
685
|
|
|
582
|
-
## Programmatic
|
|
686
|
+
## Programmatic API and errors
|
|
687
|
+
|
|
688
|
+
### Entry points
|
|
689
|
+
|
|
690
|
+
`defineCommand` preserves the handler's return type. `runCommand(command, argv, options)` handles
|
|
691
|
+
help and version output without exiting, throws errors to its caller, and returns a promise of the
|
|
692
|
+
awaited handler result, unioned with results from every possible routed descendant and `undefined`
|
|
693
|
+
for help/version. Routing-only commands add no handler result. Async and void handlers retain their
|
|
694
|
+
result types; `runMain` deliberately discards results.
|
|
695
|
+
|
|
696
|
+
`runMain(command)` reads `process.argv`, prints validation or usage errors, and exits with status 1.
|
|
697
|
+
Use `argv` to inject arguments. Omitting it uses `process.argv.slice(2)`; an empty array is an
|
|
698
|
+
explicit empty invocation. `runMain` returns `Promise<void>`. A reusable entry point is an ordinary
|
|
699
|
+
local function:
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
import { runMain } from "hacksh";
|
|
703
|
+
|
|
704
|
+
const mainEntry = (argv: string[] = ["--verbose"]) => runMain(main, { argv });
|
|
705
|
+
await mainEntry(); // uses the local default
|
|
706
|
+
await mainEntry([]); // explicitly supplies no arguments
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Help and the usage appended to validation or routing errors use the invocation's printer.
|
|
710
|
+
Usage events target stdout for help and stderr for errors. Pass `onError` to replace the entire
|
|
711
|
+
default error display, including usage; output written by this application callback remains its
|
|
712
|
+
own responsibility:
|
|
713
|
+
|
|
714
|
+
```ts
|
|
715
|
+
await runMain(main, {
|
|
716
|
+
argv: ["--help"],
|
|
717
|
+
async onError(error) {
|
|
718
|
+
console.error(`gh-pr-commits: ${error instanceof Error ? error.message : String(error)}`);
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Printers and events
|
|
724
|
+
|
|
725
|
+
The root `hacksh` entry exposes command-definition and execution APIs. Import printer factories,
|
|
726
|
+
output-mode helpers, `Printer`, `PrintEvent`, and renderer-facing event and summary types from
|
|
727
|
+
`hacksh/printer`. A custom printer can narrow `PrintEvent` on `event.type` without importing
|
|
728
|
+
each individual event type.
|
|
583
729
|
|
|
584
|
-
`
|
|
585
|
-
to its caller, and returns the selected `run` handler's result. Pass a `printer` in `options` to
|
|
586
|
-
receive all output produced by that invocation as structured events:
|
|
730
|
+
Pass a `printer` in `options` to receive all output produced by an invocation as structured events:
|
|
587
731
|
|
|
588
732
|
```ts
|
|
589
733
|
import { runCommand } from "hacksh";
|
|
@@ -600,27 +744,24 @@ await runCommand(main, ["--help"], { printer });
|
|
|
600
744
|
// events[0] is { type: "usage", usage, text, stream: "stdout" }
|
|
601
745
|
```
|
|
602
746
|
|
|
603
|
-
The current events are `usage`, `version`, `error`, `log`, `prompt`, and `exec`. Usage events include the resolved
|
|
747
|
+
The current events are `usage`, `version`, `error`, `log`, `prompt`, `output`, and `exec`. Usage events include the resolved
|
|
604
748
|
command's schema-free name, version, description, synopsis, arguments, options, and visible child
|
|
605
749
|
commands, plus rendered text and the intended `stdout` or `stderr` stream. Version events include
|
|
606
750
|
the version string. Error events contain a schema-free summary with an explicit `kind`, structured
|
|
607
751
|
validation issues, standard `Error` fields, custom enumerable properties, and recursively
|
|
608
752
|
canonical causes; log events contain the structured record described above. Prompt events contain
|
|
609
|
-
the prompt kind, status, current value or choices, placeholder, and validation message.
|
|
753
|
+
the prompt kind, status, current value or choices, placeholder, and validation message. Output
|
|
754
|
+
events carry `kind: "text"` with `text` or `kind: "json"` with `data`. Printer calls are serialized, may be asynchronous, and are
|
|
610
755
|
drained before `runCommand` or `runMain` completes or exits. A printer rejection rejects
|
|
611
756
|
`runCommand`.
|
|
612
757
|
|
|
613
|
-
The root `hacksh` entry exposes command-definition and execution APIs. Import printer factories,
|
|
614
|
-
output-mode helpers, `Printer`, `PrintEvent`, and renderer-facing event and summary types from
|
|
615
|
-
`hacksh/printer`. A custom printer can narrow `PrintEvent` on `event.type` without importing
|
|
616
|
-
each individual event type.
|
|
617
|
-
|
|
618
758
|
Use `createPrettyPrinter({ color, unicode, stdout, stderr })` to reuse the built-in human-readable
|
|
619
|
-
renderer with custom writable targets, or `createJsonPrinter({ stdout, stderr })` to reuse the
|
|
759
|
+
renderer with custom writable targets; `color` is one boolean or a `{ stdout, stderr }` pair, or `createJsonPrinter({ stdout, stderr })` to reuse the
|
|
620
760
|
built-in JSON-lines behavior. The pretty renderer has no public theme API; replacing the fixed
|
|
621
|
-
presentation requires a custom `Printer`. `
|
|
622
|
-
|
|
623
|
-
|
|
761
|
+
presentation requires a custom `Printer`. `resolveOutputMode(...)` exposes the selection contract
|
|
762
|
+
for wrappers and presentation or prompting layers.
|
|
763
|
+
|
|
764
|
+
### Standalone parsing and help
|
|
624
765
|
|
|
625
766
|
For parsing and usage rendering without execution, use the composition APIs. `parseArgs`
|
|
626
767
|
parses exactly the supplied command definition and argument segment, returns its schema output only,
|
|
@@ -629,7 +770,8 @@ refinements and transformations. It does not route subcommands, act on help or v
|
|
|
629
770
|
bypass validation; callers that resolve commands themselves must pass the selected command and
|
|
630
771
|
its segment. `renderUsage` returns a string synchronously with no output side effects. `showUsage`
|
|
631
772
|
requires an explicit `printer`, sends the same text and structured help summary as a usage event,
|
|
632
|
-
and awaits the printer. Its optional `stream` defaults to `"stdout"`.
|
|
773
|
+
and awaits the printer. Its optional `stream` defaults to `"stdout"`. `summarizeUsage(command)`
|
|
774
|
+
returns the same canonical help metadata carried by a usage event.
|
|
633
775
|
|
|
634
776
|
```ts
|
|
635
777
|
import { parseArgs, renderUsage, showUsage } from "hacksh";
|
|
@@ -646,83 +788,16 @@ await showUsage(main, {
|
|
|
646
788
|
});
|
|
647
789
|
```
|
|
648
790
|
|
|
649
|
-
|
|
650
|
-
is represented by a completed copy whose `name` is its key; the original definition is not mutated.
|
|
651
|
-
`args` contains validated schema output. `input` separates the original selected command segment
|
|
652
|
-
(`argv`), accepted unknown tokens (`unknown`), and tokens after `--` (`passthrough`).
|
|
653
|
-
For a routed child, `input.argv` excludes the parent options and subcommand names.
|
|
654
|
-
|
|
655
|
-
`defineCommand` preserves the handler's return type. `runCommand` returns a promise of the awaited
|
|
656
|
-
handler result, unioned with results from every possible routed descendant and `undefined` for
|
|
657
|
-
help/version. Routing-only commands add no handler result. Async and void handlers retain their
|
|
658
|
-
result types; `runMain` deliberately discards results.
|
|
659
|
-
|
|
660
|
-
`runMain(command)` reads `process.argv`, prints validation or usage errors, and exits with status 1.
|
|
661
|
-
Use `argv` to inject arguments. Omitting it uses `process.argv.slice(2)`; an empty array is an
|
|
662
|
-
explicit empty invocation. `runMain` returns `Promise<void>`. A reusable entry point is an ordinary
|
|
663
|
-
local function:
|
|
664
|
-
|
|
665
|
-
```ts
|
|
666
|
-
import { runMain } from "hacksh";
|
|
667
|
-
|
|
668
|
-
const mainEntry = (argv: string[] = ["--verbose"]) => runMain(main, { argv });
|
|
669
|
-
await mainEntry(); // uses the local default
|
|
670
|
-
await mainEntry([]); // explicitly supplies no arguments
|
|
671
|
-
```
|
|
791
|
+
### Errors
|
|
672
792
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
async onError(error) {
|
|
682
|
-
console.error(`gh-pr-commits: ${error instanceof Error ? error.message : String(error)}`);
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
Usage failures are `HackshUsageError` instances. Argument failures are
|
|
688
|
-
`HackshValidationError` instances whose `issues` identify CLI names such as `--repo` or `<PR>`.
|
|
689
|
-
Prompt failures are `HackshPromptError` instances with structured `reason` and `prompt` fields.
|
|
690
|
-
|
|
691
|
-
## Cancellation and exit status
|
|
692
|
-
|
|
693
|
-
Every `run` context has a `signal`, an `AbortSignal` for that invocation. `exec` calls and prompts
|
|
694
|
-
already follow it; pass it to `fetch` and other APIs that accept one. Cancellation is cooperative:
|
|
695
|
-
hacksh does not stop code that ignores the signal.
|
|
696
|
-
|
|
697
|
-
```ts
|
|
698
|
-
import { defineCommand, HackshExitError, runCommand } from "hacksh";
|
|
699
|
-
|
|
700
|
-
const check = defineCommand({
|
|
701
|
-
name: "check",
|
|
702
|
-
async run({ signal }) {
|
|
703
|
-
const response = await fetch("https://example.com/health", { signal });
|
|
704
|
-
if (!response.ok) throw new HackshExitError(2, `Health check returned ${response.status}`);
|
|
705
|
-
},
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const controller = new AbortController();
|
|
709
|
-
await runCommand(check, [], { signal: controller.signal });
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
`runCommand` accepts a `signal` option and never listens for process signals or exits. If that
|
|
713
|
-
signal is already aborted, `runCommand` rejects with its reason without calling `run`; aborting it
|
|
714
|
-
later aborts the context's `signal`.
|
|
715
|
-
|
|
716
|
-
`runMain` listens for `SIGINT` and `SIGTERM` while it runs and removes those listeners before it
|
|
717
|
-
returns or exits. The first signal aborts the invocation with an `AbortError` named after the
|
|
718
|
-
signal; once `run` settles, output is drained and the process exits with 130 for `SIGINT` or 143 for
|
|
719
|
-
`SIGTERM`, even if `run` threw. A second signal while `run` is still pending exits immediately with
|
|
720
|
-
that signal's status.
|
|
721
|
-
|
|
722
|
-
Throw `HackshExitError(exitCode, message?)` to choose the exit status, an integer from 0 to 255.
|
|
723
|
-
`runMain` prints a non-empty message as an ordinary error and prints nothing when the message is
|
|
724
|
-
omitted; `onError` still receives the error. `runCommand` rethrows it, so library callers read
|
|
725
|
-
`exitCode` from the caught error. Other failures exit with 1, or 130 for a cancelled prompt.
|
|
793
|
+
- `HackshUsageError` — routing and usage failures (such as an unknown subcommand).
|
|
794
|
+
- `HackshValidationError` — argument and option validation failures, whose `issues` identify CLI
|
|
795
|
+
names such as `--repo` or `<PR>` and rejected unknown tokens in `issues[].arg`.
|
|
796
|
+
- `HackshPromptError` — interactive prompt failures, with structured `reason` and `prompt` fields.
|
|
797
|
+
- `HackshExecError` — external command failures when `reject` is not `false`, with the full failure
|
|
798
|
+
in `error.result`.
|
|
799
|
+
- `HackshExitError` — thrown by a command handler to exit with a specific status code (`0`–`255`)
|
|
800
|
+
and optional message.
|
|
726
801
|
|
|
727
802
|
## Development
|
|
728
803
|
|
|
@@ -741,3 +816,15 @@ under that version, or supply an absolute executable path explicitly:
|
|
|
741
816
|
```sh
|
|
742
817
|
HACKSH_MINIMUM_NODE=/absolute/path/to/node-v22.18.0 pnpm test:packed
|
|
743
818
|
```
|
|
819
|
+
|
|
820
|
+
## Name and acknowledgments
|
|
821
|
+
|
|
822
|
+
The name comes from 拍手 (_hakushu_), the Japanese word for "clap", as a nod to
|
|
823
|
+
[clap](https://github.com/clap-rs/clap), the command-line argument parser for Rust. It is spelled
|
|
824
|
+
with _hack_ and _sh_, as in shell.
|
|
825
|
+
|
|
826
|
+
hacksh draws heavily on [Citty](https://github.com/unjs/citty) for its command API design and
|
|
827
|
+
[Clack](https://github.com/bombshell-dev/clack) for its interactive prompts and terminal output.
|
|
828
|
+
Many thanks to both projects and their contributors. hacksh uses Zod schemas for argument validation
|
|
829
|
+
and type inference, including tuples for positional arguments; it does not guarantee API or behavior
|
|
830
|
+
compatibility with Citty or Clack.
|