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,252 @@
|
|
|
1
|
+
"""Read-only legacy-layout scanner (Stage 7, 7c, DATA-02).
|
|
2
|
+
|
|
3
|
+
Walks the pre-Stage-7 AISC namespaces in a workspace and classifies every
|
|
4
|
+
file against the domain allowlist (``domain/data_migration.py``): owned /
|
|
5
|
+
transient / unknown / conflict, plus namespace-level ``foreign`` detection
|
|
6
|
+
(a ``.claude`` that predates AISC — no init marker — is the user's own and
|
|
7
|
+
is never migrated). Produces the migration manifest draft
|
|
8
|
+
(``aisc.data-migration/v1``) that 7d executes.
|
|
9
|
+
|
|
10
|
+
Safety properties:
|
|
11
|
+
- strictly read-only: no directory is created, no file touched;
|
|
12
|
+
- symlinks/reparse points inside a namespace are never followed — they
|
|
13
|
+
classify as ``unknown`` (quarantine candidate), so a link can neither
|
|
14
|
+
leak content out of the workspace nor smuggle a target in;
|
|
15
|
+
- ``owned``/``unknown`` files are hashed (SHA-256, streamed) because the
|
|
16
|
+
manifest commits to exact bytes; conflicts compare hashes, never mtimes.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import os
|
|
23
|
+
import stat
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Dict, List
|
|
27
|
+
|
|
28
|
+
from aisc.domain.data_migration import (
|
|
29
|
+
AISC_INIT_MARKERS,
|
|
30
|
+
ENTRY_CONFLICT,
|
|
31
|
+
ENTRY_OWNED,
|
|
32
|
+
ENTRY_TRANSIENT,
|
|
33
|
+
ENTRY_UNKNOWN,
|
|
34
|
+
MIGRATION_PROTOCOL,
|
|
35
|
+
MIGRATION_SCHEMA_VERSION,
|
|
36
|
+
NAMESPACE_AISC,
|
|
37
|
+
NAMESPACE_FOREIGN,
|
|
38
|
+
STATE_PREPARED,
|
|
39
|
+
MigrationEntry,
|
|
40
|
+
MigrationManifest,
|
|
41
|
+
classify,
|
|
42
|
+
)
|
|
43
|
+
from aisc.domain.data_root import ResolvedDataRoot
|
|
44
|
+
|
|
45
|
+
_HASH_CHUNK = 1 << 16
|
|
46
|
+
|
|
47
|
+
_FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 # windows.h (symlink, junction, …)
|
|
48
|
+
_IO_REPARSE_TAG_AF_UNIX = 0x80000023 # winsock AF_UNIX socket files
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_reparse(path: Path) -> bool:
|
|
52
|
+
"""True for symlinks AND junctions/OneDrive placeholders (Python's
|
|
53
|
+
``is_symlink`` misses junctions; the attribute bit does not)."""
|
|
54
|
+
try:
|
|
55
|
+
st = os.lstat(path)
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
if stat.S_ISLNK(st.st_mode):
|
|
59
|
+
return True
|
|
60
|
+
return bool(getattr(st, "st_file_attributes", 0) & _FILE_ATTRIBUTE_REPARSE_POINT)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _sha256_file(path: Path) -> str:
|
|
64
|
+
digest = hashlib.sha256()
|
|
65
|
+
with open(path, "rb") as f:
|
|
66
|
+
while True:
|
|
67
|
+
chunk = f.read(_HASH_CHUNK)
|
|
68
|
+
if not chunk:
|
|
69
|
+
break
|
|
70
|
+
digest.update(chunk)
|
|
71
|
+
return digest.hexdigest()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class NamespaceFinding:
|
|
76
|
+
"""Namespace-level report row (doctor / dry-run UX)."""
|
|
77
|
+
|
|
78
|
+
namespace: str # e.g. ".cc-switch" or ".local/state/cc-switch"
|
|
79
|
+
kind: str # NAMESPACE_AISC | NAMESPACE_FOREIGN
|
|
80
|
+
file_count: int = 0
|
|
81
|
+
total_bytes: int = 0
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> Dict[str, object]:
|
|
84
|
+
return {
|
|
85
|
+
"namespace": self.namespace,
|
|
86
|
+
"kind": self.kind,
|
|
87
|
+
"file_count": self.file_count,
|
|
88
|
+
"total_bytes": self.total_bytes,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class LegacyScan:
|
|
94
|
+
"""Full read-only scan result for one workspace."""
|
|
95
|
+
|
|
96
|
+
workspace: Path
|
|
97
|
+
resolved: ResolvedDataRoot
|
|
98
|
+
findings: List[NamespaceFinding] = field(default_factory=list)
|
|
99
|
+
entries: List[MigrationEntry] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
# -- rollups ------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def counts(self) -> Dict[str, int]:
|
|
104
|
+
out = dict.fromkeys(("owned", "transient", "unknown", "conflict"), 0)
|
|
105
|
+
for e in self.entries:
|
|
106
|
+
out[e.classification] = out.get(e.classification, 0) + 1
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
def owned_bytes(self) -> int:
|
|
110
|
+
return sum(e.size for e in self.entries if e.classification == ENTRY_OWNED)
|
|
111
|
+
|
|
112
|
+
def summary(self) -> Dict[str, object]:
|
|
113
|
+
"""Doctor/dry-run payload (counts and sizes only; paths stay in the
|
|
114
|
+
manifest the user explicitly exports)."""
|
|
115
|
+
return {
|
|
116
|
+
"schema": MIGRATION_PROTOCOL,
|
|
117
|
+
"workspace_hash": self.resolved.workspace_hash,
|
|
118
|
+
"namespaces": [f.to_dict() for f in self.findings],
|
|
119
|
+
"counts": self.counts(),
|
|
120
|
+
"owned_bytes": self.owned_bytes(),
|
|
121
|
+
"has_unknowns": any(e.classification == ENTRY_UNKNOWN for e in self.entries),
|
|
122
|
+
"has_conflicts": any(e.classification == ENTRY_CONFLICT for e in self.entries),
|
|
123
|
+
"has_foreign": any(f.kind == NAMESPACE_FOREIGN for f in self.findings),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def to_manifest(self) -> MigrationManifest:
|
|
127
|
+
"""Draft manifest: 7d persists it (state=prepared) before copying."""
|
|
128
|
+
return MigrationManifest(
|
|
129
|
+
schema=MIGRATION_PROTOCOL,
|
|
130
|
+
schema_version=MIGRATION_SCHEMA_VERSION,
|
|
131
|
+
workspace_hash=self.resolved.workspace_hash,
|
|
132
|
+
source=str(self.workspace.resolve()),
|
|
133
|
+
target=str(self.resolved.workspace_dir),
|
|
134
|
+
entries=list(self.entries),
|
|
135
|
+
state=STATE_PREPARED,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_aisc_initialized(workspace: Path) -> bool:
|
|
140
|
+
return any((workspace / m).exists() for m in AISC_INIT_MARKERS)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def scan_legacy_workspace(workspace: Path, resolved: ResolvedDataRoot) -> LegacyScan:
|
|
144
|
+
"""Scan *workspace* for legacy AISC namespaces. Read-only; missing
|
|
145
|
+
namespaces are simply absent from the findings."""
|
|
146
|
+
ws = Path(workspace)
|
|
147
|
+
scan = LegacyScan(workspace=ws, resolved=resolved)
|
|
148
|
+
aisc_initialized = _is_aisc_initialized(ws)
|
|
149
|
+
|
|
150
|
+
for namespace in (".aisc", ".claude", ".codex", ".cc-switch", ".local"):
|
|
151
|
+
ns_root = ws / namespace
|
|
152
|
+
if not ns_root.exists():
|
|
153
|
+
continue
|
|
154
|
+
finding = NamespaceFinding(namespace=namespace, kind=NAMESPACE_AISC)
|
|
155
|
+
if not aisc_initialized:
|
|
156
|
+
# None of the AISC init markers exist: these namespaces are the
|
|
157
|
+
# user's own (D7-03) — report, never migrate.
|
|
158
|
+
finding.kind = NAMESPACE_FOREIGN
|
|
159
|
+
finding.file_count, finding.total_bytes = _count_tree(ns_root)
|
|
160
|
+
scan.findings.append(finding)
|
|
161
|
+
continue
|
|
162
|
+
scan.findings.append(finding)
|
|
163
|
+
_scan_namespace(ws, namespace, ns_root, resolved, scan, finding)
|
|
164
|
+
|
|
165
|
+
scan.entries.sort(key=lambda e: e.relative)
|
|
166
|
+
return scan
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _scan_namespace(
|
|
170
|
+
ws: Path,
|
|
171
|
+
namespace: str,
|
|
172
|
+
ns_root: Path,
|
|
173
|
+
resolved: ResolvedDataRoot,
|
|
174
|
+
scan: LegacyScan,
|
|
175
|
+
finding: NamespaceFinding,
|
|
176
|
+
) -> None:
|
|
177
|
+
for dirpath, dirnames, filenames in os.walk(ns_root, followlinks=False):
|
|
178
|
+
# Never descend into symlinked/junctioned dirs (junctions are not
|
|
179
|
+
# ``is_symlink`` — check the reparse attribute too).
|
|
180
|
+
dirnames[:] = [d for d in dirnames if not _is_reparse(Path(dirpath) / d)]
|
|
181
|
+
for fname in filenames:
|
|
182
|
+
abs_path = Path(dirpath) / fname
|
|
183
|
+
rel_ns = abs_path.relative_to(ns_root).as_posix()
|
|
184
|
+
finding.file_count += 1
|
|
185
|
+
|
|
186
|
+
if _is_reparse(abs_path):
|
|
187
|
+
# Windows AF_UNIX sockets are reparse points (Python on
|
|
188
|
+
# Windows does not set S_IFSOCK — decode the tag); they are
|
|
189
|
+
# transient coordination files, not quarantine candidates.
|
|
190
|
+
try:
|
|
191
|
+
st = os.lstat(abs_path)
|
|
192
|
+
is_socket = stat.S_ISSOCK(st.st_mode) or (
|
|
193
|
+
getattr(st, "st_reparse_tag", 0) == _IO_REPARSE_TAG_AF_UNIX
|
|
194
|
+
)
|
|
195
|
+
if is_socket:
|
|
196
|
+
scan.entries.append(
|
|
197
|
+
MigrationEntry(relative=f"{namespace}/{rel_ns}",
|
|
198
|
+
classification=ENTRY_TRANSIENT)
|
|
199
|
+
)
|
|
200
|
+
continue
|
|
201
|
+
except OSError:
|
|
202
|
+
pass
|
|
203
|
+
# Never follow or stat the target (a symlink to a huge file
|
|
204
|
+
# must not inflate sizes); quarantine candidate.
|
|
205
|
+
scan.entries.append(
|
|
206
|
+
MigrationEntry(relative=f"{namespace}/{rel_ns}",
|
|
207
|
+
classification=ENTRY_UNKNOWN)
|
|
208
|
+
)
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
size = abs_path.stat().st_size
|
|
213
|
+
except OSError:
|
|
214
|
+
size = 0
|
|
215
|
+
finding.total_bytes += size
|
|
216
|
+
|
|
217
|
+
classification, target_rel = classify(namespace, rel_ns)
|
|
218
|
+
if classification == ENTRY_TRANSIENT:
|
|
219
|
+
scan.entries.append(
|
|
220
|
+
MigrationEntry(relative=f"{namespace}/{rel_ns}",
|
|
221
|
+
classification=ENTRY_TRANSIENT, size=size)
|
|
222
|
+
)
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
digest = _sha256_file(abs_path)
|
|
226
|
+
if classification == ENTRY_OWNED and target_rel:
|
|
227
|
+
target_abs = resolved.workspace_dir / Path(*target_rel.split("/"))
|
|
228
|
+
if target_abs.exists():
|
|
229
|
+
# Fail closed on any mismatch; identical bytes = already
|
|
230
|
+
# migrated (skipped by 7d, not re-copied).
|
|
231
|
+
classification = (
|
|
232
|
+
ENTRY_OWNED if _sha256_file(target_abs) == digest
|
|
233
|
+
else ENTRY_CONFLICT
|
|
234
|
+
)
|
|
235
|
+
scan.entries.append(
|
|
236
|
+
MigrationEntry(relative=f"{namespace}/{rel_ns}",
|
|
237
|
+
classification=classification,
|
|
238
|
+
sha256=digest, size=size, target=target_rel)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _count_tree(root: Path) -> tuple[int, int]:
|
|
243
|
+
files = 0
|
|
244
|
+
total = 0
|
|
245
|
+
for dirpath, _dirnames, filenames in os.walk(root, followlinks=False):
|
|
246
|
+
for fname in filenames:
|
|
247
|
+
files += 1
|
|
248
|
+
try:
|
|
249
|
+
total += (Path(dirpath) / fname).stat().st_size
|
|
250
|
+
except OSError:
|
|
251
|
+
pass
|
|
252
|
+
return files, total
|