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,72 @@
|
|
|
1
|
+
"""Conversation commands implementation (v2.1.8 T3).
|
|
2
|
+
|
|
3
|
+
Implements ``aisc conversation`` subcommands: list, preflight.
|
|
4
|
+
Per docs/plans/2.1.8-dev-plans/01-design.md §1b. Both are captured,
|
|
5
|
+
read-only commands — ``conversation resume`` deliberately does NOT exist
|
|
6
|
+
as a CLI command (方案 B 冻结): resume is the Workbench two-call
|
|
7
|
+
orchestration (preflight IPC + open_session --resume-id).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
from aisc.application.conversation import (
|
|
15
|
+
delete_conversation,
|
|
16
|
+
list_conversations,
|
|
17
|
+
preflight_conversation,
|
|
18
|
+
rename_conversation,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_conversation_list(workspace: str) -> Dict[str, Any]:
|
|
23
|
+
"""Execute ``aisc conversation list`` per design §1b."""
|
|
24
|
+
return list_conversations(workspace)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_conversation_preflight(workspace: str, conversation_id: str,
|
|
28
|
+
agent: str) -> Dict[str, Any]:
|
|
29
|
+
"""Execute ``aisc conversation preflight`` per design §2."""
|
|
30
|
+
return preflight_conversation(workspace, conversation_id, agent)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_conversation_delete(workspace: str, conversation_id: str,
|
|
34
|
+
agent: str) -> Dict[str, Any]:
|
|
35
|
+
"""Execute ``aisc conversation delete`` (v2.1.8 T4 手测反馈 #4)."""
|
|
36
|
+
return delete_conversation(workspace, conversation_id, agent)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_conversation_rename(workspace: str, conversation_id: str, agent: str,
|
|
40
|
+
title: str) -> Dict[str, Any]:
|
|
41
|
+
"""Execute ``aisc conversation rename`` (v2.1.8 T4 手测反馈 #2)."""
|
|
42
|
+
return rename_conversation(workspace, conversation_id, agent, title)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_conversation_text(subcommand: str, data: Any, errors: list) -> None:
|
|
46
|
+
"""Minimal human-readable output for ``aisc conversation`` text mode."""
|
|
47
|
+
if errors:
|
|
48
|
+
for e in errors:
|
|
49
|
+
print(f"Error: {e.get('message', '')}")
|
|
50
|
+
return
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
return
|
|
53
|
+
if subcommand == "list":
|
|
54
|
+
conversations = data.get("conversations", [])
|
|
55
|
+
if not conversations:
|
|
56
|
+
print("(no conversations)")
|
|
57
|
+
return
|
|
58
|
+
for c in conversations:
|
|
59
|
+
line = (f"{c.get('conversation_id', '')} agent={c.get('agent', '')} "
|
|
60
|
+
f"{c.get('last_at') or '-'} {c.get('title', '')}")
|
|
61
|
+
if not c.get("resumable", True):
|
|
62
|
+
line += f" [unresumable: {c.get('unavailable_reason', '')}]"
|
|
63
|
+
print(line)
|
|
64
|
+
elif subcommand == "preflight":
|
|
65
|
+
print(f"preflight ok: {data.get('conversation_id', '')} "
|
|
66
|
+
f"agent={data.get('agent', '')}")
|
|
67
|
+
elif subcommand == "delete":
|
|
68
|
+
print(f"deleted: {data.get('conversation_id', '')} "
|
|
69
|
+
f"agent={data.get('agent', '')}")
|
|
70
|
+
elif subcommand == "rename":
|
|
71
|
+
print(f"renamed: {data.get('conversation_id', '')} "
|
|
72
|
+
f"agent={data.get('agent', '')} title={data.get('title', '')}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""``aisc data-root`` commands (Stage 7, 7d) — doctor / migrate / rollback.
|
|
2
|
+
|
|
3
|
+
Non-interactive by contract (03-ux-flow): conflicts and unconsented unknowns
|
|
4
|
+
exit non-zero via CliError instead of guessing. All output is
|
|
5
|
+
JSON-serializable for the aisc.cli/v1 envelope; text mode prints a short
|
|
6
|
+
summary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from aisc.adapters.data_root_store import DataRootStore
|
|
15
|
+
from aisc.application.data_migration import MigrationExecutor
|
|
16
|
+
from aisc.application.data_root import DataRootResolver
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _executor(workspace: Optional[str]) -> MigrationExecutor:
|
|
20
|
+
ws = Path(workspace).resolve() if workspace else Path.cwd()
|
|
21
|
+
resolved = DataRootResolver().resolve(ws)
|
|
22
|
+
return MigrationExecutor(ws, resolved, store=DataRootStore(resolved))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_data_root_doctor(workspace: Optional[str] = None) -> Dict[str, Any]:
|
|
26
|
+
return _executor(workspace).doctor()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_data_root_migrate(
|
|
30
|
+
workspace: Optional[str] = None,
|
|
31
|
+
*,
|
|
32
|
+
dry_run: bool = False,
|
|
33
|
+
quarantine_unknown: bool = False,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
executor = _executor(workspace)
|
|
36
|
+
if dry_run:
|
|
37
|
+
return executor.dry_run()
|
|
38
|
+
return executor.migrate(quarantine_unknown=quarantine_unknown).to_dict()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_data_root_rollback(
|
|
42
|
+
workspace: Optional[str] = None,
|
|
43
|
+
manifest: Optional[str] = None,
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
return _executor(workspace).rollback(
|
|
46
|
+
manifest_path=Path(manifest) if manifest else None
|
|
47
|
+
).to_dict()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_data_root_text(data: Any) -> None:
|
|
51
|
+
"""Minimal human-readable output for text mode."""
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
return
|
|
54
|
+
if "legacy" in data and "data_root" in data:
|
|
55
|
+
root = data["data_root"]
|
|
56
|
+
print(f"data root : {root.get('root', '')} ({root.get('origin')})")
|
|
57
|
+
print(f"writable : {root.get('writable')}")
|
|
58
|
+
print(f"ws hash : {root.get('workspace_hash', '')}")
|
|
59
|
+
legacy = data.get("legacy") or {}
|
|
60
|
+
counts = legacy.get("counts") or {}
|
|
61
|
+
print("legacy : "
|
|
62
|
+
f"owned={counts.get('owned', 0)} transient={counts.get('transient', 0)} "
|
|
63
|
+
f"unknown={counts.get('unknown', 0)} conflict={counts.get('conflict', 0)}")
|
|
64
|
+
pending = data.get("pending_manifest")
|
|
65
|
+
if pending:
|
|
66
|
+
print(f"manifest : {pending}")
|
|
67
|
+
return
|
|
68
|
+
if "plan" in data:
|
|
69
|
+
print(f"dry-run : {data['plan'].get('copy_count', 0)} file(s) to copy, "
|
|
70
|
+
f"~{data['plan'].get('owned_bytes', 0)} bytes")
|
|
71
|
+
for conflict in data.get("conflicts", []):
|
|
72
|
+
print(f"CONFLICT : {conflict}")
|
|
73
|
+
for unknown in data.get("unknowns", []):
|
|
74
|
+
print(f"UNKNOWN : {unknown}")
|
|
75
|
+
return
|
|
76
|
+
outcome = data.get("outcome")
|
|
77
|
+
if outcome == "rolled_back":
|
|
78
|
+
print(f"rolled back: removed={data.get('removed', 0)} "
|
|
79
|
+
f"kept={data.get('kept', 0)} restored={data.get('restored', 0)}")
|
|
80
|
+
else:
|
|
81
|
+
print(f"migration {outcome}: copied={data.get('copied', 0)} "
|
|
82
|
+
f"skipped={data.get('skipped', 0)} quarantined={data.get('quarantined', 0)}")
|
|
83
|
+
if data.get("markers"):
|
|
84
|
+
print(f"markers : {', '.join(data['markers'])}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""``aisc logs`` command layer (lifecycle-logging P2).
|
|
2
|
+
|
|
3
|
+
Read-only views over the shared JSONL timeline
|
|
4
|
+
(``<data root>/logs/aisc.log``): `show` returns the recent event tail
|
|
5
|
+
(filtered by writer), `path` prints the file location. Lines are secret-free
|
|
6
|
+
by construction (P1's allowlisted schema) — this layer adds no redaction
|
|
7
|
+
because there is nothing sensitive to redact.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
from aisc.applog import log_file_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_logs_show(args: Any) -> Dict[str, Any]:
|
|
19
|
+
lines = int(getattr(args, "lines", 200) or 200)
|
|
20
|
+
source = getattr(args, "source", "all") or "all"
|
|
21
|
+
path = log_file_path()
|
|
22
|
+
events: List[Dict[str, Any]] = []
|
|
23
|
+
if path is not None and path.exists():
|
|
24
|
+
# Current file only — rotated (.1/.2) stay cold history. The file is
|
|
25
|
+
# size-capped (≤2MB) so a full read is cheap.
|
|
26
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
27
|
+
for line in raw.splitlines():
|
|
28
|
+
if not line.strip():
|
|
29
|
+
continue
|
|
30
|
+
try:
|
|
31
|
+
rec = json.loads(line)
|
|
32
|
+
except ValueError:
|
|
33
|
+
continue # a torn line (crash mid-append) never breaks the view
|
|
34
|
+
if not isinstance(rec, dict):
|
|
35
|
+
continue
|
|
36
|
+
if source != "all" and rec.get("source") != source:
|
|
37
|
+
continue
|
|
38
|
+
events.append(rec)
|
|
39
|
+
return {
|
|
40
|
+
"path": str(path) if path is not None else None,
|
|
41
|
+
"source": source,
|
|
42
|
+
"returned": len(events[-lines:]),
|
|
43
|
+
"total_matched": len(events),
|
|
44
|
+
"lines": events[-lines:],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_logs_path(args: Any) -> Dict[str, Any]:
|
|
49
|
+
path = log_file_path()
|
|
50
|
+
return {
|
|
51
|
+
"path": str(path) if path is not None else None,
|
|
52
|
+
"exists": bool(path is not None and path.exists()),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_logs_text(data: Dict[str, Any], is_show: bool) -> None:
|
|
57
|
+
"""Human-readable output."""
|
|
58
|
+
if not isinstance(data, dict):
|
|
59
|
+
return
|
|
60
|
+
path = data.get("path")
|
|
61
|
+
if not is_show:
|
|
62
|
+
print(path or "(log location unresolvable — data root error)")
|
|
63
|
+
return
|
|
64
|
+
if path is None:
|
|
65
|
+
print("(log location unresolvable — data root error)")
|
|
66
|
+
return
|
|
67
|
+
lines = data.get("lines") or []
|
|
68
|
+
if not lines:
|
|
69
|
+
print(f"No events recorded yet ({path})")
|
|
70
|
+
return
|
|
71
|
+
for rec in lines:
|
|
72
|
+
ts = str(rec.get("ts", ""))[:19]
|
|
73
|
+
level = str(rec.get("level", "?")).upper()
|
|
74
|
+
src = rec.get("source", "?")
|
|
75
|
+
event = rec.get("event", "?")
|
|
76
|
+
extras = []
|
|
77
|
+
if rec.get("run_id"):
|
|
78
|
+
extras.append(f"run={str(rec['run_id'])[:13]}")
|
|
79
|
+
for key in ("action", "command", "phase", "container", "outcome",
|
|
80
|
+
"exit_code", "duration_ms", "error_code", "state",
|
|
81
|
+
"detail"):
|
|
82
|
+
if key in rec and rec[key] is not None:
|
|
83
|
+
extras.append(f"{key}={rec[key]}")
|
|
84
|
+
suffix = " ".join(extras)
|
|
85
|
+
print(f"{ts} {level:<5} {src:<3} {event} {suffix}".rstrip())
|
|
86
|
+
shown = data.get("returned", len(lines))
|
|
87
|
+
total = data.get("total_matched", len(lines))
|
|
88
|
+
if shown < total:
|
|
89
|
+
print(f"-- showing {shown} of {total} matched events ({path})")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""``aisc network subscription`` command layer (IDEA-2, 2b).
|
|
2
|
+
|
|
3
|
+
Thin wrappers over ``aisc.application.network_subscription``. The
|
|
4
|
+
subscription URL is a credential: it rides stdin (``import`` reads one URL
|
|
5
|
+
line; ``import-file`` reads the full subscription content), never argv.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
from aisc.domain.models import CliError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _read_stdin_text(*, what: str) -> str:
|
|
17
|
+
raw = sys.stdin.read()
|
|
18
|
+
if not raw.strip():
|
|
19
|
+
raise CliError(
|
|
20
|
+
message=f"{what} on stdin is required (secrets never ride argv)",
|
|
21
|
+
exit_code=2,
|
|
22
|
+
error_code="AISC_ERR_USAGE",
|
|
23
|
+
)
|
|
24
|
+
return raw
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_network_subscription_import(args: Any) -> Dict[str, Any]:
|
|
28
|
+
from aisc.application.network_subscription import import_subscription
|
|
29
|
+
|
|
30
|
+
url = _read_stdin_text(what="a subscription URL").strip()
|
|
31
|
+
return import_subscription(url)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_network_subscription_import_file(args: Any) -> Dict[str, Any]:
|
|
35
|
+
from aisc.application.network_subscription import import_subscription_content
|
|
36
|
+
|
|
37
|
+
content = sys.stdin.buffer.read()
|
|
38
|
+
return import_subscription_content(content)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_network_subscription_store_downloaded(args: Any) -> Dict[str, Any]:
|
|
42
|
+
"""挂账①: stdin carries a JSON document {url, content_b64, userinfo} —
|
|
43
|
+
the body of a Rust-side (reqwest) download. b64 keeps the payload
|
|
44
|
+
byte-exact through the text stdin channel."""
|
|
45
|
+
import base64
|
|
46
|
+
import json
|
|
47
|
+
|
|
48
|
+
from aisc.application.network_subscription import store_downloaded
|
|
49
|
+
|
|
50
|
+
raw = sys.stdin.read()
|
|
51
|
+
try:
|
|
52
|
+
request = json.loads(raw) if raw.strip() else {}
|
|
53
|
+
except json.JSONDecodeError as exc:
|
|
54
|
+
raise CliError(
|
|
55
|
+
message=f"store-downloaded request is not valid JSON: {exc}",
|
|
56
|
+
exit_code=2,
|
|
57
|
+
error_code="AISC_ERR_USAGE",
|
|
58
|
+
) from exc
|
|
59
|
+
if not isinstance(request, dict):
|
|
60
|
+
raise CliError(
|
|
61
|
+
message="store-downloaded request must be a JSON object",
|
|
62
|
+
exit_code=2,
|
|
63
|
+
error_code="AISC_ERR_USAGE",
|
|
64
|
+
)
|
|
65
|
+
b64 = str(request.get("content_b64") or "")
|
|
66
|
+
try:
|
|
67
|
+
content = base64.b64decode(b64, validate=True) if b64 else b""
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
raise CliError(
|
|
70
|
+
message=f"content_b64 is not valid base64: {exc}",
|
|
71
|
+
exit_code=2,
|
|
72
|
+
error_code="AISC_ERR_USAGE",
|
|
73
|
+
) from exc
|
|
74
|
+
url = request.get("url")
|
|
75
|
+
userinfo = request.get("userinfo")
|
|
76
|
+
return store_downloaded(
|
|
77
|
+
str(url) if isinstance(url, str) and url else None,
|
|
78
|
+
content,
|
|
79
|
+
str(userinfo) if isinstance(userinfo, str) else "",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_network_subscription_refresh(args: Any) -> Dict[str, Any]:
|
|
84
|
+
from aisc.application.network_subscription import refresh_subscription
|
|
85
|
+
|
|
86
|
+
return refresh_subscription()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_network_subscription_show(args: Any) -> Dict[str, Any]:
|
|
90
|
+
from aisc.application.network_subscription import show_subscription
|
|
91
|
+
|
|
92
|
+
return show_subscription()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_network_subscription_clear(args: Any) -> Dict[str, Any]:
|
|
96
|
+
from aisc.application.network_subscription import clear_subscription
|
|
97
|
+
|
|
98
|
+
if not getattr(args, "confirm", False):
|
|
99
|
+
raise CliError(
|
|
100
|
+
message="clear requires --confirm",
|
|
101
|
+
exit_code=2,
|
|
102
|
+
error_code="AISC_ERR_USAGE",
|
|
103
|
+
)
|
|
104
|
+
return clear_subscription()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def print_network_subscription_text(data: Dict[str, Any]) -> None:
|
|
108
|
+
"""Human-readable output (secret-free: masked URL only)."""
|
|
109
|
+
if not isinstance(data, dict):
|
|
110
|
+
return
|
|
111
|
+
if not data.get("configured"):
|
|
112
|
+
print("No subscription configured.")
|
|
113
|
+
print("Import one: aisc network subscription import (URL on stdin)")
|
|
114
|
+
print(" or: aisc network subscription import-file (content on stdin)")
|
|
115
|
+
return
|
|
116
|
+
print(f"Source: {data.get('source') or 'manual'}")
|
|
117
|
+
print(f"URL: {data.get('url_masked') or '(manually imported)'}")
|
|
118
|
+
print(f"Updated: {data.get('fetched_at') or '(unknown)'}")
|
|
119
|
+
print(f"Config: {data.get('config_path') or ''} ({data.get('config_sha256') or '?'})")
|
|
120
|
+
userinfo = data.get("userinfo")
|
|
121
|
+
if isinstance(userinfo, dict) and userinfo:
|
|
122
|
+
total = userinfo.get("total")
|
|
123
|
+
used = userinfo.get("upload", 0) + userinfo.get("download", 0)
|
|
124
|
+
if total:
|
|
125
|
+
pct = min(100.0, used * 100.0 / total)
|
|
126
|
+
print(f"Usage: {used} / {total} bytes ({pct:.1f}%)")
|
|
127
|
+
else:
|
|
128
|
+
print(f"Usage: {used} bytes (unlimited plan)")
|
|
129
|
+
if "expire" in userinfo:
|
|
130
|
+
print(f"Expires: epoch {userinfo['expire']}")
|
|
131
|
+
else:
|
|
132
|
+
print("Usage: (subscription provided no usage header)")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""CLI command implementations for ``aisc profile list`` and ``aisc profile show``.
|
|
2
|
+
|
|
3
|
+
Read-only — returns built-in safe/unsafe profiles only.
|
|
4
|
+
No user-defined profiles, no confirmation flow, no run integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from aisc.application.profile_service import (
|
|
12
|
+
run_profile_list, ProfileListResult,
|
|
13
|
+
run_profile_show, ProfileShowResult,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_profile_list() -> ProfileListResult:
|
|
18
|
+
"""Execute ``profile list``."""
|
|
19
|
+
return run_profile_list()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_profile_show(name: str) -> ProfileShowResult:
|
|
23
|
+
"""Execute ``profile show NAME``."""
|
|
24
|
+
return run_profile_show(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_profile_list_text(data: Dict[str, Any]) -> None:
|
|
28
|
+
"""Print profile list in human-readable format."""
|
|
29
|
+
profiles = data.get("profiles", [])
|
|
30
|
+
print("=== Profile List ===")
|
|
31
|
+
print()
|
|
32
|
+
if not profiles:
|
|
33
|
+
print("No profiles found.")
|
|
34
|
+
else:
|
|
35
|
+
for p in profiles:
|
|
36
|
+
print(f" {p['name']:12s} dangerously_skip_permissions={p['dangerously_skip_permissions']}")
|
|
37
|
+
print(f" {p['description']}")
|
|
38
|
+
print()
|
|
39
|
+
print()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print_profile_show_text(data: Dict[str, Any]) -> None:
|
|
43
|
+
"""Print a single profile in human-readable format."""
|
|
44
|
+
if not data:
|
|
45
|
+
print("Profile not found.")
|
|
46
|
+
return
|
|
47
|
+
print("=== Profile Detail ===")
|
|
48
|
+
print()
|
|
49
|
+
for key in ("name", "description"):
|
|
50
|
+
print(f" {key:30s} {data.get(key, '')}")
|
|
51
|
+
print(f" dangerously_skip_permissions {data.get('dangerously_skip_permissions', False)}")
|
|
52
|
+
print()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Provider command implementation (Workbench Phase 0 S0.4).
|
|
2
|
+
|
|
3
|
+
Implements ``aisc provider current``. The legacy interactive ``set-key``
|
|
4
|
+
subcommand stays in ``aisc.cli.commands.container`` / ``main.py``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from aisc.adapters.docker_ import DockerExecutor
|
|
13
|
+
from aisc.adapters.docker_sdk_backed import default_executor
|
|
14
|
+
from aisc.application.provider import current_provider
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _resolve_workspace_and_registry(workspace: Optional[str]):
|
|
18
|
+
from aisc.application.data_root import workspace_state_dir
|
|
19
|
+
|
|
20
|
+
ws_path = Path(workspace).resolve() if workspace else Path.cwd()
|
|
21
|
+
return str(ws_path), workspace_state_dir(ws_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_provider_current(
|
|
25
|
+
runtime_id: str,
|
|
26
|
+
agent: str,
|
|
27
|
+
workspace: Optional[str] = None,
|
|
28
|
+
executor: Optional[DockerExecutor] = None,
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""Execute ``aisc provider current`` per contract §七."""
|
|
31
|
+
exec_ = executor or default_executor()
|
|
32
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
33
|
+
status = current_provider(
|
|
34
|
+
runtime_id=runtime_id,
|
|
35
|
+
agent=agent,
|
|
36
|
+
executor=exec_,
|
|
37
|
+
registry_root=reg_root,
|
|
38
|
+
)
|
|
39
|
+
return status.to_dict()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print_provider_current_text(data: Any) -> None:
|
|
43
|
+
"""Minimal human-readable output for ``aisc provider current`` text mode."""
|
|
44
|
+
if not isinstance(data, dict):
|
|
45
|
+
return
|
|
46
|
+
print(f"{data.get('agent', '')}: {data.get('provider_name', '')} "
|
|
47
|
+
f"({data.get('provider_id', '')})")
|
|
48
|
+
print(f" route_mode : {data.get('route_mode', '')}")
|
|
49
|
+
print(f" auth_status: {data.get('auth_status', '')}")
|