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,74 @@
|
|
|
1
|
+
"""``aisc usage`` command layer (IDEA-2 2c).
|
|
2
|
+
|
|
3
|
+
Thin wrapper over ``aisc.application.usage`` — subscription status plus
|
|
4
|
+
per-provider token usage aggregated across all data-root workspaces.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_usage_overview(args: Any) -> Dict[str, Any]:
|
|
13
|
+
from aisc.application.usage import usage_overview
|
|
14
|
+
|
|
15
|
+
return usage_overview(
|
|
16
|
+
range_key=getattr(args, "range", "7d") or "7d",
|
|
17
|
+
workspace=getattr(args, "workspace", None),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fmt_tokens(n: int) -> str:
|
|
22
|
+
if n >= 1_000_000_000:
|
|
23
|
+
return f"{n / 1_000_000_000:.2f}B"
|
|
24
|
+
if n >= 1_000_000:
|
|
25
|
+
return f"{n / 1_000_000:.2f}M"
|
|
26
|
+
if n >= 1_000:
|
|
27
|
+
return f"{n / 1_000:.1f}k"
|
|
28
|
+
return str(n)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_usage_overview_text(data: Optional[Dict[str, Any]]) -> None:
|
|
32
|
+
"""Human-readable output (secret-free: masked subscription URL only)."""
|
|
33
|
+
if not isinstance(data, dict):
|
|
34
|
+
return
|
|
35
|
+
sub = data.get("subscription") or {}
|
|
36
|
+
if sub.get("configured"):
|
|
37
|
+
userinfo = sub.get("userinfo")
|
|
38
|
+
if isinstance(userinfo, dict):
|
|
39
|
+
total = userinfo.get("total")
|
|
40
|
+
used = userinfo.get("upload", 0) + userinfo.get("download", 0)
|
|
41
|
+
usage_bit = (f"{_fmt_tokens(used)} / {_fmt_tokens(total)} bytes"
|
|
42
|
+
if total else f"{_fmt_tokens(used)} bytes (unlimited)")
|
|
43
|
+
else:
|
|
44
|
+
# null userinfo = the source provided no usage header (manual
|
|
45
|
+
# content import, or a provider that omits subscription-userinfo)
|
|
46
|
+
usage_bit = "no usage info"
|
|
47
|
+
print(f"Subscription: {sub.get('url_masked') or '(manually imported)'}"
|
|
48
|
+
f" [{sub.get('source')}] {usage_bit}")
|
|
49
|
+
else:
|
|
50
|
+
print("Subscription: none configured")
|
|
51
|
+
|
|
52
|
+
print(f"Range: {data.get('range')}")
|
|
53
|
+
for ws in data.get("workspaces", []):
|
|
54
|
+
name = ws.get("workspace_path") or ws.get("workspace_hash") or "?"
|
|
55
|
+
try:
|
|
56
|
+
from pathlib import Path as _P
|
|
57
|
+
name = _P(str(name)).name or str(name)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
state = ("live" if ws.get("source") == "live"
|
|
61
|
+
else "cache" if ws.get("source") == "cache" else "no data")
|
|
62
|
+
print(f"\nWorkspace {name} [{state}]")
|
|
63
|
+
for p in ws.get("providers", []):
|
|
64
|
+
print(f" {p['app']:7} {p['provider_name']:24} "
|
|
65
|
+
f"req={p['requests']:>6} ok={p['success']:>6} "
|
|
66
|
+
f"tokens={_fmt_tokens(p['tokens_total']):>9} "
|
|
67
|
+
f"${p['cost_estimate']:.4f}")
|
|
68
|
+
if not ws.get("providers"):
|
|
69
|
+
print(" (no usage rows)")
|
|
70
|
+
|
|
71
|
+
totals = data.get("totals") or {}
|
|
72
|
+
print(f"\nTotals: requests={totals.get('requests', 0)} "
|
|
73
|
+
f"tokens={_fmt_tokens(totals.get('tokens_total', 0))} "
|
|
74
|
+
f"cost=${totals.get('cost_estimate', 0.0):.4f}")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Build configuration wizard for aisc build command.
|
|
2
|
+
|
|
3
|
+
Interactive wizard that prompts for:
|
|
4
|
+
- Image name/tag
|
|
5
|
+
- Proxy configuration
|
|
6
|
+
- Build cache options
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _prompt(message: str, default: str = "") -> str:
|
|
17
|
+
"""Prompt user for input with optional default."""
|
|
18
|
+
if default:
|
|
19
|
+
prompt_text = f"{message} [{default}]: "
|
|
20
|
+
else:
|
|
21
|
+
prompt_text = f"{message}: "
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
response = input(prompt_text).strip()
|
|
25
|
+
return response if response else default
|
|
26
|
+
except (EOFError, KeyboardInterrupt):
|
|
27
|
+
print("\nCancelled.", file=sys.stderr)
|
|
28
|
+
sys.exit(130)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _prompt_yes_no(message: str, default: bool = False) -> bool:
|
|
32
|
+
"""Prompt user for yes/no confirmation."""
|
|
33
|
+
default_str = "Y/n" if default else "y/N"
|
|
34
|
+
prompt_text = f"{message} [{default_str}]: "
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
response = input(prompt_text).strip().lower()
|
|
38
|
+
if not response:
|
|
39
|
+
return default
|
|
40
|
+
return response in ("y", "yes")
|
|
41
|
+
except (EOFError, KeyboardInterrupt):
|
|
42
|
+
print("\nCancelled.", file=sys.stderr)
|
|
43
|
+
sys.exit(130)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_build_wizard(
|
|
47
|
+
default_tag: str = "super-claude:latest",
|
|
48
|
+
aisc_root: Optional[Path] = None,
|
|
49
|
+
) -> Tuple[str, bool, bool, Optional[str]]:
|
|
50
|
+
"""Run interactive build configuration wizard.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
default_tag: Default image tag to suggest
|
|
54
|
+
aisc_root: AISC repository root for proxy config path
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (tag, no_cache, pull, proxy_config_path)
|
|
58
|
+
- tag: Docker image tag
|
|
59
|
+
- no_cache: Whether to disable build cache
|
|
60
|
+
- pull: Whether to pull base image
|
|
61
|
+
- proxy_config_path: Path to proxy config file, or None
|
|
62
|
+
"""
|
|
63
|
+
print("\n🚀 AISC Build Configuration Wizard")
|
|
64
|
+
print("=" * 50)
|
|
65
|
+
|
|
66
|
+
# 1. Image name/tag
|
|
67
|
+
print("\n📦 Docker Image Configuration")
|
|
68
|
+
tag = _prompt("Image tag", default=default_tag)
|
|
69
|
+
if ":" not in tag:
|
|
70
|
+
tag = f"{tag}:latest"
|
|
71
|
+
|
|
72
|
+
# 2. Build cache
|
|
73
|
+
print("\n🔧 Build Options")
|
|
74
|
+
no_cache = _prompt_yes_no("Disable Docker build cache?", default=False)
|
|
75
|
+
pull = _prompt_yes_no("Always pull base image?", default=False)
|
|
76
|
+
|
|
77
|
+
# 3. Proxy configuration
|
|
78
|
+
print("\n🌐 Proxy Configuration (for container network access)")
|
|
79
|
+
use_proxy = _prompt_yes_no("Configure proxy network?", default=False)
|
|
80
|
+
|
|
81
|
+
proxy_config_path: Optional[str] = None
|
|
82
|
+
if use_proxy:
|
|
83
|
+
print("\nProxy configuration options:")
|
|
84
|
+
print(" 1) Local file - Enter absolute path to config file")
|
|
85
|
+
print(" 2) URL - Enter subscription/config URL")
|
|
86
|
+
|
|
87
|
+
mode = _prompt("Select [1/2]", default="2")
|
|
88
|
+
|
|
89
|
+
if mode == "1":
|
|
90
|
+
# Local file → store as manually imported subscription content
|
|
91
|
+
path_input = _prompt("Local config file absolute path")
|
|
92
|
+
if path_input and Path(path_input).is_file():
|
|
93
|
+
from aisc.application.network_subscription import (
|
|
94
|
+
import_subscription_content,
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
snap = import_subscription_content(Path(path_input).read_bytes())
|
|
98
|
+
proxy_config_path = snap["config_path"]
|
|
99
|
+
print(f"✅ Config imported: {snap['config_path']}")
|
|
100
|
+
except Exception as e: # noqa: BLE001 - wizard stays chatty
|
|
101
|
+
print(f"❌ Import failed: {e}")
|
|
102
|
+
else:
|
|
103
|
+
print(f"⚠️ File not found: {path_input}, skipping proxy.")
|
|
104
|
+
else:
|
|
105
|
+
# URL download (IDEA-2: data-root subscription store; the clash
|
|
106
|
+
# UA, retries and userinfo capture live in the application layer)
|
|
107
|
+
url = _prompt("Config URL")
|
|
108
|
+
if url:
|
|
109
|
+
from aisc.application.network_subscription import import_subscription
|
|
110
|
+
from aisc.domain.models import CliError
|
|
111
|
+
|
|
112
|
+
print("⬇️ Downloading subscription...")
|
|
113
|
+
try:
|
|
114
|
+
snap = import_subscription(url)
|
|
115
|
+
proxy_config_path = snap["config_path"]
|
|
116
|
+
print(f"✅ Subscription imported: {snap['config_path']}")
|
|
117
|
+
if snap.get("userinfo"):
|
|
118
|
+
print(f"📊 Usage header captured: {snap['userinfo']}")
|
|
119
|
+
except CliError as e:
|
|
120
|
+
print(f"❌ Import failed: {e.message}")
|
|
121
|
+
|
|
122
|
+
print("\n" + "=" * 50)
|
|
123
|
+
print("Configuration Summary:")
|
|
124
|
+
print(f" Image tag: {tag}")
|
|
125
|
+
print(f" No cache: {no_cache}")
|
|
126
|
+
print(f" Pull base: {pull}")
|
|
127
|
+
print(f" Proxy config: {proxy_config_path or 'None (direct connection)'}")
|
|
128
|
+
print("=" * 50 + "\n")
|
|
129
|
+
|
|
130
|
+
return tag, no_cache, pull, proxy_config_path
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""F2-C addendum (user request 2026-09-09): ``aisc workspaces`` — the
|
|
2
|
+
machine-wide management view of CLI-activated workspaces.
|
|
3
|
+
|
|
4
|
+
Scans every workspace registry under the data root, joins the activation
|
|
5
|
+
history for aliases, and cross-references live Docker state. ``--stop``
|
|
6
|
+
sweeps the RUNNING CLI-owned activations (Workbench-managed runtimes are
|
|
7
|
+
never touched — the GUI owns their lifecycle).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from aisc.adapters.container_registry import (
|
|
16
|
+
gc as registry_gc,
|
|
17
|
+
list_containers,
|
|
18
|
+
unregister,
|
|
19
|
+
)
|
|
20
|
+
from aisc.adapters.docker_ import DockerExecutor, RealDockerExecutor
|
|
21
|
+
from aisc.application.data_root import DataRootResolver
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def registry_entries(
|
|
25
|
+
executor: Optional[DockerExecutor] = None,
|
|
26
|
+
data_root: Optional[str] = None,
|
|
27
|
+
) -> Tuple[List[Any], set]:
|
|
28
|
+
"""Every ``(name, meta)`` across all workspace registries (CLI and
|
|
29
|
+
Workbench-owned alike — the caller filters), plus the set of registry
|
|
30
|
+
DEFAULT pointers (the bare ``aisc claude`` target of each workspace —
|
|
31
|
+
r2 #B: stars must mark the pointer, not "same workspace").
|
|
32
|
+
|
|
33
|
+
Lazy-GCs each registry so stale records from deleted containers do not
|
|
34
|
+
surface. *data_root* (a dir containing ``workspaces/``) overrides the
|
|
35
|
+
resolved shared root — injection seam for tests and ``aisc ps``.
|
|
36
|
+
"""
|
|
37
|
+
from aisc.adapters.container_registry import get_default
|
|
38
|
+
|
|
39
|
+
base = Path(data_root) if data_root else DataRootResolver().resolve_shared_root()
|
|
40
|
+
shared = base / "workspaces"
|
|
41
|
+
out: List[Any] = []
|
|
42
|
+
defaults: set = set()
|
|
43
|
+
if not shared.is_dir():
|
|
44
|
+
return out, defaults
|
|
45
|
+
for reg_dir in sorted(shared.glob("*/runtime")):
|
|
46
|
+
if (reg_dir / "containers.json").is_file():
|
|
47
|
+
try:
|
|
48
|
+
registry_gc(reg_dir, executor)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
out.extend(list_containers(reg_dir).items())
|
|
52
|
+
try:
|
|
53
|
+
d = get_default(reg_dir)
|
|
54
|
+
except Exception:
|
|
55
|
+
d = ""
|
|
56
|
+
if d:
|
|
57
|
+
defaults.add(d)
|
|
58
|
+
return out, defaults
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def docker_states(executor: DockerExecutor) -> Tuple[Dict[str, str], bool]:
|
|
62
|
+
"""name → docker status string (one ``docker ps -a`` round trip).
|
|
63
|
+
|
|
64
|
+
Second element: whether docker answered at all. When False the map is
|
|
65
|
+
empty and callers must NOT read absence as "container gone".
|
|
66
|
+
"""
|
|
67
|
+
proc = executor.run_captured([
|
|
68
|
+
"ps", "-a", "--format", "{{.Names}}\t{{.Status}}",
|
|
69
|
+
])
|
|
70
|
+
states: Dict[str, str] = {}
|
|
71
|
+
if proc.exit_code != 0:
|
|
72
|
+
return states, False
|
|
73
|
+
for line in (proc.stdout or "").splitlines():
|
|
74
|
+
name, _, status = line.partition("\t")
|
|
75
|
+
if name:
|
|
76
|
+
states[name] = status
|
|
77
|
+
return states, True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_workspaces(
|
|
81
|
+
executor: Optional[DockerExecutor] = None,
|
|
82
|
+
) -> List[Dict[str, Any]]:
|
|
83
|
+
"""List every CLI-activated workspace with live Docker state."""
|
|
84
|
+
exec_ = executor or RealDockerExecutor()
|
|
85
|
+
from aisc.cli.commands.runs import list_runs
|
|
86
|
+
|
|
87
|
+
alias_by_path = {r.get("path", ""): r.get("alias", "") for r in list_runs()}
|
|
88
|
+
states, _docker_ok = docker_states(exec_)
|
|
89
|
+
entries, defaults = registry_entries(exec_)
|
|
90
|
+
|
|
91
|
+
rows: List[Dict[str, Any]] = []
|
|
92
|
+
for cname, meta in entries:
|
|
93
|
+
meta = meta or {}
|
|
94
|
+
if meta.get("owner") == "workbench":
|
|
95
|
+
continue
|
|
96
|
+
name = str(cname)
|
|
97
|
+
if not name:
|
|
98
|
+
continue
|
|
99
|
+
ws = str(meta.get("workspace", ""))
|
|
100
|
+
status = states.get(name, "")
|
|
101
|
+
rows.append({
|
|
102
|
+
"workspace": ws,
|
|
103
|
+
"alias": alias_by_path.get(ws, ""),
|
|
104
|
+
"label": str(meta.get("label", "")),
|
|
105
|
+
"container": name,
|
|
106
|
+
"image": str(meta.get("image", "")),
|
|
107
|
+
"status": status,
|
|
108
|
+
"running": status.startswith("Up"),
|
|
109
|
+
# r2 #E: which row a bare `aisc claude` actually lands in —
|
|
110
|
+
# the registry default pointer, not "any row of that workspace"
|
|
111
|
+
"active": name in defaults,
|
|
112
|
+
})
|
|
113
|
+
rows.sort(key=lambda r: (not r["running"], r["workspace"]))
|
|
114
|
+
return rows
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_workspaces_stop(
|
|
118
|
+
executor: Optional[DockerExecutor] = None,
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""Stop + remove every RUNNING CLI-owned activation (the batch sweep)."""
|
|
121
|
+
exec_ = executor or RealDockerExecutor()
|
|
122
|
+
from aisc.application.data_root import DataRootResolver
|
|
123
|
+
|
|
124
|
+
rows = [r for r in cmd_workspaces(executor=exec_) if r["running"]]
|
|
125
|
+
stopped: List[Dict[str, Any]] = []
|
|
126
|
+
shared = DataRootResolver().resolve_shared_root() / "workspaces"
|
|
127
|
+
for row in rows:
|
|
128
|
+
name = row["container"]
|
|
129
|
+
stop = exec_.run_captured(["stop", name], timeout=30.0)
|
|
130
|
+
rm = exec_.run_captured(["rm", "-f", name], timeout=30.0)
|
|
131
|
+
# unregister from its workspace registry (best-effort locate)
|
|
132
|
+
try:
|
|
133
|
+
for reg_dir in shared.glob("*/runtime"):
|
|
134
|
+
if (reg_dir / "containers.json").is_file():
|
|
135
|
+
unregister(reg_dir, name)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
stopped.append({
|
|
139
|
+
"workspace": row["workspace"],
|
|
140
|
+
"alias": row["alias"],
|
|
141
|
+
"container": name,
|
|
142
|
+
"exit_code": rm.exit_code if rm.exit_code != 0 else stop.exit_code,
|
|
143
|
+
})
|
|
144
|
+
return {"stopped": stopped, "running_found": len(rows)}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def print_workspaces_text(rows: List[Dict[str, Any]]) -> None:
|
|
148
|
+
"""Human table: alias, status, workspace (ACTIVE workspace starred)."""
|
|
149
|
+
if not rows:
|
|
150
|
+
print("当前没有 CLI 激活的工作区 — aisc run <路径> 开始")
|
|
151
|
+
return
|
|
152
|
+
width = max(len(r.get("alias", "") or "-")
|
|
153
|
+
+ (len(r.get("label", "")) + 4 if r.get("label") else 0)
|
|
154
|
+
for r in rows) + 2
|
|
155
|
+
for r in rows:
|
|
156
|
+
alias = r.get("alias", "") or "-"
|
|
157
|
+
if r.get("label"):
|
|
158
|
+
alias = f"{alias}[{r['label']}]" # bypass slot — distinguish
|
|
159
|
+
alias += " *" if r.get("active") else ""
|
|
160
|
+
status = r.get("status", "") or "(容器不存在)"
|
|
161
|
+
print(f" {alias:<{width}} {status:<22} {r['workspace']}")
|
|
162
|
+
print("批量停止运行中的: aisc workspaces --stop · 单个: aisc stop")
|