eleven / messenger Public

tools: pass flags through `./tools/dev <command>` (#1219)

`./tools/dev up --clean` printed "Unknown command" — dispatch joined the whole
argv into the lookup key ("up --clean"), so the `up` handler that already
forwarded flags was never reached. Match on the command words alone and hand
everything after them to the command.

`test go` now forwards args too (`-run TestFoo`, package paths), keeping ./...
unless packages are named. `test` and `test web` refuse stray args instead of
misdirecting them — npm's test script is an `&&` chain, so `npm test -- args`
would reach only the last suite.
Co-authored-by: Claude <noreply@anthropic.com>
James Adam fd98125aac76c5f9dbb8bf255281515953df1393 parent fa01466
2 files changed, +56 −14
  • CLAUDE.md +5 −2
  • tools/dev +51 −12
diff --git a/CLAUDE.md b/CLAUDE.md
index 4449c4b..1468a9b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -646,13 +646,16 @@ Simple command-line interface for common dev tasks. Run from anywhere in the rep
./tools/dev run mac # Launch the Mac app
./tools/dev run ios # Run iOS app on simulator
./tools/dev alt [slug] # Build + launch a side-by-side dev alt of the Mac app
-./tools/dev up # Start local dev server (persistent config)
+./tools/dev up [flags] # Start local dev server (persistent config)
# + the webhook-ingest sidecar (--no-hooks skips)
+ # flags pass to lchat-dev-up: --clean, --onboarding,
+ # --port N, --help
./tools/dev seed-mac # Seed Mac app with dev server credentials
./tools/dev link # Print dev server sign-in link
./tools/dev test # Run full test suite (Go + Node)
./tools/dev test web # Run web tests only
-./tools/dev test go # Run Go tests only
+./tools/dev test go [args] # Run Go tests only (args go to `go test`,
+ # e.g. -run TestFoo ./internal/bot)
```
All build and test commands are thin wrappers around the underlying toolchain (xcodebuild, go, npm). The `dev` subcommands orchestrate the dev server (`tools/lchat-dev-up.mjs`) and app seeding scripts. The `run` commands automatically build if needed, so you can skip the explicit build step. **Reuse and extend this as the single entry point for all dev workflows** — don't hand-roll xcodebuild invocations or embed them in Claude prompts. When adding new dev commands, document them here and in the script's usage header.
diff --git a/tools/dev b/tools/dev
index cfaa769..adb744c 100755
--- a/tools/dev
+++ b/tools/dev
@@ -7,14 +7,17 @@
// ./tools/dev run mac Launch the Mac app
// ./tools/dev alt [slug] Build + launch a side-by-side dev alt of the Mac app
// ./tools/dev run ios Run the iOS app on simulator (see instructions)
-// ./tools/dev up Start the local dev server (persistent, reusable config)
-// + the webhook-ingest sidecar (skip with --no-hooks)
+// ./tools/dev up [flags] Start the local dev server (persistent, reusable config)
+// + the webhook-ingest sidecar. Flags after the command
+// pass straight to tools/lchat-dev-up.mjs: --clean,
+// --onboarding, --no-hooks, --port N, --help.
// ./tools/dev seed-mac Seed the Mac app with dev server credentials
// ./tools/dev seed-ios Seed the iOS app with dev server credentials
// ./tools/dev link Print the sign-in link for the dev server
// ./tools/dev test Run the test suite (Go + Node)
// ./tools/dev test web Run web tests only
-// ./tools/dev test go Run Go tests only
+// ./tools/dev test go [args] Run Go tests only (args forwarded to go test,
+// e.g. -run TestFoo ./internal/bot)
//
// All commands assume you're at the repo root. Use ./tools/dev to invoke from anywhere.
@@ -132,7 +135,7 @@ const commands = {
"up": async () => {
console.log("Starting dev server (lchat-dev-up)...");
// Pass flags through (--clean, --onboarding, --no-hooks, --port N, …).
- await runAsync("node", ["tools/lchat-dev-up.mjs", ...process.argv.slice(3)]);
+ await runAsync("node", ["tools/lchat-dev-up.mjs", ...cmdArgs]);
},
"down": () => {
@@ -348,6 +351,12 @@ const commands = {
},
test: () => {
+ // Both suites run here, so there's no unambiguous target for extra args —
+ // say so rather than silently handing Go's flags to npm (or vice versa).
+ if (cmdArgs.length) {
+ console.error("Extra arguments need a specific suite: ./tools/dev test go|web <args>");
+ process.exit(1);
+ }
console.log("Running full test suite (Go + Node)...");
run("go", ["test", "./..."], { cwd: REPO_ROOT });
run("npm", ["test"], { cwd: REPO_ROOT });
@@ -355,6 +364,14 @@ const commands = {
},
"test web": () => {
+ // The npm "test" script is a long `&&` chain, so `npm test -- args` would
+ // hand them to the LAST file only — silently not what anyone means. Run a
+ // single suite by invoking node directly instead.
+ if (cmdArgs.length) {
+ console.error("npm test takes no arguments (it chains every suite).");
+ console.error("Run one suite directly, e.g.: node test/format.test.mjs");
+ process.exit(1);
+ }
console.log("Running web (Node) tests...");
run("npm", ["test"], { cwd: REPO_ROOT });
console.log("✓ Web tests passed");
@@ -362,7 +379,11 @@ const commands = {
"test go": () => {
console.log("Running Go tests...");
- run("go", ["test", "./..."], { cwd: REPO_ROOT });
+ // e.g. ./tools/dev test go -run TestFoo — flags alone still mean the whole
+ // tree, so keep ./... unless the caller named packages themselves (bare
+ // `go test -run X` would quietly test only the repo-root package).
+ const named = cmdArgs.some((a) => !a.startsWith("-") && (a.includes("/") || a === "."));
+ run("go", ["test", ...cmdArgs, ...(named ? [] : ["./..."])], { cwd: REPO_ROOT });
console.log("✓ Go tests passed");
},
};
@@ -438,9 +459,24 @@ async function altMac(args) {
run("open", [appPath]);
}
-const cmd = process.argv.slice(2).join(" ");
+// Command names are one or two bare words ("up", "build mac"); everything
+// after them belongs to the command. Match on the words alone — joining the
+// whole argv made any flag part of the lookup key, so `up --clean` fell
+// through to "Unknown command" instead of reaching up's pass-through.
+const argv = process.argv.slice(2);
+const words = [];
+for (const a of argv) {
+ if (a.startsWith("-")) break;
+ words.push(a);
+ if (words.length === 2) break;
+}
+const cmd = commands[words.join(" ")] ? words.join(" ") : words[0] || "";
+const cmdArgs = argv.slice(cmd ? cmd.split(" ").length : 0);
+// `-h`/`--help` is ours only when it stands alone — after a command word it
+// belongs to that command (`up --help` prints the dev server's own usage).
+const wantsHelp = cmd === "help" || (!cmd && argv.some((a) => a === "-h" || a === "--help"));
-if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
+if (!cmd || wantsHelp) {
console.log("dev — messenger dev CLI\n");
console.log("Usage:");
console.log(" ./tools/dev build mac Build native Mac app");
@@ -450,19 +486,22 @@ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
console.log(" (orange icon; --icon live = green, the");
console.log(" convention for an alt aimed at prod home)");
console.log(" ./tools/dev run ios Run iOS app on simulator");
- console.log(" ./tools/dev up Start local dev server");
+ console.log(" ./tools/dev up [flags] Start local dev server (flags go to");
+ console.log(" lchat-dev-up: --clean, --onboarding,");
+ console.log(" --no-hooks, --port N, --help)");
console.log(" ./tools/dev seed-mac Seed Mac app with dev credentials");
console.log(" ./tools/dev seed-ios Seed iOS app with dev credentials");
console.log(" ./tools/dev link Print dev server sign-in link");
console.log(" ./tools/dev test Run all tests");
console.log(" ./tools/dev test web Run web tests only");
- console.log(" ./tools/dev test go Run Go tests only");
+ console.log(" ./tools/dev test go [args] Run Go tests only (args go to go test,");
+ console.log(" e.g. -run TestFoo ./internal/bot)");
console.log("");
- process.exit(cmd ? 0 : 1);
+ process.exit(wantsHelp ? 0 : 1);
}
-if (process.argv[2] === "alt") {
- Promise.resolve(altMac(process.argv.slice(3))).catch((e) => {
+if (cmd === "alt") {
+ Promise.resolve(altMac(cmdArgs)).catch((e) => {
console.error(`Error: ${e.message}`);
process.exit(1);
});