Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cmd/ggo/studio/studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewStudioCmd() *cobra.Command {
Supported platforms:
- wsl: Windows Subsystem for Linux (Windows only)
- colima: Colima container runtime (macOS/Linux)
- apple-container: Apple Container (macOS 26+)
- docker: Native Docker
- k8s: Kubernetes (kind, minikube, etc.)
- auto: Auto-detect best available platform
Expand All @@ -57,6 +58,9 @@ Examples:
# Create with specific mode
ggo studio create my-studio --mode wsl -s "https://..."

# Create with Apple Container (macOS 26+)
ggo studio create my-studio --mode apple-container -s "https://..."

# Create with specific Colima profile
ggo studio create my-studio --mode colima --colima-profile myprofile

Expand Down Expand Up @@ -178,7 +182,7 @@ Examples:
RunE: runCreate,
}

cmd.Flags().StringVarP(&mode, "mode", "m", "", "Container/VM mode (wsl, colima, docker, k8s, auto)")
cmd.Flags().StringVarP(&mode, "mode", "m", "", "Container/VM mode (wsl, colima, apple-container, docker, k8s, auto)")
cmd.Flags().StringVarP(&image, "image", "i", "tensorfusion/studio-torch:latest", "Container image")
cmd.Flags().StringVarP(&shareLink, "share-link", "s", "", "Share link or share code to remote vGPU worker (required)")
cmd.Flags().StringVar(&serverURL, "server", api.GetDefaultBaseURL(), "Server URL for resolving share links")
Expand Down Expand Up @@ -813,8 +817,10 @@ func (r *backendsResult) RenderTUI(out *tui.Output) {
out.Println()
out.Println(styles.Subtitle.Render("Install one of the following:"))
out.Println()
out.Println(" • " + styles.Bold.Render("Apple Container (macOS 26+):") + " " + tui.URL("https://github.com/apple/container/releases"))
out.Println(" • " + styles.Bold.Render("Docker:") + " " + tui.URL("https://docs.docker.com/get-docker/"))
out.Println(" • " + styles.Bold.Render("Colima (macOS):") + " " + tui.Code("brew install colima"))
out.Println(" • " + styles.Bold.Render("OrbStack (macOS):") + " " + tui.Code("brew install orbstack"))
out.Println(" • " + styles.Bold.Render("WSL (Windows):") + " " + tui.URL("https://docs.microsoft.com/en-us/windows/wsl/install"))
out.Println()
return
Expand Down
42 changes: 42 additions & 0 deletions docs/plans/2026-02-06-apple-container-backend-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Apple Container Backend Design

**Date:** 2026-02-06

## Goal
Enable `ggo studio create` to use Apple Container on macOS 26+ as a first-choice fallback when no Docker socket is available, while preserving existing Docker/Colima/OrbStack flows and providing clear install/upgrade guidance.

## Key Behaviors
- Replace the `apple` mode with `apple-container` across CLI flags, help text, and internal mode constants.
- On macOS 26+:
- If no Docker socket is found, prefer Apple Container for auto mode.
- If `--mode apple-container` is specified but Apple Container is missing, prompt install.
- If no Docker socket is found and Apple Container is missing, prompt install.
- On macOS < 26:
- If `--mode apple-container` is specified, error with OS upgrade requirement.
- If no runtime is available, recommend installing Colima (plus other options) and note Apple Container needs macOS 26.

## Backend Strategy
- Implement Apple Container backend using the `container` CLI (not Docker).
- Detect availability with `container system status` and presence of the `container` CLI.
- Implement list/get/start/stop/exec/logs/delete via `container` subcommands.
- Parse JSON from `container list --format json` / `container inspect` to build `Environment` objects.

## Runtime Detection
- Add helper for macOS major version detection using `runtime` + `internal/platform` helper.
- Add Docker socket discovery for common paths:
- `DOCKER_HOST` (unix socket)
- `/var/run/docker.sock`
- `~/.colima/*/docker.sock`
- `~/.orbstack/run/docker.sock`
- Use socket presence to decide preference order on macOS 26+.

## User-Facing Messaging
- Update CLI help strings and examples to include `apple-container`.
- Provide install guidance for:
- Apple Container: download signed pkg from GitHub releases.
- Colima/OrbStack: `brew install` suggestions.

## Testing
- Add ginkgo tests covering backend selection rules and macOS version gating.
- Run `container` CLI end-to-end tests on a common arm64 image (e.g., `alpine:latest`), ensuring create/list/exec/logs/stop/delete flows work.

194 changes: 194 additions & 0 deletions docs/plans/2026-02-06-apple-container-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Apple Container Backend Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add Apple Container (`container` CLI) support for `ggo studio create` on macOS 26+, with correct auto-selection, install/upgrade messaging, and updated CLI/docs/UI strings.

**Architecture:** Introduce platform helpers for macOS version and Docker socket detection, then update backend selection to prefer Apple Container only when no Docker socket is present on macOS 26+. Rebuild the Apple backend around the `container` CLI with JSON parsing for list/inspect, and ensure install/runtime hints are precise.

**Tech Stack:** Go, Cobra, Ginkgo/Gomega, Apple `container` CLI.

---

### Task 1: Add Ginkgo suite + failing tests for macOS selection rules

**Files:**
- Create: `internal/studio/studio_suite_test.go`
- Create: `internal/studio/manager_apple_ginkgo_test.go`

**Step 1: Write the failing test**
```go
var _ = Describe("Apple container selection", func() {
It("prefers apple-container on macOS 26+ when no Docker socket is present", func() {
// stub platform helpers to darwin/26/no-socket
// register apple + docker backends (both available)
// expect ModeAppleContainer for ModeAuto
})

It("prefers docker/colima when Docker socket exists on macOS 26+", func() {
// stub platform helpers to darwin/26/has-socket
// register apple + docker backends (both available)
// expect ModeDocker for ModeAuto
})

It("rejects apple-container on macOS < 26 when explicitly requested", func() {
// stub platform helpers to darwin/25
// expect error mentioning macOS 26 upgrade
})
})
```

**Step 2: Run test to verify it fails**
Run: `go test ./internal/studio -run Apple`
Expected: FAIL with missing helper stubs / selection logic not implemented

**Step 3: Write minimal implementation**
Implement platform helper stubs and selection logic hooks (function vars) to satisfy the tests.

**Step 4: Run test to verify it passes**
Run: `go test ./internal/studio -run Apple`
Expected: PASS

**Step 5: Commit**
```bash
git add internal/studio/studio_suite_test.go internal/studio/manager_apple_ginkgo_test.go
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "test: add apple-container selection tests"
```

### Task 2: Implement macOS version + Docker socket helpers and update Manager selection/hints

**Files:**
- Create: `internal/platform/macos_version_darwin.go`
- Create: `internal/platform/macos_version_other.go`
- Create: `internal/platform/docker_socket.go`
- Modify: `internal/studio/manager.go`

**Step 1: Write the failing test**
Extend existing Ginkgo tests (Task 1) to assert explicit error messaging for unsupported macOS version and missing Apple Container install guidance.

**Step 2: Run test to verify it fails**
Run: `go test ./internal/studio -run Apple`
Expected: FAIL with error text mismatch or missing helpers

**Step 3: Write minimal implementation**
- Add `platform.MacOSMajorVersion()` using `syscall.Sysctl("kern.osproductversion")` on darwin; return `0` elsewhere.
- Add `platform.HasDockerSocket()` to scan `DOCKER_HOST`, `/var/run/docker.sock`, `~/.colima/*/docker.sock`, `~/.orbstack/run/docker.sock`.
- Update `Manager.detectBestBackend` to:
- On darwin 26+: prefer apple-container only when no Docker socket is found.
- On darwin < 26: exclude apple-container from auto preference list.
- Update `platformBackendHint` to include install guidance:
- Apple Container: download pkg from GitHub releases.
- Colima/OrbStack: `brew install`.
- Docker: link to Docker Desktop.
- When `--mode apple-container` is requested on macOS < 26, return a clear upgrade error.

**Step 4: Run test to verify it passes**
Run: `go test ./internal/studio -run Apple`
Expected: PASS

**Step 5: Commit**
```bash
git add internal/platform/macos_version_darwin.go internal/platform/macos_version_other.go internal/platform/docker_socket.go internal/studio/manager.go
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "feat: add macOS version/socket helpers and selection rules"
```

### Task 3: Rebuild Apple backend to use `container` CLI + parsing helpers

**Files:**
- Modify: `internal/studio/backend_apple.go`
- Create: `internal/studio/apple_container_parse_test.go`

**Step 1: Write the failing test**
```go
var _ = Describe("Apple container parsing", func() {
It("maps container list JSON to Environment and SSH port", func() {
// Provide sample JSON from `container list --format json`
// Expect label filtering, SSH port extraction, GPU_WORKER_URL parsing
})
})
```

**Step 2: Run test to verify it fails**
Run: `go test ./internal/studio -run Apple`
Expected: FAIL (parsing helpers not implemented)

**Step 3: Write minimal implementation**
- Replace docker CLI usage with `container` CLI subcommands:
- `container system status` for availability
- `container system start` in `EnsureRunning`
- `container run --detach` for create
- `container list --format json` for list
- `container inspect` for get
- `container exec`, `container logs`, `container start`, `container stop`, `container delete --force`
- Add JSON parsing helpers for list/inspect output.
- Normalize memory suffixes for `container` CLI (`Gi`->`G`, `Mi`->`M`).
- Use labels `ggo.managed=true`, `ggo.name`, `ggo.mode=apple-container`.

**Step 4: Run test to verify it passes**
Run: `go test ./internal/studio -run Apple`
Expected: PASS

**Step 5: Commit**
```bash
git add internal/studio/backend_apple.go internal/studio/apple_container_parse_test.go
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "feat: implement apple-container backend via container CLI"
```

### Task 4: Update CLI/Docs/UI strings for `apple-container`

**Files:**
- Modify: `internal/studio/types.go`
- Modify: `cmd/ggo/studio/studio.go`
- Modify: `docs/studio-guide.md`
- Modify: `vscode-extension/src/views/createStudioPanel.ts`

**Step 1: Write the failing test**
(Documentation/UI change; no automated test required)

**Step 2: Run test to verify it fails**
Skip

**Step 3: Write minimal implementation**
- Replace `apple` mode string with `apple-container`.
- Update CLI help, examples, and backend listing output to include install guidance.
- Update Studio guide table and VS Code UI mode labels.

**Step 4: Run test to verify it passes**
Run: `go test ./cmd/ggo/studio ./internal/studio`
Expected: PASS

**Step 5: Commit**
```bash
git add internal/studio/types.go cmd/ggo/studio/studio.go docs/studio-guide.md vscode-extension/src/views/createStudioPanel.ts
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "docs: update apple-container mode strings"
```

### Task 5: Full functional Apple Container CLI test (manual)

**Files:**
- None (manual verification)

**Step 1: Run container services**
Run: `container system start`
Expected: services start successfully

**Step 2: Run a common arm64 image**
Run: `container run --name ggo-apple-test --detach --rm -p 18022:22/tcp alpine:latest sleep 600`
Expected: container ID printed, `container list` shows it running

**Step 3: Exec/logs/stop/delete**
Run:
- `container exec ggo-apple-test sh -c "echo ok"`
- `container logs ggo-apple-test`
- `container stop ggo-apple-test`
Expected: commands succeed

**Step 4: Record results**
Note any failures or deviations.

---

**Global verification after each Go change:**
- Run: `golangci-lint run --fix`
- Run: `go test ./...` (at least once before finalization)

2 changes: 1 addition & 1 deletion docs/studio-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ggo studio create my-studio -s abc123 --mode docker
| `docker` | 原生 Docker | 所有 |
| `colima` | Colima 容器运行时 | macOS/Linux |
| `wsl` | Windows Subsystem for Linux | Windows |
| `apple` | Apple Virtualization Framework | macOS |
| `apple-container` | Apple Container(macOS 26+) | macOS |

### 卷挂载(Volume Mounts)

Expand Down
55 changes: 55 additions & 0 deletions internal/platform/docker_socket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package platform

import (
"os"
"path/filepath"
"strings"
)

// HasDockerSocket checks common Docker socket locations for macOS runtimes.
func HasDockerSocket() bool {
if socketPath := dockerHostSocketPath(os.Getenv("DOCKER_HOST")); socketPath != "" {
if fileExists(socketPath) {
return true
}
}

if fileExists("/var/run/docker.sock") {
return true
}

homeDir, err := os.UserHomeDir()
if err != nil {
return false
}

colimaGlob := filepath.Join(homeDir, ".colima", "*", "docker.sock")
if matches, err := filepath.Glob(colimaGlob); err == nil {
for _, match := range matches {
if fileExists(match) {
return true
}
}
}

orbstackSock := filepath.Join(homeDir, ".orbstack", "run", "docker.sock")
return fileExists(orbstackSock)
}

func dockerHostSocketPath(dockerHost string) string {
if dockerHost == "" {
return ""
}
if !strings.HasPrefix(dockerHost, "unix://") {
return ""
}
return strings.TrimPrefix(dockerHost, "unix://")
}

func fileExists(path string) bool {
if path == "" {
return false
}
_, err := os.Stat(path)
return err == nil
}
31 changes: 31 additions & 0 deletions internal/platform/macos_version_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build darwin

package platform

import (
"strconv"
"strings"
"syscall"
)

// MacOSMajorVersion returns the macOS major version (e.g., 26).
// Returns 0 when the version cannot be determined.
func MacOSMajorVersion() int {
version, err := syscall.Sysctl("kern.osproductversion")
if err != nil {
return 0
}
version = strings.TrimSpace(version)
if version == "" {
return 0
}
parts := strings.Split(version, ".")
if len(parts) == 0 {
return 0
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0
}
return major
}
8 changes: 8 additions & 0 deletions internal/platform/macos_version_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !darwin

package platform

// MacOSMajorVersion returns 0 on non-macOS platforms.
func MacOSMajorVersion() int {
return 0
}
Loading