Skip to content

Commit 6e8b128

Browse files
centdixclaude
andauthored
feat: add wmdev init onboarding command (#65)
* feat: add `wmdev init` onboarding command Interactive CLI wizard using @clack/prompts that validates the environment, checks required/optional dependencies, and scaffolds .workmux.yaml and .wmdev.yaml config files for new projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: convert bin/init.js to TypeScript Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback - Use explicit process.exit(0) in wmdev.js - Remove step numbers from helper section comments - Add comment on empty catch block in detectProjectName Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move bin scripts to TypeScript with bundled output - Move bin/wmdev.js → bin/src/wmdev.ts (with types) - Move bin/init.ts → bin/src/init.ts - Build step bundles both into bin/wmdev.js (init inlined) - Move @clack/prompts to devDependencies (bundled, no runtime dep) - Published package ships only bin/wmdev.js (self-contained) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6a2206d commit 6e8b128

4 files changed

Lines changed: 207 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ frontend/node_modules/
66
frontend/bun.lock
77
backend/dist/
88
frontend/dist/
9+
bin/wmdev.js
910
frontend/.vite/
1011
public/
1112
.env

bin/src/init.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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!");

bin/wmdev.js renamed to bin/src/wmdev.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { resolve, dirname, join } from "node:path";
44
import { existsSync } from "node:fs";
55
import { fileURLToPath } from "node:url";
6+
import type { Subprocess } from "bun";
67

78
// ── Helpers ──────────────────────────────────────────────────────────────────
89

@@ -14,6 +15,7 @@ wmdev — Dev dashboard for managing Git worktrees
1415
1516
Usage:
1617
wmdev Start the dashboard
18+
wmdev init Interactive project setup
1719
wmdev --port N Set port (default: 5111)
1820
wmdev --debug Show debug-level logs
1921
wmdev --help Show this help message
@@ -26,6 +28,12 @@ Environment:
2628
// ── Parse args ───────────────────────────────────────────────────────────────
2729

2830
const args = process.argv.slice(2);
31+
32+
if (args[0] === "init") {
33+
await import("./init.ts");
34+
process.exit(0);
35+
}
36+
2937
let port = parseInt(process.env.BACKEND_PORT || "5111");
3038
let debug = false;
3139

@@ -54,7 +62,7 @@ for (let i = 0; i < args.length; i++) {
5462

5563
// ── Load env files from CWD (.env.local overrides .env) ─────────────────────
5664

57-
async function loadEnvFile(path) {
65+
async function loadEnvFile(path: string) {
5866
if (!existsSync(path)) return;
5967
const lines = (await Bun.file(path).text()).split("\n");
6068
for (const line of lines) {
@@ -79,7 +87,7 @@ const baseEnv = { ...process.env, BACKEND_PORT: String(port), WMDEV_PROJECT_DIR:
7987

8088
// ── Prefixed output ──────────────────────────────────────────────────────────
8189

82-
function pipeWithPrefix(stream, prefix) {
90+
function pipeWithPrefix(stream: ReadableStream<Uint8Array>, prefix: string) {
8391
const reader = stream.getReader();
8492
const decoder = new TextDecoder();
8593
let buffer = "";
@@ -90,7 +98,7 @@ function pipeWithPrefix(stream, prefix) {
9098
if (done) break;
9199
buffer += decoder.decode(value, { stream: true });
92100
const lines = buffer.split("\n");
93-
buffer = lines.pop();
101+
buffer = lines.pop()!;
94102
for (const line of lines) {
95103
console.log(`${prefix} ${line}`);
96104
}
@@ -103,7 +111,7 @@ function pipeWithPrefix(stream, prefix) {
103111

104112
// ── Process management ───────────────────────────────────────────────────────
105113

106-
const children = [];
114+
const children: Subprocess[] = [];
107115
let exiting = false;
108116

109117
function cleanup() {

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@
2626
"scripts": {
2727
"dev": "bash dev.sh",
2828
"start": "bun bin/wmdev.js",
29-
"build": "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js",
29+
"build": "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js && bun build bin/src/wmdev.ts --target=bun --outfile=bin/wmdev.js",
3030
"prepublishOnly": "bun run build",
3131
"test": "bun run --cwd backend test && bun run --cwd frontend test",
3232
"test:coverage": "bun run --cwd backend test --coverage && bun run --cwd frontend test:coverage"
3333
},
3434
"files": [
35-
"bin/",
35+
"bin/wmdev.js",
3636
"backend/dist/",
3737
"frontend/dist/"
3838
],
3939
"devDependencies": {
40+
"@clack/prompts": "^1.1.0",
4041
"@sveltejs/vite-plugin-svelte": "^5.0.0",
4142
"@tailwindcss/vite": "^4.2.0",
4243
"@types/bun": "latest",

0 commit comments

Comments
 (0)