hacksh 0.13.0 → 0.15.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 +109 -89
- package/dist/command-pPKGCjNA.d.ts +389 -0
- package/dist/index.d.ts +17 -418
- package/dist/index.js +139 -1159
- package/dist/index.js.map +1 -1
- package/dist/pretty-BFub611x.js +1012 -0
- package/dist/pretty-BFub611x.js.map +1 -0
- package/dist/printer-api.d.ts +13 -0
- package/dist/printer-api.js +2 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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`,
|
|
5
|
+
with the context’s `exec`, use its `logger` for progress, and request interactive input with `prompt`.
|
|
6
6
|
|
|
7
7
|
The name comes from 拍手 (_hakushu_), the Japanese word for "clap", as a nod to
|
|
8
8
|
[clap](https://github.com/clap-rs/clap), the command-line argument parser for Rust. It is spelled
|
|
@@ -35,12 +35,13 @@ const main = defineCommand({
|
|
|
35
35
|
version: "0.1.0",
|
|
36
36
|
description: "Show a commit summary",
|
|
37
37
|
args: z.object({
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
positionals: z.tuple([
|
|
39
|
+
z.string().default("HEAD").meta({ title: "REF", description: "Commit to inspect" }),
|
|
40
|
+
]),
|
|
41
|
+
repo: z.string().optional().meta({ description: "Repository directory", alias: "C" }),
|
|
40
42
|
}),
|
|
41
|
-
argOptions: { repo: { alias: "C" } },
|
|
42
43
|
async run({ args, exec }) {
|
|
43
|
-
const [ref] = args.
|
|
44
|
+
const [ref] = args.positionals;
|
|
44
45
|
const { stdout } = await exec(
|
|
45
46
|
"git",
|
|
46
47
|
["show", "--no-patch", "--format=%h %s", "--end-of-options", ref],
|
|
@@ -85,27 +86,27 @@ Boolean flags also accept the explicit forms `--verbose=true` and
|
|
|
85
86
|
|
|
86
87
|
## Positional arguments
|
|
87
88
|
|
|
88
|
-
Declare positional arguments as a `z.tuple` in the `
|
|
89
|
+
Declare positional arguments as a `z.tuple` in the `positionals` field. Tuple items are assigned in order,
|
|
89
90
|
and the tuple rest collects all remaining values. Optional or defaulted items must come after
|
|
90
|
-
required ones, and items cannot be booleans or arrays. The `
|
|
91
|
+
required ones, and items cannot be booleans or arrays. The `positionals` field is never a named option.
|
|
91
92
|
|
|
92
93
|
```ts
|
|
93
94
|
const copy = defineCommand({
|
|
94
95
|
name: "copy",
|
|
95
96
|
args: z.object({
|
|
96
|
-
|
|
97
|
-
[z.string().
|
|
98
|
-
z.string().
|
|
97
|
+
positionals: z.tuple(
|
|
98
|
+
[z.string().meta({ title: "DESTINATION", description: "Output directory" })],
|
|
99
|
+
z.string().meta({ title: "FILES", description: "Input files" }),
|
|
99
100
|
),
|
|
100
101
|
}),
|
|
101
102
|
run({ args }) {
|
|
102
|
-
const [destination, ...files] = args.
|
|
103
|
+
const [destination, ...files] = args.positionals; // [string, ...string[]]
|
|
103
104
|
},
|
|
104
105
|
});
|
|
105
106
|
```
|
|
106
107
|
|
|
107
108
|
```sh
|
|
108
|
-
copy out a.txt b.txt # {
|
|
109
|
+
copy out a.txt b.txt # { positionals: ["out", "a.txt", "b.txt"] }
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
Help and validation errors label each item with its `.meta({ title })`, such as `<DESTINATION>` and
|
|
@@ -117,17 +118,17 @@ additional positional arguments are rejected by default, as are unknown options.
|
|
|
117
118
|
|
|
118
119
|
A command rejects unknown options and excess positional arguments before its `run` handler or
|
|
119
120
|
schema transforms execute. For ordinary commands, declare the accepted arguments in `args`;
|
|
120
|
-
there is no need to inspect `
|
|
121
|
+
there is no need to inspect `input.unknown` manually. Errors are `HackshValidationError` instances,
|
|
121
122
|
with each rejected token in `issues[].arg`.
|
|
122
123
|
|
|
123
|
-
Everything after `--` is always collected unchanged in `passthrough`, independently of this policy.
|
|
124
|
+
Everything after `--` is always collected unchanged in `input.passthrough`, independently of this policy.
|
|
124
125
|
Use that explicit boundary when forwarding arguments to another executable:
|
|
125
126
|
|
|
126
127
|
```ts
|
|
127
128
|
const forward = defineCommand({
|
|
128
129
|
name: "git-forward",
|
|
129
|
-
async run({ exec,
|
|
130
|
-
const { stdout } = await exec("git", passthrough);
|
|
130
|
+
async run({ exec, input }) {
|
|
131
|
+
const { stdout } = await exec("git", input.passthrough);
|
|
131
132
|
console.log(stdout);
|
|
132
133
|
},
|
|
133
134
|
});
|
|
@@ -139,17 +140,17 @@ For a wrapper that also accepts undeclared arguments before `--`, set `allowUnkn
|
|
|
139
140
|
```ts
|
|
140
141
|
const wrapper = defineCommand({
|
|
141
142
|
allowUnknownArgs: true,
|
|
142
|
-
async run({
|
|
143
|
-
await exec("some-tool", [...
|
|
143
|
+
async run({ input, exec }) {
|
|
144
|
+
await exec("some-tool", [...input.unknown, ...input.passthrough]);
|
|
144
145
|
},
|
|
145
146
|
});
|
|
146
147
|
```
|
|
147
148
|
|
|
148
|
-
`
|
|
149
|
+
`input.unknown` preserves the order of unknown option tokens and excess positional values. Unknown
|
|
149
150
|
options do not consume a following value: if the schema has an unfilled positional slot, that
|
|
150
151
|
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.
|
|
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.
|
|
153
154
|
|
|
154
155
|
`allowUnknownArgs` defaults to `false`, inherits down the selected subcommand route, and can be
|
|
155
156
|
overridden by each child. A child's setting does not change validation of its parent's arguments.
|
|
@@ -161,37 +162,40 @@ including unknown arguments. Routing errors such as an unknown subcommand still
|
|
|
161
162
|
undeclared `--version` is an unknown argument, since the version builtin belongs to the root.
|
|
162
163
|
|
|
163
164
|
**Migrating from 0.11.0:** unknown input was previously accepted by default. Add
|
|
164
|
-
`allowUnknownArgs: true` to commands that intentionally use `
|
|
165
|
+
`allowUnknownArgs: true` to commands that intentionally use `input.unknown`, or to their shared parent.
|
|
165
166
|
No migration is needed for `--` forwarding.
|
|
166
167
|
|
|
167
|
-
## Argument
|
|
168
|
+
## Argument metadata
|
|
168
169
|
|
|
169
|
-
Use
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
Use Zod 4 `.meta({ alias, valueName, env, title, description })` on each argument schema.
|
|
171
|
+
Importing hacksh types `alias`, `env`, and `valueName` on Zod's global metadata; any other key stays
|
|
172
|
+
allowed. Metadata does not change schema keys, parsed output, or validation error paths. `z.enum` choices
|
|
173
|
+
and descriptions appear in generated help, including inside supported schema wrappers and tuple items.
|
|
172
174
|
|
|
173
175
|
```ts
|
|
174
176
|
const convert = defineCommand({
|
|
175
177
|
name: "convert",
|
|
176
178
|
args: z.object({
|
|
177
|
-
|
|
178
|
-
[z.enum(["small", "large"]).
|
|
179
|
-
z.string().
|
|
179
|
+
positionals: z.tuple(
|
|
180
|
+
[z.enum(["small", "large"]).meta({ title: "SIZE", description: "Item size" })],
|
|
181
|
+
z.string().meta({ title: "INPUT", description: "Input files" }),
|
|
180
182
|
),
|
|
181
|
-
format: z
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
format: z
|
|
184
|
+
.enum(["json", "yaml"])
|
|
185
|
+
.optional()
|
|
186
|
+
.meta({ description: "Output format", alias: "f", valueName: "FORMAT" }),
|
|
187
|
+
overwrite: z.boolean().default(true).meta({ description: "Replace existing files" }),
|
|
188
|
+
verbose: z
|
|
189
|
+
.boolean()
|
|
190
|
+
.default(false)
|
|
191
|
+
.meta({ description: "Show details", alias: ["v", "V"] }),
|
|
184
192
|
}),
|
|
185
|
-
argOptions: {
|
|
186
|
-
format: { alias: "f", valueName: "FORMAT" },
|
|
187
|
-
verbose: { alias: ["v", "V"] },
|
|
188
|
-
},
|
|
189
193
|
});
|
|
190
194
|
```
|
|
191
195
|
|
|
192
196
|
```sh
|
|
193
197
|
convert large -f yaml -v a.txt b.txt
|
|
194
|
-
# {
|
|
198
|
+
# { positionals: ["large", "a.txt", "b.txt"], format: "yaml", overwrite: true, verbose: true }
|
|
195
199
|
```
|
|
196
200
|
|
|
197
201
|
```text
|
|
@@ -211,18 +215,24 @@ OPTIONS
|
|
|
211
215
|
|
|
212
216
|
- `alias` gives a named option one or more single-character short forms.
|
|
213
217
|
- `valueName` sets the value label of a named option.
|
|
218
|
+
- `env` overrides an option's automatic environment variable name, or disables it with `false`.
|
|
219
|
+
- `title` labels a positional argument; `description` supplies its help text.
|
|
220
|
+
|
|
221
|
+
Place metadata on the outer schema to override metadata inherited through wrappers. Zod's native
|
|
222
|
+
`.describe(text)` remains equivalent to description metadata; new code should use `.meta({ description: text })`.
|
|
223
|
+
With `zod/mini`, use `.check(mini.meta({ ... }))`.
|
|
214
224
|
|
|
215
225
|
Boolean flags are shown as `--[no-]name` because they also accept the `--no-` form. The
|
|
216
226
|
`negatable` field of `UsageOptionSummary` carries the same information for machine-readable
|
|
217
227
|
help, where `flags` keeps the literal `--name` spelling.
|
|
218
228
|
|
|
219
|
-
`
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
`defineCommand` validates metadata types and throws a `TypeError` for empty or multi-character
|
|
230
|
+
aliases, duplicate aliases, and option-only metadata (`alias`, `env`, `valueName`) on positional
|
|
231
|
+
schemas. Without an alias of the same letter, `-h` and `-v` are shorthands
|
|
222
232
|
for `--help` and `--version`. Defining an `h` or `v` alias replaces the shorthand, so `convert -v`
|
|
223
|
-
above sets `verbose`; `--help` and `--version` remain available.
|
|
224
|
-
`TypeError` from `defineCommand
|
|
225
|
-
|
|
233
|
+
above sets `verbose`; `--help` and `--version` remain available. Both `help` and `version` option
|
|
234
|
+
names are reserved and throw a `TypeError` from `defineCommand` on every command.
|
|
235
|
+
Output selection does not reserve
|
|
226
236
|
any additional command-line flags, so names such as `--json`, `--output`, and `--log-format` remain
|
|
227
237
|
available to application schemas.
|
|
228
238
|
|
|
@@ -234,8 +244,11 @@ upper snake case.
|
|
|
234
244
|
|
|
235
245
|
```ts
|
|
236
246
|
const deploy = defineCommand({
|
|
237
|
-
args: z.object({
|
|
238
|
-
|
|
247
|
+
args: z.object({
|
|
248
|
+
target: z.string(),
|
|
249
|
+
dryRun: z.boolean().default(false),
|
|
250
|
+
token: z.string().meta({ env: "GITHUB_TOKEN" }),
|
|
251
|
+
}),
|
|
239
252
|
run: ({ args }) => args,
|
|
240
253
|
});
|
|
241
254
|
const main = defineCommand({ name: "my-cli", subCommands: { deploy } });
|
|
@@ -254,7 +267,7 @@ MY_CLI_DEPLOY_TARGET=prod MY_CLI_DEPLOY_DRY_RUN=true GITHUB_TOKEN=t my-cli deplo
|
|
|
254
267
|
positional arguments, and commands under an unnamed root get no derived name.
|
|
255
268
|
- Help shows the name as `(Env: MY_CLI_DEPLOY_TARGET)`, and `UsageOptionSummary.env` carries it.
|
|
256
269
|
Errors name the variable but never include its value.
|
|
257
|
-
- `runCommand
|
|
270
|
+
- `runCommand` and `runMain` read the `env` option, defaulting to `process.env`.
|
|
258
271
|
`parseArgs(command, argv, { env })` reads the environment only when `env` is given.
|
|
259
272
|
- Two options that resolve to the same name on one route, such as a root `deployTarget` and a
|
|
260
273
|
`deploy` subcommand's `target`, make the invocation throw a `TypeError`.
|
|
@@ -327,15 +340,15 @@ finishes. A printer failure rejects the invocation without changing a successful
|
|
|
327
340
|
|
|
328
341
|
## Interactive input
|
|
329
342
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
343
|
+
Every `run` context has a `prompt` with `select`, `confirm`, and `text` methods. Interactive input
|
|
344
|
+
is separate from `logger`: prompts share the invocation's selected printer, serialized output queue,
|
|
345
|
+
terminal lock, output mode, and abort signal.
|
|
333
346
|
|
|
334
347
|
```ts
|
|
335
348
|
const deploy = defineCommand({
|
|
336
349
|
name: "deploy",
|
|
337
|
-
async run({ logger }) {
|
|
338
|
-
const environment = await
|
|
350
|
+
async run({ logger, prompt }) {
|
|
351
|
+
const environment = await prompt.select({
|
|
339
352
|
message: "Environment",
|
|
340
353
|
options: [
|
|
341
354
|
{ value: "staging", hint: "safe default" },
|
|
@@ -343,9 +356,9 @@ const deploy = defineCommand({
|
|
|
343
356
|
] as const,
|
|
344
357
|
initialValue: "staging",
|
|
345
358
|
}); // "staging" | "production"
|
|
346
|
-
const confirmed = await
|
|
359
|
+
const confirmed = await prompt.confirm({ message: `Deploy to ${environment}?` });
|
|
347
360
|
if (!confirmed) return;
|
|
348
|
-
const release = await
|
|
361
|
+
const release = await prompt.text({
|
|
349
362
|
message: "Release name",
|
|
350
363
|
placeholder: "v1.2.3",
|
|
351
364
|
validate: (value) => (value.length === 0 ? "A release name is required" : undefined),
|
|
@@ -574,7 +587,8 @@ to its caller, and returns the selected `run` handler's result. Pass a `printer`
|
|
|
574
587
|
receive all output produced by that invocation as structured events:
|
|
575
588
|
|
|
576
589
|
```ts
|
|
577
|
-
import { runCommand
|
|
590
|
+
import { runCommand } from "hacksh";
|
|
591
|
+
import type { PrintEvent, Printer } from "hacksh/printer";
|
|
578
592
|
|
|
579
593
|
const events: PrintEvent[] = [];
|
|
580
594
|
const printer: Printer = {
|
|
@@ -587,7 +601,7 @@ await runCommand(main, ["--help"], { printer });
|
|
|
587
601
|
// events[0] is { type: "usage", usage, text, stream: "stdout" }
|
|
588
602
|
```
|
|
589
603
|
|
|
590
|
-
The current events are `usage`, `version`, `error`, `log`, and `
|
|
604
|
+
The current events are `usage`, `version`, `error`, `log`, `prompt`, and `exec`. Usage events include the resolved
|
|
591
605
|
command's schema-free name, version, description, synopsis, arguments, options, and visible child
|
|
592
606
|
commands, plus rendered text and the intended `stdout` or `stderr` stream. Version events include
|
|
593
607
|
the version string. Error events contain a schema-free summary with an explicit `kind`, structured
|
|
@@ -597,68 +611,74 @@ the prompt kind, status, current value or choices, placeholder, and validation m
|
|
|
597
611
|
drained before `runCommand` or `runMain` completes or exits. A printer rejection rejects
|
|
598
612
|
`runCommand`.
|
|
599
613
|
|
|
614
|
+
The root `hacksh` entry exposes command-definition and execution APIs. Import printer factories,
|
|
615
|
+
output-mode helpers, `Printer`, `PrintEvent`, and renderer-facing event and summary types from
|
|
616
|
+
`hacksh/printer`. A custom printer can narrow `PrintEvent` on `event.type` without importing
|
|
617
|
+
each individual event type.
|
|
618
|
+
|
|
600
619
|
Use `createPrettyPrinter({ color, unicode, stdout, stderr })` to reuse the built-in human-readable
|
|
601
620
|
renderer with custom writable targets, or `createJsonPrinter({ stdout, stderr })` to reuse the
|
|
602
621
|
built-in JSON-lines behavior. The pretty renderer has no public theme API; replacing the fixed
|
|
603
|
-
presentation requires a custom `Printer`. `
|
|
604
|
-
undecorated compatibility adapters. `summarizeUsage(command)` returns the same canonical help
|
|
622
|
+
presentation requires a custom `Printer`. `summarizeUsage(command)` returns the same canonical help
|
|
605
623
|
metadata carried by a usage event. `resolveOutputMode(...)` exposes the selection contract for
|
|
606
|
-
wrappers and
|
|
624
|
+
wrappers and presentation or prompting layers.
|
|
607
625
|
|
|
608
|
-
For parsing and usage rendering without execution, use the
|
|
626
|
+
For parsing and usage rendering without execution, use the composition APIs. `parseArgs`
|
|
609
627
|
parses exactly the supplied command definition and argument segment, returns its schema output only,
|
|
610
|
-
and throws `HackshValidationError` on failure. It
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
takes precedence over printer selection; an explicit `printer` also retains its existing behavior.
|
|
617
|
-
These functions are async so future command-resolution features can be added without changing
|
|
618
|
-
their return types.
|
|
628
|
+
and throws `HackshValidationError` on failure. It remains asynchronous to support async Zod
|
|
629
|
+
refinements and transformations. It does not route subcommands, act on help or version tokens, or
|
|
630
|
+
bypass validation; callers that resolve commands themselves must pass the selected command and
|
|
631
|
+
its segment. `renderUsage` returns a string synchronously with no output side effects. `showUsage`
|
|
632
|
+
requires an explicit `printer`, sends the same text and structured help summary as a usage event,
|
|
633
|
+
and awaits the printer. Its optional `stream` defaults to `"stdout"`.
|
|
619
634
|
|
|
620
635
|
```ts
|
|
621
|
-
import {
|
|
636
|
+
import { parseArgs, renderUsage, showUsage } from "hacksh";
|
|
637
|
+
import { createPrettyPrinter } from "hacksh/printer";
|
|
622
638
|
|
|
623
639
|
const args = await parseArgs(main, ["42", "--repo", "owner/repo"]);
|
|
624
640
|
// args is inferred from the schema as:
|
|
625
|
-
// {
|
|
641
|
+
// { positionals: [string]; repo?: string }
|
|
626
642
|
|
|
627
|
-
const text: string =
|
|
643
|
+
const text: string = renderUsage(main); // does not write output
|
|
628
644
|
await showUsage(main, {
|
|
629
|
-
printer:
|
|
645
|
+
printer: createPrettyPrinter({ color: false, unicode: true }),
|
|
630
646
|
stream: "stderr",
|
|
631
647
|
});
|
|
632
648
|
```
|
|
633
649
|
|
|
634
|
-
The `run` context includes `
|
|
635
|
-
represented by a completed copy whose `name` is its key; the original definition is not mutated.
|
|
650
|
+
The `run` context includes `command`, the actual resolved command definition. An unnamed subcommand
|
|
651
|
+
is represented by a completed copy whose `name` is its key; the original definition is not mutated.
|
|
652
|
+
`args` contains validated schema output. `input` separates the original selected command segment
|
|
653
|
+
(`argv`), accepted unknown tokens (`unknown`), and tokens after `--` (`passthrough`).
|
|
654
|
+
For a routed child, `input.argv` excludes the parent options and subcommand names.
|
|
655
|
+
|
|
656
|
+
`defineCommand` preserves the handler's return type. `runCommand` returns a promise of the awaited
|
|
657
|
+
handler result, unioned with results from every possible routed descendant and `undefined` for
|
|
658
|
+
help/version. Routing-only commands add no handler result. Async and void handlers retain their
|
|
659
|
+
result types; `runMain` deliberately discards results.
|
|
636
660
|
|
|
637
661
|
`runMain(command)` reads `process.argv`, prints validation or usage errors, and exits with status 1.
|
|
638
|
-
Use `argv` to inject arguments
|
|
639
|
-
|
|
640
|
-
|
|
662
|
+
Use `argv` to inject arguments. Omitting it uses `process.argv.slice(2)`; an empty array is an
|
|
663
|
+
explicit empty invocation. `runMain` returns `Promise<void>`. A reusable entry point is an ordinary
|
|
664
|
+
local function:
|
|
641
665
|
|
|
642
666
|
```ts
|
|
643
|
-
import {
|
|
667
|
+
import { runMain } from "hacksh";
|
|
644
668
|
|
|
645
|
-
const mainEntry =
|
|
646
|
-
await mainEntry(); // uses the
|
|
647
|
-
await mainEntry([]); //
|
|
669
|
+
const mainEntry = (argv: string[] = ["--verbose"]) => runMain(main, { argv });
|
|
670
|
+
await mainEntry(); // uses the local default
|
|
671
|
+
await mainEntry([]); // explicitly supplies no arguments
|
|
648
672
|
```
|
|
649
673
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
output written by this application callback remains its own responsibility:
|
|
674
|
+
Help and the usage appended to validation or routing errors use the invocation's printer.
|
|
675
|
+
Usage events target stdout for help and stderr for errors. Pass `onError` to replace the entire
|
|
676
|
+
default error display, including usage; output written by this application callback remains its
|
|
677
|
+
own responsibility:
|
|
655
678
|
|
|
656
679
|
```ts
|
|
657
680
|
await runMain(main, {
|
|
658
681
|
argv: ["--help"],
|
|
659
|
-
async showUsage(command, { write }) {
|
|
660
|
-
await write(await renderUsage(command));
|
|
661
|
-
},
|
|
662
682
|
async onError(error) {
|
|
663
683
|
console.error(`gh-pr-commits: ${error instanceof Error ? error.message : String(error)}`);
|
|
664
684
|
},
|