diff --git a/cli/package.json b/cli/package.json index 4c08544..8ebebe9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cleanslice/ranch", - "version": "0.1.12", + "version": "0.1.13", "type": "module", "description": "Ranch project CLI", "license": "MIT", diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts index c065243..3cccf49 100644 --- a/cli/src/commands/dev.ts +++ b/cli/src/commands/dev.ts @@ -3,7 +3,7 @@ import { consola } from "consola"; import { ensureRanchRoot } from "../utils/setup"; import { run } from "../utils/exec"; import { freePorts } from "../utils/ports"; -import { ensureK3dRunning } from "../utils/k3d"; +import { ensureK3dInstalled, ensureK3dNetwork, ensureK3dRunning } from "../utils/k3d"; import { ensurePortForwards } from "../utils/port-forward"; import { ensureDepsInstalled } from "../utils/deps"; import { ensureDockerRunning } from "../utils/docker"; @@ -46,8 +46,15 @@ export const devCommand = defineCommand({ } const needsDocker = !target || target === "api"; + const needsK3d = !args["no-k3d"] && needsDocker; if (needsDocker) { ensureDockerRunning(); + if (needsK3d) { + // Fail before any image pull — compose needs k3d-ranch, which k3d creates. + ensureK3dInstalled(); + } else { + ensureK3dNetwork({ createIfMissing: true }); + } await ensureBrowserPoolImage(root); } @@ -80,9 +87,9 @@ export const devCommand = defineCommand({ turboArgs.push(`--filter=${target}`); } - const needsK3d = !args["no-k3d"] && (!target || target === "api"); if (needsK3d) { await ensureK3dRunning(root); + ensureK3dNetwork(); ensurePortForwards([ { label: "Argo Workflows", namespace: "argo", service: "argo-workflows-server", port: 2746 }, ]); diff --git a/cli/src/utils/k3d.test.ts b/cli/src/utils/k3d.test.ts new file mode 100644 index 0000000..27e3a21 --- /dev/null +++ b/cli/src/utils/k3d.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test"; +import { k3dInstallHint } from "./k3d"; + +describe("k3dInstallHint", () => { + test("windows uses winget/choco/scoop", () => { + const hint = k3dInstallHint("win32"); + expect(hint).toContain("winget install"); + expect(hint).toContain("choco install k3d"); + expect(hint).not.toContain("brew install k3d"); + }); + + test("mac uses brew", () => { + expect(k3dInstallHint("darwin")).toBe("brew install k3d"); + }); + + test("linux uses the k3d install script", () => { + expect(k3dInstallHint("linux")).toContain("install.sh"); + }); +}); diff --git a/cli/src/utils/k3d.ts b/cli/src/utils/k3d.ts index aa0fc9c..0d54ea1 100644 --- a/cli/src/utils/k3d.ts +++ b/cli/src/utils/k3d.ts @@ -1,4 +1,4 @@ -import { execSync, spawn } from "node:child_process"; +import { execSync, spawn, spawnSync } from "node:child_process"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; @@ -10,8 +10,54 @@ const READY_TIMEOUT_MS = 90_000; const READY_POLL_MS = 2_000; const CLUSTER = "ranch"; +export const K3D_NETWORK = "k3d-ranch"; const KUBECONFIG_LOCAL = join(homedir(), ".kube", "ranch-local.yaml"); +export function k3dInstallHint(platform = process.platform): string { + if (platform === "win32") { + return "winget install -e --id k3d.k3d\n or: choco install k3d\n or: scoop install k3d"; + } + if (platform === "darwin") { + return "brew install k3d"; + } + return "curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash"; +} + +export function ensureK3dInstalled(): void { + if (hasBinary("k3d")) return; + consola.error( + `k3d is not installed. compose needs the ${K3D_NETWORK} docker network that k3d creates — install it before ranch pulls any images.\n\nInstall:\n ${k3dInstallHint()}\n\nThen re-run \`ranch dev\` (it creates the cluster).`, + ); + process.exit(1); +} + +export function k3dNetworkExists(): boolean { + const result = spawnSync("docker", ["network", "inspect", K3D_NETWORK], { + stdio: "ignore", + windowsHide: true, + }); + return result.status === 0; +} + +export function ensureK3dNetwork(opts: { createIfMissing?: boolean } = {}): void { + if (k3dNetworkExists()) return; + if (opts.createIfMissing) { + consola.start(`Creating docker network ${K3D_NETWORK}...`); + const created = spawnSync("docker", ["network", "create", K3D_NETWORK], { + encoding: "utf8", + windowsHide: true, + }); + if (created.status === 0 && k3dNetworkExists()) { + consola.success(`Created ${K3D_NETWORK} (no k3d cluster)`); + return; + } + } + consola.error( + `Docker network ${K3D_NETWORK} is missing. Install k3d and re-run \`ranch dev\`, or pass --no-k3d to create a dummy network.\n\nInstall:\n ${k3dInstallHint()}`, + ); + process.exit(1); +} + type ClusterState = "running" | "stopped" | "missing"; function clusterState(): ClusterState { @@ -125,10 +171,7 @@ async function bootstrap(root: string): Promise { } export async function ensureK3dRunning(root: string): Promise { - if (!hasBinary("k3d")) { - consola.warn("k3d not installed — skipping cluster start. Install: brew install k3d"); - return; - } + ensureK3dInstalled(); const state = clusterState(); if (state === "running") { diff --git a/scripts/docker-up.sh b/scripts/docker-up.sh index 1485e2a..ced1316 100755 --- a/scripts/docker-up.sh +++ b/scripts/docker-up.sh @@ -7,6 +7,36 @@ set -o pipefail cd "$(dirname "$0")/../api" || exit 1 +k3d_install_hint() { + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) echo "winget install -e --id k3d.k3d (or: choco install k3d / scoop install k3d)" ;; + Darwin*) echo "brew install k3d" ;; + *) echo "curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash" ;; + esac +} + +# Fail before compose starts pulling images. The k3d-ranch network is +# `external: true` — compose otherwise downloads postgres/minio and then dies. +K3D_NET=k3d-ranch +if ! docker network inspect "$K3D_NET" >/dev/null 2>&1; then + cat >&2 <&2 @@ -54,7 +84,10 @@ EOF reach them. It's created by k3d when the local cluster exists. Fix: - 1. brew install k3d (if not installed) + 1. Install k3d: + Windows: winget install -e --id k3d.k3d + macOS: brew install k3d + Linux: curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash 2. k3d cluster create ranch --api-port 6550 --wait 3. Re-run `ranch dev` (it also does this automatically when k3d is installed — see cli/src/utils/k3d.ts).