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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. 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
+ }