aisc-cli 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.
- aisc/VERSION +1 -0
- aisc/__init__.py +49 -0
- aisc/__main__.py +6 -0
- aisc/adapters/__init__.py +1 -0
- aisc/adapters/config_reader.py +215 -0
- aisc/adapters/container_registry.py +718 -0
- aisc/adapters/data_root_store.py +296 -0
- aisc/adapters/docker_.py +1183 -0
- aisc/adapters/docker_gateway.py +1244 -0
- aisc/adapters/docker_sdk_backed.py +274 -0
- aisc/adapters/maintenance_lock.py +97 -0
- aisc/adapters/state_file.py +184 -0
- aisc/adapters/system.py +101 -0
- aisc/adapters/windows_config_reader.py +249 -0
- aisc/adapters/workspace_lease_store.py +189 -0
- aisc/application/__init__.py +1 -0
- aisc/application/artifact.py +265 -0
- aisc/application/bundle_fetch.py +836 -0
- aisc/application/cc_switch_provider.py +245 -0
- aisc/application/cc_switch_resolver.py +363 -0
- aisc/application/config_service.py +452 -0
- aisc/application/conversation.py +516 -0
- aisc/application/data_migration.py +440 -0
- aisc/application/data_root.py +275 -0
- aisc/application/docker_lifecycle.py +659 -0
- aisc/application/doctor.py +821 -0
- aisc/application/legacy_scan.py +252 -0
- aisc/application/network_subscription.py +642 -0
- aisc/application/profile_service.py +105 -0
- aisc/application/provider.py +92 -0
- aisc/application/repair.py +387 -0
- aisc/application/resources.py +270 -0
- aisc/application/runtime.py +1826 -0
- aisc/application/session.py +320 -0
- aisc/application/tool_versions.py +210 -0
- aisc/application/toolchain.py +91 -0
- aisc/application/update.py +286 -0
- aisc/application/usage.py +305 -0
- aisc/application/version.py +133 -0
- aisc/application/web_gateway.py +459 -0
- aisc/application/workspace_reconcile.py +318 -0
- aisc/applog.py +103 -0
- aisc/cli/__init__.py +1 -0
- aisc/cli/build_progress_parser.py +217 -0
- aisc/cli/commands/__init__.py +1 -0
- aisc/cli/commands/agents.py +117 -0
- aisc/cli/commands/artifact.py +210 -0
- aisc/cli/commands/build.py +566 -0
- aisc/cli/commands/bundle.py +107 -0
- aisc/cli/commands/cc_switch.py +158 -0
- aisc/cli/commands/config.py +104 -0
- aisc/cli/commands/container.py +787 -0
- aisc/cli/commands/conversation.py +72 -0
- aisc/cli/commands/data_root.py +84 -0
- aisc/cli/commands/logs.py +89 -0
- aisc/cli/commands/network.py +132 -0
- aisc/cli/commands/profile.py +52 -0
- aisc/cli/commands/provider.py +49 -0
- aisc/cli/commands/run.py +494 -0
- aisc/cli/commands/runs.py +125 -0
- aisc/cli/commands/runtime.py +450 -0
- aisc/cli/commands/serve.py +550 -0
- aisc/cli/commands/serve_fs.py +369 -0
- aisc/cli/commands/session.py +167 -0
- aisc/cli/commands/update.py +145 -0
- aisc/cli/commands/usage.py +74 -0
- aisc/cli/commands/wizard.py +130 -0
- aisc/cli/commands/workspaces.py +162 -0
- aisc/cli/help_i18n.py +385 -0
- aisc/cli/main.py +2926 -0
- aisc/cli/output.py +239 -0
- aisc/cli/tutorial.py +109 -0
- aisc/domain/__init__.py +1 -0
- aisc/domain/artifacts.py +279 -0
- aisc/domain/cc_switch_release.py +303 -0
- aisc/domain/config.py +64 -0
- aisc/domain/data_migration.py +259 -0
- aisc/domain/data_root.py +124 -0
- aisc/domain/docker_ownership.py +148 -0
- aisc/domain/gateway.py +168 -0
- aisc/domain/models.py +674 -0
- aisc/domain/toolchain.py +72 -0
- aisc/domain/web_services.py +355 -0
- aisc/domain/workspace_lease.py +139 -0
- aisc/schemas/__init__.py +1 -0
- aisc/schemas/config_schema.py +145 -0
- aisc_cli-0.1.0.dist-info/METADATA +420 -0
- aisc_cli-0.1.0.dist-info/RECORD +92 -0
- aisc_cli-0.1.0.dist-info/WHEEL +5 -0
- aisc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- aisc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- aisc_cli-0.1.0.dist-info/top_level.txt +1 -0
aisc/cli/main.py
ADDED
|
@@ -0,0 +1,2926 @@
|
|
|
1
|
+
"""Main CLI entry point — argument parsing, command dispatch, output formatting.
|
|
2
|
+
|
|
3
|
+
``aisc version``, ``aisc doctor``, ``aisc build``, and ``aisc run`` commands.
|
|
4
|
+
``python -m aisc`` and console_script ``aisc`` are both supported.
|
|
5
|
+
|
|
6
|
+
All docker operations are injected through a ``DockerExecutor``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
from aisc import __version__
|
|
19
|
+
from aisc.cli.output import (
|
|
20
|
+
JsonlEmitter,
|
|
21
|
+
build_envelope,
|
|
22
|
+
build_error,
|
|
23
|
+
emit_json,
|
|
24
|
+
emit_json_usage_error,
|
|
25
|
+
print_doctor_text,
|
|
26
|
+
)
|
|
27
|
+
from aisc.domain.models import CheckStatus, CliError, DoctorReport, VersionInfo
|
|
28
|
+
from aisc.domain.models import SessionAgent
|
|
29
|
+
from aisc.domain.models import RuntimeErrorCode
|
|
30
|
+
from aisc.domain.artifacts import ArtifactAction, ArtifactKind, ArtifactOpenWith
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Custom argument parser — JSON-aware error handling
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
class _AiscArgumentParser(argparse.ArgumentParser):
|
|
38
|
+
"""ArgumentParser that respects ``--format json`` when reporting errors."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
41
|
+
kwargs.setdefault("allow_abbrev", False)
|
|
42
|
+
super().__init__(*args, **kwargs)
|
|
43
|
+
self._aisc_format: Optional[str] = None
|
|
44
|
+
self._aisc_command: str = "aisc"
|
|
45
|
+
|
|
46
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
47
|
+
if self._aisc_format == "json":
|
|
48
|
+
emit_json_usage_error(
|
|
49
|
+
command=self._aisc_command or "aisc",
|
|
50
|
+
version=__version__,
|
|
51
|
+
message=message,
|
|
52
|
+
)
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
else:
|
|
55
|
+
super().error(message)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Parser setup
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _add_global_args(p: argparse.ArgumentParser, *, is_subparser: bool = False) -> None:
|
|
63
|
+
"""Add global options."""
|
|
64
|
+
default_val = argparse.SUPPRESS if is_subparser else "text"
|
|
65
|
+
p.add_argument(
|
|
66
|
+
"--format",
|
|
67
|
+
choices=["text", "json"],
|
|
68
|
+
default=default_val,
|
|
69
|
+
help="Output format (default: text)",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument(
|
|
72
|
+
"--no-color",
|
|
73
|
+
action="store_true",
|
|
74
|
+
default=argparse.SUPPRESS if is_subparser else False,
|
|
75
|
+
help="Disable ANSI color output",
|
|
76
|
+
)
|
|
77
|
+
p.add_argument(
|
|
78
|
+
"--aisc-root",
|
|
79
|
+
type=str,
|
|
80
|
+
default=argparse.SUPPRESS if is_subparser else None,
|
|
81
|
+
help="Path to AISC repository root",
|
|
82
|
+
)
|
|
83
|
+
p.add_argument(
|
|
84
|
+
"--events",
|
|
85
|
+
action="store_true",
|
|
86
|
+
default=argparse.SUPPRESS if is_subparser else False,
|
|
87
|
+
help="Enable JSONL event stream output (build/run commands)",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_parser() -> _AiscArgumentParser:
|
|
92
|
+
"""Build the top-level argument parser."""
|
|
93
|
+
parser = _AiscArgumentParser(
|
|
94
|
+
prog="aisc",
|
|
95
|
+
description="AISC CLI — Super Claude workstation management",
|
|
96
|
+
)
|
|
97
|
+
_add_global_args(parser)
|
|
98
|
+
|
|
99
|
+
sub = parser.add_subparsers(dest="command", title="commands")
|
|
100
|
+
|
|
101
|
+
# --- version ---
|
|
102
|
+
vp = sub.add_parser("version", help="Show version information", allow_abbrev=False)
|
|
103
|
+
_add_global_args(vp, is_subparser=True)
|
|
104
|
+
|
|
105
|
+
# --- doctor ---
|
|
106
|
+
dp = sub.add_parser("doctor", help="Run environment diagnostics", allow_abbrev=False)
|
|
107
|
+
_add_global_args(dp, is_subparser=True)
|
|
108
|
+
|
|
109
|
+
# --- serve (2.1.10 R1): long-lived remote channel on stdio ---
|
|
110
|
+
svp = sub.add_parser(
|
|
111
|
+
"serve",
|
|
112
|
+
help="Serve remote callers: newline-delimited JSON frames on stdio",
|
|
113
|
+
allow_abbrev=False,
|
|
114
|
+
)
|
|
115
|
+
svp.add_argument(
|
|
116
|
+
"--stdio",
|
|
117
|
+
action="store_true",
|
|
118
|
+
help="speak the frame protocol on stdin/stdout (the only transport in this version)",
|
|
119
|
+
)
|
|
120
|
+
_add_global_args(svp, is_subparser=True)
|
|
121
|
+
|
|
122
|
+
# --- build ---
|
|
123
|
+
bp = sub.add_parser("build", help="Build Docker image", allow_abbrev=False)
|
|
124
|
+
_add_global_args(bp, is_subparser=True)
|
|
125
|
+
bp.add_argument("--tag", "-t", type=str, default="super-claude:latest",
|
|
126
|
+
help="Image tag (default: super-claude:latest)")
|
|
127
|
+
bp.add_argument("--no-cache", action="store_true", default=False,
|
|
128
|
+
help="Disable Docker build cache")
|
|
129
|
+
bp.add_argument("--pull", action="store_true", default=False,
|
|
130
|
+
help="Always pull the base image")
|
|
131
|
+
bp.add_argument("--dry-run", action="store_true", default=False,
|
|
132
|
+
help="Plan the build without executing")
|
|
133
|
+
# Stage 8b (CS-01/CS-02): cc-switch release resolution. `latest` resolves
|
|
134
|
+
# the newest stable upstream release live (fail closed, never a silent
|
|
135
|
+
# pin); an explicit vX.Y.Z is reproducible; --cc-switch-manifest builds
|
|
136
|
+
# fully offline from a previously written resolution receipt.
|
|
137
|
+
bp.add_argument("--cc-switch-version", type=str, default=argparse.SUPPRESS,
|
|
138
|
+
help="cc-switch version: 'latest' (default) or vX.Y.Z "
|
|
139
|
+
"(env: CC_SWITCH_VERSION)")
|
|
140
|
+
bp.add_argument("--cc-switch-channel", type=str, default=argparse.SUPPRESS,
|
|
141
|
+
choices=["stable"],
|
|
142
|
+
help="cc-switch release channel (default: stable; "
|
|
143
|
+
"env: CC_SWITCH_CHANNEL)")
|
|
144
|
+
bp.add_argument("--cc-switch-manifest", type=str, default=None, metavar="PATH",
|
|
145
|
+
help="Build from a resolver manifest file (offline / "
|
|
146
|
+
"reproducible; skips the live resolve)")
|
|
147
|
+
bp.add_argument("--require-pin", action="store_true", default=False,
|
|
148
|
+
help="Fail the build when the cc-switch version cannot be "
|
|
149
|
+
"resolved (default: an offline build continues WITHOUT "
|
|
150
|
+
"the pin — unverified, CN-mirror download)")
|
|
151
|
+
|
|
152
|
+
# --- run ---
|
|
153
|
+
# F2-C: run ACTIVATES a workspace (detached, then exit). Interactive
|
|
154
|
+
# surfaces live on their own verbs: shell / claude / codex.
|
|
155
|
+
rp = sub.add_parser("run", help="Activate a workspace (detached container)",
|
|
156
|
+
allow_abbrev=False)
|
|
157
|
+
_add_global_args(rp, is_subparser=True)
|
|
158
|
+
rp.add_argument("path", type=str, nargs="?", default=None,
|
|
159
|
+
help="Workspace directory to activate (e.g. ./ or /home/user/proj)")
|
|
160
|
+
rp.add_argument("--resume", type=str, default=None, metavar="N|PATH",
|
|
161
|
+
help="Re-activate from history (see `aisc runs`); explicit flags win")
|
|
162
|
+
rp.add_argument("--image", "-i", type=str, default="super-claude:latest",
|
|
163
|
+
help="Docker image (default: super-claude:latest)")
|
|
164
|
+
rp.add_argument("--name", type=str, default="super-claude-station",
|
|
165
|
+
help="Workspace alias — also the container name prefix; "
|
|
166
|
+
"resume later by it (aisc run --resume <别名>)")
|
|
167
|
+
rp.add_argument("--network", type=str, choices=["direct", "proxy"],
|
|
168
|
+
default=argparse.SUPPRESS,
|
|
169
|
+
help="Network mode: direct or proxy (default: direct)")
|
|
170
|
+
rp.add_argument("--profile", type=str, choices=["proxy"],
|
|
171
|
+
default=None,
|
|
172
|
+
help="Compatibility alias for --network proxy (prefer --network proxy)")
|
|
173
|
+
rp.add_argument("--dry-run", action="store_true", default=False,
|
|
174
|
+
help="Plan the activation without executing")
|
|
175
|
+
rp.add_argument("--label", type=str, default="",
|
|
176
|
+
help="Container label for multi-container addressing (optional)")
|
|
177
|
+
|
|
178
|
+
# --- F2-C: agent sugar + activation history ---
|
|
179
|
+
for _agent in ("claude", "codex"):
|
|
180
|
+
_ap = sub.add_parser(_agent, help=f"Open {_agent} in the active workspace",
|
|
181
|
+
allow_abbrev=False)
|
|
182
|
+
_add_global_args(_ap, is_subparser=True)
|
|
183
|
+
_ap.add_argument("--workspace", type=str, default=None,
|
|
184
|
+
help="Target a registered workspace instead of the active one")
|
|
185
|
+
_ap.add_argument("--name", type=str, default=None,
|
|
186
|
+
help="Container name (overrides registry discovery)")
|
|
187
|
+
_ap.add_argument("rest", nargs=argparse.REMAINDER, default=[],
|
|
188
|
+
help=f"Args passed to {_agent} verbatim (use -- first: aisc {_agent} -- -c)")
|
|
189
|
+
|
|
190
|
+
rsp = sub.add_parser("runs", help="Activation history (aisc run records)",
|
|
191
|
+
allow_abbrev=False)
|
|
192
|
+
_add_global_args(rsp, is_subparser=True)
|
|
193
|
+
|
|
194
|
+
wsp = sub.add_parser("workspaces", help="Manage running CLI workspaces",
|
|
195
|
+
allow_abbrev=False)
|
|
196
|
+
_add_global_args(wsp, is_subparser=True)
|
|
197
|
+
wsp.add_argument("--stop", action="store_true", default=False,
|
|
198
|
+
help="Stop & remove every RUNNING CLI-owned workspace")
|
|
199
|
+
|
|
200
|
+
# --- config ---
|
|
201
|
+
cp = sub.add_parser("config", help="Config management", allow_abbrev=False)
|
|
202
|
+
_add_global_args(cp, is_subparser=True)
|
|
203
|
+
csub = cp.add_subparsers(dest="config_command", title="config commands",
|
|
204
|
+
parser_class=_AiscArgumentParser)
|
|
205
|
+
|
|
206
|
+
cv = csub.add_parser("validate", help="Validate config files", allow_abbrev=False)
|
|
207
|
+
_add_global_args(cv, is_subparser=True)
|
|
208
|
+
cv.add_argument("--config", type=str, default=None,
|
|
209
|
+
help="Explicit user config file path")
|
|
210
|
+
cv.add_argument("--workspace", type=str, default=None,
|
|
211
|
+
help="Workspace root path")
|
|
212
|
+
|
|
213
|
+
ce = csub.add_parser("effective", help="Show effective config", allow_abbrev=False)
|
|
214
|
+
_add_global_args(ce, is_subparser=True)
|
|
215
|
+
ce.add_argument("--config", type=str, default=None,
|
|
216
|
+
help="Explicit user config file path")
|
|
217
|
+
ce.add_argument("--workspace", type=str, default=None,
|
|
218
|
+
help="Workspace root path")
|
|
219
|
+
|
|
220
|
+
cshow = csub.add_parser("show", help="Alias for 'config effective' (compatibility name)",
|
|
221
|
+
allow_abbrev=False)
|
|
222
|
+
_add_global_args(cshow, is_subparser=True)
|
|
223
|
+
cshow.add_argument("--config", type=str, default=None,
|
|
224
|
+
help="Explicit user config file path")
|
|
225
|
+
cshow.add_argument("--workspace", type=str, default=None,
|
|
226
|
+
help="Workspace root path")
|
|
227
|
+
|
|
228
|
+
# --- profile ---
|
|
229
|
+
prp = sub.add_parser("profile", help="Profile management", allow_abbrev=False)
|
|
230
|
+
_add_global_args(prp, is_subparser=True)
|
|
231
|
+
prsub = prp.add_subparsers(dest="profile_command", title="profile commands",
|
|
232
|
+
parser_class=_AiscArgumentParser)
|
|
233
|
+
|
|
234
|
+
prl = prsub.add_parser("list", help="List available profiles", allow_abbrev=False)
|
|
235
|
+
_add_global_args(prl, is_subparser=True)
|
|
236
|
+
|
|
237
|
+
prs = prsub.add_parser("show", help="Show profile details", allow_abbrev=False)
|
|
238
|
+
_add_global_args(prs, is_subparser=True)
|
|
239
|
+
prs.add_argument("name", type=str, nargs="?", default=None,
|
|
240
|
+
help="Profile name (default: safe)")
|
|
241
|
+
|
|
242
|
+
# --- status ---
|
|
243
|
+
stp = sub.add_parser("status", help="Show container status", allow_abbrev=False)
|
|
244
|
+
_add_global_args(stp, is_subparser=True)
|
|
245
|
+
stp.add_argument("--name", type=str, default=None,
|
|
246
|
+
help="Container name (overrides registry discovery)")
|
|
247
|
+
stp.add_argument("--label", type=str, default=None,
|
|
248
|
+
help="Target container by label")
|
|
249
|
+
|
|
250
|
+
# --- stop ---
|
|
251
|
+
spp = sub.add_parser("stop", help="Stop & remove the active workspace container",
|
|
252
|
+
allow_abbrev=False)
|
|
253
|
+
_add_global_args(spp, is_subparser=True)
|
|
254
|
+
spp.add_argument("--name", type=str, default=None,
|
|
255
|
+
help="Container name (overrides registry discovery)")
|
|
256
|
+
spp.add_argument("--label", type=str, default=None,
|
|
257
|
+
help="Target container by label")
|
|
258
|
+
spp.add_argument("--all", action="store_true", default=False,
|
|
259
|
+
help="Stop & remove EVERY CLI-owned container (Workbench runtimes untouched)")
|
|
260
|
+
|
|
261
|
+
# --- restart ---
|
|
262
|
+
rsp = sub.add_parser("restart", help="Restart the container", allow_abbrev=False)
|
|
263
|
+
_add_global_args(rsp, is_subparser=True)
|
|
264
|
+
rsp.add_argument("--name", type=str, default=None,
|
|
265
|
+
help="Container name (overrides registry discovery)")
|
|
266
|
+
rsp.add_argument("--label", type=str, default=None,
|
|
267
|
+
help="Target container by label")
|
|
268
|
+
|
|
269
|
+
# --- shell ---
|
|
270
|
+
shp = sub.add_parser("shell", help="Open a bash shell in the container", allow_abbrev=False)
|
|
271
|
+
_add_global_args(shp, is_subparser=True)
|
|
272
|
+
shp.add_argument("--name", type=str, default=None,
|
|
273
|
+
help="Container name (overrides registry discovery)")
|
|
274
|
+
shp.add_argument("--label", type=str, default=None,
|
|
275
|
+
help="Target container by label")
|
|
276
|
+
shp.add_argument("rest", nargs=argparse.REMAINDER, default=[],
|
|
277
|
+
help="One-shot command in the container (use -- first: aisc shell -- ls -la)")
|
|
278
|
+
|
|
279
|
+
# --- switch ---
|
|
280
|
+
swp = sub.add_parser("switch", help="Switch AI provider in the container", allow_abbrev=False)
|
|
281
|
+
_add_global_args(swp, is_subparser=True)
|
|
282
|
+
swp.add_argument("--name", type=str, default=None,
|
|
283
|
+
help="Container name (overrides registry discovery)")
|
|
284
|
+
swp.add_argument("--label", type=str, default=None,
|
|
285
|
+
help="Target container by label")
|
|
286
|
+
swp.add_argument("--quick", type=str, default=None,
|
|
287
|
+
help="Provider id or alias for quick switch (e.g. deepseek)")
|
|
288
|
+
|
|
289
|
+
# --- provider ---
|
|
290
|
+
prp = sub.add_parser("provider", help="Manage AI provider configuration", allow_abbrev=False)
|
|
291
|
+
_add_global_args(prp, is_subparser=True)
|
|
292
|
+
prsub = prp.add_subparsers(dest="provider_command", title="provider commands",
|
|
293
|
+
parser_class=_AiscArgumentParser)
|
|
294
|
+
|
|
295
|
+
prsk = prsub.add_parser(
|
|
296
|
+
"set-key",
|
|
297
|
+
help="Open the secure interactive editor for a provider",
|
|
298
|
+
allow_abbrev=False,
|
|
299
|
+
)
|
|
300
|
+
_add_global_args(prsk, is_subparser=True)
|
|
301
|
+
prsk.add_argument("provider_id", type=str,
|
|
302
|
+
help="Provider ID (e.g., deepseek, zhipu, kimi)")
|
|
303
|
+
prsk.add_argument("--name", type=str, default=None,
|
|
304
|
+
help="Container name (overrides registry discovery)")
|
|
305
|
+
prsk.add_argument("--label", type=str, default=None,
|
|
306
|
+
help="Target container by label")
|
|
307
|
+
prsk.add_argument("--agent", type=str, default="claude",
|
|
308
|
+
help="Target agent (default: claude)")
|
|
309
|
+
|
|
310
|
+
# provider current (Workbench S0.4): non-interactive, --format json
|
|
311
|
+
prpc = prsub.add_parser(
|
|
312
|
+
"current",
|
|
313
|
+
help="Show the current provider status for an agent (Workbench)",
|
|
314
|
+
allow_abbrev=False,
|
|
315
|
+
)
|
|
316
|
+
_add_global_args(prpc, is_subparser=True)
|
|
317
|
+
prpc.add_argument("--runtime-id", type=str, required=True,
|
|
318
|
+
help="Runtime ID (UUID v4)")
|
|
319
|
+
prpc.add_argument("--agent", type=str, required=True,
|
|
320
|
+
choices=["claude", "codex"],
|
|
321
|
+
help="Agent (claude|codex)")
|
|
322
|
+
prpc.add_argument("--workspace", type=str, default=None,
|
|
323
|
+
help="Workspace path (default: current directory)")
|
|
324
|
+
|
|
325
|
+
# --- cc-switch (Stage 8d: provider data plane, aisc.cc-switch-provider/v1) ---
|
|
326
|
+
csp = sub.add_parser(
|
|
327
|
+
"cc-switch", help="Manage cc-switch providers (list/add/edit/delete)",
|
|
328
|
+
allow_abbrev=False,
|
|
329
|
+
)
|
|
330
|
+
_add_global_args(csp, is_subparser=True)
|
|
331
|
+
cssub = csp.add_subparsers(dest="cc_switch_command", title="cc-switch commands",
|
|
332
|
+
parser_class=_AiscArgumentParser)
|
|
333
|
+
|
|
334
|
+
def _cc_switch_common(p):
|
|
335
|
+
_add_global_args(p, is_subparser=True)
|
|
336
|
+
p.add_argument("--runtime-id", type=str, required=True,
|
|
337
|
+
help="Runtime ID (UUID v4)")
|
|
338
|
+
p.add_argument("--agent", type=str, required=True,
|
|
339
|
+
choices=["claude", "codex"], help="Agent (claude|codex)")
|
|
340
|
+
p.add_argument("--workspace", type=str, default=None,
|
|
341
|
+
help="Workspace path (default: current directory)")
|
|
342
|
+
|
|
343
|
+
csl = cssub.add_parser("list", help="List providers (secret-free snapshot)",
|
|
344
|
+
allow_abbrev=False)
|
|
345
|
+
_cc_switch_common(csl)
|
|
346
|
+
csl.add_argument("--reveal-id", type=str, default=None, metavar="ID",
|
|
347
|
+
help="Additionally emit the FULL api_key of this provider "
|
|
348
|
+
"(edit-time explicit view, 2.1.11 P1)")
|
|
349
|
+
|
|
350
|
+
csa = cssub.add_parser("add", help="Add a provider (request JSON on stdin)",
|
|
351
|
+
allow_abbrev=False)
|
|
352
|
+
_cc_switch_common(csa)
|
|
353
|
+
# KI-7①: default None, NOT "simple" — the Workbench never passes --mode,
|
|
354
|
+
# and a truthy argparse default unconditionally clobbered the stdin
|
|
355
|
+
# document's "mode":"custom" (observed as "unknown preset provider").
|
|
356
|
+
csa.add_argument("--mode", choices=["simple", "custom"], default=None,
|
|
357
|
+
help="simple = preset provider + api key; custom = full "
|
|
358
|
+
"fields (the stdin request's mode wins unless set)")
|
|
359
|
+
csa.add_argument("--provider", type=str, default=None,
|
|
360
|
+
help="Preset provider id (simple mode, e.g. deepseek)")
|
|
361
|
+
csa.add_argument("--id", dest="new_id", type=str, default=None,
|
|
362
|
+
help="Provider id to create (default: preset id / name slug)")
|
|
363
|
+
|
|
364
|
+
cse = cssub.add_parser("edit", help="Edit a provider (patch JSON on stdin)",
|
|
365
|
+
allow_abbrev=False)
|
|
366
|
+
_cc_switch_common(cse)
|
|
367
|
+
cse.add_argument("provider_id", type=str, help="Provider ID to edit")
|
|
368
|
+
|
|
369
|
+
css = cssub.add_parser("switch", help="Activate a provider (make it current)",
|
|
370
|
+
allow_abbrev=False)
|
|
371
|
+
_cc_switch_common(css)
|
|
372
|
+
css.add_argument("provider_id", type=str, help="Provider ID to activate")
|
|
373
|
+
|
|
374
|
+
csd = cssub.add_parser("delete", help="Delete a provider", allow_abbrev=False)
|
|
375
|
+
_cc_switch_common(csd)
|
|
376
|
+
csd.add_argument("provider_id", type=str, help="Provider ID to delete")
|
|
377
|
+
csd.add_argument("--confirm", action="store_true", default=False,
|
|
378
|
+
help="Required confirmation flag")
|
|
379
|
+
|
|
380
|
+
# IDEA-5 (5c): remote model list for the mapping dropdown.
|
|
381
|
+
csf = cssub.add_parser("fetch-models",
|
|
382
|
+
help="Fetch the remote model list for a provider",
|
|
383
|
+
allow_abbrev=False)
|
|
384
|
+
_cc_switch_common(csf)
|
|
385
|
+
csf.add_argument("provider_id", type=str, nargs="?", default="",
|
|
386
|
+
help="Provider ID to query (empty = add-mode inline probe; "
|
|
387
|
+
"the stdin document then carries base_url)")
|
|
388
|
+
|
|
389
|
+
# --- network (IDEA-2: mihomo subscription data plane) ---
|
|
390
|
+
nwp = sub.add_parser(
|
|
391
|
+
"network", help="Network management (mihomo subscription for container TUN)",
|
|
392
|
+
allow_abbrev=False,
|
|
393
|
+
)
|
|
394
|
+
_add_global_args(nwp, is_subparser=True)
|
|
395
|
+
nwsub = nwp.add_subparsers(dest="network_command", title="network commands",
|
|
396
|
+
parser_class=_AiscArgumentParser, required=True)
|
|
397
|
+
|
|
398
|
+
nws = nwsub.add_parser(
|
|
399
|
+
"subscription", help="Manage the proxy subscription (IDEA-2)",
|
|
400
|
+
allow_abbrev=False,
|
|
401
|
+
)
|
|
402
|
+
_add_global_args(nws, is_subparser=True)
|
|
403
|
+
nwssub = nws.add_subparsers(dest="subscription_command",
|
|
404
|
+
title="subscription commands",
|
|
405
|
+
parser_class=_AiscArgumentParser, required=True)
|
|
406
|
+
|
|
407
|
+
nwi = nwssub.add_parser(
|
|
408
|
+
"import", help="Import a subscription (URL on stdin — a credential, "
|
|
409
|
+
"never argv)", allow_abbrev=False)
|
|
410
|
+
_add_global_args(nwi, is_subparser=True)
|
|
411
|
+
|
|
412
|
+
nwif = nwssub.add_parser(
|
|
413
|
+
"import-file", help="Import manually supplied subscription content "
|
|
414
|
+
"(full content on stdin; fallback for sources "
|
|
415
|
+
"that reject automated downloads)", allow_abbrev=False)
|
|
416
|
+
_add_global_args(nwif, is_subparser=True)
|
|
417
|
+
|
|
418
|
+
nwr = nwssub.add_parser(
|
|
419
|
+
"refresh", help="Re-fetch the stored subscription URL", allow_abbrev=False)
|
|
420
|
+
_add_global_args(nwr, is_subparser=True)
|
|
421
|
+
|
|
422
|
+
# 挂账①: persistence endpoint for the Rust (reqwest) downloader.
|
|
423
|
+
nwsd = nwssub.add_parser(
|
|
424
|
+
"store-downloaded",
|
|
425
|
+
help="Persist a Rust-side download ({url, content_b64, userinfo} JSON "
|
|
426
|
+
"on stdin — the fingerprint-wall-safe transport)",
|
|
427
|
+
allow_abbrev=False)
|
|
428
|
+
_add_global_args(nwsd, is_subparser=True)
|
|
429
|
+
|
|
430
|
+
nww = nwssub.add_parser(
|
|
431
|
+
"show", help="Show subscription status (secret-free)", allow_abbrev=False)
|
|
432
|
+
_add_global_args(nww, is_subparser=True)
|
|
433
|
+
|
|
434
|
+
nwc = nwssub.add_parser("clear", help="Remove the stored subscription",
|
|
435
|
+
allow_abbrev=False)
|
|
436
|
+
_add_global_args(nwc, is_subparser=True)
|
|
437
|
+
nwc.add_argument("--confirm", action="store_true", default=False,
|
|
438
|
+
help="Required confirmation flag")
|
|
439
|
+
|
|
440
|
+
# --- usage (IDEA-2: provider token usage aggregation) ---
|
|
441
|
+
usp = sub.add_parser(
|
|
442
|
+
"usage", help="Provider token usage statistics (all workspaces)",
|
|
443
|
+
allow_abbrev=False,
|
|
444
|
+
)
|
|
445
|
+
_add_global_args(usp, is_subparser=True)
|
|
446
|
+
ussub = usp.add_subparsers(dest="usage_command", title="usage commands",
|
|
447
|
+
parser_class=_AiscArgumentParser, required=True)
|
|
448
|
+
uso = ussub.add_parser(
|
|
449
|
+
"overview", help="Subscription status + per-provider token usage",
|
|
450
|
+
allow_abbrev=False)
|
|
451
|
+
_add_global_args(uso, is_subparser=True)
|
|
452
|
+
uso.add_argument("--range", dest="range", type=str, default="7d",
|
|
453
|
+
choices=["today", "7d", "30d"],
|
|
454
|
+
help="Time window (default: 7d)")
|
|
455
|
+
uso.add_argument("--workspace", type=str, default=None,
|
|
456
|
+
help="Limit to one workspace path (default: all)")
|
|
457
|
+
|
|
458
|
+
# --- logs (lifecycle-logging: the shared JSONL timeline) ---
|
|
459
|
+
lgp = sub.add_parser(
|
|
460
|
+
"logs", help="Lifecycle event log (full-flow debugging timeline)",
|
|
461
|
+
allow_abbrev=False,
|
|
462
|
+
)
|
|
463
|
+
_add_global_args(lgp, is_subparser=True)
|
|
464
|
+
lgsub = lgp.add_subparsers(dest="logs_command", title="logs commands",
|
|
465
|
+
parser_class=_AiscArgumentParser, required=True)
|
|
466
|
+
lgs = lgsub.add_parser(
|
|
467
|
+
"show", help="Show recent lifecycle events (secret-free by construction)",
|
|
468
|
+
allow_abbrev=False)
|
|
469
|
+
_add_global_args(lgs, is_subparser=True)
|
|
470
|
+
lgs.add_argument("--lines", type=int, default=200, metavar="N",
|
|
471
|
+
help="Number of recent events (default: 200; current file only)")
|
|
472
|
+
lgs.add_argument("--source", choices=["app", "cli", "ui", "all"],
|
|
473
|
+
default="all",
|
|
474
|
+
help="Filter by writer (default: all)")
|
|
475
|
+
lgt = lgsub.add_parser(
|
|
476
|
+
"path", help="Print the log file path (for scripts and the Workbench)",
|
|
477
|
+
allow_abbrev=False)
|
|
478
|
+
_add_global_args(lgt, is_subparser=True)
|
|
479
|
+
|
|
480
|
+
# --- ps ---
|
|
481
|
+
psp = sub.add_parser("ps", help="List all registered containers", allow_abbrev=False)
|
|
482
|
+
_add_global_args(psp, is_subparser=True)
|
|
483
|
+
|
|
484
|
+
# --- maintenance (docker-resource-lifecycle B) ---
|
|
485
|
+
mtp = sub.add_parser("maintenance", help="Installer-facing Docker lifecycle ops",
|
|
486
|
+
allow_abbrev=False)
|
|
487
|
+
_add_global_args(mtp, is_subparser=True)
|
|
488
|
+
mtsub = mtp.add_subparsers(dest="maintenance_command", title="maintenance commands",
|
|
489
|
+
parser_class=_AiscArgumentParser)
|
|
490
|
+
|
|
491
|
+
mts = mtsub.add_parser("docker-scan", help="Read-only ownership classification",
|
|
492
|
+
allow_abbrev=False)
|
|
493
|
+
_add_global_args(mts, is_subparser=True)
|
|
494
|
+
mts.add_argument("--context", choices=["first_install", "upgrade", "uninstall"],
|
|
495
|
+
default="upgrade",
|
|
496
|
+
help="Install context drives legacy-image evidence rules")
|
|
497
|
+
mts.add_argument("--old-image-id", action="append", default=[],
|
|
498
|
+
help="Upgrade-captured old image ID (temporary evidence; repeatable)")
|
|
499
|
+
|
|
500
|
+
mtc = mtsub.add_parser("docker-cleanup", help="Remove owned/legacy containers+images",
|
|
501
|
+
allow_abbrev=False)
|
|
502
|
+
_add_global_args(mtc, is_subparser=True)
|
|
503
|
+
mtc.add_argument("--context", choices=["first_install", "upgrade", "uninstall"],
|
|
504
|
+
default="uninstall")
|
|
505
|
+
mtc.add_argument("--old-image-id", action="append", default=[])
|
|
506
|
+
|
|
507
|
+
mtr = mtsub.add_parser("docker-rebuild", help="No-cache rebuild with old-ID handoff",
|
|
508
|
+
allow_abbrev=False)
|
|
509
|
+
_add_global_args(mtr, is_subparser=True)
|
|
510
|
+
mtr.add_argument("--root", required=True, help="Bundle root (contains Dockerfile)")
|
|
511
|
+
mtr.add_argument("--tag", default="super-claude:latest")
|
|
512
|
+
mtr.add_argument("--old-image-id", default="")
|
|
513
|
+
mtr.add_argument("--no-cache", action="store_true", default=True)
|
|
514
|
+
mtr.add_argument("--pull", action="store_true", default=False)
|
|
515
|
+
|
|
516
|
+
mtu = mtsub.add_parser("cache-usage", help="Read-only docker system df summary",
|
|
517
|
+
allow_abbrev=False)
|
|
518
|
+
_add_global_args(mtu, is_subparser=True)
|
|
519
|
+
|
|
520
|
+
mtcc = mtsub.add_parser("cache-cleanup",
|
|
521
|
+
help="Prune builder cache + dangling images (until-filtered)",
|
|
522
|
+
allow_abbrev=False)
|
|
523
|
+
_add_global_args(mtcc, is_subparser=True)
|
|
524
|
+
mtcc.add_argument("--min-age-hours", type=int, default=24,
|
|
525
|
+
help="Only clear cache entries older than this (default 24)")
|
|
526
|
+
|
|
527
|
+
# --- bundle (0.1.0 A3, guide 3.3.3) ---
|
|
528
|
+
bp = sub.add_parser("bundle", help="Runtime bundle store (fetch from GitHub Releases)",
|
|
529
|
+
allow_abbrev=False)
|
|
530
|
+
_add_global_args(bp, is_subparser=True)
|
|
531
|
+
bsub = bp.add_subparsers(dest="bundle_command", title="bundle commands",
|
|
532
|
+
parser_class=_AiscArgumentParser)
|
|
533
|
+
bf = bsub.add_parser("fetch", help="Download+verify+install the bundle for a version",
|
|
534
|
+
allow_abbrev=False)
|
|
535
|
+
_add_global_args(bf, is_subparser=True)
|
|
536
|
+
bf.add_argument("--version", default=None,
|
|
537
|
+
help="Version to fetch (default: this CLI's version; exact match)")
|
|
538
|
+
bf.add_argument("--from-file", default=None,
|
|
539
|
+
help="Offline path: install from a local release archive (requires --sha256)")
|
|
540
|
+
bf.add_argument("--sha256", default=None,
|
|
541
|
+
help="Expected sha256 of --from-file (same strength as the online API digest)")
|
|
542
|
+
bf.add_argument("--allow-mismatch", action="store_true", default=False,
|
|
543
|
+
help="Escape hatch: install even if the manifest gate rejects this CLI")
|
|
544
|
+
bl = bsub.add_parser("list", help="List installed bundles", allow_abbrev=False)
|
|
545
|
+
_add_global_args(bl, is_subparser=True)
|
|
546
|
+
brm = bsub.add_parser("remove", help="Remove an installed bundle (refuses the active one)",
|
|
547
|
+
allow_abbrev=False)
|
|
548
|
+
_add_global_args(brm, is_subparser=True)
|
|
549
|
+
brm.add_argument("version", help="Bundle version to remove")
|
|
550
|
+
bpth = bsub.add_parser("path", help="Print the bundles directory", allow_abbrev=False)
|
|
551
|
+
_add_global_args(bpth, is_subparser=True)
|
|
552
|
+
|
|
553
|
+
# --- update (0.1.0 A4, D-7) ---
|
|
554
|
+
up = sub.add_parser("update", help="Self-update the aisc CLI (frozen forms)",
|
|
555
|
+
allow_abbrev=False)
|
|
556
|
+
_add_global_args(up, is_subparser=True)
|
|
557
|
+
up.add_argument("--check", action="store_true", default=False,
|
|
558
|
+
help="Show the update plan without touching anything")
|
|
559
|
+
up.add_argument("--version", default=None,
|
|
560
|
+
help="Target version (default: latest final release)")
|
|
561
|
+
up.add_argument("--from-file", default=None,
|
|
562
|
+
help="Offline path: update from a local release archive (requires --sha256)")
|
|
563
|
+
up.add_argument("--sha256", default=None,
|
|
564
|
+
help="Expected sha256 of --from-file")
|
|
565
|
+
up.add_argument("--rebuild", action="store_true", default=False,
|
|
566
|
+
help="After replacing, run the installer-style no-cache image rebuild")
|
|
567
|
+
up.add_argument("--pin-tool", action="append", default=None, metavar="KEY=VALUE",
|
|
568
|
+
help="Pin a container tool version in the data-root user layer "
|
|
569
|
+
"(repeatable; CLAUDE_CODE_VERSION / CODEX_VERSION / CC_SWITCH_VERSION); "
|
|
570
|
+
"applies at the next image build and survives updates")
|
|
571
|
+
|
|
572
|
+
# --- runtime ---
|
|
573
|
+
rtp = sub.add_parser("runtime", help="Runtime control plane (Workbench Phase 0)", allow_abbrev=False)
|
|
574
|
+
_add_global_args(rtp, is_subparser=True)
|
|
575
|
+
rtsub = rtp.add_subparsers(dest="runtime_command", title="runtime commands",
|
|
576
|
+
parser_class=_AiscArgumentParser)
|
|
577
|
+
|
|
578
|
+
rtpf = rtsub.add_parser("preflight", help="Preflight checks for runtime start", allow_abbrev=False)
|
|
579
|
+
_add_global_args(rtpf, is_subparser=True)
|
|
580
|
+
rtpf.add_argument("--runtime-id", type=str, required=True,
|
|
581
|
+
help="Runtime ID (UUID v4, provided by Workbench)")
|
|
582
|
+
rtpf.add_argument("--workspace", type=str, default=None,
|
|
583
|
+
help="Workspace path (default: current directory)")
|
|
584
|
+
rtpf.add_argument("--image", type=str, default="super-claude:latest",
|
|
585
|
+
help="Docker image (default: super-claude:latest)")
|
|
586
|
+
rtpf.add_argument("--network", type=str, choices=["direct", "proxy"],
|
|
587
|
+
default="direct",
|
|
588
|
+
help="Network mode (default: direct)")
|
|
589
|
+
rtpf.add_argument("--scope", type=str, choices=["project", "temporary"],
|
|
590
|
+
default="project",
|
|
591
|
+
help="Runtime scope (default: project)")
|
|
592
|
+
rtpf.add_argument("--owner", type=str, default="workbench",
|
|
593
|
+
help="Owner identifier (default: workbench)")
|
|
594
|
+
|
|
595
|
+
# --- runtime start ---
|
|
596
|
+
rts = rtsub.add_parser("start", help="Start a Workbench runtime", allow_abbrev=False)
|
|
597
|
+
_add_global_args(rts, is_subparser=True)
|
|
598
|
+
rts.add_argument("--runtime-id", type=str, required=True,
|
|
599
|
+
help="Runtime ID (UUID v4, provided by Workbench)")
|
|
600
|
+
rts.add_argument("--workspace", type=str, default=None,
|
|
601
|
+
help="Workspace path (default: current directory)")
|
|
602
|
+
rts.add_argument("--image", type=str, default="super-claude:latest",
|
|
603
|
+
help="Docker image (default: super-claude:latest)")
|
|
604
|
+
rts.add_argument("--network", type=str, choices=["direct", "proxy"],
|
|
605
|
+
default="direct", help="Network mode (default: direct)")
|
|
606
|
+
rts.add_argument("--scope", type=str, choices=["project", "temporary"],
|
|
607
|
+
default="project", help="Runtime scope (default: project)")
|
|
608
|
+
rts.add_argument("--owner", type=str, default="workbench",
|
|
609
|
+
help="Owner identifier (default: workbench)")
|
|
610
|
+
rts.add_argument("--proxy-config", type=str, default=None,
|
|
611
|
+
help="Host path to mihomo config.yaml to mount for --network proxy "
|
|
612
|
+
"(S0.2: proxy caps/device are set; without this, TUN runs without a config)")
|
|
613
|
+
rts.add_argument("--host-mcp-url", type=str, default=None,
|
|
614
|
+
help="F2 (D-10): full host-tools MCP URL incl. token query; forwarded "
|
|
615
|
+
"into the container as AISC_HOST_MCP_URL for agent registration")
|
|
616
|
+
rts.add_argument("--max-memory", type=str, default=None,
|
|
617
|
+
help="PERF P8 (D-13): container memory limit (docker --memory, e.g. 3g)")
|
|
618
|
+
rts.add_argument("--max-cpus", type=float, default=None,
|
|
619
|
+
help="PERF P8 (D-13): container CPU limit (docker --cpus, e.g. 1.5)")
|
|
620
|
+
|
|
621
|
+
# --- runtime list ---
|
|
622
|
+
rtl = rtsub.add_parser("list", help="List runtimes with Docker reconciliation",
|
|
623
|
+
allow_abbrev=False)
|
|
624
|
+
_add_global_args(rtl, is_subparser=True)
|
|
625
|
+
rtl.add_argument("--workspace", type=str, default=None,
|
|
626
|
+
help="Workspace path (default: current directory)")
|
|
627
|
+
rtl.add_argument("--owner", type=str, default=None,
|
|
628
|
+
help="Filter by owner (e.g. workbench)")
|
|
629
|
+
|
|
630
|
+
# --- runtime inspect ---
|
|
631
|
+
rti = rtsub.add_parser("inspect", help="Show a single runtime", allow_abbrev=False)
|
|
632
|
+
_add_global_args(rti, is_subparser=True)
|
|
633
|
+
rti.add_argument("--runtime-id", type=str, required=True,
|
|
634
|
+
help="Runtime ID (UUID v4)")
|
|
635
|
+
rti.add_argument("--workspace", type=str, default=None,
|
|
636
|
+
help="Workspace path (default: current directory)")
|
|
637
|
+
|
|
638
|
+
# --- runtime status (PERF P1: merged inspect+services, one invocation) ---
|
|
639
|
+
rtstt = rtsub.add_parser("status", help="Snapshot + services in one call (poll path)",
|
|
640
|
+
allow_abbrev=False)
|
|
641
|
+
_add_global_args(rtstt, is_subparser=True)
|
|
642
|
+
rtstt.add_argument("--runtime-id", type=str, required=True,
|
|
643
|
+
help="Runtime ID (UUID v4)")
|
|
644
|
+
rtstt.add_argument("--workspace", type=str, default=None,
|
|
645
|
+
help="Workspace path (default: current directory)")
|
|
646
|
+
|
|
647
|
+
# --- runtime stop ---
|
|
648
|
+
rtst = rtsub.add_parser("stop", help="Stop a runtime (keep container + metadata)",
|
|
649
|
+
allow_abbrev=False)
|
|
650
|
+
_add_global_args(rtst, is_subparser=True)
|
|
651
|
+
rtst.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
652
|
+
rtst.add_argument("--workspace", type=str, default=None,
|
|
653
|
+
help="Workspace path (default: current directory)")
|
|
654
|
+
rtst.add_argument("--grace", type=int, default=10,
|
|
655
|
+
help="docker stop grace period in seconds (1..600; default 10)")
|
|
656
|
+
|
|
657
|
+
# --- runtime restart ---
|
|
658
|
+
rtr = rtsub.add_parser("restart", help="Restart a runtime with original config",
|
|
659
|
+
allow_abbrev=False)
|
|
660
|
+
_add_global_args(rtr, is_subparser=True)
|
|
661
|
+
rtr.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
662
|
+
rtr.add_argument("--workspace", type=str, default=None,
|
|
663
|
+
help="Workspace path (default: current directory)")
|
|
664
|
+
|
|
665
|
+
# --- runtime remove ---
|
|
666
|
+
rtrm = rtsub.add_parser("remove", help="Remove a runtime (container + registry)",
|
|
667
|
+
allow_abbrev=False)
|
|
668
|
+
_add_global_args(rtrm, is_subparser=True)
|
|
669
|
+
rtrm.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
670
|
+
rtrm.add_argument("--workspace", type=str, default=None,
|
|
671
|
+
help="Workspace path (default: current directory)")
|
|
672
|
+
rtrm.add_argument("--force", action="store_true", default=False,
|
|
673
|
+
help="Remove even if the runtime is running")
|
|
674
|
+
|
|
675
|
+
# --- runtime services (svc-2, aisc.runtime-services/v1) ---
|
|
676
|
+
# NOTE: the parent keeps --runtime-id/--workspace OPTIONAL and each
|
|
677
|
+
# subcommand redefines them required — Python 3.14 argparse rejects
|
|
678
|
+
# "services list --runtime-id X" when the parent marks the same option
|
|
679
|
+
# required (the subparser's parse no longer satisfies the parent's
|
|
680
|
+
# required check). The bare form is validated in _cmd_runtime dispatch.
|
|
681
|
+
rtsv = rtsub.add_parser("services", help="Web service gateway info + registered services",
|
|
682
|
+
allow_abbrev=False)
|
|
683
|
+
_add_global_args(rtsv, is_subparser=True)
|
|
684
|
+
rtsv.add_argument("--runtime-id", type=str, default=None,
|
|
685
|
+
help="Runtime ID (UUID v4)")
|
|
686
|
+
rtsv.add_argument("--workspace", type=str, default=None,
|
|
687
|
+
help="Workspace path (default: current directory)")
|
|
688
|
+
rtsvsub = rtsv.add_subparsers(dest="runtime_services_command",
|
|
689
|
+
title="runtime services commands",
|
|
690
|
+
parser_class=_AiscArgumentParser)
|
|
691
|
+
|
|
692
|
+
rtsvl = rtsvsub.add_parser("list", help="List registered services (default)",
|
|
693
|
+
allow_abbrev=False)
|
|
694
|
+
_add_global_args(rtsvl, is_subparser=True)
|
|
695
|
+
rtsvl.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
696
|
+
rtsvl.add_argument("--workspace", type=str, default=None,
|
|
697
|
+
help="Workspace path (default: current directory)")
|
|
698
|
+
|
|
699
|
+
rtsve = rtsvsub.add_parser("expose", help="Register a service port (ops/test entry)",
|
|
700
|
+
allow_abbrev=False)
|
|
701
|
+
_add_global_args(rtsve, is_subparser=True)
|
|
702
|
+
rtsve.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
703
|
+
rtsve.add_argument("--port", type=str, required=True,
|
|
704
|
+
help="Container service port (1024..65535)")
|
|
705
|
+
rtsve.add_argument("--name", type=str, default="",
|
|
706
|
+
help="Display label (safe short text)")
|
|
707
|
+
rtsve.add_argument("--workspace", type=str, default=None,
|
|
708
|
+
help="Workspace path (default: current directory)")
|
|
709
|
+
|
|
710
|
+
rtsvu = rtsvsub.add_parser("unexpose", help="Unregister a service port",
|
|
711
|
+
allow_abbrev=False)
|
|
712
|
+
_add_global_args(rtsvu, is_subparser=True)
|
|
713
|
+
rtsvu.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
714
|
+
rtsvu.add_argument("--port", type=str, required=True,
|
|
715
|
+
help="Container service port (1024..65535)")
|
|
716
|
+
rtsvu.add_argument("--workspace", type=str, default=None,
|
|
717
|
+
help="Workspace path (default: current directory)")
|
|
718
|
+
|
|
719
|
+
# --- runtime reconcile (runtime-lifecycle-ux Stage 1, 02 §3) ---
|
|
720
|
+
rtrc = rtsub.add_parser("reconcile",
|
|
721
|
+
help="Classify + auto-recycle stale runtimes for a workspace",
|
|
722
|
+
allow_abbrev=False)
|
|
723
|
+
_add_global_args(rtrc, is_subparser=True)
|
|
724
|
+
rtrc.add_argument("--workspace", type=str, default=None,
|
|
725
|
+
help="Workspace path (default: current directory)")
|
|
726
|
+
rtrc.add_argument("--instance-id", type=str, required=True,
|
|
727
|
+
help="This Workbench instance's UUID v4")
|
|
728
|
+
rtrc.add_argument("--workspace-key", type=str, default=None,
|
|
729
|
+
help="Optional cross-check: expected sha256 workspace key")
|
|
730
|
+
|
|
731
|
+
# --- runtime lease (runtime-lifecycle-ux Stage 1, 02 §2) ---
|
|
732
|
+
# Same py3.14 pattern as `services`: shared options on the group are
|
|
733
|
+
# optional, children re-require them; the bare form is usage-rejected
|
|
734
|
+
# in dispatch.
|
|
735
|
+
rtlg = rtsub.add_parser("lease", help="Workspace lease claim/heartbeat/release/inspect",
|
|
736
|
+
allow_abbrev=False)
|
|
737
|
+
_add_global_args(rtlg, is_subparser=True)
|
|
738
|
+
rtlg.add_argument("--workspace", type=str, default=None,
|
|
739
|
+
help="Workspace path (default: current directory)")
|
|
740
|
+
rtlg.add_argument("--instance-id", type=str, default=None,
|
|
741
|
+
help="This Workbench instance's UUID v4")
|
|
742
|
+
rtlgsub = rtlg.add_subparsers(dest="runtime_lease_command",
|
|
743
|
+
title="runtime lease commands",
|
|
744
|
+
parser_class=_AiscArgumentParser)
|
|
745
|
+
for _name, _help in (
|
|
746
|
+
("claim", "Claim the workspace lease"),
|
|
747
|
+
("heartbeat", "Refresh the lease last-seen timestamp"),
|
|
748
|
+
("release", "Release the workspace lease"),
|
|
749
|
+
("inspect", "Show the current lease"),
|
|
750
|
+
):
|
|
751
|
+
_p = rtlgsub.add_parser(_name, help=_help, allow_abbrev=False)
|
|
752
|
+
_add_global_args(_p, is_subparser=True)
|
|
753
|
+
_req = _name in ("claim", "heartbeat", "release")
|
|
754
|
+
_p.add_argument("--workspace", type=str, default=None,
|
|
755
|
+
help="Workspace path (default: current directory)")
|
|
756
|
+
_p.add_argument("--instance-id", type=str, required=_req,
|
|
757
|
+
help="This Workbench instance's UUID v4")
|
|
758
|
+
_p.add_argument("--lease-id", type=str, default=None,
|
|
759
|
+
help="Lease ID (heartbeat/release match guard)")
|
|
760
|
+
|
|
761
|
+
# --- session ---
|
|
762
|
+
ssp = sub.add_parser("session", help="Session data plane (Workbench Phase 0)", allow_abbrev=False)
|
|
763
|
+
_add_global_args(ssp, is_subparser=True)
|
|
764
|
+
ssub = ssp.add_subparsers(dest="session_command", title="session commands",
|
|
765
|
+
parser_class=_AiscArgumentParser)
|
|
766
|
+
|
|
767
|
+
# session open
|
|
768
|
+
sso = ssub.add_parser("open", help="Open an interactive agent session", allow_abbrev=False)
|
|
769
|
+
_add_global_args(sso, is_subparser=True)
|
|
770
|
+
sso.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
771
|
+
sso.add_argument("--session-id", type=str, required=True, help="Session ID (UUID v4)")
|
|
772
|
+
sso.add_argument("--agent", type=str, required=True,
|
|
773
|
+
choices=list(SessionAgent.ALL),
|
|
774
|
+
help="Agent type (claude|codex|bash|cc-switch)")
|
|
775
|
+
sso.add_argument("--workspace", type=str, default=None,
|
|
776
|
+
help="Workspace path (default: current directory)")
|
|
777
|
+
sso.add_argument("--resume-id", type=str, default=None,
|
|
778
|
+
help="Provider conversation ID to resume (claude|codex only, v2.1.8 T4)")
|
|
779
|
+
|
|
780
|
+
# session list
|
|
781
|
+
ssl = ssub.add_parser("list", help="List sessions in a runtime", allow_abbrev=False)
|
|
782
|
+
_add_global_args(ssl, is_subparser=True)
|
|
783
|
+
ssl.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
784
|
+
ssl.add_argument("--workspace", type=str, default=None,
|
|
785
|
+
help="Workspace path (default: current directory)")
|
|
786
|
+
|
|
787
|
+
# session terminate
|
|
788
|
+
sst = ssub.add_parser("terminate", help="Terminate a session", allow_abbrev=False)
|
|
789
|
+
_add_global_args(sst, is_subparser=True)
|
|
790
|
+
sst.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
791
|
+
sst.add_argument("--session-id", type=str, required=True, help="Session ID (UUID v4)")
|
|
792
|
+
sst.add_argument("--workspace", type=str, default=None,
|
|
793
|
+
help="Workspace path (default: current directory)")
|
|
794
|
+
sst.add_argument("--grace", type=float, default=5.0,
|
|
795
|
+
help="Grace period in seconds before SIGKILL (default: 5.0)")
|
|
796
|
+
|
|
797
|
+
# --- conversation (v2.1.8 T3) ---
|
|
798
|
+
cvp = sub.add_parser("conversation",
|
|
799
|
+
help="Agent conversation discovery (Workbench v2.1.8)",
|
|
800
|
+
allow_abbrev=False)
|
|
801
|
+
_add_global_args(cvp, is_subparser=True)
|
|
802
|
+
cvsub = cvp.add_subparsers(dest="conversation_command",
|
|
803
|
+
title="conversation commands",
|
|
804
|
+
parser_class=_AiscArgumentParser)
|
|
805
|
+
|
|
806
|
+
# conversation list
|
|
807
|
+
cvl = cvsub.add_parser("list", help="List agent history conversations in a workspace",
|
|
808
|
+
allow_abbrev=False)
|
|
809
|
+
_add_global_args(cvl, is_subparser=True)
|
|
810
|
+
cvl.add_argument("--workspace", type=str, default=None,
|
|
811
|
+
help="Workspace path (default: current directory)")
|
|
812
|
+
|
|
813
|
+
# conversation preflight
|
|
814
|
+
cvp2 = cvsub.add_parser("preflight",
|
|
815
|
+
help="Validate a conversation is resumable (captured, JSON)",
|
|
816
|
+
allow_abbrev=False)
|
|
817
|
+
_add_global_args(cvp2, is_subparser=True)
|
|
818
|
+
cvp2.add_argument("--workspace", type=str, default=None,
|
|
819
|
+
help="Workspace path (default: current directory)")
|
|
820
|
+
cvp2.add_argument("--conversation-id", type=str, required=True,
|
|
821
|
+
help="Provider-native conversation ID (UUID)")
|
|
822
|
+
# No argparse choices: an unsupported agent must surface as
|
|
823
|
+
# AISC_ERR_CONVERSATION_INVALID_AGENT in the JSON envelope (design §2),
|
|
824
|
+
# not as an argparse usage error.
|
|
825
|
+
cvp2.add_argument("--agent", type=str, required=True,
|
|
826
|
+
help="Agent type (claude|codex)")
|
|
827
|
+
|
|
828
|
+
# conversation delete (v2.1.8 T4 手测反馈 #4)
|
|
829
|
+
cvd = cvsub.add_parser("delete", help="Delete a conversation's session file",
|
|
830
|
+
allow_abbrev=False)
|
|
831
|
+
_add_global_args(cvd, is_subparser=True)
|
|
832
|
+
cvd.add_argument("--workspace", type=str, default=None,
|
|
833
|
+
help="Workspace path (default: current directory)")
|
|
834
|
+
cvd.add_argument("--conversation-id", type=str, required=True,
|
|
835
|
+
help="Provider-native conversation ID (UUID)")
|
|
836
|
+
cvd.add_argument("--agent", type=str, required=True,
|
|
837
|
+
help="Agent type (claude|codex)")
|
|
838
|
+
|
|
839
|
+
# conversation rename (v2.1.8 T4 手测反馈 #2): Workbench display title
|
|
840
|
+
cvr = cvsub.add_parser("rename", help="Set a conversation's display title",
|
|
841
|
+
allow_abbrev=False)
|
|
842
|
+
_add_global_args(cvr, is_subparser=True)
|
|
843
|
+
cvr.add_argument("--workspace", type=str, default=None,
|
|
844
|
+
help="Workspace path (default: current directory)")
|
|
845
|
+
cvr.add_argument("--conversation-id", type=str, required=True,
|
|
846
|
+
help="Provider-native conversation ID (UUID)")
|
|
847
|
+
cvr.add_argument("--agent", type=str, required=True,
|
|
848
|
+
help="Agent type (claude|codex)")
|
|
849
|
+
cvr.add_argument("--title", type=str, required=True,
|
|
850
|
+
help="New display title (sanitized, ≤80 chars)")
|
|
851
|
+
|
|
852
|
+
# --- artifact (Stage 3, ART-02) ---
|
|
853
|
+
arp = sub.add_parser("artifact", help="Agent Artifact fact protocol (Stage 3)",
|
|
854
|
+
allow_abbrev=False)
|
|
855
|
+
_add_global_args(arp, is_subparser=True)
|
|
856
|
+
arsub = arp.add_subparsers(dest="artifact_command", title="artifact commands",
|
|
857
|
+
parser_class=_AiscArgumentParser)
|
|
858
|
+
|
|
859
|
+
arrec = arsub.add_parser("record", help="Record an agent artifact fact",
|
|
860
|
+
allow_abbrev=False)
|
|
861
|
+
_add_global_args(arrec, is_subparser=True)
|
|
862
|
+
# v2.1.9 T3a (R2): the Workbench injects AISC_RUNTIME_ID (docker create)
|
|
863
|
+
# and AISC_TERMINAL_SESSION_ID (session wrapper) into every agent
|
|
864
|
+
# process — use them as argparse defaults so agents don't need to know
|
|
865
|
+
# their IDs. Flag still wins when both are present; explicit check in
|
|
866
|
+
# the command layer reports a stable usage error when neither is given
|
|
867
|
+
# (instead of argparse's bare "required" wall of text).
|
|
868
|
+
arrec.add_argument("--runtime-id", type=str,
|
|
869
|
+
default=os.environ.get("AISC_RUNTIME_ID"),
|
|
870
|
+
help="Runtime ID (UUID v4; default: env AISC_RUNTIME_ID)")
|
|
871
|
+
arrec.add_argument("--session-id", type=str,
|
|
872
|
+
default=os.environ.get("AISC_TERMINAL_SESSION_ID"),
|
|
873
|
+
help="Session ID (UUID v4; default: env AISC_TERMINAL_SESSION_ID)")
|
|
874
|
+
arrec.add_argument("--agent", type=str,
|
|
875
|
+
default=os.environ.get("AISC_AGENT"),
|
|
876
|
+
required=os.environ.get("AISC_AGENT") is None,
|
|
877
|
+
choices=["claude", "codex", "bash", "cc-switch"],
|
|
878
|
+
help="Producer agent (default: env AISC_AGENT)")
|
|
879
|
+
arrec.add_argument("--path", type=str, required=True,
|
|
880
|
+
help="Workspace-relative path of the artifact")
|
|
881
|
+
arrec.add_argument("--action", type=str, choices=ArtifactAction.ALL, default="created",
|
|
882
|
+
help="created|modified|deleted|renamed")
|
|
883
|
+
arrec.add_argument("--kind", type=str, choices=ArtifactKind.ALL, default="deliverable",
|
|
884
|
+
help="deliverable|source_change|generated_output")
|
|
885
|
+
arrec.add_argument("--media-type", type=str, default=None,
|
|
886
|
+
help="media type, e.g. text/markdown")
|
|
887
|
+
arrec.add_argument("--label", type=str, default="", help="Human label (<=256 chars)")
|
|
888
|
+
arrec.add_argument("--open-with", type=str, choices=ArtifactOpenWith.ALL,
|
|
889
|
+
default="preview", help="preview|system|reveal|none")
|
|
890
|
+
arrec.add_argument("--previous-path", type=str, default=None,
|
|
891
|
+
help="previous relative path (required for renamed)")
|
|
892
|
+
arrec.add_argument("--workspace", type=str, default=None,
|
|
893
|
+
help="Workspace path (default: current directory)")
|
|
894
|
+
|
|
895
|
+
arlist = arsub.add_parser("list", help="List artifact records", allow_abbrev=False)
|
|
896
|
+
_add_global_args(arlist, is_subparser=True)
|
|
897
|
+
arlist.add_argument("--workspace", type=str, default=None,
|
|
898
|
+
help="Workspace path (default: current directory)")
|
|
899
|
+
arlist.add_argument("--session-id", type=str, default=None,
|
|
900
|
+
help="Filter by session id")
|
|
901
|
+
arlist.add_argument("--kind", type=str, choices=ArtifactKind.ALL, default=None,
|
|
902
|
+
help="Filter by kind")
|
|
903
|
+
|
|
904
|
+
arinsp = arsub.add_parser("inspect", help="Inspect one artifact by id",
|
|
905
|
+
allow_abbrev=False)
|
|
906
|
+
_add_global_args(arinsp, is_subparser=True)
|
|
907
|
+
arinsp.add_argument("--artifact-id", type=str, required=True, help="Artifact ID (UUID)")
|
|
908
|
+
arinsp.add_argument("--workspace", type=str, default=None,
|
|
909
|
+
help="Workspace path (default: current directory)")
|
|
910
|
+
|
|
911
|
+
arclear = arsub.add_parser("clear-session", help="Remove a session's registry",
|
|
912
|
+
allow_abbrev=False)
|
|
913
|
+
_add_global_args(arclear, is_subparser=True)
|
|
914
|
+
arclear.add_argument("--runtime-id", type=str, required=True, help="Runtime ID (UUID v4)")
|
|
915
|
+
arclear.add_argument("--session-id", type=str, required=True, help="Session ID (UUID v4)")
|
|
916
|
+
arclear.add_argument("--workspace", type=str, default=None,
|
|
917
|
+
help="Workspace path (default: current directory)")
|
|
918
|
+
|
|
919
|
+
# --- data-root (Stage 7, 7d) ---
|
|
920
|
+
drp = sub.add_parser("data-root", help="Data root diagnostics and legacy migration",
|
|
921
|
+
allow_abbrev=False)
|
|
922
|
+
_add_global_args(drp, is_subparser=True)
|
|
923
|
+
drsub = drp.add_subparsers(dest="data_root_command", title="data-root commands",
|
|
924
|
+
parser_class=_AiscArgumentParser)
|
|
925
|
+
|
|
926
|
+
drdoc = drsub.add_parser("doctor", help="Resolve + legacy findings + manifest state",
|
|
927
|
+
allow_abbrev=False)
|
|
928
|
+
_add_global_args(drdoc, is_subparser=True)
|
|
929
|
+
drdoc.add_argument("--workspace", type=str, default=None,
|
|
930
|
+
help="Workspace path (default: current directory)")
|
|
931
|
+
|
|
932
|
+
drmig = drsub.add_parser("migrate", help="Migrate legacy layout into the data root",
|
|
933
|
+
allow_abbrev=False)
|
|
934
|
+
_add_global_args(drmig, is_subparser=True)
|
|
935
|
+
drmig.add_argument("--workspace", type=str, default=None,
|
|
936
|
+
help="Workspace path (default: current directory)")
|
|
937
|
+
drmig.add_argument("--dry-run", action="store_true", default=False,
|
|
938
|
+
help="Report the plan without touching anything")
|
|
939
|
+
drmig.add_argument("--apply", action="store_true", default=False,
|
|
940
|
+
help="Execute the migration (default when --dry-run is absent)")
|
|
941
|
+
drmig.add_argument("--quarantine-unknown", action="store_true", default=False,
|
|
942
|
+
help="Move unknown files to the migration quarantine "
|
|
943
|
+
"(explicit consent; sources kept)")
|
|
944
|
+
|
|
945
|
+
drroll = drsub.add_parser("rollback", help="Undo one migration via its manifest",
|
|
946
|
+
allow_abbrev=False)
|
|
947
|
+
_add_global_args(drroll, is_subparser=True)
|
|
948
|
+
drroll.add_argument("--workspace", type=str, default=None,
|
|
949
|
+
help="Workspace path (default: current directory)")
|
|
950
|
+
drroll.add_argument("manifest", nargs="?", default=None,
|
|
951
|
+
help="Manifest path (default: this workspace's manifest)")
|
|
952
|
+
|
|
953
|
+
return parser
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# ---------------------------------------------------------------------------
|
|
957
|
+
# JSON format / events detection from raw argv
|
|
958
|
+
# ---------------------------------------------------------------------------
|
|
959
|
+
|
|
960
|
+
def _detect_json_format(argv: List[str]) -> bool:
|
|
961
|
+
for i, arg in enumerate(argv):
|
|
962
|
+
if arg == "--format" and i + 1 < len(argv) and argv[i + 1] == "json":
|
|
963
|
+
return True
|
|
964
|
+
if arg == "--format=json":
|
|
965
|
+
return True
|
|
966
|
+
return False
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _detect_events(argv: List[str]) -> bool:
|
|
970
|
+
return "--events" in argv
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _detect_command(argv: List[str]) -> Optional[str]:
|
|
974
|
+
known = {"version", "doctor", "build", "run", "config", "profile",
|
|
975
|
+
"status", "stop", "restart", "shell", "switch", "provider",
|
|
976
|
+
"cc-switch", "network", "usage", "logs", "ps", "runtime", "session",
|
|
977
|
+
"artifact", "data-root", "maintenance"}
|
|
978
|
+
for arg in argv:
|
|
979
|
+
if arg in known:
|
|
980
|
+
return arg
|
|
981
|
+
return None
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _resolve_aisc_root_from_argv(argv: List[str]) -> Optional[str]:
|
|
985
|
+
"""Resolve ``--aisc-root`` from raw *argv* with last-wins semantics.
|
|
986
|
+
|
|
987
|
+
Returns the last ``--aisc-root VALUE`` or ``--aisc-root=VALUE``,
|
|
988
|
+
or ``None`` if the flag is absent.
|
|
989
|
+
"""
|
|
990
|
+
last = None
|
|
991
|
+
for i, arg in enumerate(argv):
|
|
992
|
+
if arg == "--aisc-root" and i + 1 < len(argv):
|
|
993
|
+
last = argv[i + 1]
|
|
994
|
+
elif arg.startswith("--aisc-root="):
|
|
995
|
+
last = arg.split("=", 1)[1]
|
|
996
|
+
return last
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
# ---------------------------------------------------------------------------
|
|
1000
|
+
# Arg post-processing: resolve last-wins --format
|
|
1001
|
+
# ---------------------------------------------------------------------------
|
|
1002
|
+
|
|
1003
|
+
def _resolve_format(args: argparse.Namespace, argv: List[str]) -> str:
|
|
1004
|
+
last = "text"
|
|
1005
|
+
for i, arg in enumerate(argv):
|
|
1006
|
+
if arg == "--format" and i + 1 < len(argv):
|
|
1007
|
+
val = argv[i + 1]
|
|
1008
|
+
if val in ("text", "json"):
|
|
1009
|
+
last = val
|
|
1010
|
+
elif arg.startswith("--format="):
|
|
1011
|
+
val = arg.split("=", 1)[1]
|
|
1012
|
+
if val in ("text", "json"):
|
|
1013
|
+
last = val
|
|
1014
|
+
return last
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
# ---------------------------------------------------------------------------
|
|
1018
|
+
# Command implementations (existing, unchanged)
|
|
1019
|
+
# ---------------------------------------------------------------------------
|
|
1020
|
+
|
|
1021
|
+
def _cmd_version(args: argparse.Namespace) -> VersionInfo:
|
|
1022
|
+
from aisc.application.version import gather_version_info
|
|
1023
|
+
from aisc.application.resources import _RootSourceError
|
|
1024
|
+
try:
|
|
1025
|
+
info = gather_version_info(explicit_root=args.aisc_root, cwd=Path.cwd())
|
|
1026
|
+
except _RootSourceError as exc:
|
|
1027
|
+
raise CliError(message=str(exc), exit_code=1,
|
|
1028
|
+
error_code="AISC_ERR_GENERAL") from exc
|
|
1029
|
+
except Exception as exc:
|
|
1030
|
+
raise CliError(message=str(exc), exit_code=1,
|
|
1031
|
+
error_code="AISC_ERR_GENERAL") from exc
|
|
1032
|
+
return info
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _cmd_doctor(
|
|
1036
|
+
args: argparse.Namespace,
|
|
1037
|
+
effective_format: str,
|
|
1038
|
+
) -> Tuple[Dict[str, Any], DoctorReport]:
|
|
1039
|
+
from aisc.application.doctor import run_doctor
|
|
1040
|
+
from aisc.application.resources import locate_aisc_root, _RootSourceError
|
|
1041
|
+
root = None
|
|
1042
|
+
root_error: Optional[str] = None
|
|
1043
|
+
try:
|
|
1044
|
+
root = locate_aisc_root(explicit_root=args.aisc_root)
|
|
1045
|
+
except _RootSourceError as exc:
|
|
1046
|
+
root_error = str(exc)
|
|
1047
|
+
except Exception as exc:
|
|
1048
|
+
root_error = str(exc)
|
|
1049
|
+
|
|
1050
|
+
report = run_doctor(root=root, root_error=root_error)
|
|
1051
|
+
|
|
1052
|
+
# Check if Docker is missing and offer to install (interactive mode only)
|
|
1053
|
+
docker_missing = any(
|
|
1054
|
+
c.name == "docker-cli"
|
|
1055
|
+
and c.status == CheckStatus.FAIL
|
|
1056
|
+
and c.message == "Docker CLI not found"
|
|
1057
|
+
for c in report.checks
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
interactive_text = (
|
|
1061
|
+
effective_format == "text"
|
|
1062
|
+
and sys.stdin.isatty()
|
|
1063
|
+
and sys.stdout.isatty()
|
|
1064
|
+
)
|
|
1065
|
+
if docker_missing and interactive_text:
|
|
1066
|
+
# Interactive mode - offer to install Docker
|
|
1067
|
+
from aisc.application.repair import install_docker_interactive
|
|
1068
|
+
|
|
1069
|
+
print("\n" + "=" * 60)
|
|
1070
|
+
print("Docker is required but not installed.")
|
|
1071
|
+
print("=" * 60)
|
|
1072
|
+
|
|
1073
|
+
success = install_docker_interactive()
|
|
1074
|
+
|
|
1075
|
+
if success:
|
|
1076
|
+
print("\n\nRe-running diagnostics...\n")
|
|
1077
|
+
# Re-run doctor to check if Docker is now available
|
|
1078
|
+
report = run_doctor(root=root, root_error=root_error)
|
|
1079
|
+
|
|
1080
|
+
data: Dict[str, Any] = {"host": report.to_dict(), "container": None}
|
|
1081
|
+
return data, report
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _cmd_profile(
|
|
1085
|
+
args: argparse.Namespace,
|
|
1086
|
+
effective_format: str,
|
|
1087
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1088
|
+
"""Dispatch profile list/show."""
|
|
1089
|
+
from aisc.cli.commands.profile import cmd_profile_list, cmd_profile_show
|
|
1090
|
+
|
|
1091
|
+
sub = getattr(args, "profile_command", None)
|
|
1092
|
+
if sub not in ("list", "show"):
|
|
1093
|
+
if effective_format == "json":
|
|
1094
|
+
emit_json_usage_error(
|
|
1095
|
+
command="profile", version=__version__,
|
|
1096
|
+
message="Unknown profile subcommand",
|
|
1097
|
+
)
|
|
1098
|
+
else:
|
|
1099
|
+
print("Error: Unknown profile subcommand", file=sys.stderr)
|
|
1100
|
+
sys.exit(2)
|
|
1101
|
+
|
|
1102
|
+
if sub == "list":
|
|
1103
|
+
result = cmd_profile_list()
|
|
1104
|
+
else:
|
|
1105
|
+
# show — optional NAME defaults to "safe"
|
|
1106
|
+
name = getattr(args, "name", None) or "safe"
|
|
1107
|
+
result = cmd_profile_show(name)
|
|
1108
|
+
|
|
1109
|
+
errors: List[Dict[str, Any]] = []
|
|
1110
|
+
if result.exit_code != 0:
|
|
1111
|
+
errors.append(build_error(result.error_code or "AISC_ERR_GENERAL",
|
|
1112
|
+
result.error_message))
|
|
1113
|
+
|
|
1114
|
+
return result.data, result.exit_code, errors
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# ---------------------------------------------------------------------------
|
|
1120
|
+
# Build / Run command dispatch — all docker through injected executor
|
|
1121
|
+
# ---------------------------------------------------------------------------
|
|
1122
|
+
|
|
1123
|
+
def _write_cc_switch_manifest(root: Path, resolved: Any) -> Optional[Path]:
|
|
1124
|
+
"""Write the resolution receipt next to the build outputs (Stage 8b).
|
|
1125
|
+
|
|
1126
|
+
Best-effort: an unwritable cache warns and builds on (the receipt also
|
|
1127
|
+
lives in the image labels + BuildResult). Patched in tests to avoid
|
|
1128
|
+
touching the real filesystem.
|
|
1129
|
+
|
|
1130
|
+
Location: the shared data-root cache (NOT the aisc bundle dir — a file
|
|
1131
|
+
created inside the installed bundle survives the uninstaller's file list
|
|
1132
|
+
and trips the NSIS clean-uninstall gate; resolver artifacts belong with
|
|
1133
|
+
the resolver's metadata cache anyway)."""
|
|
1134
|
+
from aisc.application.data_root import shared_root
|
|
1135
|
+
|
|
1136
|
+
manifest_path = shared_root() / "cache" / "cc-switch" / "last-resolved.json"
|
|
1137
|
+
try:
|
|
1138
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1139
|
+
from aisc.application.cc_switch_resolver import resolved_at_now
|
|
1140
|
+
|
|
1141
|
+
manifest_path.write_text(
|
|
1142
|
+
json.dumps(resolved.to_manifest(resolved_at=resolved_at_now()), indent=2),
|
|
1143
|
+
encoding="utf-8",
|
|
1144
|
+
)
|
|
1145
|
+
return manifest_path
|
|
1146
|
+
except OSError as exc:
|
|
1147
|
+
sys.stderr.write(f"⚠️ could not write cc-switch manifest ({exc}); "
|
|
1148
|
+
f"build continues\n")
|
|
1149
|
+
return None
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _cmd_build(
|
|
1153
|
+
args: argparse.Namespace,
|
|
1154
|
+
emitter: Optional[JsonlEmitter],
|
|
1155
|
+
effective_format: str,
|
|
1156
|
+
) -> Tuple[Optional[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1157
|
+
"""Execute ``aisc build``. Returns (data, exit_code, errors)."""
|
|
1158
|
+
from aisc.cli.commands.build import plan_build, run_build, BuildResult
|
|
1159
|
+
from aisc.application.resources import locate_aisc_root, _RootSourceError
|
|
1160
|
+
|
|
1161
|
+
root = None
|
|
1162
|
+
try:
|
|
1163
|
+
root = locate_aisc_root(explicit_root=args.aisc_root)
|
|
1164
|
+
except _RootSourceError as exc:
|
|
1165
|
+
raise CliError(message=str(exc), exit_code=1,
|
|
1166
|
+
error_code="AISC_ERR_GENERAL") from exc
|
|
1167
|
+
if root is None:
|
|
1168
|
+
# A3 (guide 3.3.4): the pip install form has no repo and no frozen
|
|
1169
|
+
# bundle — its build needs `aisc bundle fetch` first. Keep the
|
|
1170
|
+
# source/frozen wording intact (repo users still get their hint).
|
|
1171
|
+
import aisc as _aisc_pkg
|
|
1172
|
+
|
|
1173
|
+
pip_form = (
|
|
1174
|
+
not getattr(sys, "frozen", False)
|
|
1175
|
+
and _aisc_pkg.__file__ is not None
|
|
1176
|
+
and "site-packages" in str(Path(_aisc_pkg.__file__).resolve())
|
|
1177
|
+
)
|
|
1178
|
+
if pip_form:
|
|
1179
|
+
raise CliError(
|
|
1180
|
+
message="AISC root not found: a pip/pipx install ships no build "
|
|
1181
|
+
"resources. Run `aisc bundle fetch` to download this "
|
|
1182
|
+
"version's bundle, or point --aisc-root at an extracted "
|
|
1183
|
+
"bundle directory.",
|
|
1184
|
+
exit_code=1, error_code="AISC_ERR_GENERAL",
|
|
1185
|
+
)
|
|
1186
|
+
raise CliError(
|
|
1187
|
+
message="AISC root not found. Use --aisc-root to specify a path, "
|
|
1188
|
+
"or run from within an AISC repository.",
|
|
1189
|
+
exit_code=1, error_code="AISC_ERR_GENERAL",
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
# v2.1.7 hotfix (2026-08-28 field report): failures BEFORE the event
|
|
1193
|
+
# stream starts (root location, cc-switch resolution — a network-blocked
|
|
1194
|
+
# machine fails here in seconds) used to print a bare error envelope to
|
|
1195
|
+
# stdout. The Workbench stream parser saw no terminal event and showed a
|
|
1196
|
+
# generic "AISC CLI 返回错误" with zero diagnosis. In --events mode a
|
|
1197
|
+
# pre-run CliError now emits a TERMINAL build.failed carrying the real
|
|
1198
|
+
# code + message, then re-raises for the normal exit path.
|
|
1199
|
+
def _emit_prerun_failure(exc: CliError) -> None:
|
|
1200
|
+
if emitter is None:
|
|
1201
|
+
return
|
|
1202
|
+
emitter.emit_terminal("build.failed", exc.exit_code, extra_data={
|
|
1203
|
+
"image_tag": getattr(args, "tag", "super-claude:latest"),
|
|
1204
|
+
"docker_exit_code": None,
|
|
1205
|
+
"error_code": exc.error_code,
|
|
1206
|
+
"message": exc.message,
|
|
1207
|
+
"phase": "prerun",
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
def _warn_unpinned_build(exc: ResolveError, emitter_: Optional[JsonlEmitter]) -> None:
|
|
1211
|
+
"""S8b: announce the degraded unpinned build (fail-visible, never
|
|
1212
|
+
silent) — stderr for text mode, a build.warning event for the
|
|
1213
|
+
Workbench stream (additive event type; older frontends ignore it)."""
|
|
1214
|
+
warning = (
|
|
1215
|
+
"cc-switch 版本解析失败(网络受限):本次构建不带解析钉版,"
|
|
1216
|
+
"Dockerfile 将从国内镜像源下载 cc-switch(无 sha256 校验,"
|
|
1217
|
+
"不保证可复现)。配好网络后重试可恢复钉版;--require-pin 可将"
|
|
1218
|
+
"此情况设为致命错误。原因: " + exc.message
|
|
1219
|
+
)
|
|
1220
|
+
sys.stderr.write(f"⚠️ {warning}\n")
|
|
1221
|
+
sys.stderr.flush()
|
|
1222
|
+
if emitter_ is not None:
|
|
1223
|
+
emitter_.emit("build.warning", data={"message": warning})
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
# Run wizard if in interactive text mode and no explicit flags provided
|
|
1227
|
+
tag = getattr(args, "tag", "super-claude:latest")
|
|
1228
|
+
no_cache = getattr(args, "no_cache", False)
|
|
1229
|
+
pull = getattr(args, "pull", False)
|
|
1230
|
+
dry_run = getattr(args, "dry_run", False)
|
|
1231
|
+
|
|
1232
|
+
# Check if user provided explicit build options
|
|
1233
|
+
tag_provided = any(arg in sys.argv for arg in ["--tag", "-t"])
|
|
1234
|
+
cache_provided = "--no-cache" in sys.argv
|
|
1235
|
+
pull_provided = "--pull" in sys.argv
|
|
1236
|
+
|
|
1237
|
+
# Run wizard if: text mode + interactive + no explicit options
|
|
1238
|
+
if (effective_format == "text" and emitter is None and
|
|
1239
|
+
not dry_run and not tag_provided and not cache_provided and not pull_provided):
|
|
1240
|
+
from aisc.cli.commands.wizard import run_build_wizard
|
|
1241
|
+
try:
|
|
1242
|
+
tag, no_cache, pull, proxy_config = run_build_wizard(
|
|
1243
|
+
default_tag=tag, aisc_root=root
|
|
1244
|
+
)
|
|
1245
|
+
except KeyboardInterrupt:
|
|
1246
|
+
raise CliError(message="Build cancelled by user", exit_code=130,
|
|
1247
|
+
error_code="AISC_ERR_CANCELLED")
|
|
1248
|
+
|
|
1249
|
+
# Stage 8b: resolve the cc-switch release BEFORE planning (D8-02 — the
|
|
1250
|
+
# Dockerfile only consumes resolved facts, never resolves on its own).
|
|
1251
|
+
import platform as _platform
|
|
1252
|
+
from aisc.domain.cc_switch_release import (
|
|
1253
|
+
CC_SWITCH_ERROR_NETWORK,
|
|
1254
|
+
CC_SWITCH_ERROR_RATE_LIMITED,
|
|
1255
|
+
ResolveError,
|
|
1256
|
+
normalize_arch,
|
|
1257
|
+
)
|
|
1258
|
+
from aisc.application.cc_switch_resolver import CcSwitchResolver
|
|
1259
|
+
|
|
1260
|
+
cs_version = getattr(args, "cc_switch_version", None) or os.environ.get("CC_SWITCH_VERSION", "latest")
|
|
1261
|
+
cs_channel = getattr(args, "cc_switch_channel", None) or os.environ.get("CC_SWITCH_CHANNEL", "stable")
|
|
1262
|
+
cs_manifest = getattr(args, "cc_switch_manifest", None)
|
|
1263
|
+
require_pin = bool(getattr(args, "require_pin", False)) or (
|
|
1264
|
+
os.environ.get("AISC_REQUIRE_PIN", "").strip().lower() not in ("", "0", "false")
|
|
1265
|
+
)
|
|
1266
|
+
try:
|
|
1267
|
+
cs_arch = normalize_arch(_platform.machine())
|
|
1268
|
+
except ResolveError:
|
|
1269
|
+
cs_arch = "x64" # validated again inside the Dockerfile (arch assert)
|
|
1270
|
+
resolver = CcSwitchResolver()
|
|
1271
|
+
resolved: Any = None
|
|
1272
|
+
try:
|
|
1273
|
+
resolved = resolver.resolve(
|
|
1274
|
+
channel=cs_channel,
|
|
1275
|
+
version=cs_version,
|
|
1276
|
+
arch=cs_arch,
|
|
1277
|
+
libc="musl",
|
|
1278
|
+
manifest_path=Path(cs_manifest) if cs_manifest else None,
|
|
1279
|
+
)
|
|
1280
|
+
except ResolveError as exc:
|
|
1281
|
+
# S8b (2026-08-28 user ruling): a NETWORK-SHAPED failure degrades to
|
|
1282
|
+
# an UNPINNED build instead of a fatal error — the Dockerfile's
|
|
1283
|
+
# no-pin branch downloads cc-switch from CN mirrors (unverified,
|
|
1284
|
+
# explicitly warned), the same semantics docker_rebuild has had
|
|
1285
|
+
# since v2.1.6. Everything else (bad manifest, version not found on
|
|
1286
|
+
# a LIVE api, invalid channel) and --require-pin stay fail-closed.
|
|
1287
|
+
unpinned = (
|
|
1288
|
+
exc.code in (CC_SWITCH_ERROR_NETWORK, CC_SWITCH_ERROR_RATE_LIMITED)
|
|
1289
|
+
and not require_pin
|
|
1290
|
+
)
|
|
1291
|
+
cli_error = CliError(
|
|
1292
|
+
message=f"cc-switch release resolution failed: {exc.message}",
|
|
1293
|
+
exit_code=1, error_code=exc.code,
|
|
1294
|
+
)
|
|
1295
|
+
if not unpinned:
|
|
1296
|
+
_emit_prerun_failure(cli_error)
|
|
1297
|
+
raise cli_error from exc
|
|
1298
|
+
_warn_unpinned_build(exc, emitter)
|
|
1299
|
+
|
|
1300
|
+
# Reproducibility receipt: always written next to the build outputs.
|
|
1301
|
+
manifest_path = (
|
|
1302
|
+
_write_cc_switch_manifest(root, resolved) if resolved is not None else None
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
if resolved is not None and effective_format == "text" and emitter is None:
|
|
1306
|
+
sys.stderr.write(
|
|
1307
|
+
f"cc-switch: {resolved.tag} ({resolved.asset_name}, "
|
|
1308
|
+
f"sha256 {resolved.asset_sha256[:12]}…, source {resolved.source})\n"
|
|
1309
|
+
)
|
|
1310
|
+
sys.stderr.flush()
|
|
1311
|
+
|
|
1312
|
+
plan = plan_build(
|
|
1313
|
+
root=root,
|
|
1314
|
+
tag=tag,
|
|
1315
|
+
no_cache=no_cache,
|
|
1316
|
+
pull=pull,
|
|
1317
|
+
dry_run=dry_run,
|
|
1318
|
+
cc_switch=resolved,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# text mode → streaming (real-time build log); json/events → captured
|
|
1322
|
+
is_streaming = (effective_format == "text" and emitter is None)
|
|
1323
|
+
result = run_build(
|
|
1324
|
+
plan,
|
|
1325
|
+
emitter=emitter,
|
|
1326
|
+
streaming=is_streaming,
|
|
1327
|
+
cc_switch_summary=resolved.to_manifest() if resolved is not None else None,
|
|
1328
|
+
cc_switch_manifest_path=str(manifest_path) if manifest_path else "",
|
|
1329
|
+
)
|
|
1330
|
+
return result.to_dict(), 0, []
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def _cmd_run(
|
|
1334
|
+
args: argparse.Namespace,
|
|
1335
|
+
emitter: Optional[JsonlEmitter],
|
|
1336
|
+
effective_format: str,
|
|
1337
|
+
aisc_root_arg: Optional[str],
|
|
1338
|
+
) -> Tuple[Optional[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1339
|
+
"""Execute ``aisc run`` — F2-C activation semantics: detached start,
|
|
1340
|
+
registry default pointer, history record, activation summary."""
|
|
1341
|
+
from aisc.cli.commands.run import plan_run, run_container
|
|
1342
|
+
from aisc.cli.commands import runs as cli_runs
|
|
1343
|
+
from aisc.application.resources import locate_aisc_root, _RootSourceError
|
|
1344
|
+
|
|
1345
|
+
aisc_root = None
|
|
1346
|
+
try:
|
|
1347
|
+
aisc_root = locate_aisc_root(explicit_root=aisc_root_arg)
|
|
1348
|
+
except _RootSourceError as exc:
|
|
1349
|
+
raise CliError(message=str(exc), exit_code=1,
|
|
1350
|
+
error_code="AISC_ERR_GENERAL") from exc
|
|
1351
|
+
|
|
1352
|
+
# --resume: adopt the record's config; explicit flags win
|
|
1353
|
+
resume = getattr(args, "resume", None)
|
|
1354
|
+
rec = None
|
|
1355
|
+
if resume:
|
|
1356
|
+
rec = cli_runs.require_resume(resume)
|
|
1357
|
+
|
|
1358
|
+
path = getattr(args, "path", None)
|
|
1359
|
+
if not path:
|
|
1360
|
+
if rec:
|
|
1361
|
+
path = rec["path"]
|
|
1362
|
+
else:
|
|
1363
|
+
raise CliError(
|
|
1364
|
+
message="aisc run needs a workspace path (e.g. `aisc run ./`) — "
|
|
1365
|
+
"or --resume N|PATH from `aisc runs`",
|
|
1366
|
+
exit_code=2, error_code="AISC_ERR_USAGE",
|
|
1367
|
+
)
|
|
1368
|
+
ws = Path(path).expanduser()
|
|
1369
|
+
|
|
1370
|
+
profile = getattr(args, "profile", None)
|
|
1371
|
+
network = getattr(args, "network", argparse.SUPPRESS)
|
|
1372
|
+
network_explicit = network is not argparse.SUPPRESS
|
|
1373
|
+
if network is argparse.SUPPRESS:
|
|
1374
|
+
network = (rec or {}).get("network", "direct") if rec else "direct"
|
|
1375
|
+
network_explicit = False
|
|
1376
|
+
if profile == "proxy":
|
|
1377
|
+
if network_explicit and network == "direct":
|
|
1378
|
+
raise CliError(
|
|
1379
|
+
message="--profile proxy conflicts with --network direct",
|
|
1380
|
+
exit_code=2, error_code="AISC_ERR_USAGE",
|
|
1381
|
+
)
|
|
1382
|
+
network = "proxy"
|
|
1383
|
+
|
|
1384
|
+
image = getattr(args, "image", "super-claude:latest")
|
|
1385
|
+
if rec and image == "super-claude:latest" and rec.get("image"):
|
|
1386
|
+
image = rec["image"]
|
|
1387
|
+
label = getattr(args, "label", "")
|
|
1388
|
+
if rec and not label and rec.get("label"):
|
|
1389
|
+
label = rec["label"]
|
|
1390
|
+
|
|
1391
|
+
raw_name = getattr(args, "name", "super-claude-station") or "super-claude-station"
|
|
1392
|
+
if rec and raw_name == "super-claude-station" and rec.get("alias"):
|
|
1393
|
+
raw_name = rec["alias"] # resume keeps the alias-named containers
|
|
1394
|
+
plan = plan_run(
|
|
1395
|
+
image=image,
|
|
1396
|
+
workspace=str(ws),
|
|
1397
|
+
name=raw_name,
|
|
1398
|
+
network=network,
|
|
1399
|
+
dry_run=getattr(args, "dry_run", False),
|
|
1400
|
+
interactive=False, # F2-C: activation is always detached (-d)
|
|
1401
|
+
non_interactive=False,
|
|
1402
|
+
label=label,
|
|
1403
|
+
keep_alive=True, # F2-C: no --rm — the workspace STAYS activated
|
|
1404
|
+
aisc_root=aisc_root,
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
result = run_container(plan, emitter=emitter, capture=True,
|
|
1408
|
+
aisc_root=aisc_root)
|
|
1409
|
+
|
|
1410
|
+
if not plan.dry_run:
|
|
1411
|
+
abs_ws = str(Path(plan.workspace).resolve())
|
|
1412
|
+
alias = "" if raw_name == "super-claude-station" else raw_name
|
|
1413
|
+
if rec and not alias and rec.get("alias"):
|
|
1414
|
+
alias = rec["alias"] # --resume without a fresh --name keeps it
|
|
1415
|
+
cli_runs.record(abs_ws, plan.image, plan.network, plan.label, alias)
|
|
1416
|
+
cli_runs.set_active(abs_ws)
|
|
1417
|
+
|
|
1418
|
+
out = result.to_dict()
|
|
1419
|
+
out["workspace"] = plan.workspace
|
|
1420
|
+
out["commands"] = [
|
|
1421
|
+
"aisc claude", "aisc codex", "aisc switch", "aisc shell",
|
|
1422
|
+
"aisc status", "aisc stop", "aisc runs",
|
|
1423
|
+
]
|
|
1424
|
+
|
|
1425
|
+
if effective_format == "text" and emitter is None:
|
|
1426
|
+
if out.get("reused"):
|
|
1427
|
+
print(f"ℹ 工作区已在运行中: {plan.workspace}")
|
|
1428
|
+
print(f" 容器 {out['reused']}(Up) · 复用现有容器,未新建")
|
|
1429
|
+
print(" 进入方式: aisc claude | aisc codex | aisc switch | aisc shell")
|
|
1430
|
+
print(" 如需重建: 先 aisc stop 再重新 aisc run")
|
|
1431
|
+
return out, 0, []
|
|
1432
|
+
print(f"💯 工作区已激活: {plan.workspace}")
|
|
1433
|
+
print(f" 容器 {plan.name}(detached) · 镜像 {plan.image} · 网络 {plan.network}")
|
|
1434
|
+
if out.get("replaced"):
|
|
1435
|
+
names = ", ".join(out["replaced"])
|
|
1436
|
+
print(f" ♻ 已替换同工作区旧容器: {names}")
|
|
1437
|
+
if plan.web_gateway_host_port and not plan.dry_run:
|
|
1438
|
+
print(f" 🌐 Web 服务网关: http://p<端口>.localhost:{plan.web_gateway_host_port}/ "
|
|
1439
|
+
f"(容器内: aisc-web-expose <端口>)")
|
|
1440
|
+
print(" 进入方式: aisc claude | aisc codex | aisc switch | aisc shell")
|
|
1441
|
+
print(" 停止: aisc stop · 历史: aisc runs")
|
|
1442
|
+
|
|
1443
|
+
return out, 0, []
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _cmd_agent(
|
|
1447
|
+
args: argparse.Namespace,
|
|
1448
|
+
effective_format: str,
|
|
1449
|
+
) -> Tuple[Optional[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1450
|
+
"""Execute ``aisc claude`` / ``aisc codex`` (F2-C agent sugar)."""
|
|
1451
|
+
from aisc.cli.commands.agents import cmd_agent
|
|
1452
|
+
|
|
1453
|
+
rest = [a for a in (getattr(args, "rest", None) or [])]
|
|
1454
|
+
if rest and rest[0] == "--":
|
|
1455
|
+
rest = rest[1:]
|
|
1456
|
+
|
|
1457
|
+
capture = effective_format != "text"
|
|
1458
|
+
outcome = cmd_agent(
|
|
1459
|
+
str(args.command),
|
|
1460
|
+
rest,
|
|
1461
|
+
workspace=getattr(args, "workspace", None),
|
|
1462
|
+
name_override=getattr(args, "name", None),
|
|
1463
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1464
|
+
capture=capture,
|
|
1465
|
+
)
|
|
1466
|
+
if capture:
|
|
1467
|
+
return dict(outcome), int(outcome.get("exit_code", 0) or 0), []
|
|
1468
|
+
# Text mode: the TTY was live; report the exit like shell does.
|
|
1469
|
+
return {"container": None, "agent": str(args.command), "args": rest,
|
|
1470
|
+
"exit_code": outcome.exit_code}, 0, []
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _cmd_runs(
|
|
1474
|
+
args: argparse.Namespace,
|
|
1475
|
+
effective_format: str,
|
|
1476
|
+
) -> Tuple[Optional[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1477
|
+
"""Execute ``aisc runs`` — the CLI activation history."""
|
|
1478
|
+
from aisc.cli.commands import runs as cli_runs
|
|
1479
|
+
|
|
1480
|
+
items = cli_runs.list_runs()
|
|
1481
|
+
if effective_format == "text":
|
|
1482
|
+
if not items:
|
|
1483
|
+
print("暂无激活历史 — aisc run <路径> 开始")
|
|
1484
|
+
for i, r in enumerate(items, 1):
|
|
1485
|
+
alias = f" @{r['alias']}" if r.get("alias") else ""
|
|
1486
|
+
print(f" {i}. {r.get('path')} [{r.get('image', '')} · {r.get('network', '')}]"
|
|
1487
|
+
f" {r.get('last_used_at', '')}{alias}")
|
|
1488
|
+
if items:
|
|
1489
|
+
print("恢复: aisc run --resume <别名|序号|路径>(别名最稳,序号随列表变动)")
|
|
1490
|
+
return None, 0, []
|
|
1491
|
+
return {"runs": items}, 0, []
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _cmd_workspaces(
|
|
1495
|
+
args: argparse.Namespace,
|
|
1496
|
+
effective_format: str,
|
|
1497
|
+
) -> Tuple[Optional[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1498
|
+
"""Execute ``aisc workspaces`` — the machine-wide management view."""
|
|
1499
|
+
from aisc.cli.commands.workspaces import (
|
|
1500
|
+
cmd_workspaces, cmd_workspaces_stop, print_workspaces_text,
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
if getattr(args, "stop", False):
|
|
1504
|
+
out = cmd_workspaces_stop()
|
|
1505
|
+
if effective_format == "text":
|
|
1506
|
+
def _short(s):
|
|
1507
|
+
if s.get("alias"):
|
|
1508
|
+
return s["alias"]
|
|
1509
|
+
ws = s.get("workspace", "")
|
|
1510
|
+
return Path(ws).name or ws
|
|
1511
|
+
names = ", ".join(_short(s) for s in out["stopped"]) or "无"
|
|
1512
|
+
print(f"已批量停止 {len(out['stopped'])} 个运行中的工作区: {names}")
|
|
1513
|
+
return out, 0, []
|
|
1514
|
+
|
|
1515
|
+
rows = cmd_workspaces()
|
|
1516
|
+
if effective_format == "text":
|
|
1517
|
+
print_workspaces_text(rows)
|
|
1518
|
+
return {"workspaces": rows}, 0, []
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _cmd_config(
|
|
1522
|
+
args: argparse.Namespace,
|
|
1523
|
+
effective_format: str,
|
|
1524
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1525
|
+
"""Dispatch config validate/effective. ServiceResult owns exit codes."""
|
|
1526
|
+
from aisc.cli.commands.config import (
|
|
1527
|
+
cmd_config_validate, cmd_config_effective,
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
sub = getattr(args, "config_command", None)
|
|
1531
|
+
if sub not in ("validate", "effective", "show"):
|
|
1532
|
+
if effective_format == "json":
|
|
1533
|
+
emit_json_usage_error(
|
|
1534
|
+
command="config", version=__version__,
|
|
1535
|
+
message="Unknown config subcommand",
|
|
1536
|
+
)
|
|
1537
|
+
else:
|
|
1538
|
+
print("Error: Unknown config subcommand", file=sys.stderr)
|
|
1539
|
+
sys.exit(2)
|
|
1540
|
+
|
|
1541
|
+
explicit_config = getattr(args, "config", None)
|
|
1542
|
+
workspace = getattr(args, "workspace", None)
|
|
1543
|
+
|
|
1544
|
+
if sub == "validate":
|
|
1545
|
+
result = cmd_config_validate(
|
|
1546
|
+
explicit_config=explicit_config, workspace=workspace,
|
|
1547
|
+
)
|
|
1548
|
+
else:
|
|
1549
|
+
# effective / show — same handler
|
|
1550
|
+
result = cmd_config_effective(
|
|
1551
|
+
explicit_config=explicit_config, workspace=workspace,
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
# Build errors list — exactly one top-level error on failure
|
|
1555
|
+
errors: List[Dict[str, Any]] = []
|
|
1556
|
+
if result.exit_code != 0:
|
|
1557
|
+
errors.append(build_error(result.error_code or "AISC_ERR_GENERAL",
|
|
1558
|
+
result.error_message))
|
|
1559
|
+
|
|
1560
|
+
return result.data, result.exit_code, errors
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
# ---------------------------------------------------------------------------
|
|
1564
|
+
# Container lifecycle command handlers
|
|
1565
|
+
# ---------------------------------------------------------------------------
|
|
1566
|
+
|
|
1567
|
+
def _cmd_status(
|
|
1568
|
+
args: argparse.Namespace,
|
|
1569
|
+
effective_format: str,
|
|
1570
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1571
|
+
"""Execute ``aisc status``. Supports --format json."""
|
|
1572
|
+
from aisc.cli.commands.container import cmd_status
|
|
1573
|
+
|
|
1574
|
+
result = cmd_status(
|
|
1575
|
+
name_override=getattr(args, "name", None),
|
|
1576
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1577
|
+
label_override=getattr(args, "label", None),
|
|
1578
|
+
)
|
|
1579
|
+
return result.to_dict(), 0, []
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _cmd_stop(
|
|
1583
|
+
args: argparse.Namespace,
|
|
1584
|
+
effective_format: str,
|
|
1585
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1586
|
+
"""Execute ``aisc stop`` (F2-C: stop+remove; --all sweeps CLI-owned)."""
|
|
1587
|
+
from aisc.cli.commands.container import cmd_stop, cmd_stop_all
|
|
1588
|
+
|
|
1589
|
+
if getattr(args, "all", False):
|
|
1590
|
+
data = cmd_stop_all(explicit_root=getattr(args, "aisc_root", None))
|
|
1591
|
+
if effective_format == "text":
|
|
1592
|
+
names = ", ".join(s["name"] for s in data.get("stopped", [])) or "无"
|
|
1593
|
+
print(f"已停止并移除: {names}(跳过 Workbench 管理的 {data.get('skipped', 0)} 个)")
|
|
1594
|
+
return data, 0, []
|
|
1595
|
+
data = cmd_stop(
|
|
1596
|
+
name_override=getattr(args, "name", None),
|
|
1597
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1598
|
+
label_override=getattr(args, "label", None),
|
|
1599
|
+
)
|
|
1600
|
+
return data, 0, []
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def _cmd_restart(
|
|
1604
|
+
args: argparse.Namespace,
|
|
1605
|
+
effective_format: str,
|
|
1606
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1607
|
+
"""Execute ``aisc restart``. Supports --format json."""
|
|
1608
|
+
from aisc.cli.commands.container import cmd_restart
|
|
1609
|
+
|
|
1610
|
+
data = cmd_restart(
|
|
1611
|
+
name_override=getattr(args, "name", None),
|
|
1612
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1613
|
+
label_override=getattr(args, "label", None),
|
|
1614
|
+
)
|
|
1615
|
+
return data, 0, []
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def _cmd_shell(
|
|
1619
|
+
args: argparse.Namespace,
|
|
1620
|
+
effective_format: str,
|
|
1621
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1622
|
+
"""Execute ``aisc shell``. Text-only interactive."""
|
|
1623
|
+
from aisc.cli.commands.container import cmd_shell, discover_container
|
|
1624
|
+
|
|
1625
|
+
if effective_format == "json":
|
|
1626
|
+
emit_json_usage_error(
|
|
1627
|
+
command="shell", version=__version__,
|
|
1628
|
+
message="shell 仅支持 text 输出——不支持 --format json",
|
|
1629
|
+
)
|
|
1630
|
+
sys.exit(2)
|
|
1631
|
+
|
|
1632
|
+
# Discover name once for use in returned data
|
|
1633
|
+
name_override = getattr(args, "name", None)
|
|
1634
|
+
label_override = getattr(args, "label", None)
|
|
1635
|
+
try:
|
|
1636
|
+
discovered_name = name_override or discover_container(
|
|
1637
|
+
name_override=None,
|
|
1638
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1639
|
+
label_override=label_override,
|
|
1640
|
+
)
|
|
1641
|
+
except Exception:
|
|
1642
|
+
discovered_name = name_override or ""
|
|
1643
|
+
|
|
1644
|
+
proc = cmd_shell(
|
|
1645
|
+
name_override=name_override,
|
|
1646
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1647
|
+
label_override=label_override,
|
|
1648
|
+
rest=getattr(args, "rest", None) or [],
|
|
1649
|
+
)
|
|
1650
|
+
|
|
1651
|
+
exit_code = proc.exit_code if proc.exit_code >= 0 else 1
|
|
1652
|
+
errors: List[Dict[str, Any]] = []
|
|
1653
|
+
if exit_code != 0:
|
|
1654
|
+
errors.append(build_error(
|
|
1655
|
+
"AISC_ERR_GENERAL",
|
|
1656
|
+
proc.stderr or f"docker exec exited with code {exit_code}",
|
|
1657
|
+
))
|
|
1658
|
+
|
|
1659
|
+
return {"name": discovered_name, "exit_code": exit_code}, exit_code, errors
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
def _cmd_switch(
|
|
1663
|
+
args: argparse.Namespace,
|
|
1664
|
+
effective_format: str,
|
|
1665
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1666
|
+
"""Execute ``aisc switch``. Text-only interactive."""
|
|
1667
|
+
from aisc.cli.commands.container import cmd_switch, discover_container
|
|
1668
|
+
|
|
1669
|
+
if effective_format == "json":
|
|
1670
|
+
emit_json_usage_error(
|
|
1671
|
+
command="switch", version=__version__,
|
|
1672
|
+
message="switch 仅支持 text 输出——不支持 --format json",
|
|
1673
|
+
)
|
|
1674
|
+
sys.exit(2)
|
|
1675
|
+
|
|
1676
|
+
# Discover name once for use in returned data
|
|
1677
|
+
name_override = getattr(args, "name", None)
|
|
1678
|
+
label_override = getattr(args, "label", None)
|
|
1679
|
+
try:
|
|
1680
|
+
discovered_name = name_override or discover_container(
|
|
1681
|
+
name_override=None,
|
|
1682
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1683
|
+
label_override=label_override,
|
|
1684
|
+
)
|
|
1685
|
+
except Exception:
|
|
1686
|
+
discovered_name = name_override or ""
|
|
1687
|
+
|
|
1688
|
+
quick = getattr(args, "quick", None)
|
|
1689
|
+
|
|
1690
|
+
proc = cmd_switch(
|
|
1691
|
+
name_override=name_override,
|
|
1692
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1693
|
+
quick=quick,
|
|
1694
|
+
label_override=label_override,
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
exit_code = proc.exit_code if proc.exit_code >= 0 else 1
|
|
1698
|
+
errors: List[Dict[str, Any]] = []
|
|
1699
|
+
if exit_code != 0:
|
|
1700
|
+
errors.append(build_error(
|
|
1701
|
+
"AISC_ERR_GENERAL",
|
|
1702
|
+
proc.stderr or f"docker exec exited with code {exit_code}",
|
|
1703
|
+
))
|
|
1704
|
+
|
|
1705
|
+
data: Dict[str, Any] = {
|
|
1706
|
+
"name": discovered_name,
|
|
1707
|
+
"exit_code": exit_code,
|
|
1708
|
+
}
|
|
1709
|
+
if quick:
|
|
1710
|
+
data["provider"] = quick
|
|
1711
|
+
data["quick"] = True
|
|
1712
|
+
return data, exit_code, errors
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
def _cmd_cc_switch(
|
|
1716
|
+
args: argparse.Namespace,
|
|
1717
|
+
effective_format: str,
|
|
1718
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1719
|
+
"""Execute ``aisc cc-switch`` subcommands (Stage 8d data plane).
|
|
1720
|
+
|
|
1721
|
+
All four ops are non-interactive and support ``--format json``; secrets
|
|
1722
|
+
ride the stdin request document, never argv.
|
|
1723
|
+
"""
|
|
1724
|
+
from aisc.cli.commands import cc_switch as cs_cmd
|
|
1725
|
+
|
|
1726
|
+
sub = getattr(args, "cc_switch_command", None)
|
|
1727
|
+
if sub == "list":
|
|
1728
|
+
data = cs_cmd.cmd_cc_switch_list(args)
|
|
1729
|
+
elif sub == "add":
|
|
1730
|
+
data = cs_cmd.cmd_cc_switch_add(args)
|
|
1731
|
+
elif sub == "edit":
|
|
1732
|
+
data = cs_cmd.cmd_cc_switch_edit(args)
|
|
1733
|
+
elif sub == "switch":
|
|
1734
|
+
data = cs_cmd.cmd_cc_switch_switch(args)
|
|
1735
|
+
elif sub == "delete":
|
|
1736
|
+
data = cs_cmd.cmd_cc_switch_delete(args)
|
|
1737
|
+
elif sub == "fetch-models":
|
|
1738
|
+
data = cs_cmd.cmd_cc_switch_fetch_models(args)
|
|
1739
|
+
else:
|
|
1740
|
+
raise CliError(message="unknown cc-switch subcommand",
|
|
1741
|
+
exit_code=2, error_code="AISC_ERR_USAGE")
|
|
1742
|
+
|
|
1743
|
+
if effective_format == "text":
|
|
1744
|
+
cs_cmd.print_cc_switch_text(data)
|
|
1745
|
+
return data, 0, []
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
def _cmd_network(
|
|
1749
|
+
args: argparse.Namespace,
|
|
1750
|
+
effective_format: str,
|
|
1751
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1752
|
+
"""Execute ``aisc network`` subcommands (IDEA-2 subscription data plane).
|
|
1753
|
+
|
|
1754
|
+
Non-interactive, ``--format json`` throughout; the subscription URL rides
|
|
1755
|
+
stdin for ``import`` (secrets never ride argv).
|
|
1756
|
+
"""
|
|
1757
|
+
from aisc.cli.commands import network as nw_cmd
|
|
1758
|
+
|
|
1759
|
+
if getattr(args, "network_command", None) != "subscription":
|
|
1760
|
+
raise CliError(message="unknown network subcommand",
|
|
1761
|
+
exit_code=2, error_code="AISC_ERR_USAGE")
|
|
1762
|
+
|
|
1763
|
+
sub = getattr(args, "subscription_command", None)
|
|
1764
|
+
if sub == "import":
|
|
1765
|
+
data = nw_cmd.cmd_network_subscription_import(args)
|
|
1766
|
+
elif sub == "import-file":
|
|
1767
|
+
data = nw_cmd.cmd_network_subscription_import_file(args)
|
|
1768
|
+
elif sub == "store-downloaded":
|
|
1769
|
+
data = nw_cmd.cmd_network_subscription_store_downloaded(args)
|
|
1770
|
+
elif sub == "refresh":
|
|
1771
|
+
data = nw_cmd.cmd_network_subscription_refresh(args)
|
|
1772
|
+
elif sub == "show":
|
|
1773
|
+
data = nw_cmd.cmd_network_subscription_show(args)
|
|
1774
|
+
elif sub == "clear":
|
|
1775
|
+
data = nw_cmd.cmd_network_subscription_clear(args)
|
|
1776
|
+
else:
|
|
1777
|
+
raise CliError(message="unknown network subscription subcommand",
|
|
1778
|
+
exit_code=2, error_code="AISC_ERR_USAGE")
|
|
1779
|
+
|
|
1780
|
+
if effective_format == "text":
|
|
1781
|
+
nw_cmd.print_network_subscription_text(data)
|
|
1782
|
+
return data, 0, []
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _cmd_usage(
|
|
1786
|
+
args: argparse.Namespace,
|
|
1787
|
+
effective_format: str,
|
|
1788
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1789
|
+
"""Execute ``aisc usage`` subcommands (IDEA-2 usage data plane)."""
|
|
1790
|
+
from aisc.cli.commands import usage as usage_cmd
|
|
1791
|
+
|
|
1792
|
+
if getattr(args, "usage_command", None) != "overview":
|
|
1793
|
+
raise CliError(message="unknown usage subcommand",
|
|
1794
|
+
exit_code=2, error_code="AISC_ERR_USAGE")
|
|
1795
|
+
data = usage_cmd.cmd_usage_overview(args)
|
|
1796
|
+
if effective_format == "text":
|
|
1797
|
+
usage_cmd.print_usage_overview_text(data)
|
|
1798
|
+
return data, 0, []
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _cmd_logs(
|
|
1802
|
+
args: argparse.Namespace,
|
|
1803
|
+
effective_format: str,
|
|
1804
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1805
|
+
"""Execute ``aisc logs`` subcommands (lifecycle-logging P2)."""
|
|
1806
|
+
from aisc.cli.commands import logs as logs_cmd
|
|
1807
|
+
|
|
1808
|
+
sub = getattr(args, "logs_command", None)
|
|
1809
|
+
if sub == "show":
|
|
1810
|
+
data = logs_cmd.cmd_logs_show(args)
|
|
1811
|
+
is_show = True
|
|
1812
|
+
elif sub == "path":
|
|
1813
|
+
data = logs_cmd.cmd_logs_path(args)
|
|
1814
|
+
is_show = False
|
|
1815
|
+
else:
|
|
1816
|
+
raise CliError(message="unknown logs subcommand",
|
|
1817
|
+
exit_code=2, error_code="AISC_ERR_USAGE")
|
|
1818
|
+
if effective_format == "text":
|
|
1819
|
+
logs_cmd.print_logs_text(data, is_show=is_show)
|
|
1820
|
+
return data, 0, []
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
def _cmd_provider(
|
|
1824
|
+
args: argparse.Namespace,
|
|
1825
|
+
effective_format: str,
|
|
1826
|
+
) -> Tuple[Dict[str, Any], int, List[Dict[str, Any]]]:
|
|
1827
|
+
"""Execute ``aisc provider`` subcommands.
|
|
1828
|
+
|
|
1829
|
+
``current`` is non-interactive and supports ``--format json`` (S0.4).
|
|
1830
|
+
``set-key`` is text-only interactive.
|
|
1831
|
+
"""
|
|
1832
|
+
sub = getattr(args, "provider_command", None)
|
|
1833
|
+
|
|
1834
|
+
if sub == "current":
|
|
1835
|
+
from aisc.cli.commands.provider import cmd_provider_current
|
|
1836
|
+
data = cmd_provider_current(
|
|
1837
|
+
runtime_id=args.runtime_id,
|
|
1838
|
+
agent=args.agent,
|
|
1839
|
+
workspace=args.workspace,
|
|
1840
|
+
)
|
|
1841
|
+
return data, 0, []
|
|
1842
|
+
|
|
1843
|
+
# All other provider subcommands are text-only interactive.
|
|
1844
|
+
if effective_format == "json":
|
|
1845
|
+
emit_json_usage_error(
|
|
1846
|
+
command="provider", version=__version__,
|
|
1847
|
+
message="provider set-key 仅支持 text 输出——不支持 --format json",
|
|
1848
|
+
)
|
|
1849
|
+
sys.exit(2)
|
|
1850
|
+
|
|
1851
|
+
from aisc.cli.commands.container import cmd_provider_set_key, discover_container
|
|
1852
|
+
|
|
1853
|
+
if sub != "set-key":
|
|
1854
|
+
print("Error: Unknown provider subcommand. Use 'aisc provider set-key' "
|
|
1855
|
+
"or 'aisc provider current'.", file=sys.stderr)
|
|
1856
|
+
sys.exit(2)
|
|
1857
|
+
|
|
1858
|
+
# Discover name once for use in returned data
|
|
1859
|
+
name_override = getattr(args, "name", None)
|
|
1860
|
+
label_override = getattr(args, "label", None)
|
|
1861
|
+
try:
|
|
1862
|
+
discovered_name = name_override or discover_container(
|
|
1863
|
+
name_override=None,
|
|
1864
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1865
|
+
label_override=label_override,
|
|
1866
|
+
)
|
|
1867
|
+
except Exception:
|
|
1868
|
+
discovered_name = name_override or ""
|
|
1869
|
+
|
|
1870
|
+
provider_id = getattr(args, "provider_id", None)
|
|
1871
|
+
agent = getattr(args, "agent", "claude")
|
|
1872
|
+
|
|
1873
|
+
proc = cmd_provider_set_key(
|
|
1874
|
+
name_override=name_override,
|
|
1875
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1876
|
+
provider_id=provider_id,
|
|
1877
|
+
agent=agent,
|
|
1878
|
+
label_override=label_override,
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
exit_code = proc.exit_code if proc.exit_code >= 0 else 1
|
|
1882
|
+
errors: List[Dict[str, Any]] = []
|
|
1883
|
+
if exit_code != 0:
|
|
1884
|
+
errors.append(build_error(
|
|
1885
|
+
"AISC_ERR_GENERAL",
|
|
1886
|
+
proc.stderr or f"docker exec exited with code {exit_code}",
|
|
1887
|
+
))
|
|
1888
|
+
|
|
1889
|
+
data: Dict[str, Any] = {
|
|
1890
|
+
"name": discovered_name,
|
|
1891
|
+
"exit_code": exit_code,
|
|
1892
|
+
"provider_id": provider_id,
|
|
1893
|
+
"agent": agent,
|
|
1894
|
+
}
|
|
1895
|
+
return data, exit_code, errors
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _cmd_ps(
|
|
1899
|
+
args: argparse.Namespace,
|
|
1900
|
+
effective_format: str,
|
|
1901
|
+
) -> Tuple[List[Dict[str, Any]], int, List[Dict[str, Any]]]:
|
|
1902
|
+
"""Execute ``aisc ps``. Supports --format json."""
|
|
1903
|
+
from aisc.cli.commands.container import cmd_ps
|
|
1904
|
+
|
|
1905
|
+
rows = cmd_ps(
|
|
1906
|
+
explicit_root=getattr(args, "aisc_root", None),
|
|
1907
|
+
)
|
|
1908
|
+
data = [{"name": r.name, "label": r.label, "status": r.status,
|
|
1909
|
+
"running": r.running, "image": r.image, "workspace": r.workspace,
|
|
1910
|
+
"active": r.active}
|
|
1911
|
+
for r in rows]
|
|
1912
|
+
return data, 0, []
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _cmd_maintenance(
|
|
1916
|
+
args: argparse.Namespace,
|
|
1917
|
+
effective_format: str,
|
|
1918
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
1919
|
+
"""Execute ``aisc maintenance`` subcommands (docker-resource-lifecycle B).
|
|
1920
|
+
|
|
1921
|
+
Exit codes (02 §3): 0 all-ok/absent · 3 docker unavailable · 1 partial
|
|
1922
|
+
resource failures · 2 usage. stdout carries ONLY the JSON envelope;
|
|
1923
|
+
Docker noise rides stderr via run_captured.
|
|
1924
|
+
"""
|
|
1925
|
+
from aisc.adapters.docker_ import RealDockerExecutor
|
|
1926
|
+
from aisc.application.docker_lifecycle import (
|
|
1927
|
+
docker_cleanup, docker_rebuild, docker_scan,
|
|
1928
|
+
)
|
|
1929
|
+
from aisc.domain.models import CliError
|
|
1930
|
+
|
|
1931
|
+
sub = args.maintenance_command
|
|
1932
|
+
executor = RealDockerExecutor()
|
|
1933
|
+
if sub == "docker-scan":
|
|
1934
|
+
data = docker_scan(executor, context=args.context,
|
|
1935
|
+
old_image_ids=args.old_image_id)
|
|
1936
|
+
if effective_format != "json":
|
|
1937
|
+
from aisc.application.docker_lifecycle import render_scan_text
|
|
1938
|
+
print(render_scan_text(data))
|
|
1939
|
+
return None, 0, []
|
|
1940
|
+
return data, 0, []
|
|
1941
|
+
if sub == "docker-cleanup":
|
|
1942
|
+
try:
|
|
1943
|
+
data = docker_cleanup(executor, context=args.context,
|
|
1944
|
+
old_image_ids=args.old_image_id)
|
|
1945
|
+
except CliError as exc:
|
|
1946
|
+
raise
|
|
1947
|
+
failed = bool(data["containers"]["failed"] or data["images"]["failed"])
|
|
1948
|
+
return data, (1 if failed else 0), []
|
|
1949
|
+
if sub == "docker-rebuild":
|
|
1950
|
+
data = docker_rebuild(executor, root=args.root, tag=args.tag,
|
|
1951
|
+
old_image_id=args.old_image_id,
|
|
1952
|
+
no_cache=args.no_cache, pull=args.pull)
|
|
1953
|
+
return data, (1 if data.get("failed") else 0), []
|
|
1954
|
+
if sub == "cache-usage":
|
|
1955
|
+
from aisc.application.docker_lifecycle import cache_usage
|
|
1956
|
+
return cache_usage(executor), 0, []
|
|
1957
|
+
if sub == "cache-cleanup":
|
|
1958
|
+
from aisc.application.docker_lifecycle import docker_cache_cleanup
|
|
1959
|
+
data = docker_cache_cleanup(executor, min_age_hours=args.min_age_hours)
|
|
1960
|
+
return data, (1 if data.get("warnings") else 0), []
|
|
1961
|
+
return None, 2, [build_error("AISC_ERR_USAGE", f"Unknown maintenance subcommand: {sub}")]
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def _cmd_bundle(
|
|
1965
|
+
args: argparse.Namespace,
|
|
1966
|
+
effective_format: str,
|
|
1967
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
1968
|
+
"""Execute ``aisc bundle`` subcommands. Supports --format json."""
|
|
1969
|
+
from aisc.cli.commands.bundle import (
|
|
1970
|
+
cmd_bundle_fetch,
|
|
1971
|
+
cmd_bundle_list,
|
|
1972
|
+
cmd_bundle_path,
|
|
1973
|
+
cmd_bundle_remove,
|
|
1974
|
+
print_bundle_text,
|
|
1975
|
+
)
|
|
1976
|
+
|
|
1977
|
+
sub = args.bundle_command
|
|
1978
|
+
if sub == "fetch":
|
|
1979
|
+
data = cmd_bundle_fetch(version=args.version, from_file=args.from_file,
|
|
1980
|
+
sha256=args.sha256, allow_mismatch=args.allow_mismatch)
|
|
1981
|
+
elif sub == "list":
|
|
1982
|
+
data = cmd_bundle_list()
|
|
1983
|
+
elif sub == "remove":
|
|
1984
|
+
data = cmd_bundle_remove(args.version)
|
|
1985
|
+
elif sub == "path":
|
|
1986
|
+
data = cmd_bundle_path()
|
|
1987
|
+
else:
|
|
1988
|
+
return None, 2, [build_error("AISC_ERR_USAGE", f"Unknown bundle subcommand: {sub}")]
|
|
1989
|
+
if effective_format != "json":
|
|
1990
|
+
print_bundle_text(sub, data)
|
|
1991
|
+
return None, 0, []
|
|
1992
|
+
return data, 0, []
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
def _cmd_update(
|
|
1996
|
+
args: argparse.Namespace,
|
|
1997
|
+
effective_format: str,
|
|
1998
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
1999
|
+
"""Execute ``aisc update``. Supports --format json."""
|
|
2000
|
+
from aisc.cli.commands.update import cmd_update, cmd_update_check, print_update_text
|
|
2001
|
+
|
|
2002
|
+
if args.check:
|
|
2003
|
+
data = cmd_update_check(version=args.version)
|
|
2004
|
+
print_update_text("check", data) if effective_format != "json" else None
|
|
2005
|
+
return (data, 0, []) if effective_format == "json" else (None, 0, [])
|
|
2006
|
+
data = cmd_update(version=args.version, from_file=args.from_file,
|
|
2007
|
+
sha256=args.sha256, rebuild=args.rebuild,
|
|
2008
|
+
pin_tool=args.pin_tool)
|
|
2009
|
+
if effective_format != "json":
|
|
2010
|
+
print_update_text("update", data)
|
|
2011
|
+
return None, 0, []
|
|
2012
|
+
exit_code = 0 if data.get("status") in ("updated", "up-to-date") else 1
|
|
2013
|
+
return data, exit_code, []
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
def _cmd_runtime(
|
|
2017
|
+
args: argparse.Namespace,
|
|
2018
|
+
effective_format: str,
|
|
2019
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
2020
|
+
"""Execute ``aisc runtime`` subcommands. Supports --format json."""
|
|
2021
|
+
from aisc.cli.commands.runtime import (
|
|
2022
|
+
cmd_runtime_preflight,
|
|
2023
|
+
cmd_runtime_start,
|
|
2024
|
+
cmd_runtime_list,
|
|
2025
|
+
cmd_runtime_inspect,
|
|
2026
|
+
cmd_runtime_status,
|
|
2027
|
+
cmd_runtime_stop,
|
|
2028
|
+
cmd_runtime_restart,
|
|
2029
|
+
cmd_runtime_remove,
|
|
2030
|
+
cmd_runtime_services,
|
|
2031
|
+
cmd_runtime_services_expose,
|
|
2032
|
+
cmd_runtime_services_unexpose,
|
|
2033
|
+
cmd_runtime_reconcile,
|
|
2034
|
+
cmd_runtime_lease,
|
|
2035
|
+
)
|
|
2036
|
+
|
|
2037
|
+
sub = args.runtime_command
|
|
2038
|
+
|
|
2039
|
+
if sub == "preflight":
|
|
2040
|
+
result = cmd_runtime_preflight(
|
|
2041
|
+
runtime_id=args.runtime_id,
|
|
2042
|
+
workspace=args.workspace,
|
|
2043
|
+
image=args.image,
|
|
2044
|
+
network=args.network,
|
|
2045
|
+
scope=args.scope,
|
|
2046
|
+
owner=args.owner,
|
|
2047
|
+
format=effective_format,
|
|
2048
|
+
)
|
|
2049
|
+
if "error" in result:
|
|
2050
|
+
return None, result["exit_code"], [result["error"]]
|
|
2051
|
+
return result, 0, []
|
|
2052
|
+
elif sub == "start":
|
|
2053
|
+
data = cmd_runtime_start(
|
|
2054
|
+
runtime_id=args.runtime_id,
|
|
2055
|
+
workspace=args.workspace,
|
|
2056
|
+
image=args.image,
|
|
2057
|
+
network=args.network,
|
|
2058
|
+
scope=args.scope,
|
|
2059
|
+
owner=args.owner,
|
|
2060
|
+
proxy_config=args.proxy_config,
|
|
2061
|
+
host_mcp_url=getattr(args, "host_mcp_url", None),
|
|
2062
|
+
max_memory=getattr(args, "max_memory", None),
|
|
2063
|
+
max_cpus=getattr(args, "max_cpus", None),
|
|
2064
|
+
)
|
|
2065
|
+
return data, 0, []
|
|
2066
|
+
elif sub == "list":
|
|
2067
|
+
data = cmd_runtime_list(
|
|
2068
|
+
workspace=args.workspace,
|
|
2069
|
+
owner=args.owner,
|
|
2070
|
+
)
|
|
2071
|
+
return data, 0, []
|
|
2072
|
+
elif sub == "inspect":
|
|
2073
|
+
data = cmd_runtime_inspect(
|
|
2074
|
+
runtime_id=args.runtime_id,
|
|
2075
|
+
workspace=args.workspace,
|
|
2076
|
+
)
|
|
2077
|
+
return data, 0, []
|
|
2078
|
+
elif sub == "status":
|
|
2079
|
+
data = cmd_runtime_status(
|
|
2080
|
+
runtime_id=args.runtime_id,
|
|
2081
|
+
workspace=args.workspace,
|
|
2082
|
+
)
|
|
2083
|
+
return data, 0, []
|
|
2084
|
+
elif sub == "stop":
|
|
2085
|
+
data = cmd_runtime_stop(
|
|
2086
|
+
runtime_id=args.runtime_id,
|
|
2087
|
+
workspace=args.workspace,
|
|
2088
|
+
grace_seconds=args.grace,
|
|
2089
|
+
)
|
|
2090
|
+
return data, 0, []
|
|
2091
|
+
elif sub == "restart":
|
|
2092
|
+
data = cmd_runtime_restart(
|
|
2093
|
+
runtime_id=args.runtime_id,
|
|
2094
|
+
workspace=args.workspace,
|
|
2095
|
+
)
|
|
2096
|
+
return data, 0, []
|
|
2097
|
+
elif sub == "remove":
|
|
2098
|
+
data = cmd_runtime_remove(
|
|
2099
|
+
runtime_id=args.runtime_id,
|
|
2100
|
+
workspace=args.workspace,
|
|
2101
|
+
force=args.force,
|
|
2102
|
+
)
|
|
2103
|
+
return data, 0, []
|
|
2104
|
+
elif sub == "services":
|
|
2105
|
+
sub2 = getattr(args, "runtime_services_command", None)
|
|
2106
|
+
if not getattr(args, "runtime_id", None):
|
|
2107
|
+
return None, 2, [build_error(
|
|
2108
|
+
"AISC_ERR_USAGE",
|
|
2109
|
+
"runtime services requires --runtime-id",
|
|
2110
|
+
)]
|
|
2111
|
+
if sub2 == "expose":
|
|
2112
|
+
data = cmd_runtime_services_expose(
|
|
2113
|
+
runtime_id=args.runtime_id,
|
|
2114
|
+
port=args.port,
|
|
2115
|
+
name=args.name,
|
|
2116
|
+
workspace=args.workspace,
|
|
2117
|
+
)
|
|
2118
|
+
elif sub2 == "unexpose":
|
|
2119
|
+
data = cmd_runtime_services_unexpose(
|
|
2120
|
+
runtime_id=args.runtime_id,
|
|
2121
|
+
port=args.port,
|
|
2122
|
+
workspace=args.workspace,
|
|
2123
|
+
)
|
|
2124
|
+
else: # none or "list"
|
|
2125
|
+
data = cmd_runtime_services(
|
|
2126
|
+
runtime_id=args.runtime_id,
|
|
2127
|
+
workspace=args.workspace,
|
|
2128
|
+
)
|
|
2129
|
+
return data, 0, []
|
|
2130
|
+
elif sub == "reconcile":
|
|
2131
|
+
data = cmd_runtime_reconcile(
|
|
2132
|
+
workspace=args.workspace,
|
|
2133
|
+
instance_id=args.instance_id,
|
|
2134
|
+
workspace_key=args.workspace_key,
|
|
2135
|
+
)
|
|
2136
|
+
return data, 0, []
|
|
2137
|
+
elif sub == "lease":
|
|
2138
|
+
action = getattr(args, "runtime_lease_command", None) or "inspect"
|
|
2139
|
+
if action != "inspect" and not getattr(args, "instance_id", None):
|
|
2140
|
+
return None, 2, [build_error(
|
|
2141
|
+
"AISC_ERR_USAGE",
|
|
2142
|
+
f"runtime lease {action} requires --instance-id",
|
|
2143
|
+
)]
|
|
2144
|
+
data = cmd_runtime_lease(
|
|
2145
|
+
action=action,
|
|
2146
|
+
workspace=args.workspace,
|
|
2147
|
+
instance_id=getattr(args, "instance_id", "") or "",
|
|
2148
|
+
lease_id=getattr(args, "lease_id", None),
|
|
2149
|
+
)
|
|
2150
|
+
return data, 0, []
|
|
2151
|
+
else:
|
|
2152
|
+
# Unknown runtime subcommand
|
|
2153
|
+
return None, 2, [build_error(
|
|
2154
|
+
"AISC_ERR_USAGE",
|
|
2155
|
+
f"Unknown runtime subcommand: {sub}"
|
|
2156
|
+
)]
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
def _cmd_session(
|
|
2162
|
+
args: argparse.Namespace,
|
|
2163
|
+
effective_format: str,
|
|
2164
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
2165
|
+
"""Execute ``aisc session`` subcommands. Supports --format json.
|
|
2166
|
+
|
|
2167
|
+
``session open`` is interactive: it inherits stdio via ``docker exec -it``
|
|
2168
|
+
and returns the agent exit code. ``session list`` and ``session terminate``
|
|
2169
|
+
are non-interactive and return JSON-serializable data.
|
|
2170
|
+
"""
|
|
2171
|
+
from aisc.cli.commands.session import (
|
|
2172
|
+
cmd_session_open,
|
|
2173
|
+
cmd_session_list,
|
|
2174
|
+
cmd_session_terminate,
|
|
2175
|
+
)
|
|
2176
|
+
|
|
2177
|
+
sub = args.session_command
|
|
2178
|
+
|
|
2179
|
+
if sub == "open":
|
|
2180
|
+
if effective_format == "json":
|
|
2181
|
+
emit_json_usage_error(
|
|
2182
|
+
command="session", version=__version__,
|
|
2183
|
+
message="session open 仅支持 text 输出——不支持 --format json",
|
|
2184
|
+
)
|
|
2185
|
+
sys.exit(2)
|
|
2186
|
+
data, exit_code = cmd_session_open(
|
|
2187
|
+
runtime_id=args.runtime_id,
|
|
2188
|
+
session_id=args.session_id,
|
|
2189
|
+
agent=args.agent,
|
|
2190
|
+
workspace=args.workspace,
|
|
2191
|
+
resume_conversation_id=args.resume_id,
|
|
2192
|
+
)
|
|
2193
|
+
errors: List[Dict[str, Any]] = []
|
|
2194
|
+
if exit_code != 0 and data.get("error"):
|
|
2195
|
+
errors.append(build_error(
|
|
2196
|
+
RuntimeErrorCode.SESSION_FAILED,
|
|
2197
|
+
data["error"],
|
|
2198
|
+
))
|
|
2199
|
+
return data, exit_code, errors
|
|
2200
|
+
elif sub == "list":
|
|
2201
|
+
data = cmd_session_list(
|
|
2202
|
+
runtime_id=args.runtime_id,
|
|
2203
|
+
workspace=args.workspace,
|
|
2204
|
+
)
|
|
2205
|
+
return data, 0, []
|
|
2206
|
+
elif sub == "terminate":
|
|
2207
|
+
data = cmd_session_terminate(
|
|
2208
|
+
runtime_id=args.runtime_id,
|
|
2209
|
+
session_id=args.session_id,
|
|
2210
|
+
workspace=args.workspace,
|
|
2211
|
+
grace_seconds=args.grace,
|
|
2212
|
+
)
|
|
2213
|
+
return data, 0, []
|
|
2214
|
+
else:
|
|
2215
|
+
return None, 2, [build_error(
|
|
2216
|
+
"AISC_ERR_USAGE",
|
|
2217
|
+
f"Unknown session subcommand: {sub}"
|
|
2218
|
+
)]
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
def _cmd_conversation(
|
|
2222
|
+
args: argparse.Namespace,
|
|
2223
|
+
effective_format: str,
|
|
2224
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
2225
|
+
"""Execute ``aisc conversation`` subcommands (v2.1.8 T3).
|
|
2226
|
+
|
|
2227
|
+
Both subcommands are captured/read-only and return JSON-serializable
|
|
2228
|
+
data under the aisc.cli/v1 envelope. CliErrors from the application
|
|
2229
|
+
layer (AISC_ERR_CONVERSATION_*) propagate to the unified terminal.
|
|
2230
|
+
"""
|
|
2231
|
+
from aisc.cli.commands.conversation import (
|
|
2232
|
+
cmd_conversation_delete,
|
|
2233
|
+
cmd_conversation_list,
|
|
2234
|
+
cmd_conversation_preflight,
|
|
2235
|
+
cmd_conversation_rename,
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
sub = args.conversation_command
|
|
2239
|
+
|
|
2240
|
+
if sub == "list":
|
|
2241
|
+
data = cmd_conversation_list(workspace=args.workspace)
|
|
2242
|
+
return data, 0, []
|
|
2243
|
+
elif sub == "preflight":
|
|
2244
|
+
data = cmd_conversation_preflight(
|
|
2245
|
+
workspace=args.workspace,
|
|
2246
|
+
conversation_id=args.conversation_id,
|
|
2247
|
+
agent=args.agent,
|
|
2248
|
+
)
|
|
2249
|
+
return data, 0, []
|
|
2250
|
+
elif sub == "delete":
|
|
2251
|
+
data = cmd_conversation_delete(
|
|
2252
|
+
workspace=args.workspace,
|
|
2253
|
+
conversation_id=args.conversation_id,
|
|
2254
|
+
agent=args.agent,
|
|
2255
|
+
)
|
|
2256
|
+
return data, 0, []
|
|
2257
|
+
elif sub == "rename":
|
|
2258
|
+
data = cmd_conversation_rename(
|
|
2259
|
+
workspace=args.workspace,
|
|
2260
|
+
conversation_id=args.conversation_id,
|
|
2261
|
+
agent=args.agent,
|
|
2262
|
+
title=args.title,
|
|
2263
|
+
)
|
|
2264
|
+
return data, 0, []
|
|
2265
|
+
else:
|
|
2266
|
+
return None, 2, [build_error(
|
|
2267
|
+
"AISC_ERR_USAGE",
|
|
2268
|
+
f"Unknown conversation subcommand: {sub}"
|
|
2269
|
+
)]
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
def _cmd_artifact(
|
|
2273
|
+
args: argparse.Namespace,
|
|
2274
|
+
effective_format: str,
|
|
2275
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
2276
|
+
"""Execute ``aisc artifact`` subcommands. Supports --format json.
|
|
2277
|
+
|
|
2278
|
+
``record``/``list``/``inspect``/``clear-session`` are non-interactive and
|
|
2279
|
+
return JSON-serializable data under the aisc.cli/v1 envelope.
|
|
2280
|
+
"""
|
|
2281
|
+
from aisc.cli.commands.artifact import (
|
|
2282
|
+
cmd_artifact_record,
|
|
2283
|
+
cmd_artifact_list,
|
|
2284
|
+
cmd_artifact_inspect,
|
|
2285
|
+
cmd_artifact_clear_session,
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
sub = args.artifact_command
|
|
2289
|
+
|
|
2290
|
+
if sub == "record":
|
|
2291
|
+
# T3a: env defaults mean argparse no longer enforces these — report
|
|
2292
|
+
# a stable usage error when neither flag nor env provided them.
|
|
2293
|
+
missing = [n for n, v in (
|
|
2294
|
+
("--runtime-id", args.runtime_id),
|
|
2295
|
+
("--session-id", args.session_id),
|
|
2296
|
+
) if not v]
|
|
2297
|
+
if missing:
|
|
2298
|
+
raise CliError(
|
|
2299
|
+
message=(
|
|
2300
|
+
"missing required arguments: "
|
|
2301
|
+
+ ", ".join(missing)
|
|
2302
|
+
+ " (or set AISC_RUNTIME_ID / AISC_TERMINAL_SESSION_ID — "
|
|
2303
|
+
"the Workbench injects both into agent sessions)"
|
|
2304
|
+
),
|
|
2305
|
+
exit_code=2,
|
|
2306
|
+
error_code="AISC_ERR_USAGE",
|
|
2307
|
+
)
|
|
2308
|
+
data = cmd_artifact_record(
|
|
2309
|
+
runtime_id=args.runtime_id,
|
|
2310
|
+
session_id=args.session_id,
|
|
2311
|
+
agent=args.agent,
|
|
2312
|
+
path=args.path,
|
|
2313
|
+
action=args.action,
|
|
2314
|
+
kind=args.kind,
|
|
2315
|
+
media_type=args.media_type,
|
|
2316
|
+
label=args.label,
|
|
2317
|
+
open_with=args.open_with,
|
|
2318
|
+
previous_path=args.previous_path,
|
|
2319
|
+
workspace=args.workspace,
|
|
2320
|
+
)
|
|
2321
|
+
return data, 0, []
|
|
2322
|
+
elif sub == "list":
|
|
2323
|
+
data = cmd_artifact_list(
|
|
2324
|
+
workspace=args.workspace,
|
|
2325
|
+
session_id=args.session_id,
|
|
2326
|
+
kind=args.kind,
|
|
2327
|
+
)
|
|
2328
|
+
return data, 0, []
|
|
2329
|
+
elif sub == "inspect":
|
|
2330
|
+
data = cmd_artifact_inspect(
|
|
2331
|
+
artifact_id=args.artifact_id,
|
|
2332
|
+
workspace=args.workspace,
|
|
2333
|
+
)
|
|
2334
|
+
return data, 0, []
|
|
2335
|
+
elif sub == "clear-session":
|
|
2336
|
+
data = cmd_artifact_clear_session(
|
|
2337
|
+
runtime_id=args.runtime_id,
|
|
2338
|
+
session_id=args.session_id,
|
|
2339
|
+
workspace=args.workspace,
|
|
2340
|
+
)
|
|
2341
|
+
return data, 0, []
|
|
2342
|
+
else:
|
|
2343
|
+
return None, 2, [build_error(
|
|
2344
|
+
"AISC_ERR_USAGE",
|
|
2345
|
+
f"Unknown artifact subcommand: {sub}"
|
|
2346
|
+
)]
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
def _cmd_data_root(
|
|
2350
|
+
args: argparse.Namespace,
|
|
2351
|
+
effective_format: str,
|
|
2352
|
+
) -> Tuple[Any, int, List[Dict[str, Any]]]:
|
|
2353
|
+
"""Execute ``aisc data-root`` subcommands (Stage 7, 7d).
|
|
2354
|
+
|
|
2355
|
+
doctor/migrate --dry-run/migrate --apply/rollback are non-interactive;
|
|
2356
|
+
conflicts and unconsented unknowns raise CliError (stable code, non-zero
|
|
2357
|
+
exit) instead of guessing.
|
|
2358
|
+
"""
|
|
2359
|
+
from aisc.cli.commands.data_root import (
|
|
2360
|
+
cmd_data_root_doctor,
|
|
2361
|
+
cmd_data_root_migrate,
|
|
2362
|
+
cmd_data_root_rollback,
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2365
|
+
sub = args.data_root_command
|
|
2366
|
+
if sub == "doctor":
|
|
2367
|
+
return cmd_data_root_doctor(workspace=args.workspace), 0, []
|
|
2368
|
+
elif sub == "migrate":
|
|
2369
|
+
return cmd_data_root_migrate(
|
|
2370
|
+
workspace=args.workspace,
|
|
2371
|
+
dry_run=args.dry_run,
|
|
2372
|
+
quarantine_unknown=args.quarantine_unknown,
|
|
2373
|
+
), 0, []
|
|
2374
|
+
# --apply is accepted for explicitness; applying is the default action
|
|
2375
|
+
# when --dry-run is absent (03-ux-flow contract spelling).
|
|
2376
|
+
elif sub == "rollback":
|
|
2377
|
+
return cmd_data_root_rollback(
|
|
2378
|
+
workspace=args.workspace, manifest=args.manifest,
|
|
2379
|
+
), 0, []
|
|
2380
|
+
return None, 2, [build_error(
|
|
2381
|
+
"AISC_ERR_USAGE",
|
|
2382
|
+
f"Unknown data-root subcommand: {sub}"
|
|
2383
|
+
)]
|
|
2384
|
+
|
|
2385
|
+
# ---------------------------------------------------------------------------
|
|
2386
|
+
# Main entry point
|
|
2387
|
+
# ---------------------------------------------------------------------------
|
|
2388
|
+
|
|
2389
|
+
def _log_cli_exit(
|
|
2390
|
+
command: str,
|
|
2391
|
+
exit_code: int,
|
|
2392
|
+
started: float,
|
|
2393
|
+
run_id: Optional[str],
|
|
2394
|
+
*,
|
|
2395
|
+
error_code: Optional[str] = None,
|
|
2396
|
+
) -> None:
|
|
2397
|
+
"""Lifecycle log line for a CLI invocation (lifecycle-logging P1).
|
|
2398
|
+
|
|
2399
|
+
One line per process exit at the three main() terminals — the same
|
|
2400
|
+
``run_id`` the envelope carries (env-injected by the Workbench), so
|
|
2401
|
+
app-side op events and cli exits align on one timeline. Best-effort:
|
|
2402
|
+
applog never raises.
|
|
2403
|
+
"""
|
|
2404
|
+
import time as _time
|
|
2405
|
+
|
|
2406
|
+
from aisc.applog import append_event
|
|
2407
|
+
|
|
2408
|
+
level = "info" if exit_code == 0 else ("warn" if exit_code == 130 else "error")
|
|
2409
|
+
append_event(
|
|
2410
|
+
"cli_exit", level=level, source="cli", run_id=run_id,
|
|
2411
|
+
command=command or "aisc", exit_code=exit_code,
|
|
2412
|
+
duration_ms=int((_time.monotonic() - started) * 1000),
|
|
2413
|
+
error_code=error_code,
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
2418
|
+
"""Main CLI entry point."""
|
|
2419
|
+
import time as _time
|
|
2420
|
+
|
|
2421
|
+
started = _time.monotonic()
|
|
2422
|
+
# Same id the envelope will carry (env-injected by the Workbench;
|
|
2423
|
+
# self-generated for standalone CLI use) — keeps the log timeline and
|
|
2424
|
+
# envelope.meta.run_id identical.
|
|
2425
|
+
_run_id = os.environ.get("AISC_RUN_ID") or str(__import__("uuid").uuid4())
|
|
2426
|
+
# Never let stdout's locale encoding (GBK on zh-CN Windows, cp1252 on
|
|
2427
|
+
# en-US) crash the CLI with UnicodeEncodeError. Protocol JSON is pure
|
|
2428
|
+
# ASCII by construction (ensure_ascii=True in emit_json/JsonlEmitter);
|
|
2429
|
+
# any other unencodable output (wizard emoji, non-ASCII messages)
|
|
2430
|
+
# degrades to '?' instead of aborting mid-command.
|
|
2431
|
+
try:
|
|
2432
|
+
sys.stdout.reconfigure(errors="replace")
|
|
2433
|
+
except (AttributeError, ValueError):
|
|
2434
|
+
pass # non-TextIOWrapper (PyInstaller wrapper) - protocol sites are ASCII anyway
|
|
2435
|
+
# Help localization (manual test ask 2026-09-09): follow the OS locale —
|
|
2436
|
+
# Chinese environments get Chinese help via output-layer translation
|
|
2437
|
+
# (see aisc.cli.help_i18n); every other locale keeps the English source.
|
|
2438
|
+
# MUST run before _build_parser(): argparse group titles ('options' 等)
|
|
2439
|
+
# resolve _() at construction time, not render time.
|
|
2440
|
+
from aisc.cli.help_i18n import maybe_install as _maybe_i18n
|
|
2441
|
+
_maybe_i18n()
|
|
2442
|
+
|
|
2443
|
+
parser = _build_parser()
|
|
2444
|
+
|
|
2445
|
+
# Shell completion (manual test ask 2026-09-09): argcomplete follows the
|
|
2446
|
+
# argparse tree automatically — subcommands, flags, choices all covered,
|
|
2447
|
+
# zero per-command maintenance. No-op outside a completion environment
|
|
2448
|
+
# (no _ARGCOMPLETE env); optional import so argcomplete-less installs
|
|
2449
|
+
# only lose completion, never the CLI.
|
|
2450
|
+
try:
|
|
2451
|
+
import argcomplete
|
|
2452
|
+
argcomplete.autocomplete(parser)
|
|
2453
|
+
except ImportError:
|
|
2454
|
+
pass
|
|
2455
|
+
|
|
2456
|
+
args_list = list(argv) if argv is not None else sys.argv[1:]
|
|
2457
|
+
|
|
2458
|
+
# Pre-detect format/events/command for error messages
|
|
2459
|
+
json_requested = _detect_json_format(args_list)
|
|
2460
|
+
events_requested = _detect_events(args_list)
|
|
2461
|
+
cmd_hint = _detect_command(args_list) or "aisc"
|
|
2462
|
+
parser._aisc_format = "json" if json_requested else None
|
|
2463
|
+
parser._aisc_command = cmd_hint
|
|
2464
|
+
|
|
2465
|
+
# Propagate json format to config subparser for unknown-subcommand errors
|
|
2466
|
+
if "config" in args_list:
|
|
2467
|
+
try:
|
|
2468
|
+
cfg_parser = [a for a in parser._subparsers._group_actions
|
|
2469
|
+
if a.dest == "command"][0].choices["config"]
|
|
2470
|
+
cfg_parser._aisc_format = "json" if json_requested else None
|
|
2471
|
+
cfg_parser._aisc_command = "config"
|
|
2472
|
+
except (AttributeError, IndexError, KeyError):
|
|
2473
|
+
pass
|
|
2474
|
+
|
|
2475
|
+
# Propagate json format to profile subparser for unknown-subcommand errors
|
|
2476
|
+
if "profile" in args_list:
|
|
2477
|
+
try:
|
|
2478
|
+
pf_parser = [a for a in parser._subparsers._group_actions
|
|
2479
|
+
if a.dest == "command"][0].choices["profile"]
|
|
2480
|
+
pf_parser._aisc_format = "json" if json_requested else None
|
|
2481
|
+
pf_parser._aisc_command = "profile"
|
|
2482
|
+
except (AttributeError, IndexError, KeyError):
|
|
2483
|
+
pass
|
|
2484
|
+
|
|
2485
|
+
# Propagate JSON format to provider subparser for parse-time errors.
|
|
2486
|
+
if "provider" in args_list:
|
|
2487
|
+
try:
|
|
2488
|
+
provider_parser = [a for a in parser._subparsers._group_actions
|
|
2489
|
+
if a.dest == "command"][0].choices["provider"]
|
|
2490
|
+
provider_parser._aisc_format = "json" if json_requested else None
|
|
2491
|
+
provider_parser._aisc_command = "provider"
|
|
2492
|
+
# Also propagate to provider current subparser (S0.4, supports --format json)
|
|
2493
|
+
if "current" in args_list:
|
|
2494
|
+
try:
|
|
2495
|
+
current_parser = [a for a in provider_parser._subparsers._group_actions
|
|
2496
|
+
if a.dest == "provider_command"][0].choices["current"]
|
|
2497
|
+
current_parser._aisc_format = "json" if json_requested else None
|
|
2498
|
+
current_parser._aisc_command = "provider current"
|
|
2499
|
+
except (AttributeError, IndexError, KeyError):
|
|
2500
|
+
pass
|
|
2501
|
+
except (AttributeError, IndexError, KeyError):
|
|
2502
|
+
pass
|
|
2503
|
+
|
|
2504
|
+
# Propagate JSON format to runtime subparser for parse-time errors.
|
|
2505
|
+
if "runtime" in args_list:
|
|
2506
|
+
try:
|
|
2507
|
+
runtime_parser = [a for a in parser._subparsers._group_actions
|
|
2508
|
+
if a.dest == "command"][0].choices["runtime"]
|
|
2509
|
+
runtime_parser._aisc_format = "json" if json_requested else None
|
|
2510
|
+
runtime_parser._aisc_command = "runtime"
|
|
2511
|
+
|
|
2512
|
+
# Also propagate to every runtime sub-subparser (CLI-A02/A05: any
|
|
2513
|
+
# command that supports --format json must emit a JSON usage error,
|
|
2514
|
+
# not fall back to argparse text — matches the session propagation).
|
|
2515
|
+
for _sub in ("preflight", "start", "list", "inspect", "stop",
|
|
2516
|
+
"restart", "remove", "reconcile", "lease"):
|
|
2517
|
+
if _sub in args_list:
|
|
2518
|
+
try:
|
|
2519
|
+
_sp = [a for a in runtime_parser._subparsers._group_actions
|
|
2520
|
+
if a.dest == "runtime_command"][0].choices[_sub]
|
|
2521
|
+
_sp._aisc_format = "json" if json_requested else None
|
|
2522
|
+
_sp._aisc_command = f"runtime {_sub}"
|
|
2523
|
+
except (AttributeError, IndexError, KeyError):
|
|
2524
|
+
pass
|
|
2525
|
+
except (AttributeError, IndexError, KeyError):
|
|
2526
|
+
pass
|
|
2527
|
+
|
|
2528
|
+
# Propagate JSON format to session subparser for parse-time errors.
|
|
2529
|
+
if "session" in args_list:
|
|
2530
|
+
try:
|
|
2531
|
+
session_parser = [a for a in parser._subparsers._group_actions
|
|
2532
|
+
if a.dest == "command"][0].choices["session"]
|
|
2533
|
+
session_parser._aisc_format = "json" if json_requested else None
|
|
2534
|
+
session_parser._aisc_command = "session"
|
|
2535
|
+
# Also propagate to session sub-subparsers (list/terminate support --format json).
|
|
2536
|
+
for _sub in ("open", "list", "terminate"):
|
|
2537
|
+
if _sub in args_list:
|
|
2538
|
+
try:
|
|
2539
|
+
_sp = [a for a in session_parser._subparsers._group_actions
|
|
2540
|
+
if a.dest == "session_command"][0].choices[_sub]
|
|
2541
|
+
_sp._aisc_format = "json" if json_requested else None
|
|
2542
|
+
_sp._aisc_command = f"session {_sub}"
|
|
2543
|
+
except (AttributeError, IndexError, KeyError):
|
|
2544
|
+
pass
|
|
2545
|
+
except (AttributeError, IndexError, KeyError):
|
|
2546
|
+
pass
|
|
2547
|
+
|
|
2548
|
+
# Propagate JSON format to artifact subparser for parse-time errors.
|
|
2549
|
+
if "artifact" in args_list:
|
|
2550
|
+
try:
|
|
2551
|
+
artifact_parser = [a for a in parser._subparsers._group_actions
|
|
2552
|
+
if a.dest == "command"][0].choices["artifact"]
|
|
2553
|
+
artifact_parser._aisc_format = "json" if json_requested else None
|
|
2554
|
+
artifact_parser._aisc_command = "artifact"
|
|
2555
|
+
for _sub in ("record", "list", "inspect", "clear-session"):
|
|
2556
|
+
if _sub in args_list:
|
|
2557
|
+
try:
|
|
2558
|
+
_sp = [a for a in artifact_parser._subparsers._group_actions
|
|
2559
|
+
if a.dest == "artifact_command"][0].choices[_sub]
|
|
2560
|
+
_sp._aisc_format = "json" if json_requested else None
|
|
2561
|
+
_sp._aisc_command = f"artifact {_sub}"
|
|
2562
|
+
except (AttributeError, IndexError, KeyError):
|
|
2563
|
+
pass
|
|
2564
|
+
except (AttributeError, IndexError, KeyError):
|
|
2565
|
+
pass
|
|
2566
|
+
|
|
2567
|
+
# Propagate JSON format to data-root subparser for parse-time errors.
|
|
2568
|
+
if "data-root" in args_list:
|
|
2569
|
+
try:
|
|
2570
|
+
data_root_parser = [a for a in parser._subparsers._group_actions
|
|
2571
|
+
if a.dest == "command"][0].choices["data-root"]
|
|
2572
|
+
data_root_parser._aisc_format = "json" if json_requested else None
|
|
2573
|
+
data_root_parser._aisc_command = "data-root"
|
|
2574
|
+
for _sub in ("doctor", "migrate", "rollback"):
|
|
2575
|
+
if _sub in args_list:
|
|
2576
|
+
try:
|
|
2577
|
+
_sp = [a for a in data_root_parser._subparsers._group_actions
|
|
2578
|
+
if a.dest == "data_root_command"][0].choices[_sub]
|
|
2579
|
+
_sp._aisc_format = "json" if json_requested else None
|
|
2580
|
+
_sp._aisc_command = f"data-root {_sub}"
|
|
2581
|
+
except (AttributeError, IndexError, KeyError):
|
|
2582
|
+
pass
|
|
2583
|
+
except (AttributeError, IndexError, KeyError):
|
|
2584
|
+
pass
|
|
2585
|
+
|
|
2586
|
+
try:
|
|
2587
|
+
args = parser.parse_args(args_list)
|
|
2588
|
+
except SystemExit:
|
|
2589
|
+
raise
|
|
2590
|
+
|
|
2591
|
+
# Resolve --aisc-root from raw argv with last-wins semantics.
|
|
2592
|
+
# This decouples root resolution from argparse's subparser defaults
|
|
2593
|
+
# and ensures --aisc-root works both before and after any command.
|
|
2594
|
+
resolved_root = _resolve_aisc_root_from_argv(args_list)
|
|
2595
|
+
if resolved_root is not None:
|
|
2596
|
+
args.aisc_root = resolved_root
|
|
2597
|
+
|
|
2598
|
+
# Resolve effective format
|
|
2599
|
+
effective_format = _resolve_format(args, args_list)
|
|
2600
|
+
parser._aisc_format = effective_format
|
|
2601
|
+
if getattr(args, "command", None):
|
|
2602
|
+
parser._aisc_command = args.command
|
|
2603
|
+
|
|
2604
|
+
# --events flag
|
|
2605
|
+
args_events = getattr(args, "events", False)
|
|
2606
|
+
if args_events is argparse.SUPPRESS:
|
|
2607
|
+
args_events = False
|
|
2608
|
+
|
|
2609
|
+
# --format json and --events are mutually exclusive
|
|
2610
|
+
if effective_format == "json" and args_events:
|
|
2611
|
+
emit_json_usage_error(
|
|
2612
|
+
command=args.command or cmd_hint, version=__version__,
|
|
2613
|
+
error_code="AISC_ERR_USAGE",
|
|
2614
|
+
message="--format json and --events are mutually exclusive",
|
|
2615
|
+
)
|
|
2616
|
+
sys.exit(2)
|
|
2617
|
+
|
|
2618
|
+
# Require a command
|
|
2619
|
+
if not getattr(args, "command", None):
|
|
2620
|
+
if effective_format == "json":
|
|
2621
|
+
emit_json_usage_error(command=cmd_hint, version=__version__,
|
|
2622
|
+
message="No command specified")
|
|
2623
|
+
sys.exit(2)
|
|
2624
|
+
else:
|
|
2625
|
+
parser.print_help()
|
|
2626
|
+
sys.exit(2)
|
|
2627
|
+
|
|
2628
|
+
# --- Bare grouped commands → print group help, exit 0 ---
|
|
2629
|
+
_grouped_dests: Dict[str, str] = {
|
|
2630
|
+
"config": "config_command",
|
|
2631
|
+
"profile": "profile_command",
|
|
2632
|
+
"provider": "provider_command",
|
|
2633
|
+
"runtime": "runtime_command",
|
|
2634
|
+
"session": "session_command",
|
|
2635
|
+
"artifact": "artifact_command",
|
|
2636
|
+
"conversation": "conversation_command",
|
|
2637
|
+
}
|
|
2638
|
+
if args.command in _grouped_dests:
|
|
2639
|
+
if getattr(args, _grouped_dests[args.command], None) is None:
|
|
2640
|
+
# Bare group — print the group's own help. Use the actual
|
|
2641
|
+
# argparse subparser so that future options stay synchronised
|
|
2642
|
+
# across all sub-subcommands. No JSON envelope; always text
|
|
2643
|
+
# help and exit 0.
|
|
2644
|
+
try:
|
|
2645
|
+
group_choices = [
|
|
2646
|
+
a for a in parser._subparsers._group_actions
|
|
2647
|
+
if a.dest == "command"
|
|
2648
|
+
][0].choices
|
|
2649
|
+
group_parser_obj = group_choices.get(args.command)
|
|
2650
|
+
if group_parser_obj is not None:
|
|
2651
|
+
group_parser_obj.print_help()
|
|
2652
|
+
sys.exit(0)
|
|
2653
|
+
except (AttributeError, IndexError, KeyError):
|
|
2654
|
+
pass
|
|
2655
|
+
# Fallback (should not be reached)
|
|
2656
|
+
parser.print_help()
|
|
2657
|
+
sys.exit(0)
|
|
2658
|
+
|
|
2659
|
+
use_color = sys.stdout.isatty() and not getattr(args, "no_color", False)
|
|
2660
|
+
aisc_root = getattr(args, "aisc_root", None)
|
|
2661
|
+
# Raw-argv resolver may have set this to a valid string; keep it as-is.
|
|
2662
|
+
# If the raw-argv resolver set it, argparse.SUPPRESS is irrelevant.
|
|
2663
|
+
if aisc_root is argparse.SUPPRESS:
|
|
2664
|
+
aisc_root = None
|
|
2665
|
+
|
|
2666
|
+
# Set up JSONL emitter
|
|
2667
|
+
emitter: Optional[JsonlEmitter] = None
|
|
2668
|
+
if args_events and args.command in ("build", "run"):
|
|
2669
|
+
emitter = JsonlEmitter(command=args.command)
|
|
2670
|
+
elif args_events and args.command in ("version", "doctor", "config", "profile",
|
|
2671
|
+
"status", "stop", "restart", "shell", "switch",
|
|
2672
|
+
"provider", "ps", "session", "artifact",
|
|
2673
|
+
"data-root"):
|
|
2674
|
+
if effective_format == "json":
|
|
2675
|
+
emit_json_usage_error(
|
|
2676
|
+
command=args.command, version=__version__,
|
|
2677
|
+
message=f"--events is not supported for '{args.command}' command",
|
|
2678
|
+
)
|
|
2679
|
+
sys.exit(2)
|
|
2680
|
+
else:
|
|
2681
|
+
print(f"Error: --events is not supported for '{args.command}' command",
|
|
2682
|
+
file=sys.stderr)
|
|
2683
|
+
sys.exit(2)
|
|
2684
|
+
|
|
2685
|
+
# Execute command
|
|
2686
|
+
data: Any = None
|
|
2687
|
+
report: Optional[DoctorReport] = None
|
|
2688
|
+
exit_code = 0
|
|
2689
|
+
errors: List[Dict[str, Any]] = []
|
|
2690
|
+
version_info: Optional[VersionInfo] = None
|
|
2691
|
+
|
|
2692
|
+
try:
|
|
2693
|
+
# 2.1.10 R1: serve owns its stdout completely (frame protocol) and
|
|
2694
|
+
# never participates in the envelope/text dispatch below. A missing
|
|
2695
|
+
# --stdio raises CliError here, which the shared handler reports.
|
|
2696
|
+
if args.command == "serve":
|
|
2697
|
+
from aisc.cli.commands.serve import cmd_serve
|
|
2698
|
+
sys.exit(cmd_serve(args))
|
|
2699
|
+
|
|
2700
|
+
if args.command == "version":
|
|
2701
|
+
version_info = _cmd_version(args)
|
|
2702
|
+
data = version_info.to_dict()
|
|
2703
|
+
elif args.command == "doctor":
|
|
2704
|
+
data, report = _cmd_doctor(args, effective_format)
|
|
2705
|
+
exit_code = report.exit_code
|
|
2706
|
+
if report.error_code:
|
|
2707
|
+
errors.append(build_error(report.error_code, report.error_message or ""))
|
|
2708
|
+
elif args.command == "build":
|
|
2709
|
+
data, exit_code, errors = _cmd_build(args, emitter, effective_format)
|
|
2710
|
+
elif args.command == "run":
|
|
2711
|
+
data, exit_code, errors = _cmd_run(args, emitter, effective_format, aisc_root)
|
|
2712
|
+
elif args.command in ("claude", "codex"):
|
|
2713
|
+
data, exit_code, errors = _cmd_agent(args, effective_format)
|
|
2714
|
+
elif args.command == "runs":
|
|
2715
|
+
data, exit_code, errors = _cmd_runs(args, effective_format)
|
|
2716
|
+
elif args.command == "workspaces":
|
|
2717
|
+
data, exit_code, errors = _cmd_workspaces(args, effective_format)
|
|
2718
|
+
elif args.command == "config":
|
|
2719
|
+
data, exit_code, errors = _cmd_config(args, effective_format)
|
|
2720
|
+
elif args.command == "profile":
|
|
2721
|
+
data, exit_code, errors = _cmd_profile(args, effective_format)
|
|
2722
|
+
elif args.command == "status":
|
|
2723
|
+
data, exit_code, errors = _cmd_status(args, effective_format)
|
|
2724
|
+
elif args.command == "stop":
|
|
2725
|
+
data, exit_code, errors = _cmd_stop(args, effective_format)
|
|
2726
|
+
elif args.command == "restart":
|
|
2727
|
+
data, exit_code, errors = _cmd_restart(args, effective_format)
|
|
2728
|
+
elif args.command == "shell":
|
|
2729
|
+
data, exit_code, errors = _cmd_shell(args, effective_format)
|
|
2730
|
+
elif args.command == "switch":
|
|
2731
|
+
data, exit_code, errors = _cmd_switch(args, effective_format)
|
|
2732
|
+
elif args.command == "provider":
|
|
2733
|
+
data, exit_code, errors = _cmd_provider(args, effective_format)
|
|
2734
|
+
elif args.command == "cc-switch":
|
|
2735
|
+
data, exit_code, errors = _cmd_cc_switch(args, effective_format)
|
|
2736
|
+
elif args.command == "network":
|
|
2737
|
+
data, exit_code, errors = _cmd_network(args, effective_format)
|
|
2738
|
+
elif args.command == "usage":
|
|
2739
|
+
data, exit_code, errors = _cmd_usage(args, effective_format)
|
|
2740
|
+
elif args.command == "logs":
|
|
2741
|
+
data, exit_code, errors = _cmd_logs(args, effective_format)
|
|
2742
|
+
elif args.command == "ps":
|
|
2743
|
+
data, exit_code, errors = _cmd_ps(args, effective_format)
|
|
2744
|
+
elif args.command == "maintenance":
|
|
2745
|
+
data, exit_code, errors = _cmd_maintenance(args, effective_format)
|
|
2746
|
+
elif args.command == "bundle":
|
|
2747
|
+
data, exit_code, errors = _cmd_bundle(args, effective_format)
|
|
2748
|
+
elif args.command == "update":
|
|
2749
|
+
data, exit_code, errors = _cmd_update(args, effective_format)
|
|
2750
|
+
elif args.command == "runtime":
|
|
2751
|
+
data, exit_code, errors = _cmd_runtime(args, effective_format)
|
|
2752
|
+
elif args.command == "session":
|
|
2753
|
+
data, exit_code, errors = _cmd_session(args, effective_format)
|
|
2754
|
+
elif args.command == "conversation":
|
|
2755
|
+
data, exit_code, errors = _cmd_conversation(args, effective_format)
|
|
2756
|
+
elif args.command == "artifact":
|
|
2757
|
+
data, exit_code, errors = _cmd_artifact(args, effective_format)
|
|
2758
|
+
elif args.command == "data-root":
|
|
2759
|
+
data, exit_code, errors = _cmd_data_root(args, effective_format)
|
|
2760
|
+
else:
|
|
2761
|
+
if effective_format == "json":
|
|
2762
|
+
emit_json_usage_error(
|
|
2763
|
+
command=args.command, version=__version__,
|
|
2764
|
+
message=f"Unknown command: {args.command}",
|
|
2765
|
+
)
|
|
2766
|
+
else:
|
|
2767
|
+
parser.error(f"Unknown command: {args.command}")
|
|
2768
|
+
sys.exit(2)
|
|
2769
|
+
return
|
|
2770
|
+
|
|
2771
|
+
except CliError as exc:
|
|
2772
|
+
_log_cli_exit(args.command, exc.exit_code, started, _run_id,
|
|
2773
|
+
error_code=exc.error_code)
|
|
2774
|
+
# --- unified terminal: main owns the single terminal event ---
|
|
2775
|
+
if emitter is not None and not emitter.terminated:
|
|
2776
|
+
cmd = args.command or "aisc"
|
|
2777
|
+
term_type = f"{cmd}.failed"
|
|
2778
|
+
term_data = dict(exc.data or {})
|
|
2779
|
+
term_data["exit_code"] = exc.exit_code
|
|
2780
|
+
term_data["error_code"] = exc.error_code
|
|
2781
|
+
term_data["message"] = exc.message
|
|
2782
|
+
emitter.emit(term_type, term_data, terminal=True)
|
|
2783
|
+
sys.exit(exc.exit_code)
|
|
2784
|
+
|
|
2785
|
+
# JSON envelope — use exc.data if present
|
|
2786
|
+
if effective_format == "json":
|
|
2787
|
+
envelope = build_envelope(
|
|
2788
|
+
command=args.command,
|
|
2789
|
+
exit_code=exc.exit_code,
|
|
2790
|
+
version=__version__,
|
|
2791
|
+
data=exc.data, # structured outcome, not null
|
|
2792
|
+
errors=[build_error(exc.error_code, exc.message, exc.hint)],
|
|
2793
|
+
run_id=_run_id,
|
|
2794
|
+
)
|
|
2795
|
+
emit_json(envelope)
|
|
2796
|
+
else:
|
|
2797
|
+
print(f"Error: {exc.message}", file=sys.stderr)
|
|
2798
|
+
sys.exit(exc.exit_code)
|
|
2799
|
+
return
|
|
2800
|
+
|
|
2801
|
+
except KeyboardInterrupt:
|
|
2802
|
+
_log_cli_exit(args.command, 130, started, _run_id)
|
|
2803
|
+
if emitter is not None and not emitter.terminated:
|
|
2804
|
+
cmd = args.command or "aisc"
|
|
2805
|
+
emitter.emit(f"{cmd}.cancelled", {"exit_code": 130}, terminal=True)
|
|
2806
|
+
sys.exit(130)
|
|
2807
|
+
print("\nCancelled.", file=sys.stderr)
|
|
2808
|
+
sys.exit(130)
|
|
2809
|
+
|
|
2810
|
+
# --- success: terminal event if events mode ---
|
|
2811
|
+
_log_cli_exit(args.command, exit_code, started, _run_id)
|
|
2812
|
+
if emitter is not None and not emitter.terminated:
|
|
2813
|
+
cmd = args.command or "aisc"
|
|
2814
|
+
term_data = dict(data or {})
|
|
2815
|
+
term_data["exit_code"] = exit_code
|
|
2816
|
+
emitter.emit(f"{cmd}.complete", term_data, terminal=True)
|
|
2817
|
+
sys.exit(exit_code)
|
|
2818
|
+
|
|
2819
|
+
# Output for non-events mode
|
|
2820
|
+
if effective_format == "json":
|
|
2821
|
+
envelope = build_envelope(
|
|
2822
|
+
command=args.command, exit_code=exit_code,
|
|
2823
|
+
version=__version__, data=data, errors=errors,
|
|
2824
|
+
run_id=_run_id,
|
|
2825
|
+
)
|
|
2826
|
+
emit_json(envelope)
|
|
2827
|
+
else:
|
|
2828
|
+
if args.command == "version":
|
|
2829
|
+
assert version_info is not None
|
|
2830
|
+
print(version_info.to_text())
|
|
2831
|
+
elif args.command == "doctor":
|
|
2832
|
+
assert report is not None
|
|
2833
|
+
print_doctor_text(report, use_color=use_color)
|
|
2834
|
+
elif args.command in ("build", "run"):
|
|
2835
|
+
from aisc.adapters.docker_ import format_argv_display
|
|
2836
|
+
if isinstance(data, dict) and data.get("dry_run"):
|
|
2837
|
+
label = "Build" if args.command == "build" else "Run"
|
|
2838
|
+
print(f"{label} plan (dry-run):")
|
|
2839
|
+
argv_list = data.get("docker_argv", [])
|
|
2840
|
+
print(f" docker {format_argv_display(argv_list)}")
|
|
2841
|
+
elif isinstance(data, dict) and data.get("executed"):
|
|
2842
|
+
if args.command == "build":
|
|
2843
|
+
print(f"Build succeeded: {data.get('image_tag', '')}")
|
|
2844
|
+
# run: the activation summary (_cmd_run) already spoke —
|
|
2845
|
+
# "Container finished." was the old foreground-run wording;
|
|
2846
|
+
# F2-C detached keep-alives never finish here
|
|
2847
|
+
elif args.command == "data-root":
|
|
2848
|
+
from aisc.cli.commands.data_root import print_data_root_text
|
|
2849
|
+
print_data_root_text(data)
|
|
2850
|
+
elif args.command == "config":
|
|
2851
|
+
from aisc.cli.commands.config import print_validate_text, print_effective_text
|
|
2852
|
+
from aisc.application.config_service import ServiceResult
|
|
2853
|
+
sub = getattr(args, "config_command", "validate")
|
|
2854
|
+
if sub == "validate":
|
|
2855
|
+
sr = ServiceResult(valid=data.get("valid", True), exit_code=exit_code,
|
|
2856
|
+
error_code=errors[0]["code"] if errors else "",
|
|
2857
|
+
error_message=errors[0]["message"] if errors else "",
|
|
2858
|
+
data=data)
|
|
2859
|
+
print_validate_text(sr)
|
|
2860
|
+
else:
|
|
2861
|
+
sr = ServiceResult(valid=data.get("valid", True), exit_code=exit_code,
|
|
2862
|
+
error_code=errors[0]["code"] if errors else "",
|
|
2863
|
+
error_message=errors[0]["message"] if errors else "",
|
|
2864
|
+
data=data)
|
|
2865
|
+
print_effective_text(sr)
|
|
2866
|
+
elif args.command == "profile":
|
|
2867
|
+
from aisc.cli.commands.profile import print_profile_list_text, print_profile_show_text
|
|
2868
|
+
sub = getattr(args, "profile_command", "list")
|
|
2869
|
+
if sub == "show":
|
|
2870
|
+
print_profile_show_text(data)
|
|
2871
|
+
else:
|
|
2872
|
+
print_profile_list_text(data)
|
|
2873
|
+
elif args.command == "status":
|
|
2874
|
+
from aisc.cli.commands.container import print_status_text, StatusResult
|
|
2875
|
+
sr = StatusResult(
|
|
2876
|
+
name=data.get("name", ""),
|
|
2877
|
+
exists=data.get("exists", False),
|
|
2878
|
+
running=data.get("running", False),
|
|
2879
|
+
status=data.get("status", ""),
|
|
2880
|
+
image=data.get("image", ""),
|
|
2881
|
+
container_id=data.get("container_id", ""),
|
|
2882
|
+
)
|
|
2883
|
+
print_status_text(sr)
|
|
2884
|
+
elif args.command == "stop":
|
|
2885
|
+
# --all already printed its batch summary in _cmd_stop — feeding
|
|
2886
|
+
# the batch dict to print_stop_text was a KeyError crash (r2 #1)
|
|
2887
|
+
if not getattr(args, "all", False):
|
|
2888
|
+
from aisc.cli.commands.container import print_stop_text
|
|
2889
|
+
print_stop_text(data if isinstance(data, dict) else {})
|
|
2890
|
+
elif args.command == "restart":
|
|
2891
|
+
from aisc.cli.commands.container import print_restart_text
|
|
2892
|
+
print_restart_text(data if isinstance(data, dict) else {})
|
|
2893
|
+
elif args.command == "ps":
|
|
2894
|
+
from aisc.cli.commands.container import print_ps_text, PsRow
|
|
2895
|
+
ps_rows = [PsRow(
|
|
2896
|
+
name=r.get("name", ""), label=r.get("label", ""),
|
|
2897
|
+
status=r.get("status", ""), running=r.get("running", False),
|
|
2898
|
+
image=r.get("image", ""), workspace=r.get("workspace", ""),
|
|
2899
|
+
active=r.get("active", False),
|
|
2900
|
+
) for r in (data if isinstance(data, list) else [])]
|
|
2901
|
+
print_ps_text(ps_rows)
|
|
2902
|
+
elif args.command in ("shell", "switch"):
|
|
2903
|
+
# interactive output printed directly by _cmd_*
|
|
2904
|
+
pass
|
|
2905
|
+
elif args.command == "provider":
|
|
2906
|
+
if getattr(args, "provider_command", "") == "current":
|
|
2907
|
+
from aisc.cli.commands.provider import print_provider_current_text
|
|
2908
|
+
print_provider_current_text(data)
|
|
2909
|
+
# set-key: interactive, output already printed by _cmd_*
|
|
2910
|
+
elif args.command == "runtime":
|
|
2911
|
+
from aisc.cli.commands.runtime import print_runtime_text
|
|
2912
|
+
print_runtime_text(getattr(args, "runtime_command", ""), data, errors)
|
|
2913
|
+
# maintenance text output is printed in dispatch (scan lines are
|
|
2914
|
+
# consumed by installers; cleanup/rebuild print nothing extra).
|
|
2915
|
+
elif args.command == "session":
|
|
2916
|
+
from aisc.cli.commands.session import print_session_text
|
|
2917
|
+
print_session_text(getattr(args, "session_command", ""), data, errors)
|
|
2918
|
+
elif args.command == "artifact":
|
|
2919
|
+
from aisc.cli.commands.artifact import print_artifact_text
|
|
2920
|
+
print_artifact_text(getattr(args, "artifact_command", ""), data, errors)
|
|
2921
|
+
|
|
2922
|
+
sys.exit(exit_code)
|
|
2923
|
+
|
|
2924
|
+
|
|
2925
|
+
if __name__ == "__main__":
|
|
2926
|
+
main()
|