cdx-manager 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/changelogs/CHANGELOGS_0_9_11.md +44 -0
- package/changelogs/CHANGELOGS_0_9_12.md +36 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/src/backup_bundle.py +0 -1
- package/src/claude_usage.py +19 -3
- package/src/cli.py +27 -9
- package/src/cli_args.py +631 -0
- package/src/cli_commands.py +117 -1020
- package/src/cli_helpers.py +385 -0
- package/src/cli_render.py +0 -1
- package/src/cli_view.py +1 -20
- package/src/context_store.py +1 -2
- package/src/logics_view.py +0 -1
- package/src/notify.py +0 -1
- package/src/provider_runtime.py +9 -33
- package/src/run_command.py +1 -1
- package/src/run_registry.py +2 -2
- package/src/run_usage.py +1 -2
- package/src/session_service.py +7 -20
- package/src/session_store.py +2 -2
- package/src/status_source.py +0 -19
- package/src/status_view.py +1 -1
- package/src/update_check.py +1 -2
package/src/cli_args.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Argument parsers and usage strings for cdx commands.
|
|
3
|
+
|
|
4
|
+
Split out of cli_commands.py: pure input-parsing layer (no command execution),
|
|
5
|
+
imported back there and used by the handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
|
|
10
|
+
from .config import PROVIDER_CODEX, PROVIDERS
|
|
11
|
+
from .errors import CdxError
|
|
12
|
+
from .provider_runtime import _normalize_reasoning_effort
|
|
13
|
+
|
|
14
|
+
STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | cdx status --small|-s [--refresh|--cached] [--timeout SECONDS] | cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]"
|
|
15
|
+
DOCTOR_USAGE = "Usage: cdx doctor [--json]"
|
|
16
|
+
REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
|
|
17
|
+
UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
|
|
18
|
+
EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
19
|
+
IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
20
|
+
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
21
|
+
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
22
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
|
|
23
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
|
|
24
|
+
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
25
|
+
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
26
|
+
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
27
|
+
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
28
|
+
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
29
|
+
LAST_USAGE = "Usage: cdx last [--json]"
|
|
30
|
+
RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
31
|
+
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
32
|
+
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
33
|
+
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
34
|
+
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
35
|
+
RUNS_USAGE = "Usage: cdx runs [--limit N] --json"
|
|
36
|
+
RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
37
|
+
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_json_flag(args):
|
|
41
|
+
json_flag = "--json" in args
|
|
42
|
+
cleaned = [arg for arg in args if arg != "--json"]
|
|
43
|
+
return json_flag, cleaned
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_flag_args(args, schema, usage, positionals_key=None, max_positionals=0):
|
|
47
|
+
parsed = {spec["key"]: spec.get("default") for spec in schema.values()}
|
|
48
|
+
positionals = []
|
|
49
|
+
index = 0
|
|
50
|
+
while index < len(args):
|
|
51
|
+
arg = args[index]
|
|
52
|
+
if arg in schema:
|
|
53
|
+
spec = schema[arg]
|
|
54
|
+
if spec["type"] == "bool":
|
|
55
|
+
parsed[spec["key"]] = True
|
|
56
|
+
index += 1
|
|
57
|
+
continue
|
|
58
|
+
value, index = _read_option_value(args, index, usage)
|
|
59
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
60
|
+
continue
|
|
61
|
+
if arg.startswith("--") and "=" in arg:
|
|
62
|
+
flag, value = arg.split("=", 1)
|
|
63
|
+
spec = schema.get(flag)
|
|
64
|
+
if not spec or spec["type"] == "bool":
|
|
65
|
+
raise CdxError(usage)
|
|
66
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
67
|
+
index += 1
|
|
68
|
+
continue
|
|
69
|
+
if arg.startswith("-"):
|
|
70
|
+
raise CdxError(usage)
|
|
71
|
+
positionals.append(arg)
|
|
72
|
+
if len(positionals) > max_positionals:
|
|
73
|
+
raise CdxError(usage)
|
|
74
|
+
index += 1
|
|
75
|
+
|
|
76
|
+
if positionals_key is not None:
|
|
77
|
+
parsed[positionals_key] = positionals
|
|
78
|
+
return parsed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_add_args(args):
|
|
82
|
+
parsed = _parse_flag_args(
|
|
83
|
+
args,
|
|
84
|
+
{"--model": {"key": "model", "type": "str", "default": None}},
|
|
85
|
+
"Usage: cdx add [provider] <name> [--model MODEL] [--json]",
|
|
86
|
+
positionals_key="values",
|
|
87
|
+
max_positionals=2,
|
|
88
|
+
)
|
|
89
|
+
values = parsed["values"]
|
|
90
|
+
if len(values) == 1:
|
|
91
|
+
return {"provider": PROVIDER_CODEX, "name": values[0], "model": parsed["model"]}
|
|
92
|
+
if len(values) == 2:
|
|
93
|
+
return {"provider": values[0], "name": values[1], "model": parsed["model"]}
|
|
94
|
+
raise CdxError("Usage: cdx add [provider] <name> [--model MODEL] [--json]")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_copy_args(args):
|
|
98
|
+
if len(args) != 2:
|
|
99
|
+
raise CdxError("Usage: cdx cp <source> <dest> [--json]")
|
|
100
|
+
return {"source": args[0], "dest": args[1]}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_rename_args(args):
|
|
104
|
+
if len(args) != 2:
|
|
105
|
+
raise CdxError("Usage: cdx ren <source> <dest> [--json]")
|
|
106
|
+
return {"source": args[0], "dest": args[1]}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_remove_args(args):
|
|
110
|
+
parsed = _parse_flag_args(args, {
|
|
111
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
112
|
+
}, "Usage: cdx rmv <name> [--force] [--json]", positionals_key="names", max_positionals=1)
|
|
113
|
+
if len(parsed["names"]) != 1:
|
|
114
|
+
raise CdxError("Usage: cdx rmv <name> [--force] [--json]")
|
|
115
|
+
return {"name": parsed["names"][0], "force": parsed["force"]}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _parse_toggle_args(args, usage):
|
|
119
|
+
parsed = _parse_flag_args(args, {
|
|
120
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
121
|
+
}, usage, positionals_key="names", max_positionals=1)
|
|
122
|
+
if len(parsed["names"]) != 1:
|
|
123
|
+
raise CdxError(usage)
|
|
124
|
+
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_fast_value(value):
|
|
128
|
+
text = str(value).strip().lower()
|
|
129
|
+
if text in ("on", "true", "1", "yes"):
|
|
130
|
+
return True
|
|
131
|
+
if text in ("off", "false", "0", "no"):
|
|
132
|
+
return False
|
|
133
|
+
raise CdxError(SET_USAGE)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_priority_value(value):
|
|
137
|
+
try:
|
|
138
|
+
priority = int(value)
|
|
139
|
+
except (TypeError, ValueError) as error:
|
|
140
|
+
raise CdxError(SET_USAGE) from error
|
|
141
|
+
if priority < 0 or priority > 100:
|
|
142
|
+
raise CdxError(SET_USAGE)
|
|
143
|
+
return priority
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _parse_set_args(args):
|
|
147
|
+
parsed = _parse_flag_args(args, {
|
|
148
|
+
"--power": {"key": "power", "type": "str", "default": None},
|
|
149
|
+
"--permission": {"key": "permission", "type": "str", "default": None},
|
|
150
|
+
"--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
151
|
+
"--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
152
|
+
"--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
153
|
+
"--model": {"key": "model", "type": "str", "default": None},
|
|
154
|
+
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
155
|
+
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
156
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
157
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
158
|
+
}, SET_USAGE, positionals_key="names", max_positionals=1)
|
|
159
|
+
if len(parsed["names"]) > 1:
|
|
160
|
+
raise CdxError(SET_USAGE)
|
|
161
|
+
if parsed["names"] and parsed["sessions"]:
|
|
162
|
+
raise CdxError(SET_USAGE)
|
|
163
|
+
if parsed["names"] and parsed["provider"]:
|
|
164
|
+
raise CdxError(SET_USAGE)
|
|
165
|
+
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
166
|
+
raise CdxError(SET_USAGE)
|
|
167
|
+
settings = {
|
|
168
|
+
key: parsed[key]
|
|
169
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority")
|
|
170
|
+
if parsed[key] is not None
|
|
171
|
+
}
|
|
172
|
+
if not settings:
|
|
173
|
+
raise CdxError(SET_USAGE)
|
|
174
|
+
return {
|
|
175
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
176
|
+
"sessions": parsed["sessions"],
|
|
177
|
+
"provider": parsed["provider"],
|
|
178
|
+
"settings": settings,
|
|
179
|
+
"json": parsed["json"],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _parse_unset_args(args):
|
|
184
|
+
parsed = _parse_flag_args(args, {
|
|
185
|
+
"--power": {"key": "power", "type": "bool", "default": False},
|
|
186
|
+
"--permission": {"key": "permission", "type": "bool", "default": False},
|
|
187
|
+
"--fast": {"key": "fast", "type": "bool", "default": False},
|
|
188
|
+
"--rtk": {"key": "rtk", "type": "bool", "default": False},
|
|
189
|
+
"--logics": {"key": "logics", "type": "bool", "default": False},
|
|
190
|
+
"--model": {"key": "model", "type": "bool", "default": False},
|
|
191
|
+
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
192
|
+
"--all": {"key": "all", "type": "bool", "default": False},
|
|
193
|
+
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
194
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
|
|
195
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
196
|
+
}, UNSET_USAGE, positionals_key="names", max_positionals=1)
|
|
197
|
+
if len(parsed["names"]) > 1:
|
|
198
|
+
raise CdxError(UNSET_USAGE)
|
|
199
|
+
if parsed["names"] and parsed["sessions"]:
|
|
200
|
+
raise CdxError(UNSET_USAGE)
|
|
201
|
+
if parsed["names"] and parsed["provider"]:
|
|
202
|
+
raise CdxError(UNSET_USAGE)
|
|
203
|
+
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
204
|
+
raise CdxError(UNSET_USAGE)
|
|
205
|
+
keys = ["power", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
|
|
206
|
+
key for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
|
|
207
|
+
]
|
|
208
|
+
if not keys:
|
|
209
|
+
raise CdxError(UNSET_USAGE)
|
|
210
|
+
return {
|
|
211
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
212
|
+
"sessions": parsed["sessions"],
|
|
213
|
+
"provider": parsed["provider"],
|
|
214
|
+
"keys": keys,
|
|
215
|
+
"json": parsed["json"],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _parse_config_args(args):
|
|
220
|
+
parsed = _parse_flag_args(args, {
|
|
221
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
222
|
+
}, CONFIG_USAGE, positionals_key="names", max_positionals=1)
|
|
223
|
+
if len(parsed["names"]) != 1:
|
|
224
|
+
raise CdxError(CONFIG_USAGE)
|
|
225
|
+
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _parse_configs_args(args):
|
|
229
|
+
parsed = _parse_flag_args(args, {
|
|
230
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
231
|
+
}, CONFIGS_USAGE)
|
|
232
|
+
return {"json": parsed["json"]}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _parse_select_args(args):
|
|
236
|
+
parsed = _parse_flag_args(args, {
|
|
237
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
|
|
238
|
+
"--min-reasoning-effort": {"key": "min_reasoning_effort", "type": "str", "default": None},
|
|
239
|
+
"--min-power": {"key": "min_power", "type": "str", "default": None},
|
|
240
|
+
"--require-ready": {"key": "require_ready", "type": "bool", "default": False},
|
|
241
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
242
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
243
|
+
}, SELECT_USAGE)
|
|
244
|
+
if not parsed["provider"] or not parsed["json"]:
|
|
245
|
+
raise CdxError(SELECT_USAGE)
|
|
246
|
+
effort = _normalize_reasoning_effort(
|
|
247
|
+
reasoning_effort=parsed["min_reasoning_effort"],
|
|
248
|
+
power=parsed["min_power"],
|
|
249
|
+
usage=SELECT_USAGE,
|
|
250
|
+
).get("reasoning_effort")
|
|
251
|
+
return {
|
|
252
|
+
"provider": parsed["provider"],
|
|
253
|
+
"min_reasoning_effort": effort,
|
|
254
|
+
"require_ready": parsed["require_ready"],
|
|
255
|
+
"refresh": parsed["refresh"],
|
|
256
|
+
"json": parsed["json"],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_next_args(args):
|
|
261
|
+
parsed = _parse_flag_args(args, {
|
|
262
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
263
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
264
|
+
}, NEXT_USAGE)
|
|
265
|
+
return {"json": parsed["json"], "refresh": parsed["refresh"]}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_timeout_seconds(value, usage=RUN_USAGE):
|
|
269
|
+
try:
|
|
270
|
+
parsed = float(value)
|
|
271
|
+
except (TypeError, ValueError) as error:
|
|
272
|
+
raise CdxError(usage) from error
|
|
273
|
+
if parsed <= 0:
|
|
274
|
+
raise CdxError(usage)
|
|
275
|
+
return parsed
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _normalize_run_permission(value):
|
|
279
|
+
if value is None:
|
|
280
|
+
return None
|
|
281
|
+
text = str(value).strip().lower()
|
|
282
|
+
aliases = {
|
|
283
|
+
"workspace-write": "default",
|
|
284
|
+
"read-only": "review",
|
|
285
|
+
"danger-full-access": "full",
|
|
286
|
+
}
|
|
287
|
+
text = aliases.get(text, text)
|
|
288
|
+
if text not in ("review", "default", "auto", "full"):
|
|
289
|
+
raise CdxError(RUN_USAGE)
|
|
290
|
+
return text
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _parse_run_args(args):
|
|
294
|
+
parsed = _parse_flag_args(args, {
|
|
295
|
+
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
296
|
+
"--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
|
|
297
|
+
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
298
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
|
|
299
|
+
"--model": {"key": "model", "type": "str", "default": None},
|
|
300
|
+
"--kind": {"key": "kind", "type": "str", "default": "assistant"},
|
|
301
|
+
"--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
|
|
302
|
+
"--power": {"key": "power", "type": "str", "default": None},
|
|
303
|
+
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
304
|
+
"--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_timeout_seconds},
|
|
305
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
306
|
+
}, RUN_USAGE, positionals_key="names", max_positionals=1)
|
|
307
|
+
if not parsed["json"]:
|
|
308
|
+
raise CdxError(RUN_USAGE)
|
|
309
|
+
name = parsed["names"][0] if parsed["names"] else None
|
|
310
|
+
if name and parsed["provider"]:
|
|
311
|
+
raise CdxError(RUN_USAGE)
|
|
312
|
+
if not name and not parsed["provider"]:
|
|
313
|
+
raise CdxError(RUN_USAGE)
|
|
314
|
+
if not parsed["cwd"]:
|
|
315
|
+
raise CdxError(RUN_USAGE)
|
|
316
|
+
if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
|
|
317
|
+
raise CdxError(RUN_USAGE)
|
|
318
|
+
if parsed["kind"] not in ("assistant", "code-review"):
|
|
319
|
+
raise CdxError(RUN_USAGE)
|
|
320
|
+
effort = _normalize_reasoning_effort(
|
|
321
|
+
reasoning_effort=parsed["reasoning_effort"],
|
|
322
|
+
power=parsed["power"],
|
|
323
|
+
usage=RUN_USAGE,
|
|
324
|
+
)
|
|
325
|
+
return {
|
|
326
|
+
"name": name,
|
|
327
|
+
"provider": parsed["provider"],
|
|
328
|
+
"cwd": parsed["cwd"],
|
|
329
|
+
"prompt_file": parsed["prompt_file"],
|
|
330
|
+
"prompt": parsed["prompt"],
|
|
331
|
+
"kind": parsed["kind"],
|
|
332
|
+
"model": parsed["model"],
|
|
333
|
+
"permission": parsed["permission"],
|
|
334
|
+
"timeout_seconds": parsed["timeout_seconds"],
|
|
335
|
+
"reasoning_effort": effort.get("reasoning_effort"),
|
|
336
|
+
"power": effort.get("power"),
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _parse_positive_int(value):
|
|
341
|
+
try:
|
|
342
|
+
parsed = int(value)
|
|
343
|
+
except (TypeError, ValueError) as error:
|
|
344
|
+
raise CdxError(HISTORY_USAGE) from error
|
|
345
|
+
if parsed < 1:
|
|
346
|
+
raise CdxError(HISTORY_USAGE)
|
|
347
|
+
return parsed
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _local_timezone():
|
|
351
|
+
return datetime.now().astimezone().tzinfo
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _parse_datetime_value(value, usage, end_of_day=False):
|
|
355
|
+
text = str(value or "").strip()
|
|
356
|
+
if not text:
|
|
357
|
+
raise CdxError(usage)
|
|
358
|
+
if text.endswith("Z"):
|
|
359
|
+
text = f"{text[:-1]}+00:00"
|
|
360
|
+
is_date_only = len(text) == 10 and text[4] == "-" and text[7] == "-"
|
|
361
|
+
try:
|
|
362
|
+
parsed = datetime.fromisoformat(text)
|
|
363
|
+
except ValueError as error:
|
|
364
|
+
raise CdxError(usage) from error
|
|
365
|
+
if is_date_only and end_of_day:
|
|
366
|
+
parsed = parsed.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
367
|
+
if parsed.tzinfo is None:
|
|
368
|
+
parsed = parsed.replace(tzinfo=_local_timezone())
|
|
369
|
+
return parsed.astimezone()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _parse_since_value(value, now, usage):
|
|
373
|
+
text = str(value or "").strip().lower()
|
|
374
|
+
if not text:
|
|
375
|
+
raise CdxError(usage)
|
|
376
|
+
if text == "today":
|
|
377
|
+
return now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
378
|
+
if text == "yesterday":
|
|
379
|
+
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
380
|
+
return today - timedelta(days=1)
|
|
381
|
+
unit = text[-1:]
|
|
382
|
+
amount_text = text[:-1]
|
|
383
|
+
if unit in ("m", "h", "d", "w") and amount_text.isdigit():
|
|
384
|
+
amount = int(amount_text)
|
|
385
|
+
if amount < 1:
|
|
386
|
+
raise CdxError(usage)
|
|
387
|
+
multipliers = {
|
|
388
|
+
"m": timedelta(minutes=amount),
|
|
389
|
+
"h": timedelta(hours=amount),
|
|
390
|
+
"d": timedelta(days=amount),
|
|
391
|
+
"w": timedelta(weeks=amount),
|
|
392
|
+
}
|
|
393
|
+
return now - multipliers[unit]
|
|
394
|
+
return _parse_datetime_value(value, usage, end_of_day=False)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _format_period_datetime(value):
|
|
398
|
+
if value is None:
|
|
399
|
+
return None
|
|
400
|
+
return value.isoformat(timespec="seconds")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _parse_history_period(parsed, now):
|
|
404
|
+
since = parsed.get("since")
|
|
405
|
+
from_value = parsed.get("from")
|
|
406
|
+
to_value = parsed.get("to")
|
|
407
|
+
if since and from_value:
|
|
408
|
+
raise CdxError("Usage: cdx history cannot combine --since and --from.")
|
|
409
|
+
start = _parse_since_value(since, now, HISTORY_USAGE) if since else None
|
|
410
|
+
if from_value:
|
|
411
|
+
start = _parse_datetime_value(from_value, HISTORY_USAGE, end_of_day=False)
|
|
412
|
+
end = _parse_datetime_value(to_value, HISTORY_USAGE, end_of_day=True) if to_value else None
|
|
413
|
+
if start and end and start.timestamp() > end.timestamp():
|
|
414
|
+
raise CdxError("Usage: cdx history period start must be before period end.")
|
|
415
|
+
return {
|
|
416
|
+
"from": _format_period_datetime(start),
|
|
417
|
+
"to": _format_period_datetime(end),
|
|
418
|
+
"from_ts": start.timestamp() if start else None,
|
|
419
|
+
"to_ts": end.timestamp() if end else None,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _has_history_period(period):
|
|
424
|
+
return period.get("from_ts") is not None or period.get("to_ts") is not None
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _parse_entry_timestamp(entry):
|
|
428
|
+
value = entry.get("started_at")
|
|
429
|
+
if not value:
|
|
430
|
+
return None
|
|
431
|
+
text = str(value)
|
|
432
|
+
if text.endswith("Z"):
|
|
433
|
+
text = f"{text[:-1]}+00:00"
|
|
434
|
+
try:
|
|
435
|
+
parsed = datetime.fromisoformat(text)
|
|
436
|
+
except ValueError:
|
|
437
|
+
return None
|
|
438
|
+
if parsed.tzinfo is None:
|
|
439
|
+
parsed = parsed.replace(tzinfo=_local_timezone())
|
|
440
|
+
return parsed.timestamp()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _filter_history_period(entries, period):
|
|
444
|
+
if not _has_history_period(period):
|
|
445
|
+
return entries
|
|
446
|
+
filtered = []
|
|
447
|
+
start = period.get("from_ts")
|
|
448
|
+
end = period.get("to_ts")
|
|
449
|
+
for entry in entries:
|
|
450
|
+
timestamp = _parse_entry_timestamp(entry)
|
|
451
|
+
if timestamp is None:
|
|
452
|
+
continue
|
|
453
|
+
if start is not None and timestamp < start:
|
|
454
|
+
continue
|
|
455
|
+
if end is not None and timestamp > end:
|
|
456
|
+
continue
|
|
457
|
+
filtered.append(entry)
|
|
458
|
+
return filtered
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _public_history_period(period):
|
|
462
|
+
return {
|
|
463
|
+
"from": period.get("from"),
|
|
464
|
+
"to": period.get("to"),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _parse_history_args(args, now=None):
|
|
469
|
+
now = now or datetime.now().astimezone()
|
|
470
|
+
parsed = _parse_flag_args(args, {
|
|
471
|
+
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
472
|
+
"--summary": {"key": "summary", "type": "bool", "default": False},
|
|
473
|
+
"--since": {"key": "since", "type": "str", "default": None},
|
|
474
|
+
"--from": {"key": "from", "type": "str", "default": None},
|
|
475
|
+
"--to": {"key": "to", "type": "str", "default": None},
|
|
476
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
477
|
+
}, HISTORY_USAGE, positionals_key="names", max_positionals=1)
|
|
478
|
+
period = _parse_history_period(parsed, now)
|
|
479
|
+
return {
|
|
480
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
481
|
+
"limit": parsed["limit"],
|
|
482
|
+
"summary": parsed["summary"],
|
|
483
|
+
"period": period,
|
|
484
|
+
"json": parsed["json"],
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _parse_stats_args(args, now=None):
|
|
489
|
+
now = now or datetime.now().astimezone()
|
|
490
|
+
parsed = _parse_flag_args(args, {
|
|
491
|
+
"--since": {"key": "since", "type": "str", "default": None},
|
|
492
|
+
"--from": {"key": "from", "type": "str", "default": None},
|
|
493
|
+
"--to": {"key": "to", "type": "str", "default": None},
|
|
494
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
495
|
+
}, STATS_USAGE, positionals_key="names", max_positionals=1)
|
|
496
|
+
period = _parse_history_period(parsed, now)
|
|
497
|
+
return {
|
|
498
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
499
|
+
"period": period,
|
|
500
|
+
"json": parsed["json"],
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _read_option_value(args, index, usage):
|
|
505
|
+
if index + 1 >= len(args):
|
|
506
|
+
raise CdxError(usage)
|
|
507
|
+
return args[index + 1], index + 2
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _parse_session_names(value):
|
|
511
|
+
if value is None:
|
|
512
|
+
return None
|
|
513
|
+
names = [item.strip() for item in value.split(",") if item.strip()]
|
|
514
|
+
if not names:
|
|
515
|
+
raise CdxError("At least one session name is required in --sessions.")
|
|
516
|
+
return names
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _parse_set_unset_sessions(value):
|
|
520
|
+
text = str(value or "").strip()
|
|
521
|
+
if not text:
|
|
522
|
+
raise CdxError("At least one session name is required in --sessions.")
|
|
523
|
+
if text.lower() == "all":
|
|
524
|
+
return "all"
|
|
525
|
+
return _parse_session_names(text)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _parse_provider_filter(value, usage):
|
|
529
|
+
provider = str(value or "").strip()
|
|
530
|
+
if provider not in PROVIDERS:
|
|
531
|
+
raise CdxError(usage)
|
|
532
|
+
return provider
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _parse_launch_setting_target(value, usage):
|
|
536
|
+
text = str(value or "").strip()
|
|
537
|
+
if not text:
|
|
538
|
+
raise CdxError(usage)
|
|
539
|
+
if text == "all":
|
|
540
|
+
return {"name": None, "sessions": "all", "provider": None}
|
|
541
|
+
if text.startswith("provider:"):
|
|
542
|
+
return {"name": None, "sessions": None, "provider": _parse_provider_filter(text.split(":", 1)[1], usage)}
|
|
543
|
+
if "," in text:
|
|
544
|
+
return {"name": None, "sessions": _parse_session_names(text), "provider": None}
|
|
545
|
+
return {"name": text, "sessions": None, "provider": None}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _parse_launch_setting_alias_args(kind, args):
|
|
549
|
+
json_flag, cleaned = _parse_json_flag(args)
|
|
550
|
+
if len(cleaned) != 2:
|
|
551
|
+
raise CdxError(SETTING_ALIAS_USAGE)
|
|
552
|
+
target = _parse_launch_setting_target(cleaned[0], SETTING_ALIAS_USAGE)
|
|
553
|
+
value = cleaned[1]
|
|
554
|
+
field = "permission" if kind == "perm" else kind
|
|
555
|
+
if value == "default":
|
|
556
|
+
return {**target, "keys": [field], "json": json_flag, "alias": kind}
|
|
557
|
+
if field == "fast":
|
|
558
|
+
value = _parse_fast_value(value)
|
|
559
|
+
return {**target, "settings": {field: value}, "json": json_flag, "alias": kind}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _parse_update_args(args):
|
|
563
|
+
parsed = _parse_flag_args(args, {
|
|
564
|
+
"--check": {"key": "check", "type": "bool", "default": False},
|
|
565
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
566
|
+
"--yes": {"key": "yes", "type": "bool", "default": False},
|
|
567
|
+
"--version": {"key": "version", "type": "str", "default": None},
|
|
568
|
+
}, UPDATE_USAGE)
|
|
569
|
+
if parsed["check"] and parsed["version"]:
|
|
570
|
+
raise CdxError("Usage: cdx update --check cannot be combined with --version.")
|
|
571
|
+
if parsed["version"] is not None and not parsed["version"].strip():
|
|
572
|
+
raise CdxError("Usage: cdx update [--check] [--yes] [--json] [--version TAG]")
|
|
573
|
+
return parsed
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _parse_export_args(args):
|
|
577
|
+
parsed = _parse_flag_args(args, {
|
|
578
|
+
"--include-auth": {"key": "include_auth", "type": "bool", "default": False},
|
|
579
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
580
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
581
|
+
"--sessions": {
|
|
582
|
+
"key": "session_names",
|
|
583
|
+
"type": "str",
|
|
584
|
+
"default": None,
|
|
585
|
+
"transform": _parse_session_names,
|
|
586
|
+
},
|
|
587
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
588
|
+
}, EXPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
589
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
590
|
+
if not parsed["file_path"]:
|
|
591
|
+
raise CdxError(EXPORT_USAGE)
|
|
592
|
+
if parsed["passphrase_env"] and not parsed["include_auth"]:
|
|
593
|
+
raise CdxError("--passphrase-env requires --include-auth for export.")
|
|
594
|
+
return parsed
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _parse_import_args(args):
|
|
598
|
+
parsed = _parse_flag_args(args, {
|
|
599
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
600
|
+
"--merge": {"key": "merge", "type": "bool", "default": False},
|
|
601
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
602
|
+
"--sessions": {
|
|
603
|
+
"key": "session_names",
|
|
604
|
+
"type": "str",
|
|
605
|
+
"default": None,
|
|
606
|
+
"transform": _parse_session_names,
|
|
607
|
+
},
|
|
608
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
609
|
+
}, IMPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
610
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
611
|
+
if not parsed["file_path"]:
|
|
612
|
+
raise CdxError(IMPORT_USAGE)
|
|
613
|
+
if parsed["force"] and parsed["merge"]:
|
|
614
|
+
raise CdxError("--force and --merge are mutually exclusive.")
|
|
615
|
+
return parsed
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _parse_runs_args(rest):
|
|
619
|
+
return _parse_flag_args(rest, {
|
|
620
|
+
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
621
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
622
|
+
}, RUNS_USAGE, max_positionals=0)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _parse_run_id_json_args(rest, usage):
|
|
626
|
+
parsed = _parse_flag_args(rest, {
|
|
627
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
628
|
+
}, usage, positionals_key="ids", max_positionals=1)
|
|
629
|
+
if not parsed["json"] or len(parsed["ids"]) != 1:
|
|
630
|
+
raise CdxError(usage)
|
|
631
|
+
return {"run_id": parsed["ids"][0], "json": True}
|