just-bash-util 0.1.4 → 0.1.6
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 +65 -2
- package/dist/{chunk-35QZZQ4A.js → chunk-4J5EECVQ.js} +30 -7
- package/dist/command/index.d.ts +15 -9
- package/dist/command/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,12 +63,66 @@ await serve.invoke({ port: 8080, entry: "app.ts" }, ctx);
|
|
|
63
63
|
- Subcommand nesting with automatic option inheritance
|
|
64
64
|
- `omitInherited` to exclude parent options from specific subcommands
|
|
65
65
|
- `--help` / `-h` auto-generated at every level
|
|
66
|
-
- `--no-<flag>` negation, `-abc` combined short flags, `--key=value` syntax
|
|
67
|
-
- `--`
|
|
66
|
+
- `--no-<flag>` negation, `-abc` combined short flags, `--key=value` syntax, counted flags (`-vvv` → 3)
|
|
67
|
+
- `--` end-of-options separator (remaining tokens become positional args and are available via `meta.passthrough`)
|
|
68
68
|
- Environment variable fallbacks for options
|
|
69
69
|
- Levenshtein-based "did you mean?" suggestions for typos
|
|
70
70
|
- Automatic error handling — thrown errors in handlers are caught and returned as clean `ExecResult` with `exitCode: 1`
|
|
71
71
|
|
|
72
|
+
#### Options and flags
|
|
73
|
+
|
|
74
|
+
Option keys are written in camelCase and automatically converted to kebab-case for the CLI:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
options: {
|
|
78
|
+
allowEmpty: f(), // CLI: --allow-empty handler: args.allowEmpty
|
|
79
|
+
dryRun: f().alias("n"), // CLI: --dry-run / -n handler: args.dryRun
|
|
80
|
+
message: o.string().alias("m"), // CLI: --message / -m handler: args.message
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Flags support counting mode via `.count()`. Repeated occurrences produce a number instead of a boolean — useful for verbosity levels (`-v`, `-vv`, `-vvv`):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
options: {
|
|
88
|
+
verbose: f().alias("v").count().describe("Verbosity level"),
|
|
89
|
+
}
|
|
90
|
+
// -v → 1, -vv → 2, -vvv → 3, absent → 0
|
|
91
|
+
// handler receives args.verbose as number
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Short flags (single-dash, single-character) require `.alias()`. A single-character key like `b: f()` creates the long flag `--b`, **not** the short flag `-b`. To get `-b`, use a descriptive key with an alias:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// ✗ b: f() → creates --b (long flag), not -b
|
|
98
|
+
// ✓ branch: f().alias("b") → creates --branch and -b
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Positional args
|
|
102
|
+
|
|
103
|
+
Args are required by default. Use `.optional()` for optional args, and `.variadic()` to collect remaining positionals into an array. Chain `.optional().variadic()` for zero-or-more:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
args: [
|
|
107
|
+
a.string().name("entry"), // required single arg
|
|
108
|
+
a.string().name("file").optional(), // optional single arg
|
|
109
|
+
a.string().name("files").variadic(), // required: one or more
|
|
110
|
+
a.string().name("paths").optional().variadic(), // optional: zero or more → string[]
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### The `--` separator
|
|
115
|
+
|
|
116
|
+
The `--` token signals end-of-options. Tokens after `--` are treated as positional arguments (not parsed as flags) and are also available in `meta.passthrough`:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// mycli checkout -- README.md
|
|
120
|
+
handler: (args, ctx, meta) => {
|
|
121
|
+
args.target; // "README.md" (assigned to positional arg)
|
|
122
|
+
meta.passthrough; // ["README.md"] (raw tokens after --)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
72
126
|
### `just-bash-util/config` — Config file discovery
|
|
73
127
|
|
|
74
128
|
Cosmiconfig-style config search that walks up the directory tree, trying conventional filenames at each level. Comments and trailing commas are supported out of the box.
|
|
@@ -142,6 +196,15 @@ parsePackageSpecifier("@vue/shared/dist"); // { name: "@vue/shared", subpath: ".
|
|
|
142
196
|
parsePackageSpecifier("lodash/merge"); // { name: "lodash", subpath: "./merge" }
|
|
143
197
|
```
|
|
144
198
|
|
|
199
|
+
**`join` vs `resolve`** — `join` concatenates segments and normalizes; an absolute second argument is kept as-is (appended, not replacing). `resolve` processes right-to-left and stops at the first absolute path, like Node's `path.resolve` (but without prepending `cwd` when no absolute segment exists):
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
join("/repo", "/file.txt"); // "/repo/file.txt" — concatenates with /
|
|
203
|
+
resolve("/repo", "/file.txt"); // "/file.txt" — absolute segment wins
|
|
204
|
+
resolve("/repo", "file.txt"); // "/repo/file.txt"
|
|
205
|
+
resolve("a", "b"); // "a/b" — stays relative (no cwd)
|
|
206
|
+
```
|
|
207
|
+
|
|
145
208
|
## Peer dependencies
|
|
146
209
|
|
|
147
210
|
Requires [`just-bash`](https://www.npmjs.com/package/just-bash) ^2.9.6 — provides the `CommandContext` and `ExecResult` types used throughout.
|
|
@@ -64,6 +64,13 @@ var FlagBuilder = class _FlagBuilder {
|
|
|
64
64
|
default(value) {
|
|
65
65
|
return new _FlagBuilder({ ...this._def, default: value });
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Enable counting mode. Repeated occurrences (-v -v or -vv) produce a
|
|
69
|
+
* number instead of a boolean: 0 (absent), 1, 2, 3, etc.
|
|
70
|
+
*/
|
|
71
|
+
count() {
|
|
72
|
+
return new _FlagBuilder({ ...this._def, counted: true });
|
|
73
|
+
}
|
|
67
74
|
};
|
|
68
75
|
|
|
69
76
|
// src/command/builders/arg.ts
|
|
@@ -210,6 +217,7 @@ function parseArgs(options, argDefs, tokens, env) {
|
|
|
210
217
|
if (token === "--") {
|
|
211
218
|
i++;
|
|
212
219
|
while (i < tokens.length) {
|
|
220
|
+
positionals.push(tokens[i]);
|
|
213
221
|
passthrough.push(tokens[i]);
|
|
214
222
|
i++;
|
|
215
223
|
}
|
|
@@ -230,7 +238,7 @@ function parseArgs(options, argDefs, tokens, env) {
|
|
|
230
238
|
if (longName.startsWith("no-")) {
|
|
231
239
|
const positiveEntry = longMap.get(longName.slice(3));
|
|
232
240
|
if (positiveEntry && positiveEntry.def._kind === "flag") {
|
|
233
|
-
result[positiveEntry.key] = false;
|
|
241
|
+
result[positiveEntry.key] = positiveEntry.def.counted ? 0 : false;
|
|
234
242
|
i++;
|
|
235
243
|
continue;
|
|
236
244
|
}
|
|
@@ -245,7 +253,11 @@ function parseArgs(options, argDefs, tokens, env) {
|
|
|
245
253
|
continue;
|
|
246
254
|
}
|
|
247
255
|
if (entry.def._kind === "flag") {
|
|
248
|
-
|
|
256
|
+
if (entry.def.counted) {
|
|
257
|
+
result[entry.key] = (result[entry.key] || 0) + 1;
|
|
258
|
+
} else {
|
|
259
|
+
result[entry.key] = true;
|
|
260
|
+
}
|
|
249
261
|
i++;
|
|
250
262
|
continue;
|
|
251
263
|
}
|
|
@@ -268,15 +280,23 @@ function parseArgs(options, argDefs, tokens, env) {
|
|
|
268
280
|
const ch = chars[j];
|
|
269
281
|
const entry = shortMap.get(ch);
|
|
270
282
|
if (!entry) {
|
|
283
|
+
const suggestions = [];
|
|
284
|
+
if (longMap.has(ch)) {
|
|
285
|
+
suggestions.push(`--${ch}`);
|
|
286
|
+
}
|
|
271
287
|
errors.push({
|
|
272
288
|
type: "unknown_option",
|
|
273
289
|
name: `-${ch}`,
|
|
274
|
-
suggestions
|
|
290
|
+
suggestions
|
|
275
291
|
});
|
|
276
292
|
continue;
|
|
277
293
|
}
|
|
278
294
|
if (entry.def._kind === "flag") {
|
|
279
|
-
|
|
295
|
+
if (entry.def.counted) {
|
|
296
|
+
result[entry.key] = (result[entry.key] || 0) + 1;
|
|
297
|
+
} else {
|
|
298
|
+
result[entry.key] = true;
|
|
299
|
+
}
|
|
280
300
|
continue;
|
|
281
301
|
}
|
|
282
302
|
const restOfString = chars.slice(j + 1);
|
|
@@ -337,7 +357,7 @@ function parseArgs(options, argDefs, tokens, env) {
|
|
|
337
357
|
for (const [key, def] of Object.entries(options)) {
|
|
338
358
|
if (result[key] === void 0) {
|
|
339
359
|
if (def._kind === "flag") {
|
|
340
|
-
result[key] = def.default ?? false;
|
|
360
|
+
result[key] = def.default ?? (def.counted ? 0 : false);
|
|
341
361
|
} else if (def._kind === "option") {
|
|
342
362
|
const opt = def;
|
|
343
363
|
if (opt.env && env?.[opt.env] !== void 0) {
|
|
@@ -481,6 +501,7 @@ function formatOptionsTable(schema, header) {
|
|
|
481
501
|
parts.push(`--${longName}`);
|
|
482
502
|
const descParts = [];
|
|
483
503
|
if (flag.description) descParts.push(flag.description);
|
|
504
|
+
if (flag.counted) descParts.push("(counted)");
|
|
484
505
|
if (flag.default !== void 0) descParts.push(`(default: ${flag.default})`);
|
|
485
506
|
rows.push([parts.join(" "), descParts.join(" ")]);
|
|
486
507
|
} else {
|
|
@@ -635,7 +656,9 @@ var Command = class _Command {
|
|
|
635
656
|
const value = input[key];
|
|
636
657
|
const kebab = camelToKebab(key);
|
|
637
658
|
if (def._kind === "flag") {
|
|
638
|
-
if (value ===
|
|
659
|
+
if (def.counted && typeof value === "number" && value > 0) {
|
|
660
|
+
for (let n = 0; n < value; n++) tokens.push(`--${kebab}`);
|
|
661
|
+
} else if (value === true) {
|
|
639
662
|
tokens.push(`--${kebab}`);
|
|
640
663
|
} else if (value === false && def.default === true) {
|
|
641
664
|
tokens.push(`--no-${kebab}`);
|
|
@@ -685,7 +708,7 @@ var Command = class _Command {
|
|
|
685
708
|
for (const [key, def] of Object.entries(allOpts)) {
|
|
686
709
|
if (resolved[key] === void 0) {
|
|
687
710
|
if (def._kind === "flag") {
|
|
688
|
-
resolved[key] = def.default ?? false;
|
|
711
|
+
resolved[key] = def.default ?? (def.counted ? 0 : false);
|
|
689
712
|
} else if (def._kind === "option") {
|
|
690
713
|
if (def.default !== void 0) {
|
|
691
714
|
resolved[key] = def.default;
|
package/dist/command/index.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ interface FlagDef {
|
|
|
21
21
|
readonly _kind: "flag";
|
|
22
22
|
readonly description?: string;
|
|
23
23
|
readonly short?: string;
|
|
24
|
-
readonly default?: boolean;
|
|
24
|
+
readonly default?: boolean | number;
|
|
25
|
+
readonly counted?: boolean;
|
|
25
26
|
}
|
|
26
27
|
interface ArgDef<TOut = unknown> {
|
|
27
28
|
readonly _kind: "arg";
|
|
@@ -88,16 +89,21 @@ declare function string$1(): OptionBuilder<string | undefined>;
|
|
|
88
89
|
/** Create a number option (optional by default) */
|
|
89
90
|
declare function number$1(): OptionBuilder<number | undefined>;
|
|
90
91
|
|
|
91
|
-
declare class FlagBuilder {
|
|
92
|
+
declare class FlagBuilder<TCounted extends boolean = false> {
|
|
92
93
|
/** @internal */
|
|
93
94
|
readonly _def: FlagDef;
|
|
94
95
|
constructor(def?: FlagDef);
|
|
95
96
|
/** Add a description */
|
|
96
|
-
describe(text: string): FlagBuilder
|
|
97
|
+
describe(text: string): FlagBuilder<TCounted>;
|
|
97
98
|
/** Set a short alias (single character, e.g. "v" for -v) */
|
|
98
|
-
alias(short: string): FlagBuilder
|
|
99
|
+
alias(short: string): FlagBuilder<TCounted>;
|
|
99
100
|
/** Set a default value */
|
|
100
|
-
default(value: boolean): FlagBuilder
|
|
101
|
+
default(value: TCounted extends true ? number : boolean): FlagBuilder<TCounted>;
|
|
102
|
+
/**
|
|
103
|
+
* Enable counting mode. Repeated occurrences (-v -v or -vv) produce a
|
|
104
|
+
* number instead of a boolean: 0 (absent), 1, 2, 3, etc.
|
|
105
|
+
*/
|
|
106
|
+
count(): FlagBuilder<true>;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
declare class ArgBuilder<TOut, TName extends string = never, THasDefault extends boolean = false> {
|
|
@@ -140,12 +146,12 @@ type Prettify<T> = {
|
|
|
140
146
|
[K in keyof T as string extends K ? never : K]: T[K];
|
|
141
147
|
} & {};
|
|
142
148
|
/** Builder input types — what the user passes in config */
|
|
143
|
-
type OptionInput = OptionBuilder<any, any> | FlagBuilder
|
|
149
|
+
type OptionInput = OptionBuilder<any, any> | FlagBuilder<any>;
|
|
144
150
|
type OptionsInput = Record<string, OptionInput>;
|
|
145
151
|
type ArgsInput = readonly ArgBuilder<any, any, any>[];
|
|
146
152
|
/** Infer the value types from option builder instances (handler signature) */
|
|
147
153
|
type InferOptionsFromInput<T extends OptionsInput> = {
|
|
148
|
-
[K in keyof T]: T[K] extends OptionBuilder<infer V, any> ? V : T[K] extends FlagBuilder ? boolean : never;
|
|
154
|
+
[K in keyof T]: T[K] extends OptionBuilder<infer V, any> ? V : T[K] extends FlagBuilder<true> ? number : T[K] extends FlagBuilder ? boolean : never;
|
|
149
155
|
};
|
|
150
156
|
/** Infer positional arg types from arg builder instances (handler signature) */
|
|
151
157
|
type InferArgsFromInput<T extends ArgsInput> = {
|
|
@@ -153,9 +159,9 @@ type InferArgsFromInput<T extends ArgsInput> = {
|
|
|
153
159
|
};
|
|
154
160
|
/** Infer invoke() options: required options mandatory, defaulted/optional/flags optional */
|
|
155
161
|
type InferInvokeOptions<T extends OptionsInput> = {
|
|
156
|
-
[K in keyof T as T[K] extends FlagBuilder ? never : T[K] extends OptionBuilder<infer V, infer D> ? [D] extends [true] ? never : undefined extends V ? never : K : never]: T[K] extends OptionBuilder<infer V, any> ? V : never;
|
|
162
|
+
[K in keyof T as T[K] extends FlagBuilder<any> ? never : T[K] extends OptionBuilder<infer V, infer D> ? [D] extends [true] ? never : undefined extends V ? never : K : never]: T[K] extends OptionBuilder<infer V, any> ? V : never;
|
|
157
163
|
} & {
|
|
158
|
-
[K in keyof T as T[K] extends FlagBuilder ? K : T[K] extends OptionBuilder<infer V, infer D> ? [D] extends [true] ? K : undefined extends V ? K : never : never]?: T[K] extends OptionBuilder<infer V, any> ? V : T[K] extends FlagBuilder ? boolean : never;
|
|
164
|
+
[K in keyof T as T[K] extends FlagBuilder<any> ? K : T[K] extends OptionBuilder<infer V, infer D> ? [D] extends [true] ? K : undefined extends V ? K : never : never]?: T[K] extends OptionBuilder<infer V, any> ? V : T[K] extends FlagBuilder<true> ? number : T[K] extends FlagBuilder ? boolean : never;
|
|
159
165
|
};
|
|
160
166
|
/** Infer invoke() args: required args mandatory, defaulted/optional args optional */
|
|
161
167
|
type InferInvokeArgs<T extends ArgsInput> = {
|
package/dist/command/index.js
CHANGED
package/dist/index.js
CHANGED