|
| 1 | +#!/usr/bin/env bun |
| 2 | + |
| 3 | +import * as p from "@clack/prompts"; |
| 4 | +import { existsSync, readFileSync } from "node:fs"; |
| 5 | +import { basename, join } from "node:path"; |
| 6 | +// ── Helpers ────────────────────────────────────────────────────────────────── |
| 7 | + |
| 8 | +function run(cmd: string, args: string[], opts?: { cwd?: string }) { |
| 9 | + return Bun.spawnSync([cmd, ...args], { stdout: "pipe", stderr: "pipe", ...opts }); |
| 10 | +} |
| 11 | + |
| 12 | +function which(tool: string): boolean { |
| 13 | + return run("which", [tool]).success; |
| 14 | +} |
| 15 | + |
| 16 | +// ── Git repo check ────────────────────────────────────────────────────────── |
| 17 | + |
| 18 | +function getGitRoot(): string | null { |
| 19 | + const result = run("git", ["rev-parse", "--show-toplevel"]); |
| 20 | + if (!result.success) return null; |
| 21 | + return result.stdout.toString().trim(); |
| 22 | +} |
| 23 | + |
| 24 | +// ── Dependency checks ─────────────────────────────────────────────────────── |
| 25 | + |
| 26 | +interface Dep { |
| 27 | + tool: string; |
| 28 | + required: boolean; |
| 29 | + hint: string; |
| 30 | +} |
| 31 | + |
| 32 | +const deps: Dep[] = [ |
| 33 | + { tool: "git", required: true, hint: "https://git-scm.com/downloads" }, |
| 34 | + { tool: "bun", required: true, hint: "https://bun.sh" }, |
| 35 | + { tool: "tmux", required: true, hint: "brew install tmux / sudo apt install tmux" }, |
| 36 | + { tool: "workmux", required: true, hint: "cargo install workmux or https://workmux.raine.dev" }, |
| 37 | + { tool: "gh", required: false, hint: "brew install gh then gh auth login" }, |
| 38 | + { tool: "docker", required: false, hint: "https://docs.docker.com/get-started/get-docker/" }, |
| 39 | +]; |
| 40 | + |
| 41 | +function checkDeps(): Dep[] { |
| 42 | + const missing: Dep[] = []; |
| 43 | + for (const dep of deps) { |
| 44 | + const found = which(dep.tool); |
| 45 | + if (found) { |
| 46 | + p.log.success(`${dep.tool} — found`); |
| 47 | + } else if (dep.required) { |
| 48 | + p.log.error(`${dep.tool} — not found (required)`); |
| 49 | + missing.push(dep); |
| 50 | + } else { |
| 51 | + p.log.warning(`${dep.tool} — not found (optional: ${dep.hint})`); |
| 52 | + } |
| 53 | + } |
| 54 | + return missing; |
| 55 | +} |
| 56 | + |
| 57 | +// ── .wmdev.yaml template ──────────────────────────────────────────────────── |
| 58 | + |
| 59 | +function detectProjectName(gitRoot: string): string { |
| 60 | + const pkgPath = join(gitRoot, "package.json"); |
| 61 | + if (existsSync(pkgPath)) { |
| 62 | + try { |
| 63 | + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); |
| 64 | + if (pkg.name) return pkg.name; |
| 65 | + } catch {} // malformed package.json, fall back to dir name |
| 66 | + } |
| 67 | + return basename(gitRoot); |
| 68 | +} |
| 69 | + |
| 70 | +function wmdevTemplate(name: string): string { |
| 71 | + return `# Project display name in the dashboard |
| 72 | +name: ${name} |
| 73 | +
|
| 74 | +# Service health monitoring — tracks port status for each worktree |
| 75 | +# Each worktree gets its own port range: base + (slot × step) |
| 76 | +services: |
| 77 | + - name: app |
| 78 | + portEnv: PORT |
| 79 | + portStart: 3000 # Port for the main branch (slot 0) |
| 80 | + portStep: 10 # Increment per worktree (3010, 3020, ...) |
| 81 | +
|
| 82 | +# Agent profiles determine how AI agents run in worktrees |
| 83 | +profiles: |
| 84 | + default: |
| 85 | + name: default |
| 86 | +
|
| 87 | + # --- Sandbox profile (uncomment to enable) --- |
| 88 | + # Runs agents in Docker containers for full isolation. |
| 89 | + # Requires: docker + a built image. |
| 90 | + # sandbox: |
| 91 | + # name: sandbox |
| 92 | + # image: my-project-sandbox |
| 93 | + # envPassthrough: # Env vars forwarded into the container |
| 94 | + # - DATABASE_URL |
| 95 | + # systemPrompt: > |
| 96 | + # You are running inside a sandboxed container. |
| 97 | + # Start the dev server with: npm run dev |
| 98 | +
|
| 99 | +# --- Linked repos (uncomment to enable) --- |
| 100 | +# Monitor PRs from related repos in the dashboard. |
| 101 | +# linkedRepos: |
| 102 | +# - repo: org/other-repo |
| 103 | +# alias: other |
| 104 | +
|
| 105 | +# --- Startup environment variables --- |
| 106 | +# Env vars automatically set for every new worktree. |
| 107 | +# startupEnvs: |
| 108 | +# NODE_ENV: development |
| 109 | +`; |
| 110 | +} |
| 111 | + |
| 112 | +// ── Main ───────────────────────────────────────────────────────────────────── |
| 113 | + |
| 114 | +p.intro("wmdev init"); |
| 115 | + |
| 116 | +// Step 1 — Git repo |
| 117 | +const gitRoot = getGitRoot(); |
| 118 | +if (!gitRoot) { |
| 119 | + p.log.error("Not inside a git repository. Run this from within a project."); |
| 120 | + p.outro("Aborted."); |
| 121 | + process.exit(1); |
| 122 | +} |
| 123 | +p.log.success(`Git root: ${gitRoot}`); |
| 124 | + |
| 125 | +// Step 2 — Dependency checks |
| 126 | +p.log.step("Checking dependencies..."); |
| 127 | + |
| 128 | +let missing = checkDeps(); |
| 129 | + |
| 130 | +while (missing.length > 0) { |
| 131 | + const lines = missing.map((d) => ` ${d.tool}: ${d.hint}`).join("\n"); |
| 132 | + p.note(lines, "Install these required dependencies"); |
| 133 | + |
| 134 | + const cont = await p.confirm({ message: "Press Enter once you've installed them..." }); |
| 135 | + if (p.isCancel(cont)) { |
| 136 | + p.outro("Aborted."); |
| 137 | + process.exit(1); |
| 138 | + } |
| 139 | + |
| 140 | + // Re-check all deps |
| 141 | + p.log.step("Re-checking dependencies..."); |
| 142 | + missing = checkDeps(); |
| 143 | +} |
| 144 | + |
| 145 | +// Step 3 — gh auth check |
| 146 | +if (which("gh")) { |
| 147 | + const ghAuth = run("gh", ["auth", "status"]); |
| 148 | + if (!ghAuth.success) { |
| 149 | + p.log.warning("gh is installed but not authenticated. Run: gh auth login"); |
| 150 | + } else { |
| 151 | + p.log.success("gh — authenticated"); |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +// Step 4 — .workmux.yaml |
| 156 | +p.log.step("Checking config files..."); |
| 157 | + |
| 158 | +const workmuxYaml = join(gitRoot, ".workmux.yaml"); |
| 159 | +if (existsSync(workmuxYaml)) { |
| 160 | + p.log.info(".workmux.yaml already exists, skipping"); |
| 161 | +} else { |
| 162 | + const s = p.spinner(); |
| 163 | + s.start("Running workmux init..."); |
| 164 | + const result = run("workmux", ["init"], { cwd: gitRoot }); |
| 165 | + if (result.success) { |
| 166 | + s.stop(".workmux.yaml created"); |
| 167 | + } else { |
| 168 | + s.stop("workmux init failed"); |
| 169 | + p.log.warning("Could not create .workmux.yaml. Run 'workmux init' manually."); |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +// Step 5 — .wmdev.yaml |
| 174 | +const wmdevYaml = join(gitRoot, ".wmdev.yaml"); |
| 175 | +if (existsSync(wmdevYaml)) { |
| 176 | + p.log.info(".wmdev.yaml already exists, skipping"); |
| 177 | +} else { |
| 178 | + const name = detectProjectName(gitRoot); |
| 179 | + await Bun.write(wmdevYaml, wmdevTemplate(name)); |
| 180 | + p.log.success(".wmdev.yaml created"); |
| 181 | +} |
| 182 | + |
| 183 | +// Step 6 — Summary |
| 184 | +p.note( |
| 185 | + `1. Edit .workmux.yaml to configure pane layout for your project |
| 186 | +2. Edit .wmdev.yaml to set up service ports and profiles |
| 187 | +3. Run: wmdev`, |
| 188 | + "Next steps", |
| 189 | +); |
| 190 | + |
| 191 | +p.outro("You're all set!"); |
0 commit comments