induscode 0.1.0__py3-none-any.whl
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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""The invocation reader — a generic, table-driven argv parser over
|
|
2
|
+
:data:`~.flags.FLAG_SPECS`.
|
|
3
|
+
|
|
4
|
+
This is the application's full command-line grammar (the boot layer carries
|
|
5
|
+
only a thin routing parser). It walks a sliced ``argv`` once and folds every
|
|
6
|
+
token into a fully-typed :class:`~induscode.launch.contract.Invocation`:
|
|
7
|
+
recognised flags bind through the declarative flag table, ``@file`` references
|
|
8
|
+
collect for the attachment gatherer, and remaining positionals become the
|
|
9
|
+
prompt and the positional tail.
|
|
10
|
+
|
|
11
|
+
The grammar is deliberately textbook and entirely data-driven — there is no
|
|
12
|
+
per-flag branch. One index over the table maps every canonical name and alias
|
|
13
|
+
to its row; the loop applies whichever row a token resolves to:
|
|
14
|
+
|
|
15
|
+
- **``--name=value``** and **``--name value``** both bind a ``string`` /
|
|
16
|
+
``number`` / ``list`` flag; an inline ``=value`` wins, otherwise the
|
|
17
|
+
following token is consumed (unless it is itself a flag, in which case the
|
|
18
|
+
value is empty).
|
|
19
|
+
- **longest-alias match** resolves the canonical name, so a longer spelling
|
|
20
|
+
is never shadowed by a shorter prefix (whole tokens match whole index keys).
|
|
21
|
+
- **``--``** terminates option parsing; every subsequent token is positional.
|
|
22
|
+
- **clustered short booleans** (``-pi``) expand to each single-letter switch.
|
|
23
|
+
- **``list`` flags accumulate** across repetition and split a single
|
|
24
|
+
comma-separated token into elements.
|
|
25
|
+
- **``@file``** tokens are recorded as attachment references rather than
|
|
26
|
+
positionals.
|
|
27
|
+
|
|
28
|
+
The parse is total: an unrecognised ``--flag`` is tolerated as a boolean
|
|
29
|
+
switch in the loose ``Invocation.flags`` bag (the extension-flag escape hatch)
|
|
30
|
+
rather than rejected, so nothing is silently dropped. Strong typing of the
|
|
31
|
+
named fields, and mode derivation, happen in one final fold.
|
|
32
|
+
|
|
33
|
+
Port note: the framework parser engine (``indusagi.shell_app.invocation``)
|
|
34
|
+
raises on unknown flags and has no list-kind / ``@file`` / cluster support, so
|
|
35
|
+
this table-driven reader is ported whole rather than silently adopting the
|
|
36
|
+
engine (analysis 04 risk-5).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import math
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from typing import Final, Mapping, Sequence
|
|
44
|
+
|
|
45
|
+
from ..contract import (
|
|
46
|
+
FlagValue,
|
|
47
|
+
Invocation,
|
|
48
|
+
OutputMode,
|
|
49
|
+
ThinkingEffort,
|
|
50
|
+
ToolName,
|
|
51
|
+
is_thinking_effort,
|
|
52
|
+
is_tool_name,
|
|
53
|
+
)
|
|
54
|
+
from .flags import FLAG_SPECS, GroupedFlagSpec
|
|
55
|
+
|
|
56
|
+
__all__ = ["read_file_references", "read_invocation"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_flag_token(token: str) -> bool:
|
|
60
|
+
"""Whether a token is a flag (``-x`` / ``--x``) rather than a positional
|
|
61
|
+
or the ``-`` stdin convention."""
|
|
62
|
+
return len(token) > 1 and token.startswith("-")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_long_token(token: str) -> bool:
|
|
66
|
+
"""Whether a token is a long flag (``--x``), eligible for inline
|
|
67
|
+
``=value`` and the ``--`` terminator."""
|
|
68
|
+
return token.startswith("--")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_short_cluster(token: str) -> bool:
|
|
72
|
+
"""Whether a token is a clustered short switch run (``-pi``), excluding
|
|
73
|
+
``-`` and ``--``."""
|
|
74
|
+
return len(token) > 1 and token[0] == "-" and token[1] != "-"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _flag_key(name: str) -> str:
|
|
78
|
+
"""Strip the leading dashes from a canonical flag name to get its
|
|
79
|
+
flag-bag key."""
|
|
80
|
+
return name.lstrip("-")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _build_token_index() -> dict[str, GroupedFlagSpec]:
|
|
84
|
+
"""Build the token index over the table: every canonical name and every
|
|
85
|
+
alias maps to its owning row. The longest-alias rule falls out of matching
|
|
86
|
+
whole tokens against whole keys (a longer spelling and a shorter one are
|
|
87
|
+
distinct keys, never prefixes of one lookup)."""
|
|
88
|
+
index: dict[str, GroupedFlagSpec] = {}
|
|
89
|
+
for spec in FLAG_SPECS:
|
|
90
|
+
index[spec.name] = spec
|
|
91
|
+
for alias in spec.aliases:
|
|
92
|
+
index[alias] = spec
|
|
93
|
+
return index
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#: The single shared token index; the table is frozen, so one build suffices.
|
|
97
|
+
_TOKEN_INDEX: Final[dict[str, GroupedFlagSpec]] = _build_token_index()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_short_boolean_set() -> set[str]:
|
|
101
|
+
"""The set of single-letter aliases that name a boolean flag, for cluster
|
|
102
|
+
expansion."""
|
|
103
|
+
letters: set[str] = set()
|
|
104
|
+
for spec in FLAG_SPECS:
|
|
105
|
+
if spec.kind != "boolean":
|
|
106
|
+
continue
|
|
107
|
+
for alias in spec.aliases:
|
|
108
|
+
if len(alias) == 2 and alias[0] == "-" and alias[1] != "-":
|
|
109
|
+
letters.add(alias[1])
|
|
110
|
+
return letters
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#: Single-letter boolean switches eligible to appear in a ``-pi`` cluster.
|
|
114
|
+
_SHORT_BOOLEANS: Final[set[str]] = _build_short_boolean_set()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _split_inline(token: str) -> tuple[str, str | None]:
|
|
118
|
+
"""Split a long token into its flag name and optional inline ``=value``."""
|
|
119
|
+
if _is_long_token(token):
|
|
120
|
+
eq = token.find("=")
|
|
121
|
+
if eq != -1:
|
|
122
|
+
return token[:eq], token[eq + 1 :]
|
|
123
|
+
return token, None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _split_list(value: str) -> list[str]:
|
|
127
|
+
"""Split a ``list`` flag value into its comma-separated elements, dropping
|
|
128
|
+
blanks."""
|
|
129
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _coerce_scalar(kind: str, raw: str) -> FlagValue:
|
|
133
|
+
"""Coerce a raw value token to the runtime type its kind fixes. Numbers
|
|
134
|
+
parse with JS ``Number`` semantics: the empty string is ``0`` and an
|
|
135
|
+
unparseable number yields ``NaN``, which the named-field fold below treats
|
|
136
|
+
as absent."""
|
|
137
|
+
if kind == "number":
|
|
138
|
+
if raw.strip() == "":
|
|
139
|
+
return 0.0
|
|
140
|
+
try:
|
|
141
|
+
return float(raw)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return math.nan
|
|
144
|
+
return raw
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(slots=True)
|
|
148
|
+
class _ParseState:
|
|
149
|
+
"""The mutable accumulator threaded through the parse, before the typed
|
|
150
|
+
fold."""
|
|
151
|
+
|
|
152
|
+
# The loose flag bag, keyed by canonical flag name (dashes stripped).
|
|
153
|
+
flags: dict[str, FlagValue] = field(default_factory=dict)
|
|
154
|
+
# Positional tokens (after the prompt) and, with `--`, every later token.
|
|
155
|
+
positionals: list[str] = field(default_factory=list)
|
|
156
|
+
# `@file` references collected for the attachment gatherer.
|
|
157
|
+
file_refs: list[str] = field(default_factory=list)
|
|
158
|
+
# The first positional becomes the prompt; later ones go to positionals.
|
|
159
|
+
prompt: str | None = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _push_positional(state: _ParseState, token: str) -> None:
|
|
163
|
+
"""Record a positional token: the first becomes the prompt, the rest
|
|
164
|
+
accumulate."""
|
|
165
|
+
if state.prompt is None:
|
|
166
|
+
state.prompt = token
|
|
167
|
+
else:
|
|
168
|
+
state.positionals.append(token)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _accumulate_list(state: _ParseState, key: str, items: list[str]) -> None:
|
|
172
|
+
"""Append elements to a ``list`` flag, preserving prior accumulation."""
|
|
173
|
+
prior = state.flags.get(key)
|
|
174
|
+
base = prior if isinstance(prior, list) else []
|
|
175
|
+
state.flags[key] = [*base, *items]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _apply_flag(
|
|
179
|
+
spec: GroupedFlagSpec,
|
|
180
|
+
inline: str | None,
|
|
181
|
+
argv: Sequence[str],
|
|
182
|
+
i: int,
|
|
183
|
+
state: _ParseState,
|
|
184
|
+
) -> int:
|
|
185
|
+
"""Apply one resolved flag row at position ``i``, returning the index of
|
|
186
|
+
the last token it consumed. A value flag may consume the following token;
|
|
187
|
+
a boolean consumes only itself."""
|
|
188
|
+
key = _flag_key(spec.name)
|
|
189
|
+
|
|
190
|
+
if spec.kind == "boolean":
|
|
191
|
+
state.flags[key] = True
|
|
192
|
+
return i
|
|
193
|
+
|
|
194
|
+
# Value-bearing flag: take the inline `=value`, else the next non-flag
|
|
195
|
+
# token.
|
|
196
|
+
value = inline
|
|
197
|
+
consumed = i
|
|
198
|
+
if value is None and i + 1 < len(argv) and not _is_flag_token(argv[i + 1]):
|
|
199
|
+
value = argv[i + 1]
|
|
200
|
+
consumed = i + 1
|
|
201
|
+
raw = value if value is not None else ""
|
|
202
|
+
|
|
203
|
+
if spec.kind == "list":
|
|
204
|
+
_accumulate_list(state, key, _split_list(raw))
|
|
205
|
+
else:
|
|
206
|
+
state.flags[key] = _coerce_scalar(spec.kind, raw)
|
|
207
|
+
return consumed
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _apply_short_cluster(token: str, state: _ParseState) -> bool:
|
|
211
|
+
"""Expand a clustered short-boolean token (``-pi``) into its component
|
|
212
|
+
switches, setting each in the flag bag. Returns False if any letter is not
|
|
213
|
+
a known short boolean, so the caller can fall back to treating the token
|
|
214
|
+
as an unknown flag."""
|
|
215
|
+
letters = list(token[1:])
|
|
216
|
+
if not all(letter in _SHORT_BOOLEANS for letter in letters):
|
|
217
|
+
return False
|
|
218
|
+
for letter in letters:
|
|
219
|
+
spec = _TOKEN_INDEX.get(f"-{letter}")
|
|
220
|
+
if spec is not None:
|
|
221
|
+
state.flags[_flag_key(spec.name)] = True
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _read_string(flags: Mapping[str, FlagValue], key: str) -> str | None:
|
|
226
|
+
"""Read a string flag-bag entry, or None when absent / non-string /
|
|
227
|
+
empty."""
|
|
228
|
+
value = flags.get(key)
|
|
229
|
+
return value if isinstance(value, str) and len(value) > 0 else None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _read_list(flags: Mapping[str, FlagValue], key: str) -> list[str] | None:
|
|
233
|
+
"""Read a list flag-bag entry as a string list, or None when absent."""
|
|
234
|
+
value = flags.get(key)
|
|
235
|
+
return value if isinstance(value, list) else None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _read_bool(flags: Mapping[str, FlagValue], key: str) -> bool:
|
|
239
|
+
"""Whether a boolean flag-bag entry is set."""
|
|
240
|
+
return flags.get(key) is True
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _derive_mode(flags: Mapping[str, FlagValue]) -> OutputMode:
|
|
244
|
+
"""Derive the resolved :data:`OutputMode`. The headless line protocol
|
|
245
|
+
(``--json`` / ``--rpc``) wins over a one-shot print, which in turn implies
|
|
246
|
+
the non-interactive ``json`` result mode unless the interactive switch
|
|
247
|
+
overrides it; with neither, the mode is the interactive ``text``
|
|
248
|
+
session."""
|
|
249
|
+
if _read_bool(flags, "json"):
|
|
250
|
+
return "rpc"
|
|
251
|
+
if _read_bool(flags, "print") and not _read_bool(flags, "interactive"):
|
|
252
|
+
return "json"
|
|
253
|
+
return "text"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_thinking(flags: Mapping[str, FlagValue]) -> ThinkingEffort | None:
|
|
257
|
+
"""Resolve the requested reasoning effort, ignoring an unrecognised
|
|
258
|
+
value."""
|
|
259
|
+
raw = _read_string(flags, "thinking")
|
|
260
|
+
if raw is not None and is_thinking_effort(raw):
|
|
261
|
+
return raw # type: ignore[return-value] # narrowed by the guard
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _resolve_tools(flags: Mapping[str, FlagValue]) -> tuple[ToolName, ...] | None:
|
|
266
|
+
"""Resolve the explicit tool allow-list, keeping only recognised tool
|
|
267
|
+
names."""
|
|
268
|
+
raw = _read_list(flags, "tools")
|
|
269
|
+
if raw is None:
|
|
270
|
+
return None
|
|
271
|
+
return tuple(name for name in raw if is_tool_name(name)) # type: ignore[misc]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def read_invocation(argv: Sequence[str]) -> Invocation:
|
|
275
|
+
"""Read a sliced ``argv`` into the fully-parsed
|
|
276
|
+
:class:`~induscode.launch.contract.Invocation`.
|
|
277
|
+
|
|
278
|
+
Walks the tokens once via the table index, accumulates into a parse state,
|
|
279
|
+
then folds that into the strongly-typed result: the loose ``flags`` bag is
|
|
280
|
+
retained for round-tripping and extension flags, and the named fields are
|
|
281
|
+
derived from it with per-kind coercion and validation. The ``attachments``
|
|
282
|
+
field is left unset here — the file references are surfaced for the
|
|
283
|
+
attachment gatherer to expand separately (:func:`read_file_references`).
|
|
284
|
+
|
|
285
|
+
:param argv: the already-sliced argument vector (no interpreter / script
|
|
286
|
+
path)
|
|
287
|
+
"""
|
|
288
|
+
state = _ParseState()
|
|
289
|
+
options_terminated = False
|
|
290
|
+
|
|
291
|
+
i = 0
|
|
292
|
+
while i < len(argv):
|
|
293
|
+
token = argv[i]
|
|
294
|
+
|
|
295
|
+
if options_terminated:
|
|
296
|
+
_push_positional(state, token)
|
|
297
|
+
i += 1
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if token == "--":
|
|
301
|
+
options_terminated = True
|
|
302
|
+
i += 1
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
if token.startswith("@") and len(token) > 1:
|
|
306
|
+
state.file_refs.append(token[1:])
|
|
307
|
+
i += 1
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
if not _is_flag_token(token):
|
|
311
|
+
_push_positional(state, token)
|
|
312
|
+
i += 1
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
name, inline = _split_inline(token)
|
|
316
|
+
spec = _TOKEN_INDEX.get(name)
|
|
317
|
+
if spec is not None:
|
|
318
|
+
i = _apply_flag(spec, inline, argv, i, state) + 1
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Unrecognised short cluster: expand if every letter is a known
|
|
322
|
+
# switch.
|
|
323
|
+
if _is_short_cluster(token) and _apply_short_cluster(token, state):
|
|
324
|
+
i += 1
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Unknown flag: tolerate as a boolean switch (the extension-flag
|
|
328
|
+
# hatch); an inline `=value` keeps its value.
|
|
329
|
+
state.flags[_flag_key(name)] = inline if inline is not None else True
|
|
330
|
+
i += 1
|
|
331
|
+
|
|
332
|
+
flags = state.flags
|
|
333
|
+
return Invocation(
|
|
334
|
+
mode=_derive_mode(flags),
|
|
335
|
+
prompt=state.prompt,
|
|
336
|
+
flags=flags,
|
|
337
|
+
positionals=tuple(state.positionals),
|
|
338
|
+
model=_read_string(flags, "model"),
|
|
339
|
+
account=_read_string(flags, "account"),
|
|
340
|
+
cwd=_read_string(flags, "cwd"),
|
|
341
|
+
system=_read_string(flags, "system"),
|
|
342
|
+
append_system=_read_string(flags, "append-system"),
|
|
343
|
+
thinking=_resolve_thinking(flags),
|
|
344
|
+
tools=_resolve_tools(flags),
|
|
345
|
+
no_tools=_read_bool(flags, "no-tools"),
|
|
346
|
+
mcp=tuple(_read_list(flags, "mcp") or []),
|
|
347
|
+
print=_read_bool(flags, "print"),
|
|
348
|
+
interactive=_read_bool(flags, "interactive"),
|
|
349
|
+
help=_read_bool(flags, "help"),
|
|
350
|
+
version=_read_bool(flags, "version"),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def read_file_references(argv: Sequence[str]) -> list[str]:
|
|
355
|
+
"""The ``@file`` references collected from an argv, exposed for the
|
|
356
|
+
attachment gatherer. A thin re-walk of the reader so the file references
|
|
357
|
+
can be obtained without re-deriving the whole parse where only attachments
|
|
358
|
+
are needed."""
|
|
359
|
+
refs: list[str] = []
|
|
360
|
+
options_terminated = False
|
|
361
|
+
for token in argv:
|
|
362
|
+
if options_terminated:
|
|
363
|
+
continue
|
|
364
|
+
if token == "--":
|
|
365
|
+
options_terminated = True
|
|
366
|
+
continue
|
|
367
|
+
if token.startswith("@") and len(token) > 1:
|
|
368
|
+
refs.append(token[1:])
|
|
369
|
+
return refs
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The usage renderer — generated entirely from :data:`~.flags.FLAG_SPECS`.
|
|
2
|
+
|
|
3
|
+
:func:`render_usage` produces the ``--help`` banner by walking the same
|
|
4
|
+
declarative flag table the reader parses against. There is no second,
|
|
5
|
+
hand-maintained help string; every option line is synthesised from its row
|
|
6
|
+
(its canonical name, its aliases, its value placeholder, and its one-line
|
|
7
|
+
description), grouped by the editorial :data:`~.flags.FLAG_GROUPS` sections.
|
|
8
|
+
Adding a flag to the table therefore adds it to the help with no further
|
|
9
|
+
edit, and the two can never disagree about what exists.
|
|
10
|
+
|
|
11
|
+
The renderer is pure and total: it reads only the table and returns a string.
|
|
12
|
+
|
|
13
|
+
Port note: the synopsis names the Python console script (``pindus``); the TS
|
|
14
|
+
build printed its own bin name there. Everything else is the TS layout
|
|
15
|
+
verbatim (two-space indent, 28-column signature padding, the trailing
|
|
16
|
+
``@file`` / ``--`` arguments note).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Final
|
|
22
|
+
|
|
23
|
+
from .flags import FLAG_GROUPS, FLAG_SPECS, FlagGroup, GroupedFlagSpec
|
|
24
|
+
|
|
25
|
+
__all__ = ["render_usage"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: Two-space indent for option lines within a section.
|
|
29
|
+
_INDENT: Final[str] = " "
|
|
30
|
+
|
|
31
|
+
#: Minimum column width the description is padded out to, for alignment.
|
|
32
|
+
_SIGNATURE_COLUMN: Final[int] = 28
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _placeholder(spec: GroupedFlagSpec) -> str:
|
|
36
|
+
"""The value placeholder shown after a value-bearing flag in the usage
|
|
37
|
+
text. Boolean switches take no placeholder; value, number, and list flags
|
|
38
|
+
each get a shape hint (a list shows the repeatable / comma form)."""
|
|
39
|
+
match spec.kind:
|
|
40
|
+
case "boolean":
|
|
41
|
+
return ""
|
|
42
|
+
case "number":
|
|
43
|
+
return " <n>"
|
|
44
|
+
case "list":
|
|
45
|
+
return " <a,b>"
|
|
46
|
+
case _: # "string" and anything future
|
|
47
|
+
return " <value>"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _signature(spec: GroupedFlagSpec) -> str:
|
|
51
|
+
"""Render the left "signature" column for a flag: its canonical name, then
|
|
52
|
+
any aliases in parentheses, then the value placeholder. e.g.
|
|
53
|
+
``--print (-p)`` or ``--model (-m) <value>``."""
|
|
54
|
+
alias_part = f" ({', '.join(spec.aliases)})" if spec.aliases else ""
|
|
55
|
+
return f"{spec.name}{alias_part}{_placeholder(spec)}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _option_line(spec: GroupedFlagSpec) -> str:
|
|
59
|
+
"""Render one option line: indented signature, padded, then its
|
|
60
|
+
description."""
|
|
61
|
+
sig = _signature(spec)
|
|
62
|
+
pad = " " * (_SIGNATURE_COLUMN - len(sig)) if len(sig) < _SIGNATURE_COLUMN else " "
|
|
63
|
+
return f"{_INDENT}{sig}{pad}{spec.describe}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rows_for_group(group: FlagGroup) -> list[GroupedFlagSpec]:
|
|
67
|
+
"""The flag rows filed under one group, in their table declaration
|
|
68
|
+
order."""
|
|
69
|
+
return [spec for spec in FLAG_SPECS if spec.group == group]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def render_usage() -> str:
|
|
73
|
+
"""Render the full usage banner.
|
|
74
|
+
|
|
75
|
+
Emits a synopsis line, then one section per :data:`~.flags.FLAG_GROUPS`
|
|
76
|
+
entry that has any rows, then a short note on the two positional
|
|
77
|
+
conventions the reader honours but that are not flags: the ``@file``
|
|
78
|
+
attachment syntax and the ``--`` option terminator. Every option line is
|
|
79
|
+
derived from the table; this function holds no per-flag knowledge of its
|
|
80
|
+
own.
|
|
81
|
+
|
|
82
|
+
:returns: the complete multi-line usage string (no trailing newline)
|
|
83
|
+
"""
|
|
84
|
+
lines: list[str] = []
|
|
85
|
+
|
|
86
|
+
lines.append("Usage: pindus [options] [prompt] [@file ...]")
|
|
87
|
+
lines.append("")
|
|
88
|
+
lines.append("A terminal-first AI coding agent.")
|
|
89
|
+
|
|
90
|
+
for group in FLAG_GROUPS:
|
|
91
|
+
rows = _rows_for_group(group.id)
|
|
92
|
+
if not rows:
|
|
93
|
+
continue
|
|
94
|
+
lines.append("")
|
|
95
|
+
lines.append(f"{group.title}:")
|
|
96
|
+
for spec in rows:
|
|
97
|
+
lines.append(_option_line(spec))
|
|
98
|
+
|
|
99
|
+
lines.append("")
|
|
100
|
+
lines.append("Arguments:")
|
|
101
|
+
lines.append(
|
|
102
|
+
f"{_INDENT}@file".ljust(_SIGNATURE_COLUMN + len(_INDENT))
|
|
103
|
+
+ "Attach a text or image file to the first message."
|
|
104
|
+
)
|
|
105
|
+
lines.append(
|
|
106
|
+
f"{_INDENT}--".ljust(_SIGNATURE_COLUMN + len(_INDENT))
|
|
107
|
+
+ "Stop parsing options; treat every later token as a positional."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return "\n".join(lines)
|