bantamkit-mcp 0.26.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/LICENSE +21 -0
- package/README.md +398 -0
- package/assets/contracts/default.yaml +90 -0
- package/assets/evals/devteam/manifest.yaml +351 -0
- package/assets/evals/devteam/repo/HISTORY.md +18 -0
- package/assets/evals/devteam/repo/README.md +12 -0
- package/assets/evals/devteam/repo/docs/architecture.md +17 -0
- package/assets/evals/devteam/repo/docs/runbook.md +10 -0
- package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- package/assets/evals/fixtures/.gitkeep +0 -0
- package/assets/evals/fixtures/catalog.json +6 -0
- package/assets/evals/perturbations/task-completion.yaml +576 -0
- package/assets/evals/tasks/.gitkeep +0 -0
- package/assets/evals/tasks/extract-contact.yaml +14 -0
- package/assets/evals/tasks/extract-invoice.yaml +14 -0
- package/assets/evals/tasks/extract-order.yaml +15 -0
- package/assets/evals/tasks/extract-schedule.yaml +14 -0
- package/assets/evals/tasks/extract-versions.yaml +17 -0
- package/assets/evals/tasks/nav-prod-port.yaml +84 -0
- package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- package/assets/evals/tasks/recall-db-port.yaml +17 -0
- package/assets/evals/tasks/recall-deploy.yaml +13 -0
- package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- package/assets/evals/tasks/recall-oncall.yaml +13 -0
- package/assets/evals/tasks/recall-org-quota.yaml +18 -0
- package/assets/evals/tasks/recall-owner.yaml +13 -0
- package/assets/evals/tasks/shop-basket-total.yaml +10 -0
- package/assets/evals/tasks/shop-cheapest.yaml +9 -0
- package/assets/evals/tasks/shop-compare.yaml +9 -0
- package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- package/assets/evals/tasks/shop-stock-total.yaml +9 -0
- package/assets/evals/tasks/shop-total.yaml +9 -0
- package/assets/profiles/default.yaml +31 -0
- package/assets/profiles/patient.yaml +31 -0
- package/assets/rubrics/.gitkeep +0 -0
- package/assets/rubrics/code-quality.yaml +20 -0
- package/assets/rubrics/grounded-completion.yaml +37 -0
- package/assets/rubrics/task-completion.yaml +28 -0
- package/assets/schemas/shiftwork-checkpoint.json +188 -0
- package/assets/skills/.gitkeep +0 -0
- package/assets/skills/file-graph.md +7 -0
- package/assets/skills/memory.md +35 -0
- package/assets/tools/.gitkeep +0 -0
- package/assets/tools/bantamkit_read.json +48 -0
- package/assets/tools/bantamkit_status.json +25 -0
- package/assets/tools/build_identity.json +17 -0
- package/assets/tools/document_list.json +12 -0
- package/assets/tools/document_read.json +31 -0
- package/assets/tools/file_graph.json +12 -0
- package/assets/tools/memory_compact.json +31 -0
- package/assets/tools/memory_recall.json +38 -0
- package/assets/tools/memory_save.json +61 -0
- package/assets/tools/shiftwork_clock_in.json +25 -0
- package/assets/tools/shiftwork_clock_out.json +60 -0
- package/assets/tools/shiftwork_status.json +25 -0
- package/assets/tools/skill_audit.json +70 -0
- package/assets/tools/validate_json.json +31 -0
- package/dist/assets.d.ts +48 -0
- package/dist/assets.js +109 -0
- package/dist/charsets.d.ts +17 -0
- package/dist/charsets.js +580 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +280 -0
- package/dist/contract.d.ts +145 -0
- package/dist/contract.js +608 -0
- package/dist/docread.d.ts +357 -0
- package/dist/docread.js +3315 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +14 -0
- package/dist/eventlog.d.ts +137 -0
- package/dist/eventlog.js +294 -0
- package/dist/htmlentities.d.ts +2 -0
- package/dist/htmlentities.js +16 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +13 -0
- package/dist/mcp/identity.d.ts +25 -0
- package/dist/mcp/identity.js +275 -0
- package/dist/mcp/pyargs.d.ts +80 -0
- package/dist/mcp/pyargs.js +362 -0
- package/dist/mcp/sdkjson.d.ts +56 -0
- package/dist/mcp/sdkjson.js +91 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.js +740 -0
- package/dist/mcp/status.d.ts +210 -0
- package/dist/mcp/status.js +357 -0
- package/dist/mcp/transport.d.ts +68 -0
- package/dist/mcp/transport.js +111 -0
- package/dist/mcpreport.d.ts +214 -0
- package/dist/mcpreport.js +758 -0
- package/dist/memory/cli.d.ts +2 -0
- package/dist/memory/cli.js +396 -0
- package/dist/memory/component.d.ts +256 -0
- package/dist/memory/component.js +497 -0
- package/dist/memory/factfile.d.ts +120 -0
- package/dist/memory/factfile.js +406 -0
- package/dist/memory/layers.d.ts +92 -0
- package/dist/memory/layers.js +410 -0
- package/dist/memory/pyfs.d.ts +521 -0
- package/dist/memory/pyfs.js +1608 -0
- package/dist/memory/pyyaml.d.ts +155 -0
- package/dist/memory/pyyaml.js +868 -0
- package/dist/memory/store.d.ts +478 -0
- package/dist/memory/store.js +1034 -0
- package/dist/pyargparse.d.ts +141 -0
- package/dist/pyargparse.js +813 -0
- package/dist/pyjson.d.ts +110 -0
- package/dist/pyjson.js +524 -0
- package/dist/pyjsonschema.d.ts +50 -0
- package/dist/pyjsonschema.js +951 -0
- package/dist/shiftwork.d.ts +60 -0
- package/dist/shiftwork.js +324 -0
- package/dist/skillaudit.d.ts +172 -0
- package/dist/skillaudit.js +914 -0
- package/dist/statusline.d.ts +106 -0
- package/dist/statusline.js +281 -0
- package/package.json +45 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPython 3.12 `argparse`'s USER-FACING SURFACE, ported: the help text, the usage line, the
|
|
3
|
+
* error sentences, and the parse rules that produce them.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `cli.ts` used to carry a single hardcoded `USAGE` string and a `switch`
|
|
6
|
+
* over whole argv tokens. Both were wrong in ways nothing in the repository could see until
|
|
7
|
+
* `tools/conformance/suites/cli.mjs` ran the two CLIs as PROCESSES: the help went to stderr
|
|
8
|
+
* instead of stdout and was one line instead of twelve, the constant had already gone stale
|
|
9
|
+
* (no `[--assets-root]`), and `--store=/path` — the `=`-joined form every shell user types —
|
|
10
|
+
* exited 2 with "unrecognized arguments" against a reference that accepts it. A second
|
|
11
|
+
* hardcoded copy of the usage line is exactly the defect that produced this module, so there
|
|
12
|
+
* is NO literal usage string here: everything is computed from `ParserSpec`, at the width
|
|
13
|
+
* argparse would have used, by the algorithm argparse uses.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IS PORTED, AND WHAT IS DELIBERATELY NOT. This module serves TWO reference parsers,
|
|
16
|
+
* and the second one is why it grew. `_parse_args` in
|
|
17
|
+
* `runtime-py/src/bantamkit/mcpserver.py` is optionals only; `_parse_args` in
|
|
18
|
+
* `runtime-py/src/bantamkit/memory/__main__.py` is a subparsers tree with five commands, a
|
|
19
|
+
* required subcommand, a `type=` that raises `ArgumentTypeError`, and a positional under
|
|
20
|
+
* `restore`. So `consume_positionals`, `_match_arguments_partial`, `_get_nargs_pattern`,
|
|
21
|
+
* `_check_value` and the `required=` sweep ARE here now — each written against the running
|
|
22
|
+
* reference and not from memory. What is still left out is every branch NEITHER parser can
|
|
23
|
+
* reach: the other `nargs` spellings, `fromfile_prefix_chars`, required mutually exclusive
|
|
24
|
+
* groups, `SUPPRESS`, and `parse_intermixed_args`. Each omission is marked `NOT PORTED` at
|
|
25
|
+
* the place it would have gone, and `docs/porting.md` lists them.
|
|
26
|
+
*
|
|
27
|
+
* MEASURED, NOT TRANSCRIBED. Every rule below was read out of
|
|
28
|
+
* `python3.12/argparse.py` on this machine (3.12.13) AND checked against the running
|
|
29
|
+
* reference; `runtime-ts/test/cli-surface.test.mjs` pins the outputs that
|
|
30
|
+
* `tools/conformance/suites/cli.mjs`'s width matrix does not reach.
|
|
31
|
+
*
|
|
32
|
+
* THE HEADER `options:` IS INTERPRETER-DEPENDENT. 3.10+ prints `options:`; 3.9 and earlier
|
|
33
|
+
* print `optional arguments:`. This module matches the interpreter the conformance suite
|
|
34
|
+
* runs, which is the one pinned for this repo. On a 3.9 CI the `cli` suite would go red here
|
|
35
|
+
* and the fix would be a ruling, not a code change.
|
|
36
|
+
*/
|
|
37
|
+
import { pyRepr } from './memory/pyfs.js';
|
|
38
|
+
/**
|
|
39
|
+
* `argparse.ArgumentParser.error`: the usage block, one sentence, stderr, exit 2.
|
|
40
|
+
*
|
|
41
|
+
* `spec` is WHOSE usage block, and it is not decoration. A sub-parser reports its own prog
|
|
42
|
+
* and its own usage (`python -m bantamkit.memory status: error: …`) while an unrecognized
|
|
43
|
+
* argument that a sub-parser handed back reports the TOP parser's — measured, both.
|
|
44
|
+
*/
|
|
45
|
+
export class ArgvError extends Error {
|
|
46
|
+
spec;
|
|
47
|
+
constructor(spec, message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.spec = spec;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A `-h`/`--help` action fired mid-parse. argparse prints and exits 0 THERE, not after.
|
|
54
|
+
*
|
|
55
|
+
* `spec` is whose help: `restore -h` prints the sub-parser's, not the top parser's.
|
|
56
|
+
*/
|
|
57
|
+
export class HelpRequested extends Error {
|
|
58
|
+
spec;
|
|
59
|
+
constructor(spec = null) {
|
|
60
|
+
super();
|
|
61
|
+
this.spec = spec;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `argparse.ArgumentTypeError`: a `type=` callable's own sentence, printed VERBATIM.
|
|
66
|
+
*
|
|
67
|
+
* The distinction is wire-visible and it is the whole reason this class exists:
|
|
68
|
+
* `--budget 0` is `argument --budget: must be >= 1` (the callable's message) while
|
|
69
|
+
* `--budget x` is `argument --budget: invalid _positive value: 'x'` (argparse's, because
|
|
70
|
+
* `int()` raised `ValueError`). One `catch` for both would print the wrong one half the time.
|
|
71
|
+
*/
|
|
72
|
+
export class ArgumentTypeError extends Error {
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `int(text)`, which is what `type=int` is — and the first half of `type=_positive`.
|
|
76
|
+
*
|
|
77
|
+
* It rejects everything `int()` rejects, and the caller reports that as an ARGUMENT error
|
|
78
|
+
* (exit 2). It lives here rather than beside one parser because two parsers now need it and
|
|
79
|
+
* a second hand-written copy of a conversion rule is the defect this module was built from.
|
|
80
|
+
*/
|
|
81
|
+
export function pyIntStrict(text) {
|
|
82
|
+
if (!/^\s*[+-]?\d+(?:_\d+)*\s*$/.test(text))
|
|
83
|
+
throw new TypeError('not an int');
|
|
84
|
+
return Number(text.trim().replace(/_/g, ''));
|
|
85
|
+
}
|
|
86
|
+
/** `_get_action_name`: the name argparse puts after `argument `. */
|
|
87
|
+
const actionName = (action) => action.optionStrings.length > 0
|
|
88
|
+
? action.optionStrings.join('/')
|
|
89
|
+
: (action.metavar ?? action.dest);
|
|
90
|
+
const argumentError = (spec, action, message) => new ArgvError(spec, action === null ? message : `argument ${actionName(action)}: ${message}`);
|
|
91
|
+
// ------------------------------------------------------------------------- textwrap
|
|
92
|
+
/*
|
|
93
|
+
* `textwrap.wrap`, ported. Node has no stdlib equivalent — `grep -rln 'textwrap\|wrap('
|
|
94
|
+
* runtime-ts/src/` returned nothing before this file — so this is a port, not a call.
|
|
95
|
+
*
|
|
96
|
+
* Only the defaults argparse uses are here: `break_long_words=True`, `break_on_hyphens=True`,
|
|
97
|
+
* `drop_whitespace=True`, no indents, no `max_lines`. NOT PORTED: `initial_indent` /
|
|
98
|
+
* `subsequent_indent` (argparse's only `fill` call sits at indent 0), `expand_tabs` and
|
|
99
|
+
* `replace_whitespace` (both callers pre-normalise with `_whitespace_matcher` below, so no
|
|
100
|
+
* tab or newline can reach `wrap`), `fix_sentence_endings`, and `max_lines`/`placeholder`.
|
|
101
|
+
*/
|
|
102
|
+
/** `re.compile(r'\s+', re.ASCII)` — argparse's, and ASCII on purpose. */
|
|
103
|
+
const ASCII_WHITESPACE = /[ \t\n\r\f\v]+/g;
|
|
104
|
+
/** What both `_split_lines` and `_fill_text` do before they wrap. */
|
|
105
|
+
export const collapseWhitespace = (text) => text.replace(ASCII_WHITESPACE, ' ').trim();
|
|
106
|
+
/**
|
|
107
|
+
* `textwrap.wordsep_re`, translated character class for character class.
|
|
108
|
+
*
|
|
109
|
+
* `\w` and `[^\d\W]` are ASCII in JavaScript and Unicode in Python. The help text this
|
|
110
|
+
* module wraps is ASCII by construction and the conformance suite compares bytes, so the
|
|
111
|
+
* difference is unobservable here; a non-ASCII help string would be a new measurement.
|
|
112
|
+
*/
|
|
113
|
+
const WS = '[\\t\\n\\x0b\\f\\r ]';
|
|
114
|
+
const NWS = '[^\\t\\n\\x0b\\f\\r ]';
|
|
115
|
+
const WORD_PUNCT = '[\\w!"\'&.,?]';
|
|
116
|
+
const LETTER = '[^\\d\\W]';
|
|
117
|
+
const WORDSEP = new RegExp('(' +
|
|
118
|
+
`${WS}+` +
|
|
119
|
+
'|' +
|
|
120
|
+
`(?<=${WORD_PUNCT})-{2,}(?=\\w)` +
|
|
121
|
+
'|' +
|
|
122
|
+
`${NWS}+?(?:` +
|
|
123
|
+
`-(?:(?<=${LETTER}{2}-)|(?<=${LETTER}-${LETTER}-))(?=${LETTER}-?${LETTER})` +
|
|
124
|
+
'|' +
|
|
125
|
+
`(?=${WS}|$)` +
|
|
126
|
+
'|' +
|
|
127
|
+
`(?<=${WORD_PUNCT})(?=-{2,}\\w)` +
|
|
128
|
+
')' +
|
|
129
|
+
')', 'g');
|
|
130
|
+
/**
|
|
131
|
+
* `TextWrapper._split`: `wordsep_re.split(text)` with the empties dropped.
|
|
132
|
+
*
|
|
133
|
+
* Every alternative in `WORDSEP` consumes at least one character, so the scan below always
|
|
134
|
+
* advances and the "text between two matches" arm only fires for input the pattern cannot
|
|
135
|
+
* reach (it never does for pre-normalised text; it is here because `re.split` would emit
|
|
136
|
+
* that text too).
|
|
137
|
+
*/
|
|
138
|
+
function splitChunks(text) {
|
|
139
|
+
const chunks = [];
|
|
140
|
+
WORDSEP.lastIndex = 0;
|
|
141
|
+
let last = 0;
|
|
142
|
+
let match;
|
|
143
|
+
while ((match = WORDSEP.exec(text)) !== null) {
|
|
144
|
+
if (match.index > last)
|
|
145
|
+
chunks.push(text.slice(last, match.index));
|
|
146
|
+
chunks.push(match[0]);
|
|
147
|
+
last = match.index + match[0].length;
|
|
148
|
+
}
|
|
149
|
+
if (last < text.length)
|
|
150
|
+
chunks.push(text.slice(last));
|
|
151
|
+
return chunks.filter((chunk) => chunk !== '');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `TextWrapper._handle_long_word` with `break_long_words` and `break_on_hyphens` both on.
|
|
155
|
+
*
|
|
156
|
+
* `chunk.rfind('-', 0, space_left)` searches `chunk[0:space_left]`; `lastIndexOf` clamps a
|
|
157
|
+
* negative position to 0 instead of finding nothing, so `space_left == 0` is answered here
|
|
158
|
+
* rather than by the search.
|
|
159
|
+
*/
|
|
160
|
+
function handleLongWord(reversed, line, lineLength, width) {
|
|
161
|
+
const spaceLeft = width < 1 ? 1 : width - lineLength;
|
|
162
|
+
const chunk = reversed[reversed.length - 1];
|
|
163
|
+
let end = spaceLeft;
|
|
164
|
+
if (chunk.length > spaceLeft) {
|
|
165
|
+
const hyphen = spaceLeft > 0 ? chunk.lastIndexOf('-', spaceLeft - 1) : -1;
|
|
166
|
+
if (hyphen > 0 && [...chunk.slice(0, hyphen)].some((ch) => ch !== '-'))
|
|
167
|
+
end = hyphen + 1;
|
|
168
|
+
}
|
|
169
|
+
line.push(chunk.slice(0, end));
|
|
170
|
+
reversed[reversed.length - 1] = chunk.slice(end);
|
|
171
|
+
}
|
|
172
|
+
/** `textwrap.wrap(text, width)`. `text` must already be collapsed. */
|
|
173
|
+
export function wrap(text, width) {
|
|
174
|
+
const reversed = splitChunks(text).reverse();
|
|
175
|
+
const lines = [];
|
|
176
|
+
while (reversed.length > 0) {
|
|
177
|
+
const line = [];
|
|
178
|
+
let lineLength = 0;
|
|
179
|
+
if (lines.length > 0 && reversed[reversed.length - 1].trim() === '')
|
|
180
|
+
reversed.pop();
|
|
181
|
+
while (reversed.length > 0) {
|
|
182
|
+
const length = reversed[reversed.length - 1].length;
|
|
183
|
+
if (lineLength + length > width)
|
|
184
|
+
break;
|
|
185
|
+
line.push(reversed.pop());
|
|
186
|
+
lineLength += length;
|
|
187
|
+
}
|
|
188
|
+
if (reversed.length > 0 && reversed[reversed.length - 1].length > width) {
|
|
189
|
+
handleLongWord(reversed, line, lineLength, width);
|
|
190
|
+
lineLength = line.reduce((total, chunk) => total + chunk.length, 0);
|
|
191
|
+
}
|
|
192
|
+
if (line.length > 0 && line[line.length - 1].trim() === '') {
|
|
193
|
+
lineLength -= line[line.length - 1].length;
|
|
194
|
+
line.pop();
|
|
195
|
+
}
|
|
196
|
+
if (line.length > 0)
|
|
197
|
+
lines.push(line.join(''));
|
|
198
|
+
}
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
/** `textwrap.fill(text, width)` at indent 0. */
|
|
202
|
+
export const fill = (text, width) => wrap(text, width).join('\n');
|
|
203
|
+
// ---------------------------------------------------------------------- the width
|
|
204
|
+
/** `int(...)` as `shutil.get_terminal_size` calls it: a whole number, nothing else. */
|
|
205
|
+
function pyIntOrNull(text) {
|
|
206
|
+
if (!/^[ \t\n\r\f\v]*[+-]?\d+(?:_\d+)*[ \t\n\r\f\v]*$/.test(text))
|
|
207
|
+
return null;
|
|
208
|
+
return Number(text.trim().replace(/_/g, ''));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* `shutil.get_terminal_size().columns`, which is what `HelpFormatter` asks for.
|
|
212
|
+
*
|
|
213
|
+
* `COLUMNS` wins when it parses to a positive integer; otherwise the size of STDOUT's
|
|
214
|
+
* terminal; otherwise 80. `spawnSync` hands a child pipes rather than a tty, so 80 is the
|
|
215
|
+
* width the conformance suite and every MCP host actually see — the DEFAULT help is the
|
|
216
|
+
* wrapped form, not the wide one.
|
|
217
|
+
*/
|
|
218
|
+
export function terminalColumns() {
|
|
219
|
+
const raw = process.env.COLUMNS;
|
|
220
|
+
const fromEnv = raw === undefined ? null : pyIntOrNull(raw);
|
|
221
|
+
if (fromEnv !== null && fromEnv > 0)
|
|
222
|
+
return fromEnv;
|
|
223
|
+
if (process.stdout.isTTY && typeof process.stdout.columns === 'number' && process.stdout.columns > 0) {
|
|
224
|
+
return process.stdout.columns;
|
|
225
|
+
}
|
|
226
|
+
return 80;
|
|
227
|
+
}
|
|
228
|
+
/** `HelpFormatter.__init__`: `width = shutil.get_terminal_size().columns - 2`. */
|
|
229
|
+
export const helpWidth = () => terminalColumns() - 2;
|
|
230
|
+
// ------------------------------------------------------------------- the formatter
|
|
231
|
+
/** An action with no option strings is a POSITIONAL, and argparse branches on exactly that. */
|
|
232
|
+
const isPositional = (action) => action.optionStrings.length === 0;
|
|
233
|
+
/**
|
|
234
|
+
* `_metavar_formatter`: an explicit `metavar=`, else the CHOICES, else the default.
|
|
235
|
+
*
|
|
236
|
+
* The choices arm is what prints `{status,lint,compact,archived,restore}` — the subparsers
|
|
237
|
+
* action carries no `metavar=` in the reference, so the brace list is generated, and adding
|
|
238
|
+
* a sixth subcommand widens the usage line without anyone editing a string.
|
|
239
|
+
*/
|
|
240
|
+
const metavarOf = (action) => {
|
|
241
|
+
if (action.metavar !== undefined)
|
|
242
|
+
return action.metavar;
|
|
243
|
+
if (action.subcommands !== undefined)
|
|
244
|
+
return `{${action.subcommands.map((sub) => sub.name).join(',')}}`;
|
|
245
|
+
// `_get_default_metavar_for_positional` is the bare `dest`; the optional one upper-cases it.
|
|
246
|
+
return isPositional(action) ? action.dest : action.dest.toUpperCase();
|
|
247
|
+
};
|
|
248
|
+
/** `_format_args`: the slot(s) an action occupies in the USAGE line. `PARSER` trails ` ...`. */
|
|
249
|
+
const formatArgs = (action) => action.kind === 'subparsers' ? `${metavarOf(action)} ...` : metavarOf(action);
|
|
250
|
+
/**
|
|
251
|
+
* `_format_action_invocation` — the left column of the help table.
|
|
252
|
+
*
|
|
253
|
+
* A positional prints its metavar and NOT `_format_args`: the ` ...` that `PARSER` adds to
|
|
254
|
+
* the usage line is absent from the table, which is why `{status,…}` appears there bare.
|
|
255
|
+
*/
|
|
256
|
+
const invocationOf = (action) => {
|
|
257
|
+
if (isPositional(action))
|
|
258
|
+
return metavarOf(action);
|
|
259
|
+
return action.kind === 'store'
|
|
260
|
+
? `${action.optionStrings.join(', ')} ${metavarOf(action)}`
|
|
261
|
+
: action.optionStrings.join(', ');
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* `_format_actions_usage`, over whichever slice of the parser's actions it is handed.
|
|
265
|
+
*
|
|
266
|
+
* The SLICE matters: `_format_usage` calls this three times — optionals, positionals, and
|
|
267
|
+
* the two concatenated — because the wrapping algorithm below breaks the two lists
|
|
268
|
+
* independently. Group membership is resolved by IDENTITY rather than by index, so a group
|
|
269
|
+
* whose members are absent from the slice (every group is optionals-only) drops out instead
|
|
270
|
+
* of bracketing the wrong action.
|
|
271
|
+
*
|
|
272
|
+
* A POSITIONAL IS NEVER BRACKETED. `[]` in a usage line means "may be omitted", and both
|
|
273
|
+
* positionals in this runtime are required — the subcommand by `required=True`, `name` by
|
|
274
|
+
* `nargs=None`. NOT PORTED: the arm that strips the outer `[]` off a positional INSIDE a
|
|
275
|
+
* mutually exclusive group, the `SUPPRESS` arm (drop the action and the `|` beside it), and
|
|
276
|
+
* the `required` group arm (`(a | b)`).
|
|
277
|
+
*/
|
|
278
|
+
function formatActionsUsage(actions, groups) {
|
|
279
|
+
const inserts = new Map();
|
|
280
|
+
const grouped = new Set();
|
|
281
|
+
for (const group of groups) {
|
|
282
|
+
const indices = group.map((action) => actions.indexOf(action));
|
|
283
|
+
if (indices.some((index) => index === -1))
|
|
284
|
+
continue;
|
|
285
|
+
const start = indices[0];
|
|
286
|
+
const end = start + indices.length;
|
|
287
|
+
if (!indices.every((index, offset) => index === start + offset))
|
|
288
|
+
continue;
|
|
289
|
+
for (const index of indices)
|
|
290
|
+
grouped.add(index);
|
|
291
|
+
inserts.set(start, inserts.has(start) ? `${inserts.get(start)} [` : '[');
|
|
292
|
+
inserts.set(end, inserts.has(end) ? `${inserts.get(end)}]` : ']');
|
|
293
|
+
for (let index = start + 1; index < end; index += 1)
|
|
294
|
+
inserts.set(index, '|');
|
|
295
|
+
}
|
|
296
|
+
const parts = actions.map((action, index) => {
|
|
297
|
+
if (isPositional(action))
|
|
298
|
+
return formatArgs(action);
|
|
299
|
+
const body = action.kind === 'store'
|
|
300
|
+
? `${action.optionStrings[0]} ${metavarOf(action)}`
|
|
301
|
+
: action.optionStrings[0];
|
|
302
|
+
return grouped.has(index) ? body : `[${body}]`;
|
|
303
|
+
});
|
|
304
|
+
for (const index of [...inserts.keys()].sort((a, b) => b - a))
|
|
305
|
+
parts.splice(index, 0, inserts.get(index));
|
|
306
|
+
let text = parts.join(' ');
|
|
307
|
+
text = text.replace(/([[(]) /g, '$1');
|
|
308
|
+
text = text.replace(/ ([\])])/g, '$1');
|
|
309
|
+
text = text.replace(/[[(] *[\])]/g, '');
|
|
310
|
+
return text.trim();
|
|
311
|
+
}
|
|
312
|
+
/** `HelpFormatter._format_usage`, which ends in a BLANK line (`'\n\n'`). */
|
|
313
|
+
export function formatUsage(spec, width, prefix = 'usage: ') {
|
|
314
|
+
const prog = spec.prog;
|
|
315
|
+
// `_format_usage` splits the actions in two and formats the halves SEPARATELY, then joins
|
|
316
|
+
// `optionals + positionals` for the single-line attempt. The order is argparse's, not the
|
|
317
|
+
// declaration order: a positional declared before an option still prints last.
|
|
318
|
+
const groups = spec.groups.map((group) => group.map((index) => spec.actions[index]));
|
|
319
|
+
const optionals = spec.actions.filter((action) => !isPositional(action));
|
|
320
|
+
const positionals = spec.actions.filter(isPositional);
|
|
321
|
+
const actionUsage = formatActionsUsage([...optionals, ...positionals], groups);
|
|
322
|
+
let usage = [prog, actionUsage].filter((part) => part !== '').join(' ');
|
|
323
|
+
// `text_width = self._width - self._current_indent`, and the usage sits at indent 0.
|
|
324
|
+
const textWidth = width;
|
|
325
|
+
if (prefix.length + usage.length > textWidth) {
|
|
326
|
+
// argparse's `part_regexp`, verbatim. Bracketed groups stay whole, so
|
|
327
|
+
// `[--store STORE | --start START]` never breaks across two lines.
|
|
328
|
+
const split = (text) => text.match(/\(.*?\)+(?=\s|$)|\[.*?\]+(?=\s|$)|\S+/g) ?? [];
|
|
329
|
+
const optParts = split(formatActionsUsage(optionals, groups));
|
|
330
|
+
const posParts = split(formatActionsUsage(positionals, groups));
|
|
331
|
+
const getLines = (chunks, indent, first) => {
|
|
332
|
+
const lines = [];
|
|
333
|
+
let line = [];
|
|
334
|
+
let lineLength = (first === undefined ? indent.length : first.length) - 1;
|
|
335
|
+
for (const chunk of chunks) {
|
|
336
|
+
if (lineLength + 1 + chunk.length > textWidth && line.length > 0) {
|
|
337
|
+
lines.push(indent + line.join(' '));
|
|
338
|
+
line = [];
|
|
339
|
+
lineLength = indent.length - 1;
|
|
340
|
+
}
|
|
341
|
+
line.push(chunk);
|
|
342
|
+
lineLength += chunk.length + 1;
|
|
343
|
+
}
|
|
344
|
+
if (line.length > 0)
|
|
345
|
+
lines.push(indent + line.join(' '));
|
|
346
|
+
if (first !== undefined && lines.length > 0)
|
|
347
|
+
lines[0] = lines[0].slice(indent.length);
|
|
348
|
+
return lines;
|
|
349
|
+
};
|
|
350
|
+
let lines;
|
|
351
|
+
if (prefix.length + prog.length <= 0.75 * textWidth) {
|
|
352
|
+
// Short prog: the first optional follows it, and every continuation lines up under it.
|
|
353
|
+
// The positionals then start their OWN run at the same indent — which is why
|
|
354
|
+
// `{status,…} ...` lands on a line of its own under `python -m bantamkit.memory [-h]`
|
|
355
|
+
// instead of being packed onto it.
|
|
356
|
+
const indent = ' '.repeat(prefix.length + prog.length + 1);
|
|
357
|
+
if (optParts.length > 0) {
|
|
358
|
+
lines = getLines([prog, ...optParts], indent, prefix);
|
|
359
|
+
lines.push(...getLines(posParts, indent));
|
|
360
|
+
}
|
|
361
|
+
else if (posParts.length > 0) {
|
|
362
|
+
lines = getLines([prog, ...posParts], indent, prefix);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
lines = [prog];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Long prog: it gets a line of its own and the arguments hang at `len(prefix)`.
|
|
370
|
+
// argparse packs both lists together FIRST and only re-splits them when that took more
|
|
371
|
+
// than one line. With no positionals the two attempts are the same list and the
|
|
372
|
+
// re-split is a no-op, which is why this branch used to be one line.
|
|
373
|
+
const indent = ' '.repeat(prefix.length);
|
|
374
|
+
lines = getLines([...optParts, ...posParts], indent);
|
|
375
|
+
if (lines.length > 1) {
|
|
376
|
+
lines = [...getLines(optParts, indent), ...getLines(posParts, indent)];
|
|
377
|
+
}
|
|
378
|
+
lines = [prog, ...lines];
|
|
379
|
+
}
|
|
380
|
+
usage = lines.join('\n');
|
|
381
|
+
}
|
|
382
|
+
return `${prefix}${usage}\n\n`;
|
|
383
|
+
}
|
|
384
|
+
/** `HelpFormatter.format_help`'s tail: collapse long breaks, strip, end in exactly one `\n`. */
|
|
385
|
+
const finish = (text) => text === '' ? '' : `${text.replace(/\n\n\n+/g, '\n\n').replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
|
|
386
|
+
/** `ArgumentParser.format_usage` — the usage block alone, one trailing newline. */
|
|
387
|
+
export const formatUsageBlock = (spec, width) => finish(formatUsage(spec, width));
|
|
388
|
+
/** The `_ChoicesPseudoAction` argparse synthesises for one `add_parser(name, help=...)`. */
|
|
389
|
+
const pseudoAction = (sub) => ({
|
|
390
|
+
optionStrings: [],
|
|
391
|
+
dest: sub.name,
|
|
392
|
+
metavar: sub.name,
|
|
393
|
+
kind: 'positional',
|
|
394
|
+
help: sub.help,
|
|
395
|
+
defaultValue: null,
|
|
396
|
+
});
|
|
397
|
+
/**
|
|
398
|
+
* `HelpFormatter._format_action`, including the indented children a subparsers action owns.
|
|
399
|
+
*
|
|
400
|
+
* `indent` is `_current_indent`: 2 inside a section, 4 for a subcommand row. It is an
|
|
401
|
+
* argument rather than a constant because `action_width` is measured from it, which is what
|
|
402
|
+
* puts `status`'s help in the same column as `-h, --help`'s despite the deeper indent.
|
|
403
|
+
*/
|
|
404
|
+
function formatAction(action, indent, helpPosition, width) {
|
|
405
|
+
const helpTextWidth = Math.max(width - helpPosition, 11);
|
|
406
|
+
const actionWidth = helpPosition - indent - 2;
|
|
407
|
+
const invocation = invocationOf(action);
|
|
408
|
+
const pad = ' '.repeat(indent);
|
|
409
|
+
let body = '';
|
|
410
|
+
let indentFirst = 0;
|
|
411
|
+
if (action.help === '') {
|
|
412
|
+
// `if not action.help`: the header takes a line of its own and nothing follows it. This
|
|
413
|
+
// is the subparsers row — `{status,lint,…}` above its children.
|
|
414
|
+
body += `${pad}${invocation}\n`;
|
|
415
|
+
}
|
|
416
|
+
else if (invocation.length <= actionWidth) {
|
|
417
|
+
// Short header: the help starts on the same line, padded to the help column.
|
|
418
|
+
body += `${pad}${invocation.padEnd(actionWidth)} `;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Long header: its own line, and the help hangs at the help column.
|
|
422
|
+
body += `${pad}${invocation}\n`;
|
|
423
|
+
indentFirst = helpPosition;
|
|
424
|
+
}
|
|
425
|
+
if (action.help.trim() !== '') {
|
|
426
|
+
const lines = wrap(collapseWhitespace(action.help), helpTextWidth);
|
|
427
|
+
body += `${' '.repeat(indentFirst)}${lines[0]}\n`;
|
|
428
|
+
for (const line of lines.slice(1))
|
|
429
|
+
body += `${' '.repeat(helpPosition)}${line}\n`;
|
|
430
|
+
}
|
|
431
|
+
else if (!body.endsWith('\n')) {
|
|
432
|
+
body += '\n';
|
|
433
|
+
}
|
|
434
|
+
for (const sub of action.subcommands ?? []) {
|
|
435
|
+
body += formatAction(pseudoAction(sub), indent + 2, helpPosition, width);
|
|
436
|
+
}
|
|
437
|
+
return body;
|
|
438
|
+
}
|
|
439
|
+
/** `ArgumentParser.format_help`. */
|
|
440
|
+
export function formatHelp(spec, width) {
|
|
441
|
+
// `_max_help_position = min(24, max(width - 20, indent_increment * 2))`. It is 24 across
|
|
442
|
+
// the conformance matrix and it is NOT a constant: a longer flag or a narrow terminal
|
|
443
|
+
// moves it, and hardcoding 24 would be a latent bug the day either happens.
|
|
444
|
+
const maxHelpPosition = Math.min(24, Math.max(width - 20, 4));
|
|
445
|
+
// `add_argument` measures the action's invocation AND every subaction's, all at the
|
|
446
|
+
// section indent of 2 — `_iter_indented_subactions` restores `_current_indent` before the
|
|
447
|
+
// max is taken, so a subcommand name is NOT measured at its deeper printing indent.
|
|
448
|
+
const actionMaxLength = Math.max(...spec.actions.map((action) => Math.max(invocationOf(action).length, ...(action.subcommands ?? []).map((sub) => sub.name.length)))) + 2;
|
|
449
|
+
const helpPosition = Math.min(actionMaxLength + 2, maxHelpPosition);
|
|
450
|
+
// `_action_groups` in the order argparse creates them, and a section with no items formats
|
|
451
|
+
// to nothing — which is what keeps `positional arguments:` off a parser that has none.
|
|
452
|
+
const sections = [
|
|
453
|
+
['positional arguments', spec.actions.filter(isPositional)],
|
|
454
|
+
['options', spec.actions.filter((action) => !isPositional(action))],
|
|
455
|
+
];
|
|
456
|
+
let body = '';
|
|
457
|
+
for (const [title, actions] of sections) {
|
|
458
|
+
if (actions.length === 0)
|
|
459
|
+
continue;
|
|
460
|
+
let items = '';
|
|
461
|
+
for (const action of actions)
|
|
462
|
+
items += formatAction(action, 2, helpPosition, width);
|
|
463
|
+
body += `\n${title}:\n${items}\n`;
|
|
464
|
+
}
|
|
465
|
+
return finish(formatUsage(spec, width) +
|
|
466
|
+
// `add_text(None)` adds NOTHING, which is not the same as adding an empty paragraph:
|
|
467
|
+
// the sub-parsers below carry no `description=` and must not print a blank line for it.
|
|
468
|
+
(spec.description === ''
|
|
469
|
+
? ''
|
|
470
|
+
: `${fill(collapseWhitespace(spec.description), Math.max(width, 11))}\n\n`) +
|
|
471
|
+
body);
|
|
472
|
+
}
|
|
473
|
+
// ---------------------------------------------------------------------- the parser
|
|
474
|
+
/** `_negative_number_matcher`. */
|
|
475
|
+
const NEGATIVE_NUMBER = /^-\d+$|^-\d*\.\d+$/;
|
|
476
|
+
/**
|
|
477
|
+
* `ArgumentParser.parse_known_args`: the namespace keyed by `dest`, plus what it could not
|
|
478
|
+
* place.
|
|
479
|
+
*
|
|
480
|
+
* Throws `HelpRequested` where argparse would `parser.exit(0)` mid-parse — which is DURING
|
|
481
|
+
* the scan, not after it, so `-h --nope` prints help and exits 0 exactly as the reference
|
|
482
|
+
* does, and `restore -h --nope` prints the SUB-parser's help for the same reason.
|
|
483
|
+
*
|
|
484
|
+
* EXTRAS ARE RETURNED RATHER THAN RAISED, and that is the whole reason this function is
|
|
485
|
+
* separate from `parseArgs`. A subparser hands its leftovers BACK to the parser that
|
|
486
|
+
* dispatched to it (`_UNRECOGNIZED_ARGS_ATTR`), so `... status extra` reports
|
|
487
|
+
* `unrecognized arguments: extra` under the TOP parser's prog and usage, not under
|
|
488
|
+
* `status`'s — measured against the reference, both ways round.
|
|
489
|
+
*
|
|
490
|
+
* NOT PORTED: `parse_intermixed_args`, `fromfile_prefix_chars`, `choices` on anything but a
|
|
491
|
+
* subparsers action, required mutually exclusive groups (`one of the arguments … is
|
|
492
|
+
* required`), and every `nargs` outside the three `_getNargsPattern` names.
|
|
493
|
+
*/
|
|
494
|
+
export function parseKnownArgs(spec, argv) {
|
|
495
|
+
const byOptionString = new Map();
|
|
496
|
+
for (const action of spec.actions)
|
|
497
|
+
for (const option of action.optionStrings)
|
|
498
|
+
byOptionString.set(option, action);
|
|
499
|
+
const conflicts = new Map();
|
|
500
|
+
for (const group of spec.groups) {
|
|
501
|
+
for (const index of group) {
|
|
502
|
+
const action = spec.actions[index];
|
|
503
|
+
conflicts.set(action, group.filter((other) => other !== index).map((other) => spec.actions[other]));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const values = {};
|
|
507
|
+
for (const action of spec.actions)
|
|
508
|
+
values[action.dest] = action.defaultValue;
|
|
509
|
+
const seenNonDefault = new Set();
|
|
510
|
+
/** `seen_actions`, which is what the `required=` check at the bottom reads. */
|
|
511
|
+
const seenActions = new Set();
|
|
512
|
+
/** argparse mutates its `positionals` list as they are consumed; so does this. */
|
|
513
|
+
const remainingPositionals = spec.actions.filter(isPositional);
|
|
514
|
+
/** `_get_option_tuples`, both prefix arms. */
|
|
515
|
+
function optionTuplesFor(argString) {
|
|
516
|
+
const result = [];
|
|
517
|
+
if (argString.startsWith('--')) {
|
|
518
|
+
const equals = argString.indexOf('=');
|
|
519
|
+
const prefix = equals === -1 ? argString : argString.slice(0, equals);
|
|
520
|
+
const sep = equals === -1 ? null : '=';
|
|
521
|
+
const explicit = equals === -1 ? null : argString.slice(equals + 1);
|
|
522
|
+
for (const [option, action] of byOptionString) {
|
|
523
|
+
if (option.startsWith(prefix))
|
|
524
|
+
result.push({ action, optionString: option, sep, explicit });
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
// A single-dash string: `-h`'s value may be glued to it (`-hx`), so the two-character
|
|
529
|
+
// prefix is tried as an exact option before the abbreviation scan.
|
|
530
|
+
const equals = argString.indexOf('=');
|
|
531
|
+
const prefix = equals === -1 ? argString : argString.slice(0, equals);
|
|
532
|
+
const sep = equals === -1 ? null : '=';
|
|
533
|
+
const explicit = equals === -1 ? null : argString.slice(equals + 1);
|
|
534
|
+
const shortPrefix = argString.slice(0, 2);
|
|
535
|
+
const shortExplicit = argString.slice(2);
|
|
536
|
+
for (const [option, action] of byOptionString) {
|
|
537
|
+
if (option === shortPrefix)
|
|
538
|
+
result.push({ action, optionString: option, sep: '', explicit: shortExplicit });
|
|
539
|
+
else if (option.startsWith(prefix))
|
|
540
|
+
result.push({ action, optionString: option, sep, explicit });
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
/** `_parse_optional`: `null` means "this is not an option string at all". */
|
|
545
|
+
function parseOptional(argString) {
|
|
546
|
+
if (argString === '')
|
|
547
|
+
return null;
|
|
548
|
+
if (!argString.startsWith('-'))
|
|
549
|
+
return null;
|
|
550
|
+
const exact = byOptionString.get(argString);
|
|
551
|
+
if (exact !== undefined)
|
|
552
|
+
return [{ action: exact, optionString: argString, sep: null, explicit: null }];
|
|
553
|
+
if (argString.length === 1)
|
|
554
|
+
return null;
|
|
555
|
+
const equals = argString.indexOf('=');
|
|
556
|
+
if (equals !== -1) {
|
|
557
|
+
const head = argString.slice(0, equals);
|
|
558
|
+
const known = byOptionString.get(head);
|
|
559
|
+
if (known !== undefined) {
|
|
560
|
+
return [{ action: known, optionString: head, sep: '=', explicit: argString.slice(equals + 1) }];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const tuples = optionTuplesFor(argString);
|
|
564
|
+
if (tuples.length > 0)
|
|
565
|
+
return tuples;
|
|
566
|
+
// `_has_negative_number_optionals` is empty for this parser, so a negative number is
|
|
567
|
+
// always a positional here.
|
|
568
|
+
if (NEGATIVE_NUMBER.test(argString))
|
|
569
|
+
return null;
|
|
570
|
+
if (argString.includes(' '))
|
|
571
|
+
return null;
|
|
572
|
+
return [{ action: null, optionString: argString, sep: null, explicit: null }];
|
|
573
|
+
}
|
|
574
|
+
// The scan: an 'O' where an option string sits, an 'A' where a value sits, and a '-' for
|
|
575
|
+
// the `--` separator, after which nothing is an option any more.
|
|
576
|
+
const optionTuples = new Map();
|
|
577
|
+
const pattern = [];
|
|
578
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
579
|
+
if (argv[index] === '--') {
|
|
580
|
+
pattern.push('-');
|
|
581
|
+
for (let rest = index + 1; rest < argv.length; rest += 1)
|
|
582
|
+
pattern.push('A');
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
const tuples = parseOptional(argv[index]);
|
|
586
|
+
if (tuples === null)
|
|
587
|
+
pattern.push('A');
|
|
588
|
+
else {
|
|
589
|
+
optionTuples.set(index, tuples);
|
|
590
|
+
pattern.push('O');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const extras = [];
|
|
594
|
+
/** `_get_value`: `type=` applied, with argparse's two DIFFERENT failure sentences. */
|
|
595
|
+
function convert(action, raw) {
|
|
596
|
+
if (action.convert === undefined)
|
|
597
|
+
return raw;
|
|
598
|
+
try {
|
|
599
|
+
return action.convert(raw);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
// `ArgumentTypeError` carries the callable's OWN sentence and argparse prints it
|
|
603
|
+
// verbatim; a `ValueError`/`TypeError` gets argparse's `invalid <type> value:` instead.
|
|
604
|
+
if (error instanceof ArgumentTypeError)
|
|
605
|
+
throw argumentError(spec, action, error.message);
|
|
606
|
+
throw argumentError(spec, action, `invalid ${action.typeName ?? '?'} value: ${pyRepr(raw)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/** `_get_value` + `_check_value` + the conflict check + the action itself. */
|
|
610
|
+
function takeAction(action, args) {
|
|
611
|
+
// `seen_actions.add` happens BEFORE the value is built, so an action that fails its own
|
|
612
|
+
// conversion is still "seen" and never also reported as missing.
|
|
613
|
+
seenActions.add(action);
|
|
614
|
+
let value;
|
|
615
|
+
if (action.kind === 'subparsers') {
|
|
616
|
+
// `_get_values`, PARSER arm: convert every token, CHECK ONLY THE FIRST. The check is
|
|
617
|
+
// `_check_value` against `choices`, so a bad subcommand is an error of the parser that
|
|
618
|
+
// OWNS the subparsers action — top-level prog, top-level usage.
|
|
619
|
+
const subcommands = action.subcommands ?? [];
|
|
620
|
+
const name = args[0];
|
|
621
|
+
const chosen = subcommands.find((sub) => sub.name === name);
|
|
622
|
+
if (chosen === undefined) {
|
|
623
|
+
throw argumentError(spec, action, `invalid choice: ${pyRepr(name)} (choose from ${subcommands.map((sub) => sub.name).join(', ')})`);
|
|
624
|
+
}
|
|
625
|
+
seenNonDefault.add(action);
|
|
626
|
+
values[action.dest] = name;
|
|
627
|
+
// `_SubParsersAction.__call__`: parse the tail with the chosen parser into a FRESH
|
|
628
|
+
// namespace, copy every key over this one, and hand the leftovers back up.
|
|
629
|
+
const nested = parseKnownArgs(chosen.parser, args.slice(1));
|
|
630
|
+
for (const [key, nestedValue] of Object.entries(nested.values))
|
|
631
|
+
values[key] = nestedValue;
|
|
632
|
+
extras.push(...nested.extras);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (action.kind === 'store' || action.kind === 'positional') {
|
|
636
|
+
value = convert(action, args[0]);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
value = true;
|
|
640
|
+
}
|
|
641
|
+
seenNonDefault.add(action);
|
|
642
|
+
for (const conflict of conflicts.get(action) ?? []) {
|
|
643
|
+
if (seenNonDefault.has(conflict)) {
|
|
644
|
+
throw argumentError(spec, action, `not allowed with argument ${actionName(conflict)}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (action.kind === 'help')
|
|
648
|
+
throw new HelpRequested(spec);
|
|
649
|
+
values[action.dest] = value;
|
|
650
|
+
}
|
|
651
|
+
/** `consume_optional`: it may fire several actions (`-hx` is `-h` then `-x`). */
|
|
652
|
+
function consumeOptional(startIndex) {
|
|
653
|
+
const tuples = optionTuples.get(startIndex);
|
|
654
|
+
if (tuples.length > 1) {
|
|
655
|
+
const matches = tuples.map((tuple) => tuple.optionString).join(', ');
|
|
656
|
+
throw new ArgvError(spec, `ambiguous option: ${argv[startIndex]} could match ${matches}`);
|
|
657
|
+
}
|
|
658
|
+
let { action, optionString, sep, explicit } = tuples[0];
|
|
659
|
+
const taken = [];
|
|
660
|
+
let stop;
|
|
661
|
+
for (;;) {
|
|
662
|
+
if (action === null) {
|
|
663
|
+
extras.push(argv[startIndex]);
|
|
664
|
+
return startIndex + 1;
|
|
665
|
+
}
|
|
666
|
+
if (explicit !== null) {
|
|
667
|
+
const argCount = action.kind === 'store' ? 1 : 0;
|
|
668
|
+
if (argCount === 0 && !optionString.startsWith('--') && explicit !== '') {
|
|
669
|
+
// A glued single-dash tail: `-hx` is `-h` followed by `-x`. An `=` here, or a
|
|
670
|
+
// tail that is itself an option string, is a value nobody asked for.
|
|
671
|
+
if ((sep !== null && sep !== '') || explicit.startsWith('-')) {
|
|
672
|
+
throw argumentError(spec, action, `ignored explicit argument ${pyRepr(explicit)}`);
|
|
673
|
+
}
|
|
674
|
+
taken.push({ action, args: [] });
|
|
675
|
+
const next = optionString[0] + explicit[0];
|
|
676
|
+
const known = byOptionString.get(next);
|
|
677
|
+
if (known === undefined) {
|
|
678
|
+
extras.push(next + explicit.slice(1));
|
|
679
|
+
stop = startIndex + 1;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
action = known;
|
|
683
|
+
optionString = next;
|
|
684
|
+
const tail = explicit.slice(1);
|
|
685
|
+
if (tail === '')
|
|
686
|
+
[sep, explicit] = [null, null];
|
|
687
|
+
else if (tail.startsWith('='))
|
|
688
|
+
[sep, explicit] = ['=', tail.slice(1)];
|
|
689
|
+
else
|
|
690
|
+
[sep, explicit] = ['', tail];
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (argCount === 1) {
|
|
694
|
+
taken.push({ action, args: [explicit] });
|
|
695
|
+
stop = startIndex + 1;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
throw argumentError(spec, action, `ignored explicit argument ${pyRepr(explicit)}`);
|
|
699
|
+
}
|
|
700
|
+
if (action.kind !== 'store') {
|
|
701
|
+
taken.push({ action, args: [] });
|
|
702
|
+
stop = startIndex + 1;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
// `_get_nargs_pattern` strips every `-` for an OPTIONAL, so the pattern is `(A)`:
|
|
706
|
+
// the very next token must be a value, and `--k --store x` is "expected one argument"
|
|
707
|
+
// rather than a store named `--store`.
|
|
708
|
+
if (pattern[startIndex + 1] !== 'A')
|
|
709
|
+
throw argumentError(spec, action, 'expected one argument');
|
|
710
|
+
taken.push({ action, args: [argv[startIndex + 1]] });
|
|
711
|
+
stop = startIndex + 2;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
for (const entry of taken)
|
|
715
|
+
takeAction(entry.action, entry.args);
|
|
716
|
+
return stop;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* `_get_nargs_pattern`, the three arms this runtime declares.
|
|
720
|
+
*
|
|
721
|
+
* `-` is the `--` separator and `O` an option string, so `PARSER`'s `(-*A[-AO]*)` is what
|
|
722
|
+
* lets `status --store /tmp/x` reach the sub-parser: everything after the subcommand word
|
|
723
|
+
* belongs to it, options included. NOT PORTED: `?`, `*`, `+`, `REMAINDER`, `SUPPRESS`, an
|
|
724
|
+
* integer count, and the option-side patterns — an optional's value is taken by
|
|
725
|
+
* `consumeOptional`, which never builds a regex.
|
|
726
|
+
*/
|
|
727
|
+
const nargsPattern = (action) => action.kind === 'subparsers' ? '(-*A[-AO]*)' : '(-*A-*)';
|
|
728
|
+
/** `_match_arguments_partial`: the longest PREFIX of the positionals this argv can feed. */
|
|
729
|
+
function matchArgumentsPartial(actions, text) {
|
|
730
|
+
for (let count = actions.length; count > 0; count -= 1) {
|
|
731
|
+
// `re.match` anchors at the start and NOT at the end, which is the `^` here.
|
|
732
|
+
const match = new RegExp(`^${actions.slice(0, count).map(nargsPattern).join('')}`).exec(text);
|
|
733
|
+
if (match !== null)
|
|
734
|
+
return match.slice(1).map((group) => group.length);
|
|
735
|
+
}
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
const patternText = pattern.join('');
|
|
739
|
+
/** `consume_positionals`, including the `--` strip each `nargs` does differently. */
|
|
740
|
+
function consumePositionals(start) {
|
|
741
|
+
const counts = matchArgumentsPartial(remainingPositionals, patternText.slice(start));
|
|
742
|
+
let index = start;
|
|
743
|
+
for (let slot = 0; slot < counts.length; slot += 1) {
|
|
744
|
+
const action = remainingPositionals[slot];
|
|
745
|
+
const count = counts[slot];
|
|
746
|
+
const args = argv.slice(index, index + count);
|
|
747
|
+
if (action.kind === 'subparsers') {
|
|
748
|
+
// PARSER keeps every inner `--` (the sub-parser is entitled to see it) and drops
|
|
749
|
+
// only a leading one. `... -- status` therefore dispatches; `status -- x` does not
|
|
750
|
+
// lose the separator on its way down.
|
|
751
|
+
if (pattern[index] === '-')
|
|
752
|
+
args.splice(args.indexOf('--'), 1);
|
|
753
|
+
}
|
|
754
|
+
else if (patternText.slice(index, index + count).includes('-')) {
|
|
755
|
+
args.splice(args.indexOf('--'), 1);
|
|
756
|
+
}
|
|
757
|
+
index += count;
|
|
758
|
+
takeAction(action, args);
|
|
759
|
+
}
|
|
760
|
+
remainingPositionals.splice(0, counts.length);
|
|
761
|
+
return index;
|
|
762
|
+
}
|
|
763
|
+
let startIndex = 0;
|
|
764
|
+
const optionIndices = [...optionTuples.keys()];
|
|
765
|
+
const maxOptionIndex = optionIndices.length > 0 ? Math.max(...optionIndices) : -1;
|
|
766
|
+
while (startIndex <= maxOptionIndex) {
|
|
767
|
+
const nextOptionIndex = Math.min(...optionIndices.filter((index) => index >= startIndex));
|
|
768
|
+
if (startIndex !== nextOptionIndex) {
|
|
769
|
+
// Positionals standing before the next option string. A parser with none consumes
|
|
770
|
+
// nothing here and falls straight through to the extras arm below, which is exactly
|
|
771
|
+
// what this loop did before positionals existed.
|
|
772
|
+
const end = consumePositionals(startIndex);
|
|
773
|
+
if (end > startIndex) {
|
|
774
|
+
startIndex = end;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
startIndex = end;
|
|
778
|
+
}
|
|
779
|
+
if (!optionTuples.has(startIndex)) {
|
|
780
|
+
extras.push(...argv.slice(startIndex, nextOptionIndex));
|
|
781
|
+
startIndex = nextOptionIndex;
|
|
782
|
+
}
|
|
783
|
+
startIndex = consumeOptional(startIndex);
|
|
784
|
+
}
|
|
785
|
+
const stopIndex = consumePositionals(startIndex);
|
|
786
|
+
extras.push(...argv.slice(stopIndex));
|
|
787
|
+
// `required_actions`, and it is raised HERE rather than by `parseArgs` — which is why
|
|
788
|
+
// `--nope` alone reports the missing subcommand and not the unrecognized flag.
|
|
789
|
+
//
|
|
790
|
+
// `action.required` is EVERY positional in this runtime and no optional: `nargs=None`
|
|
791
|
+
// makes a positional required by construction, and the reference's one subparsers action
|
|
792
|
+
// is declared `add_subparsers(dest="command", required=True)`. A future optional
|
|
793
|
+
// `required=True`, or a subparsers action without it, wants a field here rather than this
|
|
794
|
+
// shorthand.
|
|
795
|
+
const missing = spec.actions
|
|
796
|
+
.filter((action) => !seenActions.has(action) && isPositional(action))
|
|
797
|
+
.map(actionName);
|
|
798
|
+
if (missing.length > 0) {
|
|
799
|
+
throw argumentError(spec, null, `the following arguments are required: ${missing.join(', ')}`);
|
|
800
|
+
}
|
|
801
|
+
return { values, extras };
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* `ArgumentParser.parse_args`: `parse_known_args`, and an error for anything left over.
|
|
805
|
+
*
|
|
806
|
+
* Returns the namespace as a plain record keyed by `dest`.
|
|
807
|
+
*/
|
|
808
|
+
export function parseArgs(spec, argv) {
|
|
809
|
+
const { values, extras } = parseKnownArgs(spec, argv);
|
|
810
|
+
if (extras.length > 0)
|
|
811
|
+
throw new ArgvError(spec, `unrecognized arguments: ${extras.join(' ')}`);
|
|
812
|
+
return values;
|
|
813
|
+
}
|