cdx-manager 0.9.10 → 0.9.11

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.
@@ -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}