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.
Files changed (92) hide show
  1. aisc/VERSION +1 -0
  2. aisc/__init__.py +49 -0
  3. aisc/__main__.py +6 -0
  4. aisc/adapters/__init__.py +1 -0
  5. aisc/adapters/config_reader.py +215 -0
  6. aisc/adapters/container_registry.py +718 -0
  7. aisc/adapters/data_root_store.py +296 -0
  8. aisc/adapters/docker_.py +1183 -0
  9. aisc/adapters/docker_gateway.py +1244 -0
  10. aisc/adapters/docker_sdk_backed.py +274 -0
  11. aisc/adapters/maintenance_lock.py +97 -0
  12. aisc/adapters/state_file.py +184 -0
  13. aisc/adapters/system.py +101 -0
  14. aisc/adapters/windows_config_reader.py +249 -0
  15. aisc/adapters/workspace_lease_store.py +189 -0
  16. aisc/application/__init__.py +1 -0
  17. aisc/application/artifact.py +265 -0
  18. aisc/application/bundle_fetch.py +836 -0
  19. aisc/application/cc_switch_provider.py +245 -0
  20. aisc/application/cc_switch_resolver.py +363 -0
  21. aisc/application/config_service.py +452 -0
  22. aisc/application/conversation.py +516 -0
  23. aisc/application/data_migration.py +440 -0
  24. aisc/application/data_root.py +275 -0
  25. aisc/application/docker_lifecycle.py +659 -0
  26. aisc/application/doctor.py +821 -0
  27. aisc/application/legacy_scan.py +252 -0
  28. aisc/application/network_subscription.py +642 -0
  29. aisc/application/profile_service.py +105 -0
  30. aisc/application/provider.py +92 -0
  31. aisc/application/repair.py +387 -0
  32. aisc/application/resources.py +270 -0
  33. aisc/application/runtime.py +1826 -0
  34. aisc/application/session.py +320 -0
  35. aisc/application/tool_versions.py +210 -0
  36. aisc/application/toolchain.py +91 -0
  37. aisc/application/update.py +286 -0
  38. aisc/application/usage.py +305 -0
  39. aisc/application/version.py +133 -0
  40. aisc/application/web_gateway.py +459 -0
  41. aisc/application/workspace_reconcile.py +318 -0
  42. aisc/applog.py +103 -0
  43. aisc/cli/__init__.py +1 -0
  44. aisc/cli/build_progress_parser.py +217 -0
  45. aisc/cli/commands/__init__.py +1 -0
  46. aisc/cli/commands/agents.py +117 -0
  47. aisc/cli/commands/artifact.py +210 -0
  48. aisc/cli/commands/build.py +566 -0
  49. aisc/cli/commands/bundle.py +107 -0
  50. aisc/cli/commands/cc_switch.py +158 -0
  51. aisc/cli/commands/config.py +104 -0
  52. aisc/cli/commands/container.py +787 -0
  53. aisc/cli/commands/conversation.py +72 -0
  54. aisc/cli/commands/data_root.py +84 -0
  55. aisc/cli/commands/logs.py +89 -0
  56. aisc/cli/commands/network.py +132 -0
  57. aisc/cli/commands/profile.py +52 -0
  58. aisc/cli/commands/provider.py +49 -0
  59. aisc/cli/commands/run.py +494 -0
  60. aisc/cli/commands/runs.py +125 -0
  61. aisc/cli/commands/runtime.py +450 -0
  62. aisc/cli/commands/serve.py +550 -0
  63. aisc/cli/commands/serve_fs.py +369 -0
  64. aisc/cli/commands/session.py +167 -0
  65. aisc/cli/commands/update.py +145 -0
  66. aisc/cli/commands/usage.py +74 -0
  67. aisc/cli/commands/wizard.py +130 -0
  68. aisc/cli/commands/workspaces.py +162 -0
  69. aisc/cli/help_i18n.py +385 -0
  70. aisc/cli/main.py +2926 -0
  71. aisc/cli/output.py +239 -0
  72. aisc/cli/tutorial.py +109 -0
  73. aisc/domain/__init__.py +1 -0
  74. aisc/domain/artifacts.py +279 -0
  75. aisc/domain/cc_switch_release.py +303 -0
  76. aisc/domain/config.py +64 -0
  77. aisc/domain/data_migration.py +259 -0
  78. aisc/domain/data_root.py +124 -0
  79. aisc/domain/docker_ownership.py +148 -0
  80. aisc/domain/gateway.py +168 -0
  81. aisc/domain/models.py +674 -0
  82. aisc/domain/toolchain.py +72 -0
  83. aisc/domain/web_services.py +355 -0
  84. aisc/domain/workspace_lease.py +139 -0
  85. aisc/schemas/__init__.py +1 -0
  86. aisc/schemas/config_schema.py +145 -0
  87. aisc_cli-0.1.0.dist-info/METADATA +420 -0
  88. aisc_cli-0.1.0.dist-info/RECORD +92 -0
  89. aisc_cli-0.1.0.dist-info/WHEEL +5 -0
  90. aisc_cli-0.1.0.dist-info/entry_points.txt +2 -0
  91. aisc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  92. 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")