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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""F2-C (fix2-design.md): `aisc claude` / `aisc codex` — agent entry sugar.
|
|
2
|
+
|
|
3
|
+
One command opens the AGENT inside the ACTIVE workspace's container (the
|
|
4
|
+
registry's default pointer = last `aisc run`); `--workspace` retargets
|
|
5
|
+
without switching the default. Args after `--` pass through verbatim
|
|
6
|
+
(`aisc claude -- -c` → container-side `claude -c`).
|
|
7
|
+
|
|
8
|
+
Text mode streams the TTY (docker exec -it); JSON mode runs captured and
|
|
9
|
+
returns the exit code in the envelope — scripting-friendly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from aisc.adapters.docker_ import DockerExecutor, RealDockerExecutor
|
|
18
|
+
from aisc.domain.models import CliError, ProcessResult
|
|
19
|
+
|
|
20
|
+
AGENTS = ("claude", "codex")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_by_workspace(workspace: str, explicit_root: Optional[str]) -> str:
|
|
24
|
+
"""Find the registered container bound to this workspace path.
|
|
25
|
+
|
|
26
|
+
The registry lives in the workspace's state dir (data-root
|
|
27
|
+
``workspaces/<hash>/runtime``). ``list_containers`` returns a
|
|
28
|
+
``name → meta`` map — the earlier list-shaped traversal here was wrong
|
|
29
|
+
and crashed every ``aisc claude --workspace <path>``.
|
|
30
|
+
"""
|
|
31
|
+
from aisc.adapters.container_registry import list_containers
|
|
32
|
+
from aisc.application.data_root import workspace_state_dir
|
|
33
|
+
|
|
34
|
+
target = Path(workspace).resolve()
|
|
35
|
+
try:
|
|
36
|
+
state_dir = workspace_state_dir(target)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
if not explicit_root:
|
|
39
|
+
raise CliError(
|
|
40
|
+
message=f"cannot resolve the workspace state dir: {exc}",
|
|
41
|
+
exit_code=1, error_code="AISC_ERR_GENERAL") from exc
|
|
42
|
+
state_dir = Path(explicit_root).resolve()
|
|
43
|
+
|
|
44
|
+
matches = [
|
|
45
|
+
name for name, meta in list_containers(state_dir).items()
|
|
46
|
+
if Path(str(meta.get("workspace", ""))).resolve() == target
|
|
47
|
+
]
|
|
48
|
+
if not matches:
|
|
49
|
+
raise CliError(
|
|
50
|
+
message=f"no active container is registered for {target} — "
|
|
51
|
+
f"start one first: aisc run {target}",
|
|
52
|
+
exit_code=4, error_code="AISC_ERR_NOT_FOUND",
|
|
53
|
+
)
|
|
54
|
+
return str(matches[0])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_agent(
|
|
58
|
+
agent: str,
|
|
59
|
+
agent_args: List[str],
|
|
60
|
+
workspace: Optional[str] = None,
|
|
61
|
+
name_override: Optional[str] = None,
|
|
62
|
+
explicit_root: Optional[str] = None,
|
|
63
|
+
capture: bool = False,
|
|
64
|
+
executor: Optional[DockerExecutor] = None,
|
|
65
|
+
) -> ProcessResult | Dict[str, Any]:
|
|
66
|
+
"""Open one agent in the active (or --workspace) container.
|
|
67
|
+
|
|
68
|
+
Returns the streaming ProcessResult in text mode; in capture mode a
|
|
69
|
+
JSON-serializable dict for the envelope.
|
|
70
|
+
"""
|
|
71
|
+
exec_ = executor or RealDockerExecutor()
|
|
72
|
+
|
|
73
|
+
if agent not in AGENTS:
|
|
74
|
+
raise CliError(message=f"unknown agent {agent!r}", exit_code=2,
|
|
75
|
+
error_code="AISC_ERR_USAGE")
|
|
76
|
+
|
|
77
|
+
if workspace:
|
|
78
|
+
name = _resolve_by_workspace(workspace, explicit_root)
|
|
79
|
+
else:
|
|
80
|
+
from aisc.cli.commands import runs as cli_runs
|
|
81
|
+
from aisc.cli.commands.container import discover_container
|
|
82
|
+
|
|
83
|
+
# Default leg: the machine-global ACTIVE workspace (never cwd — the
|
|
84
|
+
# cwd anchor trips the data-root overlap gate from $HOME and reads
|
|
85
|
+
# the wrong registry).
|
|
86
|
+
active = cli_runs.get_active()
|
|
87
|
+
if active:
|
|
88
|
+
from aisc.application.data_root import workspace_state_dir
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
root = str(workspace_state_dir(Path(active)))
|
|
92
|
+
except Exception:
|
|
93
|
+
root = explicit_root
|
|
94
|
+
else:
|
|
95
|
+
root = explicit_root
|
|
96
|
+
name = discover_container(name_override=name_override,
|
|
97
|
+
explicit_root=root,
|
|
98
|
+
executor=exec_)
|
|
99
|
+
|
|
100
|
+
# -it only when a real TTY rides along — capture mode pipes stdio, and
|
|
101
|
+
# docker exec -t without a TTY dies with "the input device is not a TTY".
|
|
102
|
+
argv = ["exec", *(["-it"] if not capture else []), name, agent, *agent_args]
|
|
103
|
+
if capture:
|
|
104
|
+
proc = exec_.run_captured(argv)
|
|
105
|
+
return {
|
|
106
|
+
"container": name,
|
|
107
|
+
"agent": agent,
|
|
108
|
+
"agent_args": list(agent_args),
|
|
109
|
+
"exit_code": proc.exit_code,
|
|
110
|
+
"stdout": proc.stdout,
|
|
111
|
+
"stderr": proc.stderr,
|
|
112
|
+
}
|
|
113
|
+
proc = exec_.run_streaming(argv)
|
|
114
|
+
if proc.command_not_found:
|
|
115
|
+
raise CliError(message="Docker CLI not found", exit_code=3,
|
|
116
|
+
error_code="AISC_ERR_DOCKER_UNAVAILABLE")
|
|
117
|
+
return proc
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""`aisc artifact` commands — Agent Artifact fact protocol (Stage 3, ART-02).
|
|
2
|
+
|
|
3
|
+
record/list/inspect/clear-session all return JSON-serializable dicts under the
|
|
4
|
+
``aisc.cli/v1`` envelope. The authoritative fact layer writes session-scoped
|
|
5
|
+
registries in the host data dir; the workspace is never touched (A-ART04-1).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from aisc.domain.artifacts import (
|
|
17
|
+
ArtifactAction,
|
|
18
|
+
ArtifactKind,
|
|
19
|
+
ArtifactOpenWith,
|
|
20
|
+
ArtifactProvenance,
|
|
21
|
+
ArtifactRecord,
|
|
22
|
+
normalize_media_type,
|
|
23
|
+
)
|
|
24
|
+
from aisc.domain.models import CliError, RuntimeErrorCode
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _iso_now() -> str:
|
|
28
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_workspace(workspace: Optional[str]) -> Path:
|
|
32
|
+
return Path(workspace).resolve() if workspace else Path.cwd().resolve()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _record_to_dict(rec: ArtifactRecord) -> Dict[str, Any]:
|
|
36
|
+
return rec.to_dict()
|
|
37
|
+
|
|
38
|
+
def _normalize_record_path(raw: str, workspace: Path) -> str:
|
|
39
|
+
"""Accept a container/host absolute path when it is inside the workspace.
|
|
40
|
+
|
|
41
|
+
Agents sometimes ignore the Skill instruction and pass `/root/app/...`
|
|
42
|
+
(the container mount). The host cannot resolve that path, but it maps
|
|
43
|
+
directly to a workspace-relative path, which is what the registry stores.
|
|
44
|
+
Anything outside the workspace is still rejected by ``validate_relative_path``.
|
|
45
|
+
"""
|
|
46
|
+
path = Path(raw)
|
|
47
|
+
if not path.is_absolute():
|
|
48
|
+
return raw
|
|
49
|
+
try:
|
|
50
|
+
rel = os.path.relpath(raw, workspace)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return raw
|
|
53
|
+
if rel == "." or rel.startswith("..") or Path(rel).is_absolute():
|
|
54
|
+
return raw
|
|
55
|
+
return rel.replace("\\", "/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Commands
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def cmd_artifact_record(
|
|
63
|
+
*,
|
|
64
|
+
runtime_id: str,
|
|
65
|
+
session_id: str,
|
|
66
|
+
agent: str,
|
|
67
|
+
path: str,
|
|
68
|
+
action: str = ArtifactAction.CREATED,
|
|
69
|
+
kind: str = ArtifactKind.DELIVERABLE,
|
|
70
|
+
media_type: Optional[str] = None,
|
|
71
|
+
label: str = "",
|
|
72
|
+
open_with: str = ArtifactOpenWith.PREVIEW,
|
|
73
|
+
previous_path: Optional[str] = None,
|
|
74
|
+
workspace: Optional[str] = None,
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""Execute ``aisc artifact record``."""
|
|
77
|
+
from aisc.application.artifact import record as _record
|
|
78
|
+
|
|
79
|
+
ws = _resolve_workspace(workspace)
|
|
80
|
+
try:
|
|
81
|
+
mt = normalize_media_type(media_type)
|
|
82
|
+
normalized_path = _normalize_record_path(path, ws)
|
|
83
|
+
normalized_previous_path = (
|
|
84
|
+
_normalize_record_path(previous_path, ws) if previous_path else None
|
|
85
|
+
)
|
|
86
|
+
rec = ArtifactRecord(
|
|
87
|
+
schema_version=1,
|
|
88
|
+
artifact_id=str(uuid.uuid4()),
|
|
89
|
+
workspace_relative_path=normalized_path,
|
|
90
|
+
action=action,
|
|
91
|
+
kind=kind,
|
|
92
|
+
media_type=mt,
|
|
93
|
+
label=label,
|
|
94
|
+
open_with=open_with,
|
|
95
|
+
producer={
|
|
96
|
+
"agent": agent,
|
|
97
|
+
"session_id": session_id,
|
|
98
|
+
"runtime_id": runtime_id,
|
|
99
|
+
},
|
|
100
|
+
state="present",
|
|
101
|
+
provenance=ArtifactProvenance.MANIFEST,
|
|
102
|
+
recorded_at=_iso_now(),
|
|
103
|
+
previous_path=normalized_previous_path,
|
|
104
|
+
extra={},
|
|
105
|
+
).validate()
|
|
106
|
+
saved = _record(ws, rec, session_id=session_id)
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
raise CliError(
|
|
109
|
+
message=str(exc),
|
|
110
|
+
exit_code=2,
|
|
111
|
+
error_code="AISC_ERR_ARTIFACT_INVALID",
|
|
112
|
+
) from exc
|
|
113
|
+
return {"artifact": _record_to_dict(saved)}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_artifact_list(
|
|
117
|
+
*,
|
|
118
|
+
workspace: Optional[str] = None,
|
|
119
|
+
session_id: Optional[str] = None,
|
|
120
|
+
kind: Optional[str] = None,
|
|
121
|
+
) -> Dict[str, Any]:
|
|
122
|
+
"""Execute ``aisc artifact list``."""
|
|
123
|
+
from aisc.application.artifact import list_records
|
|
124
|
+
|
|
125
|
+
ws = _resolve_workspace(workspace)
|
|
126
|
+
if kind is not None and kind not in ArtifactKind.ALL:
|
|
127
|
+
raise CliError(
|
|
128
|
+
message=f"invalid kind: {kind!r}",
|
|
129
|
+
exit_code=2,
|
|
130
|
+
error_code="AISC_ERR_ARTIFACT_INVALID",
|
|
131
|
+
)
|
|
132
|
+
records = list_records(ws, session_id=session_id, kind=kind)
|
|
133
|
+
return {
|
|
134
|
+
"schema_version": 1,
|
|
135
|
+
"artifacts": [_record_to_dict(r) for r in records],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_artifact_inspect(
|
|
140
|
+
*,
|
|
141
|
+
artifact_id: str,
|
|
142
|
+
workspace: Optional[str] = None,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""Execute ``aisc artifact inspect``."""
|
|
145
|
+
from aisc.application.artifact import inspect_record
|
|
146
|
+
|
|
147
|
+
ws = _resolve_workspace(workspace)
|
|
148
|
+
rec = inspect_record(ws, artifact_id)
|
|
149
|
+
if rec is None:
|
|
150
|
+
raise CliError(
|
|
151
|
+
message=f"artifact not found: {artifact_id}",
|
|
152
|
+
exit_code=1,
|
|
153
|
+
error_code="AISC_ERR_ARTIFACT_NOT_FOUND",
|
|
154
|
+
)
|
|
155
|
+
return {"artifact": _record_to_dict(rec)}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_artifact_clear_session(
|
|
159
|
+
*,
|
|
160
|
+
runtime_id: str,
|
|
161
|
+
session_id: str,
|
|
162
|
+
workspace: Optional[str] = None,
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""Execute ``aisc artifact clear-session``."""
|
|
165
|
+
from aisc.application.artifact import clear_session
|
|
166
|
+
|
|
167
|
+
ws = _resolve_workspace(workspace)
|
|
168
|
+
clear_session(ws, session_id)
|
|
169
|
+
return {
|
|
170
|
+
"runtime_id": runtime_id,
|
|
171
|
+
"session_id": session_id,
|
|
172
|
+
"cleared": True,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def print_artifact_text(
|
|
177
|
+
sub: str,
|
|
178
|
+
data: Optional[Dict[str, Any]],
|
|
179
|
+
errors: List[Dict[str, Any]],
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Human-readable output for `aisc artifact` (text mode)."""
|
|
182
|
+
if errors:
|
|
183
|
+
for err in errors:
|
|
184
|
+
print(f"Error: {err.get('message', '')}", file=__import__("sys").stderr)
|
|
185
|
+
return
|
|
186
|
+
if sub == "record" and isinstance(data, dict) and data.get("artifact"):
|
|
187
|
+
a = data["artifact"]
|
|
188
|
+
print(f"recorded {a['artifact_id']} {a['workspace_relative_path']} "
|
|
189
|
+
f"({a['kind']}, {a['action']})")
|
|
190
|
+
elif sub == "list" and isinstance(data, dict):
|
|
191
|
+
arts = data.get("artifacts", [])
|
|
192
|
+
if not arts:
|
|
193
|
+
print("(no artifacts)")
|
|
194
|
+
return
|
|
195
|
+
for a in arts:
|
|
196
|
+
print(f"{a['artifact_id']} {a['workspace_relative_path']} "
|
|
197
|
+
f"({a['kind']}, {a['action']}, {a['state']})")
|
|
198
|
+
elif sub == "inspect" and isinstance(data, dict) and data.get("artifact"):
|
|
199
|
+
a = data["artifact"]
|
|
200
|
+
print(f"artifact: {a['artifact_id']}")
|
|
201
|
+
print(f" path: {a['workspace_relative_path']}")
|
|
202
|
+
print(f" kind: {a['kind']}")
|
|
203
|
+
print(f" action: {a['action']}")
|
|
204
|
+
print(f" state: {a['state']}")
|
|
205
|
+
print(f" media: {a.get('media_type') or '-'}")
|
|
206
|
+
print(f" label: {a.get('label') or '-'}")
|
|
207
|
+
print(f" producer: {a.get('producer', {}).get('agent', '-')} "
|
|
208
|
+
f"session={a.get('producer', {}).get('session_id', '-')}")
|
|
209
|
+
elif sub == "clear-session":
|
|
210
|
+
print("cleared")
|