aibox-cli 0.3.1__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.
- aibox/__init__.py +12 -0
- aibox/cli/__init__.py +18 -0
- aibox/cli/autocomplete.py +80 -0
- aibox/cli/commands/__init__.py +25 -0
- aibox/cli/commands/config.py +228 -0
- aibox/cli/commands/images.py +173 -0
- aibox/cli/commands/init.py +199 -0
- aibox/cli/commands/profile.py +105 -0
- aibox/cli/commands/slot.py +531 -0
- aibox/cli/commands/start.py +405 -0
- aibox/cli/commands/status.py +99 -0
- aibox/cli/main.py +356 -0
- aibox/config/__init__.py +0 -0
- aibox/config/loader.py +358 -0
- aibox/config/models.py +121 -0
- aibox/containers/__init__.py +0 -0
- aibox/containers/manager.py +536 -0
- aibox/containers/orchestrator.py +566 -0
- aibox/containers/slot.py +468 -0
- aibox/containers/volumes.py +168 -0
- aibox/profiles/__init__.py +0 -0
- aibox/profiles/definitions/git.yml +15 -0
- aibox/profiles/definitions/go.yml +32 -0
- aibox/profiles/definitions/nodejs.yml +28 -0
- aibox/profiles/definitions/python.yml +30 -0
- aibox/profiles/definitions/rust.yml +36 -0
- aibox/profiles/definitions/sudo.yml +16 -0
- aibox/profiles/generator.py +396 -0
- aibox/profiles/loader.py +200 -0
- aibox/profiles/models.py +157 -0
- aibox/providers/__init__.py +34 -0
- aibox/providers/base.py +208 -0
- aibox/providers/claude.py +164 -0
- aibox/providers/gemini.py +124 -0
- aibox/providers/openai.py +191 -0
- aibox/providers/registry.py +190 -0
- aibox/utils/__init__.py +0 -0
- aibox/utils/errors.py +118 -0
- aibox/utils/hash.py +73 -0
- aibox_cli-0.3.1.dist-info/METADATA +410 -0
- aibox_cli-0.3.1.dist-info/RECORD +44 -0
- aibox_cli-0.3.1.dist-info/WHEEL +4 -0
- aibox_cli-0.3.1.dist-info/entry_points.txt +2 -0
- aibox_cli-0.3.1.dist-info/licenses/LICENSE +21 -0
aibox/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""aibox - Container-Based Multi-AI Development Environment."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("aibox-cli")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
# Package not installed, use fallback for development
|
|
9
|
+
__version__ = "0.0.0+dev"
|
|
10
|
+
|
|
11
|
+
__author__ = "Your Name"
|
|
12
|
+
__email__ = "your@email.com"
|
aibox/cli/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from aibox.cli.commands.config import config_edit, config_show, config_validate
|
|
2
|
+
from aibox.cli.commands.init import init_command
|
|
3
|
+
from aibox.cli.commands.profile import profile_info, profile_list
|
|
4
|
+
from aibox.cli.commands.slot import slot_add, slot_cleanup, slot_list
|
|
5
|
+
from aibox.cli.commands.status import status_command
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"init_command",
|
|
9
|
+
"config_edit",
|
|
10
|
+
"config_show",
|
|
11
|
+
"config_validate",
|
|
12
|
+
"profile_info",
|
|
13
|
+
"profile_list",
|
|
14
|
+
"slot_add",
|
|
15
|
+
"slot_cleanup",
|
|
16
|
+
"slot_list",
|
|
17
|
+
"status_command",
|
|
18
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Autocompletion functions for aibox CLI.
|
|
3
|
+
|
|
4
|
+
Provides custom autocompletion for dynamic values like profiles, providers, and slots.
|
|
5
|
+
These functions are called by Typer when users press TAB in their shell.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from aibox.containers.slot import SlotManager
|
|
11
|
+
from aibox.profiles.loader import ProfileLoader
|
|
12
|
+
from aibox.providers.registry import ProviderRegistry
|
|
13
|
+
from aibox.utils.hash import get_project_storage_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def complete_profile_name() -> list[str]:
|
|
17
|
+
"""
|
|
18
|
+
Autocomplete profile names with versions.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of profile specifications (e.g., ["python", "python:3.11", "nodejs:20"])
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
loader = ProfileLoader()
|
|
25
|
+
all_profiles = loader.list_profiles()
|
|
26
|
+
|
|
27
|
+
completions = []
|
|
28
|
+
for profile_name in all_profiles:
|
|
29
|
+
# Add base profile name
|
|
30
|
+
completions.append(profile_name)
|
|
31
|
+
|
|
32
|
+
# Add profile:version variants
|
|
33
|
+
try:
|
|
34
|
+
profile, _ = loader.load_profile(profile_name)
|
|
35
|
+
if profile.versions:
|
|
36
|
+
for version in profile.versions:
|
|
37
|
+
completions.append(f"{profile_name}:{version}")
|
|
38
|
+
except Exception:
|
|
39
|
+
# If we can't load profile details, just skip versions
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
return sorted(completions)
|
|
43
|
+
except Exception:
|
|
44
|
+
# Fail gracefully if autocomplete fails
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def complete_provider_name() -> list[str]:
|
|
49
|
+
"""
|
|
50
|
+
Autocomplete AI provider names.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of provider names (e.g., ["claude", "gemini", "openai"])
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
return ProviderRegistry.list_providers()
|
|
57
|
+
except Exception:
|
|
58
|
+
# Fail gracefully if autocomplete fails
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def complete_slot_number() -> list[str]:
|
|
63
|
+
"""
|
|
64
|
+
Autocomplete configured slot numbers for current project.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of configured slot numbers as strings (e.g., ["1", "2", "3"])
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
project_root = Path.cwd()
|
|
71
|
+
storage_dir = get_project_storage_dir(project_root)
|
|
72
|
+
slot_manager = SlotManager(storage_dir)
|
|
73
|
+
slots = slot_manager.list_slots()
|
|
74
|
+
|
|
75
|
+
# Return slot numbers as strings (Typer expects strings)
|
|
76
|
+
return [str(slot["slot"]) for slot in slots]
|
|
77
|
+
except Exception:
|
|
78
|
+
# Fail gracefully if autocomplete fails
|
|
79
|
+
# Return all possible slots if we can't determine configured ones
|
|
80
|
+
return [str(i) for i in range(1, 11)]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CLI command implementations for aibox."""
|
|
2
|
+
|
|
3
|
+
from aibox.cli.commands.config import config_edit, config_show, config_validate
|
|
4
|
+
from aibox.cli.commands.images import images_list, images_prune
|
|
5
|
+
from aibox.cli.commands.init import init_command
|
|
6
|
+
from aibox.cli.commands.profile import profile_info, profile_list
|
|
7
|
+
from aibox.cli.commands.slot import slot_add, slot_cleanup, slot_list
|
|
8
|
+
from aibox.cli.commands.start import start_command
|
|
9
|
+
from aibox.cli.commands.status import status_command
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"init_command",
|
|
13
|
+
"start_command",
|
|
14
|
+
"status_command",
|
|
15
|
+
"profile_list",
|
|
16
|
+
"profile_info",
|
|
17
|
+
"slot_list",
|
|
18
|
+
"slot_add",
|
|
19
|
+
"slot_cleanup",
|
|
20
|
+
"config_show",
|
|
21
|
+
"config_validate",
|
|
22
|
+
"config_edit",
|
|
23
|
+
"images_list",
|
|
24
|
+
"images_prune",
|
|
25
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management commands.
|
|
3
|
+
|
|
4
|
+
Provides commands to view and validate aibox configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.prompt import Confirm
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
|
|
17
|
+
from aibox.config.loader import get_project_config_path, load_config
|
|
18
|
+
from aibox.containers.slot import SlotManager
|
|
19
|
+
from aibox.utils.errors import ConfigNotFoundError, SlotNotFoundError
|
|
20
|
+
from aibox.utils.hash import get_project_storage_dir
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def config_show(project_root: Path, slot_number: int = 1) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Show current configuration.
|
|
28
|
+
|
|
29
|
+
Displays the merged global + project + slot configuration in YAML format.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_root: Project root directory
|
|
33
|
+
slot_number: Slot number to show configuration for (default: 1)
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ConfigNotFoundError: If project configuration not found
|
|
37
|
+
SlotNotFoundError: If specified slot doesn't exist
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
config = load_config(str(project_root))
|
|
41
|
+
|
|
42
|
+
# Build global config dict
|
|
43
|
+
config_dict = {
|
|
44
|
+
"global": {
|
|
45
|
+
"version": config.global_config.version,
|
|
46
|
+
"docker": {
|
|
47
|
+
"base_image": config.global_config.docker.base_image,
|
|
48
|
+
"default_resources": {
|
|
49
|
+
"cpus": config.global_config.docker.default_resources.cpus,
|
|
50
|
+
"memory": config.global_config.docker.default_resources.memory,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"project": {
|
|
55
|
+
"name": config.project.name,
|
|
56
|
+
"profiles": config.project.profiles,
|
|
57
|
+
"mounts": [
|
|
58
|
+
{"source": m.source, "target": m.target, "mode": m.mode}
|
|
59
|
+
for m in config.project.mounts
|
|
60
|
+
],
|
|
61
|
+
"environment": config.project.environment,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Load slot configuration
|
|
66
|
+
storage_dir = get_project_storage_dir(project_root)
|
|
67
|
+
slot_manager = SlotManager(storage_dir)
|
|
68
|
+
slot_config = slot_manager.get_slot(slot_number)
|
|
69
|
+
slot_data = slot_config.load()
|
|
70
|
+
|
|
71
|
+
if slot_data is None:
|
|
72
|
+
raise SlotNotFoundError(
|
|
73
|
+
message=f"Slot {slot_number} not found",
|
|
74
|
+
suggestion="Available slots: Use 'aibox slot list' to see configured slots, or create a new slot with 'aibox slot add'",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Add slot config to dict
|
|
78
|
+
config_dict["slot"] = {
|
|
79
|
+
"slot_number": slot_number,
|
|
80
|
+
**slot_data,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Convert to YAML
|
|
84
|
+
yaml_content = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
85
|
+
|
|
86
|
+
# Syntax highlighting
|
|
87
|
+
syntax = Syntax(yaml_content, "yaml", theme="monokai", line_numbers=False)
|
|
88
|
+
|
|
89
|
+
# Show in panel with slot number
|
|
90
|
+
panel = Panel(
|
|
91
|
+
syntax,
|
|
92
|
+
title=f"[bold]aibox Configuration (Slot {slot_number})[/bold]",
|
|
93
|
+
border_style="blue",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
console.print()
|
|
97
|
+
console.print(panel)
|
|
98
|
+
console.print()
|
|
99
|
+
|
|
100
|
+
except (ConfigNotFoundError, SlotNotFoundError):
|
|
101
|
+
raise
|
|
102
|
+
except Exception as e:
|
|
103
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def config_validate(project_root: Path) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Validate configuration files.
|
|
110
|
+
|
|
111
|
+
Checks that configuration files are valid and can be loaded.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
project_root: Project root directory
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
console.print("\n[bold blue]Validating configuration...[/bold blue]\n")
|
|
118
|
+
|
|
119
|
+
with console.status("[cyan]Loading configuration...[/cyan]", spinner="dots"):
|
|
120
|
+
config = load_config(str(project_root))
|
|
121
|
+
|
|
122
|
+
# If we got here, config is valid
|
|
123
|
+
console.print("[bold green]✓[/bold green] Configuration is valid!\n")
|
|
124
|
+
|
|
125
|
+
# Show summary
|
|
126
|
+
console.print("[bold]Summary:[/bold]")
|
|
127
|
+
console.print(f" • Project: [cyan]{config.project.name}[/cyan]")
|
|
128
|
+
console.print(f" • Profiles: [cyan]{len(config.project.profiles)}[/cyan]")
|
|
129
|
+
console.print(f" • Custom Mounts: [cyan]{len(config.project.mounts)}[/cyan]")
|
|
130
|
+
console.print()
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
console.print("\n[bold red]✗[/bold red] Configuration is invalid\n")
|
|
134
|
+
console.print(f"[red]Error:[/red] {e}\n")
|
|
135
|
+
raise SystemExit(1) from e
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def config_edit(project_root: Path) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Edit project configuration in your default editor.
|
|
141
|
+
|
|
142
|
+
Opens the project config file from ~/.aibox/projects/<hash>/config.yml
|
|
143
|
+
in the editor specified by $EDITOR or $VISUAL environment variable.
|
|
144
|
+
After editing, validates the configuration and prompts to re-edit if invalid.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
project_root: Project root directory
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ConfigNotFoundError: If project is not initialized
|
|
151
|
+
SystemExit: If user aborts or validation fails
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
# Get project config path from centralized storage
|
|
155
|
+
config_path = get_project_config_path(project_root)
|
|
156
|
+
if not config_path.exists():
|
|
157
|
+
raise ConfigNotFoundError(
|
|
158
|
+
"No project configuration found",
|
|
159
|
+
suggestion="Run 'aibox init' to initialize this project first",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Get editor from environment
|
|
163
|
+
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "nano"
|
|
164
|
+
|
|
165
|
+
console.print(f"\n[bold blue]Opening config in {editor}...[/bold blue]\n")
|
|
166
|
+
|
|
167
|
+
# Open editor in a loop to allow re-editing on validation failure
|
|
168
|
+
while True:
|
|
169
|
+
# Open the config file in the editor
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run([editor, str(config_path)], check=False)
|
|
172
|
+
if result.returncode != 0:
|
|
173
|
+
console.print(
|
|
174
|
+
f"\n[yellow]Editor exited with code {result.returncode}[/yellow]\n"
|
|
175
|
+
)
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
console.print(f"\n[red]Error:[/red] Editor '{editor}' not found\n")
|
|
178
|
+
console.print(
|
|
179
|
+
"Set $EDITOR or $VISUAL environment variable to your preferred editor\n"
|
|
180
|
+
)
|
|
181
|
+
raise SystemExit(1) from None
|
|
182
|
+
except Exception as e:
|
|
183
|
+
console.print(f"\n[red]Error:[/red] Failed to open editor: {e}\n")
|
|
184
|
+
raise SystemExit(1) from e
|
|
185
|
+
|
|
186
|
+
# Validate the edited configuration
|
|
187
|
+
console.print("\n[bold blue]Validating configuration...[/bold blue]\n")
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
with console.status("[cyan]Loading configuration...[/cyan]", spinner="dots"):
|
|
191
|
+
config = load_config(str(project_root))
|
|
192
|
+
|
|
193
|
+
# Configuration is valid
|
|
194
|
+
console.print("[bold green]✓[/bold green] Configuration is valid!\n")
|
|
195
|
+
|
|
196
|
+
# Show summary
|
|
197
|
+
console.print("[bold]Updated configuration:[/bold]")
|
|
198
|
+
console.print(f" • Project: [cyan]{config.project.name}[/cyan]")
|
|
199
|
+
console.print(
|
|
200
|
+
f" • Profiles: [cyan]{', '.join(config.project.profiles) if config.project.profiles else 'none'}[/cyan]"
|
|
201
|
+
)
|
|
202
|
+
console.print()
|
|
203
|
+
|
|
204
|
+
break # Exit the loop, config is valid
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
# Configuration is invalid
|
|
208
|
+
console.print("[bold red]✗[/bold red] Configuration is invalid\n")
|
|
209
|
+
console.print(f"[red]Error:[/red] {e}\n")
|
|
210
|
+
|
|
211
|
+
# Ask if user wants to re-edit
|
|
212
|
+
retry = Confirm.ask(
|
|
213
|
+
"[yellow]Would you like to edit the configuration again?[/yellow]",
|
|
214
|
+
default=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if not retry:
|
|
218
|
+
console.print("\n[yellow]Aborted[/yellow]\n")
|
|
219
|
+
raise SystemExit(1) from e
|
|
220
|
+
|
|
221
|
+
# Loop continues to re-edit
|
|
222
|
+
|
|
223
|
+
except ConfigNotFoundError:
|
|
224
|
+
# Re-raise to be handled by main error handler
|
|
225
|
+
raise
|
|
226
|
+
except KeyboardInterrupt:
|
|
227
|
+
console.print("\n\n[yellow]Aborted by user[/yellow]\n")
|
|
228
|
+
raise SystemExit(130) from None
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image management commands.
|
|
3
|
+
|
|
4
|
+
Provides commands to list and prune Docker images created by aibox.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from aibox.containers.manager import ContainerManager
|
|
13
|
+
from aibox.utils.errors import DockerNotFoundError
|
|
14
|
+
from aibox.utils.hash import get_project_storage_dir
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def images_list(project_root: Path) -> None:
|
|
20
|
+
"""
|
|
21
|
+
List all aibox Docker images for the current project.
|
|
22
|
+
|
|
23
|
+
Shows a table with image tags, sizes, and creation dates.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_root: Project root directory
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Initialize container manager
|
|
30
|
+
try:
|
|
31
|
+
container_manager = ContainerManager()
|
|
32
|
+
except DockerNotFoundError as e:
|
|
33
|
+
console.print(f"\n[red]✗[/red] {e.message}\n")
|
|
34
|
+
console.print(f"[bold]💡 Solution:[/bold] {e.suggestion}\n")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Get project name from storage directory
|
|
38
|
+
storage_dir = get_project_storage_dir(project_root)
|
|
39
|
+
storage_dir_name = Path(storage_dir).name
|
|
40
|
+
project_name = storage_dir_name.rsplit("-", 1)[0] if "-" in storage_dir_name else "aibox"
|
|
41
|
+
|
|
42
|
+
# List images with project name filter
|
|
43
|
+
filters = {"reference": f"aibox-{project_name}-*"}
|
|
44
|
+
images = container_manager.list_images(filters=filters)
|
|
45
|
+
|
|
46
|
+
if not images:
|
|
47
|
+
console.print(
|
|
48
|
+
f"\n[yellow]No aibox images found for project '{project_name}'[/yellow]\n"
|
|
49
|
+
)
|
|
50
|
+
console.print(
|
|
51
|
+
"[dim]Images are created automatically when you run 'aibox start'[/dim]\n"
|
|
52
|
+
)
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Create table
|
|
56
|
+
table = Table(title=f"[bold]aibox Images - {project_name}[/bold]", show_lines=False)
|
|
57
|
+
table.add_column("Tag", style="cyan", no_wrap=True)
|
|
58
|
+
table.add_column("Image ID", style="dim", no_wrap=True)
|
|
59
|
+
table.add_column("Size", style="green", justify="right")
|
|
60
|
+
table.add_column("Created", style="yellow")
|
|
61
|
+
|
|
62
|
+
total_size = 0
|
|
63
|
+
for image in images:
|
|
64
|
+
# Get all tags for this image
|
|
65
|
+
tags = image.tags if image.tags else ["<none>:<none>"]
|
|
66
|
+
|
|
67
|
+
# Get image details
|
|
68
|
+
image_id = image.short_id.replace("sha256:", "")
|
|
69
|
+
size_bytes = image.attrs.get("Size", 0)
|
|
70
|
+
size_mb = size_bytes / (1024 * 1024)
|
|
71
|
+
total_size += size_bytes
|
|
72
|
+
|
|
73
|
+
# Get created date
|
|
74
|
+
created = image.attrs.get("Created", "N/A")
|
|
75
|
+
if created != "N/A":
|
|
76
|
+
# Parse and format date
|
|
77
|
+
from datetime import datetime
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
|
|
81
|
+
created = created_dt.strftime("%Y-%m-%d %H:%M")
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Add row for each tag
|
|
86
|
+
for tag in tags:
|
|
87
|
+
# Highlight hash tags vs :latest
|
|
88
|
+
tag_display = f"[bold]{tag}[/bold]" if ":latest" in tag else tag
|
|
89
|
+
|
|
90
|
+
table.add_row(
|
|
91
|
+
tag_display,
|
|
92
|
+
image_id,
|
|
93
|
+
f"{size_mb:.1f} MB",
|
|
94
|
+
created,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
console.print()
|
|
98
|
+
console.print(table)
|
|
99
|
+
|
|
100
|
+
# Show summary
|
|
101
|
+
total_size_mb = total_size / (1024 * 1024)
|
|
102
|
+
console.print(
|
|
103
|
+
f"\n[dim]Total images: {len(images)} | Total size: {total_size_mb:.1f} MB[/dim]\n"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Show hint about cleanup
|
|
107
|
+
console.print("[dim]💡 Run [cyan]aibox images prune[/cyan] to remove unused images[/dim]\n")
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def images_prune(project_root: Path | None = None, all_projects: bool = False) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Prune dangling aibox Docker images.
|
|
117
|
+
|
|
118
|
+
Removes images with <none> tags that are no longer needed.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
project_root: Project root directory (optional for project-specific pruning)
|
|
122
|
+
all_projects: If True, prune dangling images for all aibox projects
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
# Initialize container manager
|
|
126
|
+
try:
|
|
127
|
+
container_manager = ContainerManager()
|
|
128
|
+
except DockerNotFoundError as e:
|
|
129
|
+
console.print(f"\n[red]✗[/red] {e.message}\n")
|
|
130
|
+
console.print(f"[bold]💡 Solution:[/bold] {e.suggestion}\n")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Determine scope
|
|
134
|
+
if all_projects or project_root is None:
|
|
135
|
+
console.print("\n[bold blue]Pruning dangling aibox images...[/bold blue]\n")
|
|
136
|
+
filters = None # Prune all dangling images
|
|
137
|
+
else:
|
|
138
|
+
# Get project name for filtering
|
|
139
|
+
storage_dir = get_project_storage_dir(project_root)
|
|
140
|
+
storage_dir_name = Path(storage_dir).name
|
|
141
|
+
project_name = (
|
|
142
|
+
storage_dir_name.rsplit("-", 1)[0] if "-" in storage_dir_name else "aibox"
|
|
143
|
+
)
|
|
144
|
+
console.print(
|
|
145
|
+
f"\n[bold blue]Pruning dangling images for project '{project_name}'...[/bold blue]\n"
|
|
146
|
+
)
|
|
147
|
+
filters = None # Docker doesn't support reference filter for prune
|
|
148
|
+
|
|
149
|
+
# Prune images
|
|
150
|
+
with console.status(
|
|
151
|
+
"[cyan]Removing dangling images...[/cyan]",
|
|
152
|
+
spinner="dots",
|
|
153
|
+
):
|
|
154
|
+
result = container_manager.prune_dangling_images(filters=filters)
|
|
155
|
+
|
|
156
|
+
images_deleted = result.get("ImagesDeleted") or []
|
|
157
|
+
space_reclaimed = result.get("SpaceReclaimed", 0)
|
|
158
|
+
space_mb = space_reclaimed / (1024 * 1024)
|
|
159
|
+
|
|
160
|
+
if images_deleted:
|
|
161
|
+
console.print(
|
|
162
|
+
f"[bold green]✓[/bold green] Removed {len(images_deleted)} dangling image(s)\n"
|
|
163
|
+
)
|
|
164
|
+
console.print(f"[dim]Space reclaimed: {space_mb:.1f} MB[/dim]\n")
|
|
165
|
+
else:
|
|
166
|
+
console.print("[green]✓[/green] No dangling images to remove\n")
|
|
167
|
+
|
|
168
|
+
except KeyboardInterrupt:
|
|
169
|
+
console.print("\n\n[yellow]⚠[/yellow] Cancelled by user\n")
|
|
170
|
+
raise SystemExit(1) from None
|
|
171
|
+
except Exception as e:
|
|
172
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
173
|
+
raise
|