Skip to content

Commit 542edb8

Browse files
hugocasaclaude
andcommitted
refactor: move init-authoring helpers into the backend
The scaffold + repo-detection + headless-Claude-run helpers that `webmux init` uses (buildStarterTemplate, detectInitProjectContext, buildInitPromptSpec, buildInitAgentCommand, runInitAgentCommand, stream parsing) move from bin/src/init-helpers.ts to backend/src/services/init-authoring.ts so the server can drive the same flow for an upcoming "set up project on add" feature. `bin/src/init.ts` now imports them from the backend. Generic process/git/repo primitives (run, which, getGitRoot, detectProjectName, RunResult) move to backend/src/lib/shell.ts and are re-exported from bin/src/shared.ts, so every existing `./shared` consumer is unchanged and the backend can share them without a backend→bin import. Test moves alongside the code into backend/src/__tests__. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 034b460 commit 542edb8

5 files changed

Lines changed: 52 additions & 41 deletions

File tree

bin/src/init-helpers.test.ts renamed to backend/src/__tests__/init-authoring.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { afterEach, describe, expect, it } from "bun:test";
22
import { mkdtemp, rm } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
5-
import { loadConfig } from "../../backend/src/adapters/config.ts";
5+
import { loadConfig } from "../adapters/config";
66
import {
77
buildInitAgentCommand,
88
buildInitPromptSpec,
99
buildStarterTemplate,
1010
detectInitProjectContext,
1111
parseInitAgentStreamLine,
12-
} from "./init-helpers.ts";
12+
} from "../services/init-authoring";
1313

1414
describe("buildInitAgentCommand", () => {
1515
const prompt = {

backend/src/lib/shell.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { basename, join } from "node:path";
3+
4+
export interface RunResult {
5+
success: boolean;
6+
stdout: Buffer;
7+
stderr: Buffer;
8+
}
9+
10+
/** Run a command synchronously and capture its output. Shared by the CLI
11+
* (re-exported from bin/src/shared.ts) and backend code that needs to shell
12+
* out (e.g. repo detection for project setup). */
13+
export function run(cmd: string, args: string[], opts?: { cwd?: string }): RunResult {
14+
const result = Bun.spawnSync([cmd, ...args], { stdout: "pipe", stderr: "pipe", ...opts });
15+
return {
16+
success: result.success,
17+
stdout: result.stdout as Buffer,
18+
stderr: result.stderr as Buffer,
19+
};
20+
}
21+
22+
export function which(tool: string): boolean {
23+
return run("which", [tool]).success;
24+
}
25+
26+
export function getGitRoot(): string | null {
27+
const result = run("git", ["rev-parse", "--show-toplevel"]);
28+
if (!result.success) return null;
29+
return result.stdout.toString().trim();
30+
}
31+
32+
export function detectProjectName(gitRoot: string): string {
33+
const pkgPath = join(gitRoot, "package.json");
34+
if (existsSync(pkgPath)) {
35+
try {
36+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
37+
if (pkg.name) return pkg.name;
38+
} catch {} // malformed package.json, fall back to dir name
39+
}
40+
return basename(gitRoot);
41+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, readFileSync, rmSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
4-
import { detectProjectName, run } from "./shared.ts";
4+
import { detectProjectName, run } from "../lib/shell";
55

66
export type InitAuthoringChoice = "claude" | "codex" | "manual";
77
export type InitAgent = Exclude<InitAuthoringChoice, "manual">;

bin/src/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
type InitAgent,
1414
type InitAgentStreamEvent,
1515
type InitAuthoringChoice,
16-
} from "./init-helpers.ts";
16+
} from "../../backend/src/services/init-authoring.ts";
1717

1818
// ── Dependency checks ───────────────────────────────────────────────────────
1919

bin/src/shared.ts

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,12 @@
1-
import { existsSync, readFileSync, realpathSync } from "node:fs";
2-
import { basename, dirname, join, resolve } from "node:path";
1+
import { dirname, resolve } from "node:path";
2+
import { realpathSync } from "node:fs";
33
import { createApi } from "@webmux/api-contract";
4+
import { run } from "../../backend/src/lib/shell";
45

5-
export interface RunResult {
6-
success: boolean;
7-
stdout: Buffer;
8-
stderr: Buffer;
9-
}
10-
11-
export function run(cmd: string, args: string[], opts?: { cwd?: string }): RunResult {
12-
const result = Bun.spawnSync([cmd, ...args], { stdout: "pipe", stderr: "pipe", ...opts });
13-
return {
14-
success: result.success,
15-
stdout: result.stdout as Buffer,
16-
stderr: result.stderr as Buffer,
17-
};
18-
}
19-
20-
export function which(tool: string): boolean {
21-
return run("which", [tool]).success;
22-
}
23-
24-
export function getGitRoot(): string | null {
25-
const result = run("git", ["rev-parse", "--show-toplevel"]);
26-
if (!result.success) return null;
27-
return result.stdout.toString().trim();
28-
}
6+
// Generic process/git/repo primitives live in the backend lib so backend code
7+
// (e.g. project setup) can share them; re-export here so existing CLI imports
8+
// from "./shared" keep working.
9+
export { run, which, getGitRoot, detectProjectName, type RunResult } from "../../backend/src/lib/shell";
2910

3011
/**
3112
* Thrown by argparse functions to signal usage errors (e.g. missing flag value,
@@ -34,17 +15,6 @@ export function getGitRoot(): string | null {
3415
*/
3516
export class CommandUsageError extends Error {}
3617

37-
export function detectProjectName(gitRoot: string): string {
38-
const pkgPath = join(gitRoot, "package.json");
39-
if (existsSync(pkgPath)) {
40-
try {
41-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
42-
if (pkg.name) return pkg.name;
43-
} catch {} // malformed package.json, fall back to dir name
44-
}
45-
return basename(gitRoot);
46-
}
47-
4818
/**
4919
* When the webmux server isn't reachable the bare error message is unhelpful to
5020
* users. Older Bun threw a `TypeError: fetch failed`; current Bun throws

0 commit comments

Comments
 (0)