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,105 @@
|
|
|
1
|
+
"""Profile service — read-only ``profile list`` and ``profile show``.
|
|
2
|
+
|
|
3
|
+
Reads profiles from ~/.aisc/profiles.json with fallback to built-in definitions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Built-in profile definitions (fallback when profiles.json not found)
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
_BUILTIN_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
19
|
+
"safe": {
|
|
20
|
+
"name": "safe",
|
|
21
|
+
"description": "Secure defaults (default profile)",
|
|
22
|
+
"dangerously_skip_permissions": False,
|
|
23
|
+
},
|
|
24
|
+
"unsafe": {
|
|
25
|
+
"name": "unsafe",
|
|
26
|
+
"description": "Explicit dangerous permissions for trusted projects",
|
|
27
|
+
"dangerously_skip_permissions": True,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Profile loading from .aisc/profiles.json
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _profiles_path(home: Optional[str] = None) -> Path:
|
|
37
|
+
"""Return the path to profiles.json."""
|
|
38
|
+
home_path = Path(home).expanduser() if home is not None else Path.home()
|
|
39
|
+
return home_path / ".aisc" / "profiles.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_profiles(home: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
|
|
43
|
+
"""Load profiles from ~/.aisc/profiles.json, fallback to built-in."""
|
|
44
|
+
path = _profiles_path(home)
|
|
45
|
+
try:
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
return _BUILTIN_PROFILES
|
|
48
|
+
raw = path.read_bytes()
|
|
49
|
+
data = json.loads(raw.decode("utf-8"))
|
|
50
|
+
if not isinstance(data, dict) or not isinstance(data.get("profiles"), dict):
|
|
51
|
+
return _BUILTIN_PROFILES
|
|
52
|
+
return data["profiles"]
|
|
53
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
54
|
+
return _BUILTIN_PROFILES
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Result types
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ProfileListResult:
|
|
63
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
exit_code: int = 0
|
|
65
|
+
error_code: str = ""
|
|
66
|
+
error_message: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ProfileShowResult:
|
|
71
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
exit_code: int = 0
|
|
73
|
+
error_code: str = ""
|
|
74
|
+
error_message: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Service functions
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def run_profile_list(home: Optional[str] = None) -> ProfileListResult:
|
|
82
|
+
"""Return all profiles as a list."""
|
|
83
|
+
profiles_dict = _load_profiles(home)
|
|
84
|
+
profiles: List[Dict[str, Any]] = []
|
|
85
|
+
for key in sorted(profiles_dict.keys()):
|
|
86
|
+
profiles.append(dict(profiles_dict[key]))
|
|
87
|
+
|
|
88
|
+
data: Dict[str, Any] = {"profiles": profiles}
|
|
89
|
+
return ProfileListResult(data=data, exit_code=0)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_profile_show(name: str, home: Optional[str] = None) -> ProfileShowResult:
|
|
93
|
+
"""Return a single profile by name."""
|
|
94
|
+
profiles_dict = _load_profiles(home)
|
|
95
|
+
profile = profiles_dict.get(name)
|
|
96
|
+
if profile is None:
|
|
97
|
+
return ProfileShowResult(
|
|
98
|
+
data={},
|
|
99
|
+
exit_code=1,
|
|
100
|
+
error_code="AISC_ERR_GENERAL",
|
|
101
|
+
error_message=f"Profile not found: {name}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
data = dict(profile)
|
|
105
|
+
return ProfileShowResult(data=data, exit_code=0)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Provider status application logic for Workbench Phase 0 S0.4.
|
|
2
|
+
|
|
3
|
+
Implements the business logic behind ``aisc provider current``. Per
|
|
4
|
+
docs/gui-planning/05-cli-gui-contract.md §七. Secret-free: the in-container
|
|
5
|
+
``aisc-provider-inspect`` emits only metadata; this layer adds runtime_id and
|
|
6
|
+
observed_at.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from aisc.application.runtime import resolve_running_container, validate_uuid_v4
|
|
16
|
+
from aisc.domain.models import (
|
|
17
|
+
CliError,
|
|
18
|
+
ProviderStatus,
|
|
19
|
+
RuntimeErrorCode,
|
|
20
|
+
RuntimeExitCode,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_PROVIDER_INSPECT_PATH = "/usr/local/bin/aisc-provider-inspect"
|
|
24
|
+
_AGENTS = ("claude", "codex")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _iso_now() -> str:
|
|
28
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def current_provider(
|
|
32
|
+
runtime_id: str,
|
|
33
|
+
agent: str,
|
|
34
|
+
executor: Any,
|
|
35
|
+
registry_root: Any,
|
|
36
|
+
) -> ProviderStatus:
|
|
37
|
+
"""Return the observable provider status for *agent* in runtime *runtime_id*.
|
|
38
|
+
|
|
39
|
+
Requires the runtime to be running; reads config in-container via
|
|
40
|
+
``docker exec <ct> aisc-provider-inspect <agent>``. Never returns secrets.
|
|
41
|
+
"""
|
|
42
|
+
if not validate_uuid_v4(runtime_id):
|
|
43
|
+
raise CliError(
|
|
44
|
+
message=f"Invalid runtime ID (must be UUID v4): {runtime_id}",
|
|
45
|
+
exit_code=RuntimeExitCode.INVALID_RUNTIME_ID,
|
|
46
|
+
error_code=RuntimeErrorCode.INVALID_RUNTIME_ID,
|
|
47
|
+
)
|
|
48
|
+
if agent not in _AGENTS:
|
|
49
|
+
raise CliError(
|
|
50
|
+
message=f"Invalid agent (must be one of {', '.join(_AGENTS)}): {agent}",
|
|
51
|
+
exit_code=RuntimeExitCode.USAGE_ERROR,
|
|
52
|
+
error_code=RuntimeErrorCode.INVALID_AGENT,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
container_name = resolve_running_container(runtime_id, executor, registry_root)
|
|
56
|
+
|
|
57
|
+
result = executor.run_captured(
|
|
58
|
+
["exec", container_name, _PROVIDER_INSPECT_PATH, agent],
|
|
59
|
+
timeout=15.0,
|
|
60
|
+
)
|
|
61
|
+
if result.exit_code != 0:
|
|
62
|
+
raise CliError(
|
|
63
|
+
message=f"Provider status inspection failed: "
|
|
64
|
+
f"{(result.stderr or '').strip()[:200]}",
|
|
65
|
+
exit_code=RuntimeExitCode.PROVIDER_STATUS_FAILED,
|
|
66
|
+
error_code=RuntimeErrorCode.PROVIDER_STATUS_FAILED,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(result.stdout.strip())
|
|
71
|
+
except json.JSONDecodeError as exc:
|
|
72
|
+
raise CliError(
|
|
73
|
+
message=f"Invalid provider status output: {exc}",
|
|
74
|
+
exit_code=RuntimeExitCode.PROVIDER_STATUS_FAILED,
|
|
75
|
+
error_code=RuntimeErrorCode.PROVIDER_STATUS_FAILED,
|
|
76
|
+
)
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
raise CliError(
|
|
79
|
+
message="Provider status output is not a JSON object",
|
|
80
|
+
exit_code=RuntimeExitCode.PROVIDER_STATUS_FAILED,
|
|
81
|
+
error_code=RuntimeErrorCode.PROVIDER_STATUS_FAILED,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return ProviderStatus(
|
|
85
|
+
runtime_id=runtime_id,
|
|
86
|
+
agent=agent,
|
|
87
|
+
provider_id=str(data.get("provider_id", "")),
|
|
88
|
+
provider_name=str(data.get("provider_name", "")),
|
|
89
|
+
route_mode=str(data.get("route_mode", "unknown")) or "unknown",
|
|
90
|
+
auth_status=str(data.get("auth_status", "unknown")) or "unknown",
|
|
91
|
+
observed_at=_iso_now(),
|
|
92
|
+
)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Environment repair — automatic installation of missing dependencies.
|
|
2
|
+
|
|
3
|
+
Provides interactive installation for Docker and other dependencies when
|
|
4
|
+
aisc doctor detects missing components.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from aisc.adapters.system import ProcessRunner, RealProcessRunner
|
|
17
|
+
from aisc.domain.models import ProcessResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run_install_command(
|
|
21
|
+
runner: ProcessRunner,
|
|
22
|
+
argv: list[str],
|
|
23
|
+
timeout: float,
|
|
24
|
+
) -> ProcessResult:
|
|
25
|
+
"""Run installers on the terminal when the runner supports streaming."""
|
|
26
|
+
run_streaming = getattr(runner, "run_streaming", None)
|
|
27
|
+
if callable(run_streaming):
|
|
28
|
+
return run_streaming(argv, timeout=timeout)
|
|
29
|
+
return runner.run(argv, timeout=timeout)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _find_brew_executable() -> Optional[str]:
|
|
33
|
+
discovered = shutil.which("brew")
|
|
34
|
+
if discovered:
|
|
35
|
+
return discovered
|
|
36
|
+
for candidate in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew"):
|
|
37
|
+
if Path(candidate).is_file():
|
|
38
|
+
return candidate
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_platform() -> Tuple[str, str]:
|
|
43
|
+
"""Detect OS platform and architecture.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
(platform, arch) where platform is 'linux', 'darwin', or 'windows'
|
|
47
|
+
and arch is 'x86_64' or 'arm64'
|
|
48
|
+
"""
|
|
49
|
+
system = platform.system().lower()
|
|
50
|
+
machine = platform.machine().lower()
|
|
51
|
+
|
|
52
|
+
# Normalize platform name
|
|
53
|
+
if system == "darwin":
|
|
54
|
+
plat = "darwin"
|
|
55
|
+
elif system == "linux":
|
|
56
|
+
plat = "linux"
|
|
57
|
+
elif system in ("windows", "cygwin", "msys"):
|
|
58
|
+
plat = "windows"
|
|
59
|
+
else:
|
|
60
|
+
plat = "unknown"
|
|
61
|
+
|
|
62
|
+
# Normalize architecture
|
|
63
|
+
if machine in ("x86_64", "amd64"):
|
|
64
|
+
arch = "x86_64"
|
|
65
|
+
elif machine in ("arm64", "aarch64"):
|
|
66
|
+
arch = "arm64"
|
|
67
|
+
else:
|
|
68
|
+
arch = "unknown"
|
|
69
|
+
|
|
70
|
+
return plat, arch
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def can_install_docker(runner: Optional[ProcessRunner] = None) -> Tuple[bool, str]:
|
|
74
|
+
"""Check if Docker can be automatically installed on this platform.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
(can_install, method) where method is the installation tool name
|
|
78
|
+
"""
|
|
79
|
+
if runner is None:
|
|
80
|
+
runner = RealProcessRunner()
|
|
81
|
+
|
|
82
|
+
plat, _ = detect_platform()
|
|
83
|
+
|
|
84
|
+
if plat == "linux":
|
|
85
|
+
# Check for curl (needed for get.docker.com)
|
|
86
|
+
if shutil.which("curl"):
|
|
87
|
+
return True, "get.docker.com"
|
|
88
|
+
return False, "curl not found"
|
|
89
|
+
|
|
90
|
+
elif plat == "darwin":
|
|
91
|
+
# Check for Homebrew
|
|
92
|
+
if shutil.which("brew"):
|
|
93
|
+
return True, "homebrew"
|
|
94
|
+
return True, "homebrew-install-first"
|
|
95
|
+
|
|
96
|
+
elif plat == "windows":
|
|
97
|
+
# Check for winget
|
|
98
|
+
if shutil.which("winget"):
|
|
99
|
+
return True, "winget"
|
|
100
|
+
return False, "winget not found"
|
|
101
|
+
|
|
102
|
+
return False, "unsupported platform"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_docker_install_prompt(method: str) -> str:
|
|
106
|
+
"""Get user-facing prompt for Docker installation.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
method: Installation method from can_install_docker()
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Prompt message explaining what will be done
|
|
113
|
+
"""
|
|
114
|
+
if method == "get.docker.com":
|
|
115
|
+
return """
|
|
116
|
+
Docker is not installed. AISC can automatically install it using the
|
|
117
|
+
official Docker installation script.
|
|
118
|
+
|
|
119
|
+
This will:
|
|
120
|
+
• Download and run https://get.docker.com
|
|
121
|
+
• Install Docker Engine and CLI tools
|
|
122
|
+
• Add your user to the 'docker' group (requires sudo)
|
|
123
|
+
• You may need to log out and log back in for group changes to take effect
|
|
124
|
+
|
|
125
|
+
Do you want to install Docker now?"""
|
|
126
|
+
|
|
127
|
+
elif method == "homebrew":
|
|
128
|
+
return """
|
|
129
|
+
Docker is not installed. AISC can automatically install Docker Desktop
|
|
130
|
+
using Homebrew.
|
|
131
|
+
|
|
132
|
+
This will:
|
|
133
|
+
• Run: brew install --cask docker
|
|
134
|
+
• Download and install Docker Desktop (~500MB)
|
|
135
|
+
• Automatically start Docker Desktop
|
|
136
|
+
• You may need to accept system permissions
|
|
137
|
+
|
|
138
|
+
Do you want to install Docker now?"""
|
|
139
|
+
|
|
140
|
+
elif method == "homebrew-install-first":
|
|
141
|
+
return """
|
|
142
|
+
Docker is not installed. AISC can install it via Homebrew, but Homebrew
|
|
143
|
+
is not currently installed.
|
|
144
|
+
|
|
145
|
+
This will:
|
|
146
|
+
• First install Homebrew (the macOS package manager)
|
|
147
|
+
• Then install Docker Desktop via: brew install --cask docker
|
|
148
|
+
• You will see prompts for your password and admin approval
|
|
149
|
+
|
|
150
|
+
Do you want to proceed?"""
|
|
151
|
+
|
|
152
|
+
elif method == "winget":
|
|
153
|
+
return """
|
|
154
|
+
Docker is not installed. AISC can automatically install Docker Desktop
|
|
155
|
+
using Windows Package Manager (winget).
|
|
156
|
+
|
|
157
|
+
This will:
|
|
158
|
+
• Run: winget install Docker.DockerDesktop --silent
|
|
159
|
+
• Download and install Docker Desktop (~500MB)
|
|
160
|
+
• Require administrator privileges (UAC prompt)
|
|
161
|
+
• Require logging out and back in (or restarting) after installation
|
|
162
|
+
|
|
163
|
+
Do you want to install Docker now?"""
|
|
164
|
+
|
|
165
|
+
return "Unknown installation method"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def install_docker_linux(runner: ProcessRunner) -> Tuple[bool, str]:
|
|
169
|
+
"""Install Docker on Linux using get.docker.com.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
(success, message)
|
|
173
|
+
"""
|
|
174
|
+
print("Downloading Docker installation script...")
|
|
175
|
+
|
|
176
|
+
# Download and run the official Docker install script
|
|
177
|
+
result = _run_install_command(
|
|
178
|
+
runner,
|
|
179
|
+
["sh", "-c", "curl -fsSL https://get.docker.com | sudo sh"],
|
|
180
|
+
300.0,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if result.exit_code != 0:
|
|
184
|
+
return False, f"Docker installation failed: {result.stderr or result.stdout}"
|
|
185
|
+
|
|
186
|
+
print("Docker installed successfully!")
|
|
187
|
+
print("\nConfiguring user permissions...")
|
|
188
|
+
|
|
189
|
+
# Add user to docker group
|
|
190
|
+
username = os.getenv("USER", os.getenv("USERNAME", ""))
|
|
191
|
+
if username:
|
|
192
|
+
perm_result = runner.run(
|
|
193
|
+
["sudo", "usermod", "-aG", "docker", username],
|
|
194
|
+
timeout=10.0,
|
|
195
|
+
)
|
|
196
|
+
if perm_result.exit_code == 0:
|
|
197
|
+
return True, f"""
|
|
198
|
+
Docker installed successfully!
|
|
199
|
+
|
|
200
|
+
IMPORTANT: You need to log out and log back in for group permissions to take effect.
|
|
201
|
+
|
|
202
|
+
After logging back in, run 'aisc doctor' to verify the installation.
|
|
203
|
+
"""
|
|
204
|
+
else:
|
|
205
|
+
return True, """
|
|
206
|
+
Docker installed, but failed to configure user permissions.
|
|
207
|
+
You may need to manually run: sudo usermod -aG docker $USER
|
|
208
|
+
Then log out and log back in.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
return True, "Docker installed successfully!"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def install_docker_macos(runner: ProcessRunner, install_brew_first: bool) -> Tuple[bool, str]:
|
|
215
|
+
"""Install Docker on macOS using Homebrew.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
install_brew_first: Whether to install Homebrew first
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
(success, message)
|
|
222
|
+
"""
|
|
223
|
+
if install_brew_first:
|
|
224
|
+
print("Installing Homebrew first...")
|
|
225
|
+
brew_result = _run_install_command(
|
|
226
|
+
runner,
|
|
227
|
+
[
|
|
228
|
+
"/bin/bash", "-c",
|
|
229
|
+
"curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | /bin/bash",
|
|
230
|
+
],
|
|
231
|
+
300.0,
|
|
232
|
+
)
|
|
233
|
+
if brew_result.exit_code != 0:
|
|
234
|
+
return False, "Failed to install Homebrew"
|
|
235
|
+
print("Homebrew installed successfully!")
|
|
236
|
+
|
|
237
|
+
brew_executable = _find_brew_executable()
|
|
238
|
+
if not brew_executable:
|
|
239
|
+
return False, "Homebrew installation completed, but brew was not found"
|
|
240
|
+
|
|
241
|
+
brew_dir = os.path.dirname(brew_executable)
|
|
242
|
+
path_entries = os.environ.get("PATH", "").split(os.pathsep)
|
|
243
|
+
if brew_dir not in path_entries:
|
|
244
|
+
os.environ["PATH"] = os.pathsep.join([brew_dir, *path_entries])
|
|
245
|
+
|
|
246
|
+
print("Installing Docker Desktop via Homebrew...")
|
|
247
|
+
print("This may take several minutes to download (~500MB)...")
|
|
248
|
+
|
|
249
|
+
result = _run_install_command(
|
|
250
|
+
runner,
|
|
251
|
+
[brew_executable, "install", "--cask", "docker"],
|
|
252
|
+
600.0,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if result.exit_code != 0:
|
|
256
|
+
return False, f"Docker installation failed: {result.stderr or result.stdout}"
|
|
257
|
+
|
|
258
|
+
print("Docker Desktop installed successfully!")
|
|
259
|
+
print("Starting Docker Desktop...")
|
|
260
|
+
|
|
261
|
+
# Try to start Docker Desktop
|
|
262
|
+
start_result = runner.run(
|
|
263
|
+
["open", "-a", "Docker"],
|
|
264
|
+
timeout=10.0,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if start_result.exit_code == 0:
|
|
268
|
+
return True, """
|
|
269
|
+
Docker Desktop installed and started successfully!
|
|
270
|
+
|
|
271
|
+
Docker Desktop is launching in the background. This may take a minute.
|
|
272
|
+
Please accept any system permission prompts and the Docker service agreement.
|
|
273
|
+
|
|
274
|
+
Run 'aisc doctor' in a minute to verify Docker is running.
|
|
275
|
+
"""
|
|
276
|
+
else:
|
|
277
|
+
return True, """
|
|
278
|
+
Docker Desktop installed, but failed to start automatically.
|
|
279
|
+
Please manually open Docker Desktop from your Applications folder.
|
|
280
|
+
|
|
281
|
+
After Docker Desktop is running, run 'aisc doctor' to verify.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def install_docker_windows(runner: ProcessRunner) -> Tuple[bool, str]:
|
|
286
|
+
"""Install Docker on Windows using winget.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
(success, message)
|
|
290
|
+
"""
|
|
291
|
+
print("Installing Docker Desktop via winget...")
|
|
292
|
+
print("You will see a UAC (User Account Control) prompt - please accept it.")
|
|
293
|
+
print("This may take several minutes to download (~500MB)...")
|
|
294
|
+
|
|
295
|
+
result = _run_install_command(
|
|
296
|
+
runner,
|
|
297
|
+
[
|
|
298
|
+
"winget", "install", "Docker.DockerDesktop",
|
|
299
|
+
"--silent",
|
|
300
|
+
"--accept-source-agreements",
|
|
301
|
+
"--accept-package-agreements",
|
|
302
|
+
],
|
|
303
|
+
600.0,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if result.exit_code != 0:
|
|
307
|
+
return False, f"Docker installation failed: {result.stderr or result.stdout}"
|
|
308
|
+
|
|
309
|
+
return True, """
|
|
310
|
+
Docker Desktop installed successfully!
|
|
311
|
+
|
|
312
|
+
IMPORTANT: You need to log out and log back in (or restart your computer)
|
|
313
|
+
for the installation to complete.
|
|
314
|
+
|
|
315
|
+
After logging back in:
|
|
316
|
+
1. Start Docker Desktop from the Start menu
|
|
317
|
+
2. Accept the service agreement
|
|
318
|
+
3. Run 'aisc doctor' to verify the installation
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def install_docker_interactive(runner: Optional[ProcessRunner] = None) -> bool:
|
|
323
|
+
"""Interactively install Docker if missing.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if installation was successful (or user declined)
|
|
327
|
+
False if installation failed
|
|
328
|
+
"""
|
|
329
|
+
if runner is None:
|
|
330
|
+
runner = RealProcessRunner()
|
|
331
|
+
|
|
332
|
+
can_install, method = can_install_docker(runner)
|
|
333
|
+
|
|
334
|
+
if not can_install:
|
|
335
|
+
print(f"\nCannot automatically install Docker: {method}", file=sys.stderr)
|
|
336
|
+
print("Please install Docker manually:", file=sys.stderr)
|
|
337
|
+
print(" https://docs.docker.com/get-docker/", file=sys.stderr)
|
|
338
|
+
return True # Not a failure, just unsupported
|
|
339
|
+
|
|
340
|
+
# Show installation prompt
|
|
341
|
+
prompt = get_docker_install_prompt(method)
|
|
342
|
+
print(prompt)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
response = input("\nProceed with installation? [y/N]: ").strip().lower()
|
|
346
|
+
except (EOFError, KeyboardInterrupt):
|
|
347
|
+
print("\nInstallation cancelled.")
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
if response not in ("y", "yes"):
|
|
351
|
+
print("Installation cancelled. You can install Docker manually:")
|
|
352
|
+
print(" https://docs.docker.com/get-docker/")
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
# Perform installation
|
|
356
|
+
print("\nStarting Docker installation...")
|
|
357
|
+
|
|
358
|
+
plat, _ = detect_platform()
|
|
359
|
+
success = False
|
|
360
|
+
message = ""
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
if plat == "linux" and method == "get.docker.com":
|
|
364
|
+
success, message = install_docker_linux(runner)
|
|
365
|
+
elif plat == "darwin":
|
|
366
|
+
install_brew = (method == "homebrew-install-first")
|
|
367
|
+
success, message = install_docker_macos(runner, install_brew)
|
|
368
|
+
elif plat == "windows" and method == "winget":
|
|
369
|
+
success, message = install_docker_windows(runner)
|
|
370
|
+
else:
|
|
371
|
+
print(f"Unsupported platform/method: {plat}/{method}", file=sys.stderr)
|
|
372
|
+
return False
|
|
373
|
+
except Exception as exc:
|
|
374
|
+
print(f"\nInstallation failed with error: {exc}", file=sys.stderr)
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
# Show result
|
|
378
|
+
print("\n" + "=" * 60)
|
|
379
|
+
if success:
|
|
380
|
+
print("✓ Installation completed")
|
|
381
|
+
print(message)
|
|
382
|
+
else:
|
|
383
|
+
print("✗ Installation failed")
|
|
384
|
+
print(message, file=sys.stderr)
|
|
385
|
+
print("=" * 60)
|
|
386
|
+
|
|
387
|
+
return success
|