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/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
aisc/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""AISC CLI — Super Claude workstation management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
#: PyPI distribution name. The import package and console script stay
|
|
10
|
+
#: ``aisc``; only the dist name differs (guide D-1 / decisions D-2 — the
|
|
11
|
+
#: bare ``aisc`` name is a dormant third-party PyPI package since 2019,
|
|
12
|
+
#: and a stale install of it would answer metadata queries with a
|
|
13
|
+
#: stranger's version).
|
|
14
|
+
DIST_NAME = "aisc-cli"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_version() -> str:
|
|
18
|
+
"""Resolve the project version from the canonical ``VERSION`` file."""
|
|
19
|
+
candidates = []
|
|
20
|
+
frozen_root = getattr(sys, "_MEIPASS", None)
|
|
21
|
+
if frozen_root:
|
|
22
|
+
candidates.append(Path(frozen_root) / "VERSION")
|
|
23
|
+
|
|
24
|
+
candidates.extend(
|
|
25
|
+
(
|
|
26
|
+
# A2: VERSION is package data — with_name always hits now; the
|
|
27
|
+
# repo-root candidate (parents[2]/VERSION) died with the move.
|
|
28
|
+
Path(__file__).with_name("VERSION"),
|
|
29
|
+
# Legacy installs from the data-files era (<sys.prefix>/aisc/).
|
|
30
|
+
Path(sys.prefix) / "aisc" / "VERSION",
|
|
31
|
+
Path(sys.executable).resolve().parent / "aisc-bundle" / "VERSION",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for version_file in candidates:
|
|
36
|
+
try:
|
|
37
|
+
version = version_file.read_text(encoding="utf-8").splitlines()[0].strip()
|
|
38
|
+
except (OSError, IndexError):
|
|
39
|
+
continue
|
|
40
|
+
if version:
|
|
41
|
+
return version
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return metadata.version(DIST_NAME)
|
|
45
|
+
except metadata.PackageNotFoundError:
|
|
46
|
+
return "0+unknown"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__version__ = _read_version()
|
aisc/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters — I/O and external system interfaces."""
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Safe config file reader — platform dispatch (S5.2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
MAX_FILE_BYTES = 16 * 1024
|
|
11
|
+
MAX_JSON_DEPTH = 20
|
|
12
|
+
MAX_JSON_NODES = 2000
|
|
13
|
+
MAX_JSON_STRING_BYTES = 8192
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReadError(Exception):
|
|
17
|
+
def __init__(self, code: str, message: str):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.code = code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Platform dispatch
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def safe_read_config_bytes(file_path: Path) -> bytes:
|
|
27
|
+
if os.name == "nt":
|
|
28
|
+
from aisc.adapters.windows_config_reader import _win_safe_read
|
|
29
|
+
return _win_safe_read(file_path)
|
|
30
|
+
return _posix_safe_read(file_path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Unified validators
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def check_root_exists(root_path: str) -> None:
|
|
38
|
+
"""Verify *root_path* exists and is a directory.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileNotFoundError — root does not exist at all
|
|
42
|
+
PermissionError — permission denied
|
|
43
|
+
ReadError — exists but is symlink / reparse / not a directory
|
|
44
|
+
OSError — general I/O error
|
|
45
|
+
|
|
46
|
+
No second os.path.isdir/lexists fallback needed after this call.
|
|
47
|
+
"""
|
|
48
|
+
if os.name == "nt":
|
|
49
|
+
from aisc.adapters.windows_config_reader import (
|
|
50
|
+
_get_backend, _win_open_component,
|
|
51
|
+
FILE_READ_ATTRIBUTES, SHARE_RWD,
|
|
52
|
+
FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
|
|
53
|
+
)
|
|
54
|
+
back = _get_backend()
|
|
55
|
+
flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS
|
|
56
|
+
_win_open_component(root_path, back, FILE_READ_ATTRIBUTES, SHARE_RWD, flags)
|
|
57
|
+
return # success
|
|
58
|
+
# POSIX
|
|
59
|
+
try:
|
|
60
|
+
st = os.lstat(root_path)
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
raise
|
|
63
|
+
except PermissionError:
|
|
64
|
+
raise
|
|
65
|
+
except OSError:
|
|
66
|
+
raise OSError("Cannot access config root")
|
|
67
|
+
if stat.S_ISLNK(st.st_mode):
|
|
68
|
+
raise ReadError("structural_error", "Config root is a symlink")
|
|
69
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
70
|
+
raise ReadError("structural_error", "Config root is not a directory")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_dir_component(parent_path: str, name: str) -> None:
|
|
74
|
+
"""Verify *parent_path/name* is a directory, not a reparse point.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
FileNotFoundError — component does not exist
|
|
78
|
+
PermissionError — permission denied
|
|
79
|
+
ReadError — exists but reparse / not a directory
|
|
80
|
+
OSError — general error
|
|
81
|
+
"""
|
|
82
|
+
full_path = os.path.join(parent_path, name)
|
|
83
|
+
if os.name == "nt":
|
|
84
|
+
from aisc.adapters.windows_config_reader import (
|
|
85
|
+
_get_backend, _win_open_component,
|
|
86
|
+
FILE_READ_ATTRIBUTES, SHARE_RWD,
|
|
87
|
+
FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
|
|
88
|
+
)
|
|
89
|
+
back = _get_backend()
|
|
90
|
+
flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS
|
|
91
|
+
_win_open_component(full_path, back, FILE_READ_ATTRIBUTES, SHARE_RWD, flags)
|
|
92
|
+
return
|
|
93
|
+
# POSIX
|
|
94
|
+
try:
|
|
95
|
+
st = os.lstat(full_path)
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
raise
|
|
98
|
+
except PermissionError:
|
|
99
|
+
raise
|
|
100
|
+
except OSError:
|
|
101
|
+
raise OSError("Cannot access config component")
|
|
102
|
+
if stat.S_ISLNK(st.st_mode):
|
|
103
|
+
raise ReadError("structural_error", "Config component is a symlink")
|
|
104
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
105
|
+
raise ReadError("structural_error", "Config component is not a directory")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# POSIX reader
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _posix_safe_read(file_path: Path) -> bytes:
|
|
113
|
+
fp = str(file_path)
|
|
114
|
+
try:
|
|
115
|
+
st = os.lstat(fp)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
raise
|
|
118
|
+
except PermissionError:
|
|
119
|
+
raise
|
|
120
|
+
except OSError:
|
|
121
|
+
raise OSError("Cannot access config file")
|
|
122
|
+
if stat.S_ISLNK(st.st_mode):
|
|
123
|
+
raise ReadError("structural_error", "Config file is a symlink")
|
|
124
|
+
if stat.S_ISDIR(st.st_mode):
|
|
125
|
+
raise ReadError("structural_error", "Config file is a directory")
|
|
126
|
+
if not stat.S_ISREG(st.st_mode):
|
|
127
|
+
raise ReadError("structural_error", "Config file is not a regular file")
|
|
128
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
129
|
+
try:
|
|
130
|
+
fd = os.open(fp, flags)
|
|
131
|
+
except PermissionError:
|
|
132
|
+
raise
|
|
133
|
+
except FileNotFoundError:
|
|
134
|
+
raise
|
|
135
|
+
except OSError:
|
|
136
|
+
raise OSError("Cannot open config file")
|
|
137
|
+
primary = None
|
|
138
|
+
try:
|
|
139
|
+
fd_st = os.fstat(fd)
|
|
140
|
+
if not stat.S_ISREG(fd_st.st_mode):
|
|
141
|
+
raise ReadError("structural_error", "Config file is not a regular file")
|
|
142
|
+
chunks = []
|
|
143
|
+
total = 0
|
|
144
|
+
while total <= MAX_FILE_BYTES:
|
|
145
|
+
try:
|
|
146
|
+
chunk = os.read(fd, 4096)
|
|
147
|
+
except PermissionError:
|
|
148
|
+
raise
|
|
149
|
+
except OSError:
|
|
150
|
+
raise OSError("Cannot read config file")
|
|
151
|
+
if not chunk:
|
|
152
|
+
break
|
|
153
|
+
chunks.append(chunk)
|
|
154
|
+
total += len(chunk)
|
|
155
|
+
if total > MAX_FILE_BYTES:
|
|
156
|
+
raise ReadError("structural_error", "Config file exceeds size limit")
|
|
157
|
+
return b"".join(chunks)
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
primary = exc
|
|
160
|
+
raise
|
|
161
|
+
finally:
|
|
162
|
+
_close_fd(fd, primary)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _close_fd(fd: int, primary: Exception | None) -> None:
|
|
166
|
+
try:
|
|
167
|
+
os.close(fd)
|
|
168
|
+
except OSError:
|
|
169
|
+
if primary is None:
|
|
170
|
+
raise OSError("Failed to close file descriptor")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# JSON parser
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def _walk_iterative(obj, _path="$"):
|
|
178
|
+
from collections import deque
|
|
179
|
+
q = deque([(obj, "$", 1)]); nc = 0
|
|
180
|
+
while q:
|
|
181
|
+
item, path, depth = q.popleft(); nc += 1
|
|
182
|
+
if nc > MAX_JSON_NODES: raise ValueError("JSON node limit exceeded")
|
|
183
|
+
if depth > MAX_JSON_DEPTH: raise ValueError("JSON nesting too deep")
|
|
184
|
+
if isinstance(item, dict):
|
|
185
|
+
for k, v in item.items():
|
|
186
|
+
if isinstance(k, str) and len(k.encode()) > MAX_JSON_STRING_BYTES:
|
|
187
|
+
raise ValueError("JSON key too long")
|
|
188
|
+
_cs(v)
|
|
189
|
+
if isinstance(v, (dict,list)): q.append((v,f"{path}.{k}" if path!="$" else f"$.{k}",depth+1))
|
|
190
|
+
else: nc += 1
|
|
191
|
+
elif isinstance(item, list):
|
|
192
|
+
for i, v in enumerate(item):
|
|
193
|
+
_cs(v)
|
|
194
|
+
if isinstance(v, (dict,list)): q.append((v,f"{path}[{i}]",depth+1))
|
|
195
|
+
else: nc += 1
|
|
196
|
+
if nc > MAX_JSON_NODES: raise ValueError("JSON node limit exceeded")
|
|
197
|
+
|
|
198
|
+
def _cs(v):
|
|
199
|
+
if isinstance(v,str) and len(v.encode())>MAX_JSON_STRING_BYTES: raise ValueError("JSON string too long")
|
|
200
|
+
|
|
201
|
+
def parse_config_json(raw: bytes) -> dict:
|
|
202
|
+
text = raw.decode("utf-8")
|
|
203
|
+
if len(text) > MAX_FILE_BYTES*4: raise ValueError("Config text too large")
|
|
204
|
+
def _ph(pairs):
|
|
205
|
+
seen=set()
|
|
206
|
+
for k,v in pairs:
|
|
207
|
+
if k in seen: raise ValueError("Duplicate key in JSON object")
|
|
208
|
+
seen.add(k)
|
|
209
|
+
if isinstance(k,str) and len(k.encode())>MAX_JSON_STRING_BYTES:
|
|
210
|
+
raise ValueError("JSON key too long")
|
|
211
|
+
return dict(pairs)
|
|
212
|
+
r = _json.loads(text, object_pairs_hook=_ph)
|
|
213
|
+
if not isinstance(r, dict): raise ValueError("Config must be a JSON object")
|
|
214
|
+
_walk_iterative(r)
|
|
215
|
+
return r
|