Skip to content

Commit a09918f

Browse files
centdixcodex
andauthored
fix: parse serve flags after subcommand (#95)
* use haiku * fix: parse serve flags after subcommand Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 83907d1 commit a09918f

3 files changed

Lines changed: 134 additions & 108 deletions

File tree

.webmux.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ workspace:
77

88
auto_name:
99
provider: claude
10+
model: haiku
1011

1112
startupEnvs:
1213
NODE_ENV: development

bin/src/webmux.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { dirname, join } from "node:path";
55
import { fileURLToPath } from "node:url";
6+
import { parseRootArgs } from "./webmux";
67

78
const tempDirs: string[] = [];
89
const decoder = new TextDecoder();
910
const webmuxEntry = join(dirname(fileURLToPath(import.meta.url)), "webmux.ts");
11+
const originalBackendPort = process.env.BACKEND_PORT;
1012

1113
function runOrThrow(cmd: string[], cwd: string): void {
1214
const result = Bun.spawnSync(cmd, {
@@ -33,9 +35,36 @@ async function initRepo(repoRoot: string): Promise<void> {
3335

3436
describe("webmux entrypoint", () => {
3537
afterEach(async () => {
38+
if (originalBackendPort === undefined) {
39+
delete process.env.BACKEND_PORT;
40+
} else {
41+
process.env.BACKEND_PORT = originalBackendPort;
42+
}
3643
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
3744
});
3845

46+
it("parses serve flags after the subcommand", () => {
47+
delete process.env.BACKEND_PORT;
48+
49+
expect(parseRootArgs(["serve", "--port", "8080", "--debug"])).toEqual({
50+
port: 8080,
51+
debug: true,
52+
command: "serve",
53+
commandArgs: [],
54+
});
55+
});
56+
57+
it("leaves service subcommand flags untouched", () => {
58+
delete process.env.BACKEND_PORT;
59+
60+
expect(parseRootArgs(["service", "install", "--port", "8080"])).toEqual({
61+
port: 5111,
62+
debug: false,
63+
command: "service",
64+
commandArgs: ["install", "--port", "8080"],
65+
});
66+
});
67+
3968
it("runs worktree commands from a project subdirectory", async () => {
4069
const repoRoot = await mkdtemp(join(tmpdir(), "webmux-cli-"));
4170
tempDirs.push(repoRoot);

bin/src/webmux.ts

Lines changed: 104 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ function isRootCommand(value: string): value is NonNullable<RootCommand> {
5757
|| value === "merge";
5858
}
5959

60-
function parseRootArgs(args: string[]): ParsedRootArgs {
60+
function isServeRootOption(value: string): boolean {
61+
return value === "--port" || value === "--debug" || value === "--help" || value === "-h";
62+
}
63+
64+
export function parseRootArgs(args: string[]): ParsedRootArgs {
6165
let port = parseInt(process.env.BACKEND_PORT || "5111", 10);
6266
let debug = false;
6367
let command: RootCommand = null;
@@ -67,7 +71,7 @@ function parseRootArgs(args: string[]): ParsedRootArgs {
6771
const arg = args[index];
6872
if (!arg) continue;
6973

70-
if (command) {
74+
if (command && (command !== "serve" || !isServeRootOption(arg))) {
7175
commandArgs.push(arg);
7276
continue;
7377
}
@@ -117,40 +121,6 @@ function isWorktreeCommand(command: RootCommand): command is "add" | "list" | "o
117121
|| command === "merge";
118122
}
119123

120-
// ── Parse args ───────────────────────────────────────────────────────────────
121-
122-
const args = process.argv.slice(2);
123-
let parsed: ParsedRootArgs;
124-
125-
try {
126-
parsed = parseRootArgs(args);
127-
} catch (error) {
128-
console.error(error instanceof Error ? error.message : String(error));
129-
process.exit(1);
130-
}
131-
132-
if (parsed.command === "init") {
133-
await import("./init.ts");
134-
process.exit(0);
135-
}
136-
137-
if (parsed.command === "service") {
138-
const { default: service } = await import("./service.ts");
139-
await service(parsed.commandArgs);
140-
process.exit(0);
141-
}
142-
143-
if (parsed.command === "update") {
144-
console.log("Updating webmux to the latest version...");
145-
const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
146-
stdin: "inherit",
147-
stdout: "inherit",
148-
stderr: "inherit",
149-
});
150-
const code = await proc.exited;
151-
process.exit(code);
152-
}
153-
154124
// ── Load env files from CWD (.env.local overrides .env) ─────────────────────
155125

156126
async function loadEnvFile(path: string) {
@@ -169,43 +139,6 @@ async function loadEnvFile(path: string) {
169139
}
170140
}
171141

172-
await loadEnvFile(resolve(process.cwd(), ".env.local"));
173-
await loadEnvFile(resolve(process.cwd(), ".env"));
174-
175-
if (isWorktreeCommand(parsed.command)) {
176-
const { runWorktreeCommand } = await import("./worktree-commands.ts");
177-
const exitCode = await runWorktreeCommand({
178-
command: parsed.command,
179-
args: parsed.commandArgs,
180-
projectDir: process.cwd(),
181-
port: parsed.port,
182-
});
183-
process.exit(exitCode);
184-
}
185-
186-
// ── No command → show help ───────────────────────────────────────────────────
187-
188-
if (parsed.command === null) {
189-
usage();
190-
process.exit(0);
191-
}
192-
193-
// ── serve: Check for .webmux.yaml ────────────────────────────────────────────
194-
195-
if (!existsSync(resolve(process.cwd(), ".webmux.yaml"))) {
196-
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
197-
process.exit(1);
198-
}
199-
200-
// ── Shared env for child processes ───────────────────────────────────────────
201-
202-
const baseEnv = {
203-
...process.env,
204-
BACKEND_PORT: String(parsed.port),
205-
WEBMUX_PROJECT_DIR: process.cwd(),
206-
...(parsed.debug ? { WEBMUX_DEBUG: "1" } : {}),
207-
};
208-
209142
// ── Prefixed output ──────────────────────────────────────────────────────────
210143

211144
function pipeWithPrefix(stream: ReadableStream<Uint8Array>, prefix: string) {
@@ -230,50 +163,113 @@ function pipeWithPrefix(stream: ReadableStream<Uint8Array>, prefix: string) {
230163
})();
231164
}
232165

233-
// ── Process management ───────────────────────────────────────────────────────
166+
async function main(args: string[] = process.argv.slice(2)): Promise<void> {
167+
let parsed: ParsedRootArgs;
168+
169+
try {
170+
parsed = parseRootArgs(args);
171+
} catch (error) {
172+
console.error(error instanceof Error ? error.message : String(error));
173+
process.exit(1);
174+
}
175+
176+
if (parsed.command === "init") {
177+
await import("./init.ts");
178+
process.exit(0);
179+
}
180+
181+
if (parsed.command === "service") {
182+
const { default: service } = await import("./service.ts");
183+
await service(parsed.commandArgs);
184+
process.exit(0);
185+
}
234186

235-
const children: Subprocess[] = [];
236-
let exiting = false;
187+
if (parsed.command === "update") {
188+
console.log("Updating webmux to the latest version...");
189+
const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
190+
stdin: "inherit",
191+
stdout: "inherit",
192+
stderr: "inherit",
193+
});
194+
const code = await proc.exited;
195+
process.exit(code);
196+
}
197+
198+
await loadEnvFile(resolve(process.cwd(), ".env.local"));
199+
await loadEnvFile(resolve(process.cwd(), ".env"));
200+
201+
if (isWorktreeCommand(parsed.command)) {
202+
const { runWorktreeCommand } = await import("./worktree-commands.ts");
203+
const exitCode = await runWorktreeCommand({
204+
command: parsed.command,
205+
args: parsed.commandArgs,
206+
projectDir: process.cwd(),
207+
port: parsed.port,
208+
});
209+
process.exit(exitCode);
210+
}
211+
212+
if (parsed.command === null) {
213+
usage();
214+
process.exit(0);
215+
}
237216

238-
function cleanup() {
239-
if (exiting) return;
240-
exiting = true;
241-
for (const child of children) {
242-
try { child.kill("SIGTERM"); } catch {}
217+
if (!existsSync(resolve(process.cwd(), ".webmux.yaml"))) {
218+
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
219+
process.exit(1);
243220
}
244-
// Force-kill stragglers after 1s, then exit
245-
setTimeout(() => {
221+
222+
const baseEnv = {
223+
...process.env,
224+
BACKEND_PORT: String(parsed.port),
225+
WEBMUX_PROJECT_DIR: process.cwd(),
226+
...(parsed.debug ? { WEBMUX_DEBUG: "1" } : {}),
227+
};
228+
229+
const children: Subprocess[] = [];
230+
let exiting = false;
231+
232+
function cleanup() {
233+
if (exiting) return;
234+
exiting = true;
246235
for (const child of children) {
247-
try { child.kill("SIGKILL"); } catch {}
236+
try { child.kill("SIGTERM"); } catch {}
248237
}
249-
process.exit(0);
250-
}, 1000).unref();
251-
}
238+
setTimeout(() => {
239+
for (const child of children) {
240+
try { child.kill("SIGKILL"); } catch {}
241+
}
242+
process.exit(0);
243+
}, 1000).unref();
244+
}
252245

253-
process.on("SIGINT", cleanup);
254-
process.on("SIGTERM", cleanup);
246+
process.on("SIGINT", cleanup);
247+
process.on("SIGTERM", cleanup);
255248

256-
// ── Start ────────────────────────────────────────────────────────────────────
249+
const backendEntry = join(PKG_ROOT, "backend", "dist", "server.js");
250+
const staticDir = join(PKG_ROOT, "frontend", "dist");
257251

258-
const backendEntry = join(PKG_ROOT, "backend", "dist", "server.js");
259-
const staticDir = join(PKG_ROOT, "frontend", "dist");
252+
if (!existsSync(staticDir)) {
253+
console.error(
254+
`Error: frontend/dist/ not found. Run 'bun run build' first.`,
255+
);
256+
process.exit(1);
257+
}
260258

261-
if (!existsSync(staticDir)) {
262-
console.error(
263-
`Error: frontend/dist/ not found. Run 'bun run build' first.`,
264-
);
265-
process.exit(1);
266-
}
259+
console.log(`Starting webmux on port ${parsed.port}...`);
267260

268-
console.log(`Starting webmux on port ${parsed.port}...`);
261+
const be = Bun.spawn(["bun", backendEntry], {
262+
env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
263+
stdout: "pipe",
264+
stderr: "pipe",
265+
});
266+
children.push(be);
267+
pipeWithPrefix(be.stdout, "[BE]");
268+
pipeWithPrefix(be.stderr, "[BE]");
269269

270-
const be = Bun.spawn(["bun", backendEntry], {
271-
env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
272-
stdout: "pipe",
273-
stderr: "pipe",
274-
});
275-
children.push(be);
276-
pipeWithPrefix(be.stdout, "[BE]");
277-
pipeWithPrefix(be.stderr, "[BE]");
270+
await be.exited;
271+
}
278272

279-
await be.exited;
273+
if (import.meta.main) {
274+
await main();
275+
}

0 commit comments

Comments
 (0)