argsbarg 0.1.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/.cursor/rules/code.mdc +9 -0
- package/README.md +188 -0
- package/biome.json +17 -0
- package/bun.lock +21 -0
- package/docs/completions-preview.png +0 -0
- package/docs/help-l2-preview.png +0 -0
- package/docs/help-preview.png +0 -0
- package/examples/minimal.ts +41 -0
- package/examples/nested.ts +87 -0
- package/logo.png +0 -0
- package/package.json +25 -0
- package/plan.md +194 -0
- package/src/completion.ts +523 -0
- package/src/context.ts +67 -0
- package/src/help.ts +429 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +24 -0
- package/src/parse.ts +487 -0
- package/src/runtime.ts +113 -0
- package/src/types.ts +114 -0
- package/src/utils.ts +24 -0
- package/src/validate.ts +136 -0
- package/tsconfig.json +12 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module holds the low-level string and number helpers.
|
|
3
|
+
It contains the small parsing checks that are used in multiple places without pulling
|
|
4
|
+
in heavier logic from the parser or validation modules.
|
|
5
|
+
|
|
6
|
+
It keeps numeric checks consistent across the package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Returns whether s is a valid number with no extra characters. */
|
|
10
|
+
export function fullStringIsDouble(s: string): boolean {
|
|
11
|
+
if (s.trim().length === 0) return false;
|
|
12
|
+
const num = Number(s);
|
|
13
|
+
if (Number.isNaN(num)) return false;
|
|
14
|
+
// Ensure the string isn't just whitespace or contains trailing garbage
|
|
15
|
+
// number literal check that works for scientific notation, hex, etc.
|
|
16
|
+
return !Number.isNaN(parseFloat(s)) && Number.isFinite(num);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parses a strict double from s, or returns null if the string is not a full numeric token. */
|
|
20
|
+
export function strictParseDouble(s: string): number | null {
|
|
21
|
+
if (!fullStringIsDouble(s)) return null;
|
|
22
|
+
const num = Number(s);
|
|
23
|
+
return Number.isNaN(num) ? null : num;
|
|
24
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module validates CLI schemas before execution.
|
|
3
|
+
It checks reserved command names, handler placement, fallback rules, duplicate names,
|
|
4
|
+
and positional ordering before the runtime starts.
|
|
5
|
+
|
|
6
|
+
It fails early on structural problems so invalid trees never reach parsing or dispatch.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CliCommand,
|
|
11
|
+
CliFallbackMode,
|
|
12
|
+
CliSchemaValidationError,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
const reservedCommandNames = ["completion"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates the static CliCommand tree against ArgBarg rules.
|
|
19
|
+
* Throws CliSchemaValidationError if rules are violated.
|
|
20
|
+
*/
|
|
21
|
+
export function cliValidateRoot(root: CliCommand): void {
|
|
22
|
+
// Root-level rules
|
|
23
|
+
if (root.handler !== undefined) {
|
|
24
|
+
throw new CliSchemaValidationError("Program root must not set handler");
|
|
25
|
+
}
|
|
26
|
+
if ((root.positionals ?? []).length > 0) {
|
|
27
|
+
throw new CliSchemaValidationError("Program root must not declare positionals");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for reserved command names at root
|
|
31
|
+
for (const child of root.children ?? []) {
|
|
32
|
+
if (reservedCommandNames.includes(child.key)) {
|
|
33
|
+
throw new CliSchemaValidationError(`Reserved command name: ${child.key}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Recursively validate
|
|
38
|
+
walkCommand(root, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
42
|
+
// Fallback only on root
|
|
43
|
+
if (!isRoot && cmd.fallbackCommand !== undefined) {
|
|
44
|
+
throw new CliSchemaValidationError(
|
|
45
|
+
"Fallback is only supported on the program root (not on " + cmd.key + ")",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
!isRoot &&
|
|
50
|
+
cmd.fallbackMode !== undefined &&
|
|
51
|
+
cmd.fallbackMode !== CliFallbackMode.MissingOnly
|
|
52
|
+
) {
|
|
53
|
+
throw new CliSchemaValidationError(
|
|
54
|
+
"fallbackMode may only be set on the program root (not on " + cmd.key + ")",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ((cmd.children ?? []).length > 0) {
|
|
59
|
+
if (cmd.handler !== undefined) {
|
|
60
|
+
throw new CliSchemaValidationError(`Routing command must not set handler: ${cmd.key}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
if (cmd.handler === undefined) {
|
|
64
|
+
throw new CliSchemaValidationError(`Leaf command requires handler: ${cmd.key}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for duplicate child names
|
|
69
|
+
const seenNames = new Set<string>();
|
|
70
|
+
for (const child of cmd.children ?? []) {
|
|
71
|
+
if (seenNames.has(child.key)) {
|
|
72
|
+
throw new CliSchemaValidationError(`Duplicate command name: ${child.key}`);
|
|
73
|
+
}
|
|
74
|
+
seenNames.add(child.key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate options (short name uniqueness, reserved -h)
|
|
78
|
+
const seenShorts = new Set<string>();
|
|
79
|
+
for (const opt of cmd.options ?? []) {
|
|
80
|
+
if (opt.shortName !== undefined) {
|
|
81
|
+
if (opt.shortName === "h") {
|
|
82
|
+
throw new CliSchemaValidationError(
|
|
83
|
+
`Short alias -h is reserved for help: ${cmd.key}/${opt.name}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (seenShorts.has(opt.shortName)) {
|
|
87
|
+
throw new CliSchemaValidationError(
|
|
88
|
+
`Duplicate short alias -${opt.shortName} in scope ${cmd.key}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
seenShorts.add(opt.shortName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate positionals
|
|
96
|
+
const positionals = (cmd.positionals ?? []).filter((p) => p.positional);
|
|
97
|
+
for (const p of positionals) {
|
|
98
|
+
if (p.argMin < 0) {
|
|
99
|
+
throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${cmd.key}/${p.name}`);
|
|
100
|
+
}
|
|
101
|
+
if (p.argMax < 0) {
|
|
102
|
+
throw new CliSchemaValidationError(
|
|
103
|
+
`argMax must be >= 0 (use 0 for unlimited) for positional ${cmd.key}/${p.name}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (p.argMax > 0 && p.argMin > p.argMax) {
|
|
107
|
+
throw new CliSchemaValidationError(
|
|
108
|
+
`argMin must not exceed argMax for positional ${cmd.key}/${p.name}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check positional ordering: required before optional
|
|
114
|
+
let sawOptional = false;
|
|
115
|
+
for (const p of positionals) {
|
|
116
|
+
if (p.argMin === 0) {
|
|
117
|
+
sawOptional = true;
|
|
118
|
+
} else if (sawOptional) {
|
|
119
|
+
throw new CliSchemaValidationError(`Required positional after optional in scope ${cmd.key}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check unlimited positional must be last
|
|
124
|
+
for (let idx = 0; idx < positionals.length; idx++) {
|
|
125
|
+
if (positionals[idx].argMax === 0 && idx + 1 < positionals.length) {
|
|
126
|
+
throw new CliSchemaValidationError(
|
|
127
|
+
`Unlimited positional (argMax == 0) must be last in scope ${cmd.key}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Recurse into children
|
|
133
|
+
for (const child of cmd.children ?? []) {
|
|
134
|
+
walkCommand(child, false);
|
|
135
|
+
}
|
|
136
|
+
}
|
package/tsconfig.json
ADDED