tweek 0.3.1__py3-none-any.whl → 0.4.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.
- tweek/__init__.py +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6605
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +17 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +162 -68
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_dry_run.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Dry-run command group for Tweek CLI (renamed from sandbox).
|
|
2
|
+
|
|
3
|
+
Provides the ``tweek dry-run`` CLI surface for project-level sandbox
|
|
4
|
+
isolation management. The underlying sandbox subsystem modules
|
|
5
|
+
(``tweek.sandbox.*``) are unchanged -- only the user-facing command
|
|
6
|
+
hierarchy has been renamed from ``tweek sandbox`` to ``tweek dry-run``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from tweek.cli_helpers import console
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group("dry-run")
|
|
21
|
+
def dry_run():
|
|
22
|
+
"""Project-level dry-run isolation management.
|
|
23
|
+
|
|
24
|
+
Layer 2 provides per-project security state isolation:
|
|
25
|
+
- Separate security event logs per project
|
|
26
|
+
- Project-scoped pattern overrides (additive-only)
|
|
27
|
+
- Project-scoped skill fingerprints
|
|
28
|
+
- Project-scoped configuration
|
|
29
|
+
|
|
30
|
+
Project overrides can ADD security but NEVER weaken global settings.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dry_run.command("status")
|
|
36
|
+
def sandbox_status():
|
|
37
|
+
"""Show current project's sandbox info."""
|
|
38
|
+
from tweek.sandbox.project import get_project_sandbox, _detect_project_dir
|
|
39
|
+
from tweek.sandbox.layers import get_layer_description, IsolationLayer
|
|
40
|
+
|
|
41
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
42
|
+
if not project_dir:
|
|
43
|
+
console.print("[yellow]Not inside a project directory (no .git/ or .claude/ found).[/yellow]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
sandbox = get_project_sandbox(os.getcwd())
|
|
47
|
+
if sandbox:
|
|
48
|
+
console.print(f"[bold]Project:[/bold] {sandbox.project_dir}")
|
|
49
|
+
console.print(f"[bold]Layer:[/bold] {sandbox.layer.value} ({sandbox.layer.name})")
|
|
50
|
+
console.print(f"[bold]Description:[/bold] {get_layer_description(sandbox.layer)}")
|
|
51
|
+
console.print(f"[bold]Tweek dir:[/bold] {sandbox.tweek_dir}")
|
|
52
|
+
console.print(f"[bold]Initialized:[/bold] {sandbox.is_initialized}")
|
|
53
|
+
|
|
54
|
+
if sandbox.is_initialized:
|
|
55
|
+
db_path = sandbox.tweek_dir / "security.db"
|
|
56
|
+
if db_path.exists():
|
|
57
|
+
size_kb = db_path.stat().st_size / 1024
|
|
58
|
+
console.print(f"[bold]Security DB:[/bold] {size_kb:.1f} KB")
|
|
59
|
+
else:
|
|
60
|
+
console.print(f"[bold]Project:[/bold] {project_dir}")
|
|
61
|
+
console.print(f"[bold]Layer:[/bold] 0-1 (no project isolation)")
|
|
62
|
+
console.print("[white]Run 'tweek dry-run init' to enable project isolation.[/white]")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dry_run.command("init")
|
|
66
|
+
@click.option("--layer", type=int, default=2, help="Isolation layer (0=bypass, 1=skills, 2=project)")
|
|
67
|
+
def sandbox_init(layer: int):
|
|
68
|
+
"""Initialize sandbox for current project."""
|
|
69
|
+
from tweek.sandbox.project import ProjectSandbox, _detect_project_dir
|
|
70
|
+
from tweek.sandbox.layers import IsolationLayer, get_layer_description
|
|
71
|
+
from tweek.logging.security_log import get_logger, EventType
|
|
72
|
+
|
|
73
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
74
|
+
if not project_dir:
|
|
75
|
+
console.print("[red]Not inside a project directory (no .git/ or .claude/ found).[/red]")
|
|
76
|
+
raise SystemExit(1)
|
|
77
|
+
|
|
78
|
+
isolation_layer = IsolationLayer.from_value(layer)
|
|
79
|
+
sandbox = ProjectSandbox(project_dir)
|
|
80
|
+
sandbox.config.layer = isolation_layer.value
|
|
81
|
+
sandbox.layer = isolation_layer
|
|
82
|
+
|
|
83
|
+
sandbox.initialize()
|
|
84
|
+
|
|
85
|
+
console.print(f"[green]Sandbox initialized for {project_dir}[/green]")
|
|
86
|
+
console.print(f"[bold]Layer:[/bold] {isolation_layer.value} ({isolation_layer.name})")
|
|
87
|
+
console.print(f"[bold]Description:[/bold] {get_layer_description(isolation_layer)}")
|
|
88
|
+
console.print(f"[bold]State directory:[/bold] {sandbox.tweek_dir}")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
logger = get_logger()
|
|
92
|
+
from tweek.logging.security_log import SecurityEvent
|
|
93
|
+
logger.log(SecurityEvent(
|
|
94
|
+
event_type=EventType.SANDBOX_PROJECT_INIT,
|
|
95
|
+
tool_name="cli",
|
|
96
|
+
decision="allow",
|
|
97
|
+
decision_reason=f"Project sandbox initialized at layer {isolation_layer.value}",
|
|
98
|
+
working_directory=str(project_dir),
|
|
99
|
+
))
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dry_run.command("layer")
|
|
105
|
+
@click.argument("level", type=int)
|
|
106
|
+
def sandbox_layer(level: int):
|
|
107
|
+
"""Set isolation layer for current project (0=bypass, 1=skills, 2=project)."""
|
|
108
|
+
from tweek.sandbox.project import _detect_project_dir
|
|
109
|
+
from tweek.sandbox.layers import IsolationLayer, get_layer_description
|
|
110
|
+
from tweek.sandbox.registry import get_registry
|
|
111
|
+
|
|
112
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
113
|
+
if not project_dir:
|
|
114
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
115
|
+
raise SystemExit(1)
|
|
116
|
+
|
|
117
|
+
new_layer = IsolationLayer.from_value(level)
|
|
118
|
+
registry = get_registry()
|
|
119
|
+
registry.set_layer(project_dir, new_layer)
|
|
120
|
+
|
|
121
|
+
console.print(f"[green]Layer set to {new_layer.value} ({new_layer.name})[/green]")
|
|
122
|
+
console.print(f"[bold]Description:[/bold] {get_layer_description(new_layer)}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dry_run.command("list")
|
|
126
|
+
def sandbox_list():
|
|
127
|
+
"""List all registered projects and their layers."""
|
|
128
|
+
from tweek.sandbox.registry import get_registry
|
|
129
|
+
from tweek.sandbox.layers import IsolationLayer
|
|
130
|
+
|
|
131
|
+
registry = get_registry()
|
|
132
|
+
projects = registry.list_projects()
|
|
133
|
+
|
|
134
|
+
if not projects:
|
|
135
|
+
console.print("[white]No projects registered. Run 'tweek dry-run init' in a project.[/white]")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
table = Table(title="Registered Projects")
|
|
139
|
+
table.add_column("Project", style="cyan")
|
|
140
|
+
table.add_column("Layer", style="green")
|
|
141
|
+
table.add_column("Last Used")
|
|
142
|
+
table.add_column("Auto-Init")
|
|
143
|
+
|
|
144
|
+
for p in projects:
|
|
145
|
+
layer = p["layer"]
|
|
146
|
+
table.add_row(
|
|
147
|
+
p["path"],
|
|
148
|
+
f"{layer.value} ({layer.name})",
|
|
149
|
+
p.get("last_used", "")[:19],
|
|
150
|
+
"Yes" if p.get("auto_initialized") else "No",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.print(table)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dry_run.command("config")
|
|
157
|
+
def sandbox_config():
|
|
158
|
+
"""Show effective merged config (global + project)."""
|
|
159
|
+
from tweek.sandbox.project import get_project_sandbox, _detect_project_dir
|
|
160
|
+
|
|
161
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
162
|
+
if not project_dir:
|
|
163
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
164
|
+
raise SystemExit(1)
|
|
165
|
+
|
|
166
|
+
sandbox = get_project_sandbox(os.getcwd())
|
|
167
|
+
if not sandbox:
|
|
168
|
+
console.print("[yellow]Project sandbox not active (layer < 2).[/yellow]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
console.print("[bold]Effective Configuration (global + project merge):[/bold]")
|
|
172
|
+
console.print(f" Layer: {sandbox.layer.value} ({sandbox.layer.name})")
|
|
173
|
+
console.print(f" Additive only: {sandbox.config.additive_only}")
|
|
174
|
+
console.print(f" Auto gitignore: {sandbox.config.auto_gitignore}")
|
|
175
|
+
|
|
176
|
+
overrides = sandbox.get_overrides()
|
|
177
|
+
if overrides:
|
|
178
|
+
console.print(f" Overrides loaded: Yes")
|
|
179
|
+
if hasattr(overrides, 'global_ovr') and hasattr(overrides, 'project_ovr'):
|
|
180
|
+
console.print(f" Merge type: Additive-only (global + project)")
|
|
181
|
+
else:
|
|
182
|
+
console.print(f" Merge type: Global only (no project overrides)")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dry_run.command("logs")
|
|
186
|
+
@click.option("--global", "show_global", is_flag=True, help="Show global security log instead")
|
|
187
|
+
@click.option("--limit", default=20, help="Number of events to show")
|
|
188
|
+
def sandbox_logs(show_global: bool, limit: int):
|
|
189
|
+
"""View project-scoped or global security log."""
|
|
190
|
+
from tweek.logging.security_log import SecurityLogger, get_logger
|
|
191
|
+
|
|
192
|
+
if show_global:
|
|
193
|
+
logger = get_logger()
|
|
194
|
+
console.print("[bold]Global Security Log[/bold]")
|
|
195
|
+
else:
|
|
196
|
+
from tweek.sandbox.project import get_project_sandbox
|
|
197
|
+
sandbox = get_project_sandbox(os.getcwd())
|
|
198
|
+
if sandbox:
|
|
199
|
+
logger = sandbox.get_logger()
|
|
200
|
+
console.print(f"[bold]Project Security Log[/bold] ({sandbox.project_dir})")
|
|
201
|
+
else:
|
|
202
|
+
logger = get_logger()
|
|
203
|
+
console.print("[bold]Global Security Log[/bold] (no project sandbox active)")
|
|
204
|
+
|
|
205
|
+
events = logger.get_recent_events(limit=limit)
|
|
206
|
+
if not events:
|
|
207
|
+
console.print("[white]No events found.[/white]")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
table = Table()
|
|
211
|
+
table.add_column("Time", style="white")
|
|
212
|
+
table.add_column("Type")
|
|
213
|
+
table.add_column("Tool")
|
|
214
|
+
table.add_column("Decision", style="green")
|
|
215
|
+
table.add_column("Reason")
|
|
216
|
+
|
|
217
|
+
for e in events:
|
|
218
|
+
table.add_row(
|
|
219
|
+
str(e.get("timestamp", ""))[:19],
|
|
220
|
+
e.get("event_type", ""),
|
|
221
|
+
e.get("tool_name", ""),
|
|
222
|
+
e.get("decision", ""),
|
|
223
|
+
(e.get("decision_reason", "") or "")[:60],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
console.print(table)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dry_run.command("reset")
|
|
230
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation")
|
|
231
|
+
def sandbox_reset(confirm: bool):
|
|
232
|
+
"""Remove project .tweek/ and deregister."""
|
|
233
|
+
from tweek.sandbox.project import get_project_sandbox, _detect_project_dir
|
|
234
|
+
|
|
235
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
236
|
+
if not project_dir:
|
|
237
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
238
|
+
raise SystemExit(1)
|
|
239
|
+
|
|
240
|
+
tweek_dir = project_dir / ".tweek"
|
|
241
|
+
if not tweek_dir.exists():
|
|
242
|
+
console.print("[yellow]No .tweek/ directory found in this project.[/yellow]")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if not confirm:
|
|
246
|
+
console.print(f"[yellow]This will remove {tweek_dir} and all project-scoped security state.[/yellow]")
|
|
247
|
+
if not click.confirm("Continue?"):
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
sandbox = get_project_sandbox(os.getcwd())
|
|
251
|
+
if sandbox:
|
|
252
|
+
sandbox.reset()
|
|
253
|
+
console.print(f"[green]Project sandbox removed: {tweek_dir}[/green]")
|
|
254
|
+
else:
|
|
255
|
+
# Manual cleanup
|
|
256
|
+
shutil.rmtree(tweek_dir, ignore_errors=True)
|
|
257
|
+
from tweek.sandbox.registry import get_registry
|
|
258
|
+
get_registry().deregister(project_dir)
|
|
259
|
+
console.print(f"[green]Removed: {tweek_dir}[/green]")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dry_run.command("verify")
|
|
263
|
+
def sandbox_verify():
|
|
264
|
+
"""Test that project isolation is working."""
|
|
265
|
+
from tweek.sandbox.project import get_project_sandbox, _detect_project_dir
|
|
266
|
+
from tweek.sandbox.layers import IsolationLayer
|
|
267
|
+
|
|
268
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
269
|
+
if not project_dir:
|
|
270
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
271
|
+
raise SystemExit(1)
|
|
272
|
+
|
|
273
|
+
sandbox = get_project_sandbox(os.getcwd())
|
|
274
|
+
checks_passed = 0
|
|
275
|
+
checks_total = 0
|
|
276
|
+
|
|
277
|
+
# Check 1: Project detected
|
|
278
|
+
checks_total += 1
|
|
279
|
+
console.print(f" Project detected: {project_dir}", end="")
|
|
280
|
+
console.print(" [green]OK[/green]")
|
|
281
|
+
checks_passed += 1
|
|
282
|
+
|
|
283
|
+
# Check 2: Sandbox initialized
|
|
284
|
+
checks_total += 1
|
|
285
|
+
if sandbox and sandbox.is_initialized:
|
|
286
|
+
console.print(f" Sandbox initialized: {sandbox.tweek_dir}", end="")
|
|
287
|
+
console.print(" [green]OK[/green]")
|
|
288
|
+
checks_passed += 1
|
|
289
|
+
else:
|
|
290
|
+
console.print(" Sandbox initialized: [red]NO[/red]")
|
|
291
|
+
console.print(" [white]Run 'tweek dry-run init' to enable.[/white]")
|
|
292
|
+
|
|
293
|
+
# Check 3: Layer
|
|
294
|
+
checks_total += 1
|
|
295
|
+
if sandbox and sandbox.layer >= IsolationLayer.PROJECT:
|
|
296
|
+
console.print(f" Isolation layer: {sandbox.layer.value} ({sandbox.layer.name})", end="")
|
|
297
|
+
console.print(" [green]OK[/green]")
|
|
298
|
+
checks_passed += 1
|
|
299
|
+
else:
|
|
300
|
+
layer_val = sandbox.layer.value if sandbox else 0
|
|
301
|
+
console.print(f" Isolation layer: {layer_val} [yellow]BELOW PROJECT[/yellow]")
|
|
302
|
+
|
|
303
|
+
# Check 4: Security DB exists
|
|
304
|
+
checks_total += 1
|
|
305
|
+
if sandbox and (sandbox.tweek_dir / "security.db").exists():
|
|
306
|
+
console.print(" Project security.db: exists", end="")
|
|
307
|
+
console.print(" [green]OK[/green]")
|
|
308
|
+
checks_passed += 1
|
|
309
|
+
elif sandbox:
|
|
310
|
+
console.print(" Project security.db: [yellow]NOT FOUND[/yellow]")
|
|
311
|
+
else:
|
|
312
|
+
console.print(" Project security.db: [white]N/A (sandbox inactive)[/white]")
|
|
313
|
+
|
|
314
|
+
# Check 5: .gitignore
|
|
315
|
+
checks_total += 1
|
|
316
|
+
gitignore = project_dir / ".gitignore"
|
|
317
|
+
if gitignore.exists() and ".tweek" in gitignore.read_text():
|
|
318
|
+
console.print(" .gitignore includes .tweek/:", end="")
|
|
319
|
+
console.print(" [green]OK[/green]")
|
|
320
|
+
checks_passed += 1
|
|
321
|
+
else:
|
|
322
|
+
console.print(" .gitignore includes .tweek/: [yellow]NO[/yellow]")
|
|
323
|
+
|
|
324
|
+
console.print(f"\n [bold]{checks_passed}/{checks_total} checks passed[/bold]")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Docker bridge commands
|
|
328
|
+
@dry_run.group("docker")
|
|
329
|
+
def sandbox_docker():
|
|
330
|
+
"""Docker integration for container-level isolation."""
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@sandbox_docker.command("init")
|
|
335
|
+
def docker_init():
|
|
336
|
+
"""Generate Docker Sandbox config for this project."""
|
|
337
|
+
from tweek.sandbox.docker_bridge import DockerBridge
|
|
338
|
+
|
|
339
|
+
bridge = DockerBridge()
|
|
340
|
+
if not bridge.is_docker_available():
|
|
341
|
+
console.print("[red]Docker is not installed or not running.[/red]")
|
|
342
|
+
console.print("[white]Install Docker Desktop from https://www.docker.com/products/docker-desktop/[/white]")
|
|
343
|
+
raise SystemExit(1)
|
|
344
|
+
|
|
345
|
+
from tweek.sandbox.project import _detect_project_dir
|
|
346
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
347
|
+
if not project_dir:
|
|
348
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
349
|
+
raise SystemExit(1)
|
|
350
|
+
|
|
351
|
+
compose_path = bridge.init(project_dir)
|
|
352
|
+
console.print(f"[green]Docker Sandbox config generated: {compose_path}[/green]")
|
|
353
|
+
console.print("[white]Run 'tweek dry-run docker run' to start the container.[/white]")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@sandbox_docker.command("run")
|
|
357
|
+
def docker_run():
|
|
358
|
+
"""Launch container-isolated session (requires Docker)."""
|
|
359
|
+
from tweek.sandbox.docker_bridge import DockerBridge
|
|
360
|
+
from tweek.sandbox.project import _detect_project_dir
|
|
361
|
+
|
|
362
|
+
bridge = DockerBridge()
|
|
363
|
+
if not bridge.is_docker_available():
|
|
364
|
+
console.print("[red]Docker is not available.[/red]")
|
|
365
|
+
raise SystemExit(1)
|
|
366
|
+
|
|
367
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
368
|
+
if not project_dir:
|
|
369
|
+
console.print("[red]Not inside a project directory.[/red]")
|
|
370
|
+
raise SystemExit(1)
|
|
371
|
+
|
|
372
|
+
console.print("[bold]Launching Docker sandbox...[/bold]")
|
|
373
|
+
bridge.run(project_dir)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@sandbox_docker.command("status")
|
|
377
|
+
def docker_status():
|
|
378
|
+
"""Check Docker integration status."""
|
|
379
|
+
from tweek.sandbox.docker_bridge import DockerBridge
|
|
380
|
+
|
|
381
|
+
bridge = DockerBridge()
|
|
382
|
+
console.print(f"[bold]Docker available:[/bold] {bridge.is_docker_available()}")
|
|
383
|
+
|
|
384
|
+
from tweek.sandbox.project import _detect_project_dir
|
|
385
|
+
project_dir = _detect_project_dir(os.getcwd())
|
|
386
|
+
if project_dir:
|
|
387
|
+
compose = project_dir / ".tweek" / "docker-compose.yaml"
|
|
388
|
+
console.print(f"[bold]Docker config:[/bold] {'exists' if compose.exists() else 'not generated'}")
|
|
389
|
+
else:
|
|
390
|
+
console.print("[white]Not in a project directory.[/white]")
|