codebyplan 1.13.15 → 1.13.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -2
- package/dist/cli.js +440 -39
- package/package.json +1 -1
- package/templates/hooks/README.md +23 -1
- package/templates/settings.project.base.json +10 -0
- package/templates/skills/cbp-round-execute/SKILL.md +18 -0
- package/templates/skills/cbp-setup-cmux/SKILL.md +170 -0
- package/templates/skills/cbp-task-complete/SKILL.md +14 -0
- package/templates/skills/cbp-task-start/SKILL.md +8 -0
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Print the CLI version.
|
|
|
91
91
|
|
|
92
92
|
When you run Claude Code inside a [cmux](https://github.com/nicholasgasior/cmux) workspace, the `codebyplan` plugin automatically keeps the workspace metadata in sync with your git context:
|
|
93
93
|
|
|
94
|
-
- **On session start** — the workspace title is set to the current git branch
|
|
94
|
+
- **On session start** — the workspace title is set to the current git branch, the workspace description is set to the repo folder basename, and the workspace color is applied from `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). If no color is set, a one-line nudge is printed to session output.
|
|
95
95
|
- **On `git checkout` / `git switch`** — the same sync runs automatically after the Bash tool call completes (the match is broad and a redundant sync on a file-restore checkout is harmless, since `codebyplan cmux-sync` is idempotent).
|
|
96
96
|
|
|
97
97
|
This means your cmux workspace always reflects which branch and repo you're working in, without any manual intervention.
|
|
@@ -109,7 +109,7 @@ Both hooks delegate all logic to `codebyplan cmux-sync` — no cmux or git logic
|
|
|
109
109
|
|
|
110
110
|
### Binary resolution order
|
|
111
111
|
|
|
112
|
-
The `cmux` binary is resolved in this order (by `codebyplan cmux-sync`):
|
|
112
|
+
The `cmux` binary is resolved in this order (by `codebyplan cmux-sync` and `codebyplan cmux-status`, both delegating to `resolveCmuxBin()` in `lib/cmux.ts`):
|
|
113
113
|
|
|
114
114
|
1. `$CMUX_BUNDLED_CLI_PATH` — path cmux injects into its Claude hook environment
|
|
115
115
|
2. `$CMUX_CLAUDE_HOOK_CMUX_BIN` — alternative env var the hook environment may set
|
|
@@ -119,6 +119,81 @@ The `cmux` binary is resolved in this order (by `codebyplan cmux-sync`):
|
|
|
119
119
|
|
|
120
120
|
Both hooks check for `$CMUX_WORKSPACE_ID` before doing anything. If you are not running inside a cmux workspace, the hooks exit immediately with no output and no side effects. Repos that do not use cmux are completely unaffected.
|
|
121
121
|
|
|
122
|
+
### Status surface
|
|
123
|
+
|
|
124
|
+
`codebyplan cmux-status` pushes CodeByPlan development state into the cmux workspace sidebar. The lifecycle skills (`cbp-task-start`, `cbp-task-complete`, `cbp-round-execute`) call it automatically — no manual invocation is needed.
|
|
125
|
+
|
|
126
|
+
**Flags:**
|
|
127
|
+
|
|
128
|
+
| Flag | Description |
|
|
129
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
130
|
+
| `--checkpoint <text>` | Set the `cbp-checkpoint` status key |
|
|
131
|
+
| `--task <text>` | Set the `cbp-task` status key |
|
|
132
|
+
| `--qa <text>` | Set the `cbp-qa` status key |
|
|
133
|
+
| `--progress <val>` | Set the sidebar progress bar (0.0–1.0 float, or `N/D` fraction) |
|
|
134
|
+
| `--clear` | Clear all four keys from the sidebar |
|
|
135
|
+
|
|
136
|
+
**Examples:**
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Pushed by cbp-task-start when you start a task:
|
|
140
|
+
npx codebyplan cmux-status --checkpoint "CHK-176: cmux status" --task "TASK-2: implement cmux-status"
|
|
141
|
+
|
|
142
|
+
# Pushed by cbp-task-complete when a task finishes:
|
|
143
|
+
npx codebyplan cmux-status --task "TASK-2: implement cmux-status done" --progress 2/5
|
|
144
|
+
|
|
145
|
+
# Pushed by cbp-round-execute after each round:
|
|
146
|
+
npx codebyplan cmux-status --qa "R1 completed"
|
|
147
|
+
|
|
148
|
+
# Clear all status keys:
|
|
149
|
+
npx codebyplan cmux-status --clear
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**`auto_status` toggle.** The command is gated by the `auto_status` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_status` is `false`, every call is a no-op. The default is `true` — status pushing is enabled as long as the file exists or is absent (absent defaults to enabled).
|
|
153
|
+
|
|
154
|
+
**No-op outside cmux.** Like `codebyplan cmux-sync`, this command checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately with no output, so it is safe to call unconditionally from scripts and hooks.
|
|
155
|
+
|
|
156
|
+
### Auto dev server (`codebyplan cmux-serve`)
|
|
157
|
+
|
|
158
|
+
`codebyplan cmux-serve` auto-starts the dev server for any app whose source files are touched by the current round, and opens a cmux browser pane pointing at the allocated port. `cbp-round-execute` calls it automatically at round-execution start — no manual invocation is needed.
|
|
159
|
+
|
|
160
|
+
**How it works:**
|
|
161
|
+
|
|
162
|
+
The command receives the round's changed file list via `--files <comma-separated paths>`. It reads `.codebyplan/server.json` `port_allocations[]` and resolves which apps' source directories intersect the file list. For each matching app it:
|
|
163
|
+
|
|
164
|
+
1. Probes the allocated port (500ms `node:net` connection attempt — no `curl`, no `timeout`).
|
|
165
|
+
2. If the port is **not** listening: creates a `cmux new-split down` terminal pane and sends the dev command via `cmux send`.
|
|
166
|
+
3. Opens a browser pane via `cmux new-pane --type browser --url http://localhost:<port>`.
|
|
167
|
+
|
|
168
|
+
If the port is already listening (another worktree, or a previously started server) it skips straight to step 3 — only the browser pane is opened. This mitigates the multi-worktree port collision by never starting a second server on an already-occupied port.
|
|
169
|
+
|
|
170
|
+
**App-source-dir heuristic mapping** (from `port_allocations[].label` when `command` / `working_dir` are null):
|
|
171
|
+
|
|
172
|
+
| Label | App dir |
|
|
173
|
+
| ------------------------------ | -------------------------- |
|
|
174
|
+
| `Web Dev` | `apps/web` |
|
|
175
|
+
| `Web Dev (codebyplan-desktop)` | `apps/desktop` |
|
|
176
|
+
| `Backend Dev` | `apps/backend` |
|
|
177
|
+
| `MCP Dev` | `apps/mcp` |
|
|
178
|
+
| `Docs Ingest` | `apps/docs-ingest` |
|
|
179
|
+
| `E2E Tests` | skipped (not a dev server) |
|
|
180
|
+
|
|
181
|
+
When `allocation.command` and `allocation.working_dir` are both set (Tier 1), those values are used directly — the label heuristic is skipped. When no mapping exists for a label, one log line is emitted and the allocation is skipped; the rest continue.
|
|
182
|
+
|
|
183
|
+
**`auto_dev_server` toggle.** Gated by the `auto_dev_server` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_dev_server` is `false`, every call is a no-op. The default is `true` — the feature is enabled as long as the file is absent or the key is not explicitly set to `false`.
|
|
184
|
+
|
|
185
|
+
**No-op outside cmux.** Like `codebyplan cmux-status`, this command checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately with no output and no side effects.
|
|
186
|
+
|
|
187
|
+
**Examples:**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Called automatically by cbp-round-execute with the round's files:
|
|
191
|
+
npx codebyplan cmux-serve --files "apps/web/src/app/page.tsx,apps/web/src/components/Button.tsx"
|
|
192
|
+
|
|
193
|
+
# Target a specific app dir instead of a file list:
|
|
194
|
+
npx codebyplan cmux-serve --app apps/backend
|
|
195
|
+
```
|
|
196
|
+
|
|
122
197
|
---
|
|
123
198
|
|
|
124
199
|
## MCP Server
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.13.
|
|
17
|
+
VERSION = "1.13.16";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -1987,6 +1987,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
1987
1987
|
JSON.stringify({}, null, 2) + "\n",
|
|
1988
1988
|
"utf-8"
|
|
1989
1989
|
);
|
|
1990
|
+
await writeFile6(
|
|
1991
|
+
join9(codebyplanDir, "cmux.json"),
|
|
1992
|
+
JSON.stringify({}, null, 2) + "\n",
|
|
1993
|
+
"utf-8"
|
|
1994
|
+
);
|
|
1990
1995
|
const statuslinePath = join9(codebyplanDir, "statusline.json");
|
|
1991
1996
|
let statuslineExists = false;
|
|
1992
1997
|
try {
|
|
@@ -2004,7 +2009,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
2004
2009
|
await writeLocalConfig(projectPath, { device_id: deviceId });
|
|
2005
2010
|
console.log(` Created ${codebyplanDir}/`);
|
|
2006
2011
|
console.log(
|
|
2007
|
-
` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, statusline.json`
|
|
2012
|
+
` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, cmux.json, statusline.json`
|
|
2008
2013
|
);
|
|
2009
2014
|
console.log(` device.local.json (gitignored)`);
|
|
2010
2015
|
const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
|
|
@@ -2081,8 +2086,8 @@ async function runSetup() {
|
|
|
2081
2086
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
2082
2087
|
let branch = "main";
|
|
2083
2088
|
try {
|
|
2084
|
-
const { execSync:
|
|
2085
|
-
branch =
|
|
2089
|
+
const { execSync: execSync11 } = await import("node:child_process");
|
|
2090
|
+
branch = execSync11("git symbolic-ref --short HEAD", {
|
|
2086
2091
|
cwd: projectPath,
|
|
2087
2092
|
encoding: "utf-8"
|
|
2088
2093
|
}).trim();
|
|
@@ -3720,9 +3725,9 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3720
3725
|
Install ${missingPkgs.length} missing packages? [Y/n] `
|
|
3721
3726
|
);
|
|
3722
3727
|
if (confirmed) {
|
|
3723
|
-
const { execSync:
|
|
3728
|
+
const { execSync: execSync11 } = await import("node:child_process");
|
|
3724
3729
|
try {
|
|
3725
|
-
|
|
3730
|
+
execSync11(installCmd, { cwd: projectPath, stdio: "inherit" });
|
|
3726
3731
|
console.log(" Packages installed.\n");
|
|
3727
3732
|
} catch (err) {
|
|
3728
3733
|
console.error(
|
|
@@ -6623,6 +6628,42 @@ var init_upload_e2e_images = __esm({
|
|
|
6623
6628
|
}
|
|
6624
6629
|
});
|
|
6625
6630
|
|
|
6631
|
+
// src/lib/cmux.ts
|
|
6632
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
6633
|
+
import { join as join22 } from "node:path";
|
|
6634
|
+
function insideCmux() {
|
|
6635
|
+
return !!process.env.CMUX_WORKSPACE_ID;
|
|
6636
|
+
}
|
|
6637
|
+
function resolveCmuxBin() {
|
|
6638
|
+
return process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
|
|
6639
|
+
}
|
|
6640
|
+
function readCmuxConfig(projectRoot) {
|
|
6641
|
+
let raw = {};
|
|
6642
|
+
try {
|
|
6643
|
+
const text = readFileSync6(
|
|
6644
|
+
join22(projectRoot, ".codebyplan", "cmux.json"),
|
|
6645
|
+
"utf-8"
|
|
6646
|
+
);
|
|
6647
|
+
raw = JSON.parse(text);
|
|
6648
|
+
} catch {
|
|
6649
|
+
raw = {};
|
|
6650
|
+
}
|
|
6651
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
6652
|
+
raw = {};
|
|
6653
|
+
}
|
|
6654
|
+
const config = raw;
|
|
6655
|
+
return {
|
|
6656
|
+
...config,
|
|
6657
|
+
auto_status: config.auto_status ?? true,
|
|
6658
|
+
auto_dev_server: config.auto_dev_server ?? true
|
|
6659
|
+
};
|
|
6660
|
+
}
|
|
6661
|
+
var init_cmux = __esm({
|
|
6662
|
+
"src/lib/cmux.ts"() {
|
|
6663
|
+
"use strict";
|
|
6664
|
+
}
|
|
6665
|
+
});
|
|
6666
|
+
|
|
6626
6667
|
// src/cli/cmux-sync.ts
|
|
6627
6668
|
var cmux_sync_exports = {};
|
|
6628
6669
|
__export(cmux_sync_exports, {
|
|
@@ -6632,10 +6673,10 @@ import { execSync as execSync8, execFileSync as execFileSync2 } from "node:child
|
|
|
6632
6673
|
import { basename as basename2 } from "node:path";
|
|
6633
6674
|
async function runCmuxSync() {
|
|
6634
6675
|
try {
|
|
6635
|
-
if (!
|
|
6676
|
+
if (!insideCmux()) {
|
|
6636
6677
|
process.exit(0);
|
|
6637
6678
|
}
|
|
6638
|
-
const bin =
|
|
6679
|
+
const bin = resolveCmuxBin();
|
|
6639
6680
|
let branch = "";
|
|
6640
6681
|
try {
|
|
6641
6682
|
branch = execSync8("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -6644,8 +6685,9 @@ async function runCmuxSync() {
|
|
|
6644
6685
|
} catch {
|
|
6645
6686
|
}
|
|
6646
6687
|
let folder = "";
|
|
6688
|
+
let toplevel = "";
|
|
6647
6689
|
try {
|
|
6648
|
-
|
|
6690
|
+
toplevel = execSync8("git rev-parse --show-toplevel", {
|
|
6649
6691
|
encoding: "utf8"
|
|
6650
6692
|
}).trim();
|
|
6651
6693
|
folder = basename2(toplevel);
|
|
@@ -6675,6 +6717,26 @@ async function runCmuxSync() {
|
|
|
6675
6717
|
} catch {
|
|
6676
6718
|
}
|
|
6677
6719
|
}
|
|
6720
|
+
try {
|
|
6721
|
+
const cmuxCfg = readCmuxConfig(toplevel || process.cwd());
|
|
6722
|
+
if (typeof cmuxCfg.workspace_color === "string" && cmuxCfg.workspace_color !== "") {
|
|
6723
|
+
try {
|
|
6724
|
+
execFileSync2(bin, [
|
|
6725
|
+
"workspace-action",
|
|
6726
|
+
"--action",
|
|
6727
|
+
"set-color",
|
|
6728
|
+
"--color",
|
|
6729
|
+
cmuxCfg.workspace_color
|
|
6730
|
+
]);
|
|
6731
|
+
} catch {
|
|
6732
|
+
}
|
|
6733
|
+
} else {
|
|
6734
|
+
process.stdout.write(
|
|
6735
|
+
"cmux: no workspace color set \u2014 run /cbp-setup-cmux\n"
|
|
6736
|
+
);
|
|
6737
|
+
}
|
|
6738
|
+
} catch {
|
|
6739
|
+
}
|
|
6678
6740
|
process.exit(0);
|
|
6679
6741
|
} catch (err) {
|
|
6680
6742
|
if (err instanceof ProcessExitSignal) throw err;
|
|
@@ -6685,23 +6747,346 @@ var init_cmux_sync = __esm({
|
|
|
6685
6747
|
"src/cli/cmux-sync.ts"() {
|
|
6686
6748
|
"use strict";
|
|
6687
6749
|
init_process_exit_signal();
|
|
6750
|
+
init_cmux();
|
|
6751
|
+
}
|
|
6752
|
+
});
|
|
6753
|
+
|
|
6754
|
+
// src/cli/cmux-status.ts
|
|
6755
|
+
var cmux_status_exports = {};
|
|
6756
|
+
__export(cmux_status_exports, {
|
|
6757
|
+
normalizeProgress: () => normalizeProgress,
|
|
6758
|
+
runCmuxStatus: () => runCmuxStatus
|
|
6759
|
+
});
|
|
6760
|
+
import { execSync as execSync9, execFileSync as execFileSync3 } from "node:child_process";
|
|
6761
|
+
function normalizeProgress(raw) {
|
|
6762
|
+
if (raw.includes("/")) {
|
|
6763
|
+
const [numStr, denStr] = raw.split("/", 2);
|
|
6764
|
+
const num = parseInt(numStr ?? "", 10);
|
|
6765
|
+
const den = parseInt(denStr ?? "", 10);
|
|
6766
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0) return "0";
|
|
6767
|
+
const ratio = num / den;
|
|
6768
|
+
const clamped2 = Math.max(0, Math.min(1, ratio));
|
|
6769
|
+
return String(clamped2);
|
|
6770
|
+
}
|
|
6771
|
+
const f = parseFloat(raw);
|
|
6772
|
+
if (!Number.isFinite(f)) return null;
|
|
6773
|
+
const clamped = Math.max(0, Math.min(1, f));
|
|
6774
|
+
return String(clamped);
|
|
6775
|
+
}
|
|
6776
|
+
async function runCmuxStatus(args) {
|
|
6777
|
+
try {
|
|
6778
|
+
if (!insideCmux()) {
|
|
6779
|
+
process.exit(0);
|
|
6780
|
+
}
|
|
6781
|
+
let toplevel = "";
|
|
6782
|
+
try {
|
|
6783
|
+
toplevel = execSync9("git rev-parse --show-toplevel", {
|
|
6784
|
+
encoding: "utf8"
|
|
6785
|
+
}).trim();
|
|
6786
|
+
} catch {
|
|
6787
|
+
toplevel = process.cwd();
|
|
6788
|
+
}
|
|
6789
|
+
const cfg = readCmuxConfig(toplevel);
|
|
6790
|
+
if (cfg.auto_status === false) {
|
|
6791
|
+
process.exit(0);
|
|
6792
|
+
}
|
|
6793
|
+
let checkpoint;
|
|
6794
|
+
let task;
|
|
6795
|
+
let qa;
|
|
6796
|
+
let progress;
|
|
6797
|
+
let clear = false;
|
|
6798
|
+
for (let i = 0; i < args.length; i++) {
|
|
6799
|
+
const flag = args[i];
|
|
6800
|
+
if (flag === "--checkpoint" && i + 1 < args.length) {
|
|
6801
|
+
checkpoint = args[++i];
|
|
6802
|
+
} else if (flag === "--task" && i + 1 < args.length) {
|
|
6803
|
+
task = args[++i];
|
|
6804
|
+
} else if (flag === "--qa" && i + 1 < args.length) {
|
|
6805
|
+
qa = args[++i];
|
|
6806
|
+
} else if (flag === "--progress" && i + 1 < args.length) {
|
|
6807
|
+
progress = args[++i];
|
|
6808
|
+
} else if (flag === "--clear") {
|
|
6809
|
+
clear = true;
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
const bin = resolveCmuxBin();
|
|
6813
|
+
if (clear) {
|
|
6814
|
+
try {
|
|
6815
|
+
execFileSync3(bin, ["clear-status", "cbp-checkpoint"]);
|
|
6816
|
+
} catch {
|
|
6817
|
+
}
|
|
6818
|
+
try {
|
|
6819
|
+
execFileSync3(bin, ["clear-status", "cbp-task"]);
|
|
6820
|
+
} catch {
|
|
6821
|
+
}
|
|
6822
|
+
try {
|
|
6823
|
+
execFileSync3(bin, ["clear-status", "cbp-qa"]);
|
|
6824
|
+
} catch {
|
|
6825
|
+
}
|
|
6826
|
+
try {
|
|
6827
|
+
execFileSync3(bin, ["clear-progress"]);
|
|
6828
|
+
} catch {
|
|
6829
|
+
}
|
|
6830
|
+
} else {
|
|
6831
|
+
if (checkpoint !== void 0) {
|
|
6832
|
+
try {
|
|
6833
|
+
execFileSync3(bin, ["set-status", "cbp-checkpoint", checkpoint]);
|
|
6834
|
+
} catch {
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
if (task !== void 0) {
|
|
6838
|
+
try {
|
|
6839
|
+
execFileSync3(bin, ["set-status", "cbp-task", task]);
|
|
6840
|
+
} catch {
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
if (qa !== void 0) {
|
|
6844
|
+
try {
|
|
6845
|
+
execFileSync3(bin, ["set-status", "cbp-qa", qa]);
|
|
6846
|
+
} catch {
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
if (progress !== void 0) {
|
|
6850
|
+
const decimalStr = normalizeProgress(progress);
|
|
6851
|
+
if (decimalStr !== null) {
|
|
6852
|
+
try {
|
|
6853
|
+
execFileSync3(bin, ["set-progress", decimalStr]);
|
|
6854
|
+
} catch {
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
}
|
|
6859
|
+
process.exit(0);
|
|
6860
|
+
} catch (err) {
|
|
6861
|
+
if (err instanceof ProcessExitSignal) throw err;
|
|
6862
|
+
process.exit(0);
|
|
6863
|
+
}
|
|
6864
|
+
}
|
|
6865
|
+
var init_cmux_status = __esm({
|
|
6866
|
+
"src/cli/cmux-status.ts"() {
|
|
6867
|
+
"use strict";
|
|
6868
|
+
init_process_exit_signal();
|
|
6869
|
+
init_cmux();
|
|
6870
|
+
}
|
|
6871
|
+
});
|
|
6872
|
+
|
|
6873
|
+
// src/cli/cmux-serve.ts
|
|
6874
|
+
var cmux_serve_exports = {};
|
|
6875
|
+
__export(cmux_serve_exports, {
|
|
6876
|
+
probePort: () => probePort,
|
|
6877
|
+
runCmuxServe: () => runCmuxServe
|
|
6878
|
+
});
|
|
6879
|
+
import { execSync as execSync10, execFileSync as execFileSync4 } from "node:child_process";
|
|
6880
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
6881
|
+
import * as net from "node:net";
|
|
6882
|
+
import { join as join23 } from "node:path";
|
|
6883
|
+
function resolveAppDir(allocation, toplevel) {
|
|
6884
|
+
if (allocation.command !== null && allocation.working_dir !== null) {
|
|
6885
|
+
const wd = allocation.working_dir;
|
|
6886
|
+
const dir = wd.startsWith(toplevel + "/") ? wd.slice(toplevel.length + 1) : wd;
|
|
6887
|
+
return { appDir: dir, devCommand: allocation.command };
|
|
6888
|
+
}
|
|
6889
|
+
const label = allocation.label ?? "";
|
|
6890
|
+
if (label === "E2E Tests") return { skip: "skip-e2e" };
|
|
6891
|
+
if (label.includes("Web Dev") && !label.toLowerCase().includes("desktop")) {
|
|
6892
|
+
return { appDir: "apps/web", devCommand: null };
|
|
6893
|
+
}
|
|
6894
|
+
if (label.toLowerCase().includes("desktop")) {
|
|
6895
|
+
return { appDir: "apps/desktop", devCommand: null };
|
|
6896
|
+
}
|
|
6897
|
+
const appDir = LABEL_APP_MAP[label];
|
|
6898
|
+
if (appDir !== void 0) {
|
|
6899
|
+
return { appDir, devCommand: null };
|
|
6900
|
+
}
|
|
6901
|
+
return { skip: "no-match" };
|
|
6902
|
+
}
|
|
6903
|
+
function probePort(port) {
|
|
6904
|
+
return new Promise((resolve8) => {
|
|
6905
|
+
const socket = new net.Socket();
|
|
6906
|
+
let settled = false;
|
|
6907
|
+
const settle = (result) => {
|
|
6908
|
+
if (!settled) {
|
|
6909
|
+
settled = true;
|
|
6910
|
+
socket.destroy();
|
|
6911
|
+
resolve8(result);
|
|
6912
|
+
}
|
|
6913
|
+
};
|
|
6914
|
+
socket.setTimeout(500);
|
|
6915
|
+
socket.on("connect", () => settle(true));
|
|
6916
|
+
socket.on("error", () => settle(false));
|
|
6917
|
+
socket.on("timeout", () => settle(false));
|
|
6918
|
+
socket.connect({ port, host: "127.0.0.1" });
|
|
6919
|
+
});
|
|
6920
|
+
}
|
|
6921
|
+
async function runCmuxServe(args) {
|
|
6922
|
+
try {
|
|
6923
|
+
if (!insideCmux()) {
|
|
6924
|
+
process.exit(0);
|
|
6925
|
+
}
|
|
6926
|
+
let toplevel = "";
|
|
6927
|
+
try {
|
|
6928
|
+
toplevel = execSync10("git rev-parse --show-toplevel", {
|
|
6929
|
+
encoding: "utf8"
|
|
6930
|
+
}).trim();
|
|
6931
|
+
} catch {
|
|
6932
|
+
toplevel = process.cwd();
|
|
6933
|
+
}
|
|
6934
|
+
const cfg = readCmuxConfig(toplevel);
|
|
6935
|
+
if (cfg.auto_dev_server === false) {
|
|
6936
|
+
process.exit(0);
|
|
6937
|
+
}
|
|
6938
|
+
let filesArg;
|
|
6939
|
+
let appArg;
|
|
6940
|
+
for (let i = 0; i < args.length; i++) {
|
|
6941
|
+
const flag = args[i];
|
|
6942
|
+
if (flag === "--files" && i + 1 < args.length) {
|
|
6943
|
+
filesArg = args[++i];
|
|
6944
|
+
} else if (flag === "--app" && i + 1 < args.length) {
|
|
6945
|
+
appArg = args[++i];
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
const changedFiles = filesArg !== void 0 ? filesArg.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
6949
|
+
let serverConfig = null;
|
|
6950
|
+
try {
|
|
6951
|
+
const raw = readFileSync7(
|
|
6952
|
+
join23(toplevel, ".codebyplan", "server.json"),
|
|
6953
|
+
"utf-8"
|
|
6954
|
+
);
|
|
6955
|
+
serverConfig = JSON.parse(raw);
|
|
6956
|
+
} catch {
|
|
6957
|
+
process.exit(0);
|
|
6958
|
+
}
|
|
6959
|
+
const allocations = serverConfig?.port_allocations ?? [];
|
|
6960
|
+
if (allocations.length === 0) {
|
|
6961
|
+
process.exit(0);
|
|
6962
|
+
}
|
|
6963
|
+
const bin = resolveCmuxBin();
|
|
6964
|
+
for (const allocation of allocations) {
|
|
6965
|
+
try {
|
|
6966
|
+
const resolved = resolveAppDir(allocation, toplevel);
|
|
6967
|
+
if ("skip" in resolved) {
|
|
6968
|
+
if (resolved.skip === "no-match") {
|
|
6969
|
+
process.stdout.write(
|
|
6970
|
+
`cmux-serve: no app mapping for allocation "${allocation.label ?? ""}" \u2014 skipped
|
|
6971
|
+
`
|
|
6972
|
+
);
|
|
6973
|
+
}
|
|
6974
|
+
continue;
|
|
6975
|
+
}
|
|
6976
|
+
const { appDir, devCommand } = resolved;
|
|
6977
|
+
const appDirWithSlash = appDir + "/";
|
|
6978
|
+
const intersects = appArg !== void 0 && (appArg === appDir || appArg.startsWith(appDirWithSlash)) || changedFiles.some(
|
|
6979
|
+
(f) => f === appDir || f.startsWith(appDirWithSlash)
|
|
6980
|
+
);
|
|
6981
|
+
if (!intersects) {
|
|
6982
|
+
continue;
|
|
6983
|
+
}
|
|
6984
|
+
const port = allocation.port;
|
|
6985
|
+
const listening = await probePort(port);
|
|
6986
|
+
if (!listening) {
|
|
6987
|
+
let shellCommand = null;
|
|
6988
|
+
if (devCommand !== null) {
|
|
6989
|
+
shellCommand = `cd "${join23(toplevel, appDir)}" && ${devCommand}`;
|
|
6990
|
+
} else {
|
|
6991
|
+
let hasDev = false;
|
|
6992
|
+
try {
|
|
6993
|
+
const pkgRaw = readFileSync7(
|
|
6994
|
+
join23(toplevel, appDir, "package.json"),
|
|
6995
|
+
"utf-8"
|
|
6996
|
+
);
|
|
6997
|
+
const pkg = JSON.parse(pkgRaw);
|
|
6998
|
+
hasDev = typeof pkg.scripts?.dev === "string";
|
|
6999
|
+
} catch {
|
|
7000
|
+
}
|
|
7001
|
+
if (!hasDev) {
|
|
7002
|
+
process.stdout.write(
|
|
7003
|
+
`cmux-serve: no "dev" script in ${appDir}/package.json \u2014 skipped
|
|
7004
|
+
`
|
|
7005
|
+
);
|
|
7006
|
+
continue;
|
|
7007
|
+
}
|
|
7008
|
+
shellCommand = `cd "${join23(toplevel, appDir)}" && pnpm run dev`;
|
|
7009
|
+
}
|
|
7010
|
+
let splitSurfaceRef = null;
|
|
7011
|
+
try {
|
|
7012
|
+
const splitOut = execFileSync4(
|
|
7013
|
+
bin,
|
|
7014
|
+
["new-split", "down", "--json"],
|
|
7015
|
+
{
|
|
7016
|
+
encoding: "utf8"
|
|
7017
|
+
}
|
|
7018
|
+
);
|
|
7019
|
+
const parsed = JSON.parse(splitOut);
|
|
7020
|
+
if (typeof parsed.surface_ref === "string" && parsed.surface_ref) {
|
|
7021
|
+
splitSurfaceRef = parsed.surface_ref;
|
|
7022
|
+
}
|
|
7023
|
+
} catch {
|
|
7024
|
+
}
|
|
7025
|
+
if (splitSurfaceRef !== null) {
|
|
7026
|
+
try {
|
|
7027
|
+
execFileSync4(bin, [
|
|
7028
|
+
"send",
|
|
7029
|
+
"--surface",
|
|
7030
|
+
splitSurfaceRef,
|
|
7031
|
+
`${shellCommand}
|
|
7032
|
+
`
|
|
7033
|
+
]);
|
|
7034
|
+
} catch {
|
|
7035
|
+
}
|
|
7036
|
+
} else {
|
|
7037
|
+
process.stdout.write(
|
|
7038
|
+
`cmux-serve: could not resolve new split surface for ${appDir} \u2014 dev server not auto-started (open it manually)
|
|
7039
|
+
`
|
|
7040
|
+
);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
try {
|
|
7044
|
+
execFileSync4(bin, [
|
|
7045
|
+
"new-pane",
|
|
7046
|
+
"--type",
|
|
7047
|
+
"browser",
|
|
7048
|
+
"--url",
|
|
7049
|
+
`http://localhost:${port}`
|
|
7050
|
+
]);
|
|
7051
|
+
} catch {
|
|
7052
|
+
}
|
|
7053
|
+
} catch {
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
process.exit(0);
|
|
7057
|
+
} catch (err) {
|
|
7058
|
+
if (err instanceof ProcessExitSignal) throw err;
|
|
7059
|
+
process.exit(0);
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
var LABEL_APP_MAP;
|
|
7063
|
+
var init_cmux_serve = __esm({
|
|
7064
|
+
"src/cli/cmux-serve.ts"() {
|
|
7065
|
+
"use strict";
|
|
7066
|
+
init_process_exit_signal();
|
|
7067
|
+
init_cmux();
|
|
7068
|
+
LABEL_APP_MAP = {
|
|
7069
|
+
"Backend Dev": "apps/backend",
|
|
7070
|
+
"MCP Dev": "apps/mcp",
|
|
7071
|
+
"Docs Ingest": "apps/docs-ingest"
|
|
7072
|
+
};
|
|
6688
7073
|
}
|
|
6689
7074
|
});
|
|
6690
7075
|
|
|
6691
7076
|
// src/lib/migrate-local-config.ts
|
|
6692
7077
|
import { mkdir as mkdir6, readFile as readFile16, unlink as unlink2, writeFile as writeFile12 } from "node:fs/promises";
|
|
6693
|
-
import { join as
|
|
7078
|
+
import { join as join24 } from "node:path";
|
|
6694
7079
|
function legacySharedPath(projectPath) {
|
|
6695
|
-
return
|
|
7080
|
+
return join24(projectPath, ".codebyplan.json");
|
|
6696
7081
|
}
|
|
6697
7082
|
function legacyLocalPath(projectPath) {
|
|
6698
|
-
return
|
|
7083
|
+
return join24(projectPath, ".codebyplan.local.json");
|
|
6699
7084
|
}
|
|
6700
7085
|
function newDirPath(projectPath) {
|
|
6701
|
-
return
|
|
7086
|
+
return join24(projectPath, ".codebyplan");
|
|
6702
7087
|
}
|
|
6703
7088
|
function sentinelPath(projectPath) {
|
|
6704
|
-
return
|
|
7089
|
+
return join24(projectPath, ".codebyplan", "repo.json");
|
|
6705
7090
|
}
|
|
6706
7091
|
async function statSafe(p) {
|
|
6707
7092
|
const { stat: stat2 } = await import("node:fs/promises");
|
|
@@ -6795,7 +7180,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6795
7180
|
if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
|
|
6796
7181
|
if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
|
|
6797
7182
|
await writeFile12(
|
|
6798
|
-
|
|
7183
|
+
join24(projectPath, ".codebyplan", "repo.json"),
|
|
6799
7184
|
JSON.stringify(repoJson, null, 2) + "\n",
|
|
6800
7185
|
"utf-8"
|
|
6801
7186
|
);
|
|
@@ -6808,7 +7193,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6808
7193
|
if ("port_allocations" in cfg)
|
|
6809
7194
|
serverJson.port_allocations = cfg.port_allocations;
|
|
6810
7195
|
await writeFile12(
|
|
6811
|
-
|
|
7196
|
+
join24(projectPath, ".codebyplan", "server.json"),
|
|
6812
7197
|
JSON.stringify(serverJson, null, 2) + "\n",
|
|
6813
7198
|
"utf-8"
|
|
6814
7199
|
);
|
|
@@ -6817,7 +7202,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6817
7202
|
if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
|
|
6818
7203
|
if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
|
|
6819
7204
|
await writeFile12(
|
|
6820
|
-
|
|
7205
|
+
join24(projectPath, ".codebyplan", "git.json"),
|
|
6821
7206
|
JSON.stringify(gitJson, null, 2) + "\n",
|
|
6822
7207
|
"utf-8"
|
|
6823
7208
|
);
|
|
@@ -6825,35 +7210,35 @@ async function runLocalMigration(projectPath) {
|
|
|
6825
7210
|
const shipmentJson = {};
|
|
6826
7211
|
if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
|
|
6827
7212
|
await writeFile12(
|
|
6828
|
-
|
|
7213
|
+
join24(projectPath, ".codebyplan", "shipment.json"),
|
|
6829
7214
|
JSON.stringify(shipmentJson, null, 2) + "\n",
|
|
6830
7215
|
"utf-8"
|
|
6831
7216
|
);
|
|
6832
7217
|
filesChanged.push(".codebyplan/shipment.json");
|
|
6833
7218
|
const vendorJson = {};
|
|
6834
7219
|
await writeFile12(
|
|
6835
|
-
|
|
7220
|
+
join24(projectPath, ".codebyplan", "vendor.json"),
|
|
6836
7221
|
JSON.stringify(vendorJson, null, 2) + "\n",
|
|
6837
7222
|
"utf-8"
|
|
6838
7223
|
);
|
|
6839
7224
|
filesChanged.push(".codebyplan/vendor.json");
|
|
6840
7225
|
const e2eJson = {};
|
|
6841
7226
|
await writeFile12(
|
|
6842
|
-
|
|
7227
|
+
join24(projectPath, ".codebyplan", "e2e.json"),
|
|
6843
7228
|
JSON.stringify(e2eJson, null, 2) + "\n",
|
|
6844
7229
|
"utf-8"
|
|
6845
7230
|
);
|
|
6846
7231
|
filesChanged.push(".codebyplan/e2e.json");
|
|
6847
7232
|
const eslintJson = {};
|
|
6848
7233
|
await writeFile12(
|
|
6849
|
-
|
|
7234
|
+
join24(projectPath, ".codebyplan", "eslint.json"),
|
|
6850
7235
|
JSON.stringify(eslintJson, null, 2) + "\n",
|
|
6851
7236
|
"utf-8"
|
|
6852
7237
|
);
|
|
6853
7238
|
filesChanged.push(".codebyplan/eslint.json");
|
|
6854
7239
|
if (!deviceWrittenByHelper) {
|
|
6855
7240
|
await writeFile12(
|
|
6856
|
-
|
|
7241
|
+
join24(projectPath, ".codebyplan", "device.local.json"),
|
|
6857
7242
|
JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
|
|
6858
7243
|
"utf-8"
|
|
6859
7244
|
);
|
|
@@ -6865,7 +7250,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6865
7250
|
"Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
|
|
6866
7251
|
);
|
|
6867
7252
|
}
|
|
6868
|
-
const gitignorePath =
|
|
7253
|
+
const gitignorePath = join24(projectPath, ".gitignore");
|
|
6869
7254
|
try {
|
|
6870
7255
|
const gitignoreContent = await readFile16(gitignorePath, "utf-8");
|
|
6871
7256
|
const legacyLine = ".codebyplan.local.json";
|
|
@@ -6927,7 +7312,7 @@ __export(config_exports, {
|
|
|
6927
7312
|
runConfig: () => runConfig
|
|
6928
7313
|
});
|
|
6929
7314
|
import { mkdir as mkdir7, readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
|
|
6930
|
-
import { join as
|
|
7315
|
+
import { join as join25 } from "node:path";
|
|
6931
7316
|
async function runConfig() {
|
|
6932
7317
|
const flags = parseFlags(3);
|
|
6933
7318
|
const dryRun = hasFlag("dry-run", 3);
|
|
@@ -6960,14 +7345,14 @@ async function runConfig() {
|
|
|
6960
7345
|
console.log("\n Config complete.\n");
|
|
6961
7346
|
}
|
|
6962
7347
|
async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
6963
|
-
const codebyplanDir =
|
|
7348
|
+
const codebyplanDir = join25(projectPath, ".codebyplan");
|
|
6964
7349
|
let resolvedWorktreeId;
|
|
6965
7350
|
try {
|
|
6966
7351
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
6967
7352
|
let branch = "main";
|
|
6968
7353
|
try {
|
|
6969
|
-
const { execSync:
|
|
6970
|
-
branch =
|
|
7354
|
+
const { execSync: execSync11 } = await import("node:child_process");
|
|
7355
|
+
branch = execSync11("git symbolic-ref --short HEAD", {
|
|
6971
7356
|
cwd: projectPath,
|
|
6972
7357
|
encoding: "utf-8"
|
|
6973
7358
|
}).trim();
|
|
@@ -7087,6 +7472,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
7087
7472
|
const vendorPayload = {};
|
|
7088
7473
|
const e2ePayload = {};
|
|
7089
7474
|
const eslintPayload = {};
|
|
7475
|
+
const cmuxPayload = {};
|
|
7090
7476
|
if (dryRun) {
|
|
7091
7477
|
console.log(" Config would be updated (dry-run).");
|
|
7092
7478
|
return;
|
|
@@ -7099,11 +7485,12 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
7099
7485
|
{ name: "shipment.json", payload: shipmentPayload },
|
|
7100
7486
|
{ name: "vendor.json", payload: vendorPayload },
|
|
7101
7487
|
{ name: "e2e.json", payload: e2ePayload, createOnly: true },
|
|
7102
|
-
{ name: "eslint.json", payload: eslintPayload, createOnly: true }
|
|
7488
|
+
{ name: "eslint.json", payload: eslintPayload, createOnly: true },
|
|
7489
|
+
{ name: "cmux.json", payload: cmuxPayload, createOnly: true }
|
|
7103
7490
|
];
|
|
7104
7491
|
let anyUpdated = false;
|
|
7105
7492
|
for (const { name, payload, createOnly } of files) {
|
|
7106
|
-
const filePath =
|
|
7493
|
+
const filePath = join25(codebyplanDir, name);
|
|
7107
7494
|
const newJson = JSON.stringify(payload, null, 2) + "\n";
|
|
7108
7495
|
let currentJson = "";
|
|
7109
7496
|
try {
|
|
@@ -7123,7 +7510,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
7123
7510
|
async function readRepoConfig(projectPath) {
|
|
7124
7511
|
try {
|
|
7125
7512
|
const raw = await readFile17(
|
|
7126
|
-
|
|
7513
|
+
join25(projectPath, ".codebyplan", "repo.json"),
|
|
7127
7514
|
"utf-8"
|
|
7128
7515
|
);
|
|
7129
7516
|
return JSON.parse(raw);
|
|
@@ -7134,7 +7521,7 @@ async function readRepoConfig(projectPath) {
|
|
|
7134
7521
|
async function readServerConfig(projectPath) {
|
|
7135
7522
|
try {
|
|
7136
7523
|
const raw = await readFile17(
|
|
7137
|
-
|
|
7524
|
+
join25(projectPath, ".codebyplan", "server.json"),
|
|
7138
7525
|
"utf-8"
|
|
7139
7526
|
);
|
|
7140
7527
|
return JSON.parse(raw);
|
|
@@ -7145,7 +7532,7 @@ async function readServerConfig(projectPath) {
|
|
|
7145
7532
|
async function readGitConfig(projectPath) {
|
|
7146
7533
|
try {
|
|
7147
7534
|
const raw = await readFile17(
|
|
7148
|
-
|
|
7535
|
+
join25(projectPath, ".codebyplan", "git.json"),
|
|
7149
7536
|
"utf-8"
|
|
7150
7537
|
);
|
|
7151
7538
|
return JSON.parse(raw);
|
|
@@ -7156,7 +7543,7 @@ async function readGitConfig(projectPath) {
|
|
|
7156
7543
|
async function readShipmentConfig(projectPath) {
|
|
7157
7544
|
try {
|
|
7158
7545
|
const raw = await readFile17(
|
|
7159
|
-
|
|
7546
|
+
join25(projectPath, ".codebyplan", "shipment.json"),
|
|
7160
7547
|
"utf-8"
|
|
7161
7548
|
);
|
|
7162
7549
|
return JSON.parse(raw);
|
|
@@ -7167,7 +7554,7 @@ async function readShipmentConfig(projectPath) {
|
|
|
7167
7554
|
async function readVendorConfig(projectPath) {
|
|
7168
7555
|
try {
|
|
7169
7556
|
const raw = await readFile17(
|
|
7170
|
-
|
|
7557
|
+
join25(projectPath, ".codebyplan", "vendor.json"),
|
|
7171
7558
|
"utf-8"
|
|
7172
7559
|
);
|
|
7173
7560
|
return JSON.parse(raw);
|
|
@@ -7178,7 +7565,7 @@ async function readVendorConfig(projectPath) {
|
|
|
7178
7565
|
async function readE2eConfig2(projectPath) {
|
|
7179
7566
|
try {
|
|
7180
7567
|
const raw = await readFile17(
|
|
7181
|
-
|
|
7568
|
+
join25(projectPath, ".codebyplan", "e2e.json"),
|
|
7182
7569
|
"utf-8"
|
|
7183
7570
|
);
|
|
7184
7571
|
return JSON.parse(raw);
|
|
@@ -7512,10 +7899,10 @@ async function runTechStack() {
|
|
|
7512
7899
|
);
|
|
7513
7900
|
}
|
|
7514
7901
|
try {
|
|
7515
|
-
const { execSync:
|
|
7902
|
+
const { execSync: execSync11 } = await import("node:child_process");
|
|
7516
7903
|
let branch = "main";
|
|
7517
7904
|
try {
|
|
7518
|
-
branch =
|
|
7905
|
+
branch = execSync11("git symbolic-ref --short HEAD", {
|
|
7519
7906
|
cwd: projectPath,
|
|
7520
7907
|
encoding: "utf-8"
|
|
7521
7908
|
}).trim();
|
|
@@ -8314,13 +8701,13 @@ var init_uninstall = __esm({
|
|
|
8314
8701
|
|
|
8315
8702
|
// src/index.ts
|
|
8316
8703
|
init_version();
|
|
8317
|
-
import { readFileSync as
|
|
8704
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
8318
8705
|
import { resolve as resolve7 } from "node:path";
|
|
8319
8706
|
void (async () => {
|
|
8320
8707
|
if (!process.env.CODEBYPLAN_API_KEY) {
|
|
8321
8708
|
try {
|
|
8322
8709
|
const envPath = resolve7(process.cwd(), ".env.local");
|
|
8323
|
-
const content =
|
|
8710
|
+
const content = readFileSync10(envPath, "utf-8");
|
|
8324
8711
|
for (const line of content.split("\n")) {
|
|
8325
8712
|
const trimmed = line.trim();
|
|
8326
8713
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -8463,6 +8850,18 @@ void (async () => {
|
|
|
8463
8850
|
await runCmuxSync2();
|
|
8464
8851
|
process.exit(0);
|
|
8465
8852
|
}
|
|
8853
|
+
if (arg === "cmux-status") {
|
|
8854
|
+
const { runCmuxStatus: runCmuxStatus2 } = await Promise.resolve().then(() => (init_cmux_status(), cmux_status_exports));
|
|
8855
|
+
const rest = process.argv.slice(3);
|
|
8856
|
+
await runCmuxStatus2(rest);
|
|
8857
|
+
process.exit(0);
|
|
8858
|
+
}
|
|
8859
|
+
if (arg === "cmux-serve") {
|
|
8860
|
+
const { runCmuxServe: runCmuxServe2 } = await Promise.resolve().then(() => (init_cmux_serve(), cmux_serve_exports));
|
|
8861
|
+
const rest = process.argv.slice(3);
|
|
8862
|
+
await runCmuxServe2(rest);
|
|
8863
|
+
process.exit(0);
|
|
8864
|
+
}
|
|
8466
8865
|
if (arg === "config") {
|
|
8467
8866
|
const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
8468
8867
|
await runConfig2();
|
|
@@ -8567,6 +8966,8 @@ void (async () => {
|
|
|
8567
8966
|
codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
|
|
8568
8967
|
codebyplan version-status Report installed vs latest version + update guard (JSON)
|
|
8569
8968
|
codebyplan cmux-sync Sync cmux workspace title/description to current git branch and repo folder
|
|
8969
|
+
codebyplan cmux-status Push checkpoint/task/QA + progress to the cmux workspace sidebar
|
|
8970
|
+
codebyplan cmux-serve Auto-start dev server + browser pane for the round's app files (cmux)
|
|
8570
8971
|
codebyplan help Show this help message
|
|
8571
8972
|
codebyplan --version Print version
|
|
8572
8973
|
|
package/package.json
CHANGED
|
@@ -226,7 +226,7 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
|
|
|
226
226
|
|
|
227
227
|
### `cbp-cmux-workspace-sync.sh` — SessionStart, matcher `*`
|
|
228
228
|
|
|
229
|
-
On every session start, syncs the active [cmux](https://github.com/nicholasgasior/cmux) workspace title to the current git branch
|
|
229
|
+
On every session start, syncs the active [cmux](https://github.com/nicholasgasior/cmux) workspace title to the current git branch, the workspace description to the repo folder basename (the directory that contains `.git/`), and applies the workspace color from `.codebyplan/cmux.json` via `cmux workspace-action --action set-color`. All three actions are delegated to `codebyplan cmux-sync`. If no `workspace_color` is configured, a one-line nudge is printed to stdout prompting the user to run `/cbp-setup-cmux`.
|
|
230
230
|
|
|
231
231
|
**Blocks vs warns**: never blocks — exit 0 on every path. A SessionStart hook must never prevent a session from opening.
|
|
232
232
|
|
|
@@ -252,6 +252,28 @@ After any Bash tool call that contains a `git checkout` or `git switch` invocati
|
|
|
252
252
|
|
|
253
253
|
---
|
|
254
254
|
|
|
255
|
+
### Auto dev server (`codebyplan cmux-serve`)
|
|
256
|
+
|
|
257
|
+
At the start of each round, `cbp-round-execute` (Step 2a) calls `codebyplan cmux-serve --files "<round files>"` to auto-start the dev server for any app whose source files are touched. The subcommand probes each allocated port via `node:net`, starts a `cmux new-split` terminal pane + sends the dev command for any non-listening app, then opens a browser pane. If the port is already listening (another worktree) it only opens the browser pane. No hook registration is needed — the skill invokes the subcommand directly. Gated by `auto_dev_server` in `.codebyplan/cmux.json`; no-op outside cmux.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Status surface (`codebyplan cmux-status`)
|
|
262
|
+
|
|
263
|
+
The lifecycle skills push CodeByPlan development state into the cmux workspace sidebar via `codebyplan cmux-status`. No hook registration is needed — the skills invoke the subcommand directly:
|
|
264
|
+
|
|
265
|
+
| Skill | What is pushed |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| `cbp-task-start` (Step 4.5) | `--checkpoint "CHK-NNN: title" --task "TASK-N: title"` |
|
|
268
|
+
| `cbp-task-complete` (Step 7.3) | `--task "TASK-N: title done" --progress completed/total` |
|
|
269
|
+
| `cbp-round-execute` (Step 3d) | `--qa "R{n} {status}"` where status ∈ completed / blocked / re-triggering |
|
|
270
|
+
|
|
271
|
+
**`auto_status` toggle.** Gated by the `auto_status` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_status` is `false`, every call is a no-op. Default is `true` (enabled).
|
|
272
|
+
|
|
273
|
+
**No-op outside cmux.** `codebyplan cmux-status` checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately — safe to call unconditionally from skills and hooks.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
255
277
|
## Supporting (not registered)
|
|
256
278
|
|
|
257
279
|
### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
|
|
@@ -124,6 +124,7 @@
|
|
|
124
124
|
"Skill(cbp-round-update)",
|
|
125
125
|
"Skill(cbp-session-end)",
|
|
126
126
|
"Skill(cbp-session-start)",
|
|
127
|
+
"Skill(cbp-setup-cmux)",
|
|
127
128
|
"Skill(cbp-setup-e2e)",
|
|
128
129
|
"Skill(cbp-setup-eslint)",
|
|
129
130
|
"Skill(cbp-ship-configure)",
|
|
@@ -135,6 +136,11 @@
|
|
|
135
136
|
"Skill(cbp-supabase-branch-check)",
|
|
136
137
|
"Skill(cbp-supabase-migrate)",
|
|
137
138
|
"Skill(cbp-supabase-setup)",
|
|
139
|
+
"Skill(cbp-standalone-task-check)",
|
|
140
|
+
"Skill(cbp-standalone-task-complete)",
|
|
141
|
+
"Skill(cbp-standalone-task-create)",
|
|
142
|
+
"Skill(cbp-standalone-task-start)",
|
|
143
|
+
"Skill(cbp-standalone-task-testing)",
|
|
138
144
|
"Skill(cbp-task-check)",
|
|
139
145
|
"Skill(cbp-task-complete)",
|
|
140
146
|
"Skill(cbp-task-create)",
|
|
@@ -196,6 +202,10 @@
|
|
|
196
202
|
"Bash(npx codebyplan resolve-worktree:*)",
|
|
197
203
|
"Bash(codebyplan cmux-sync:*)",
|
|
198
204
|
"Bash(npx codebyplan cmux-sync:*)",
|
|
205
|
+
"Bash(codebyplan cmux-status:*)",
|
|
206
|
+
"Bash(npx codebyplan cmux-status:*)",
|
|
207
|
+
"Bash(codebyplan cmux-serve:*)",
|
|
208
|
+
"Bash(npx codebyplan cmux-serve:*)",
|
|
199
209
|
"Bash(codebyplan version-status:*)",
|
|
200
210
|
"Bash(npx codebyplan version-status:*)",
|
|
201
211
|
"Bash(codebyplan statusline:*)",
|
|
@@ -57,6 +57,16 @@ Read the plan from round context (`context.planner_output`). If no plan: `No app
|
|
|
57
57
|
|
|
58
58
|
Read effective testing profile: `round.context.testing_profile_override` if set (user override for this round only), else `task.context.testing_profile` (set by planner Phase 4.8), else default `'web'`. Pass the effective profile to all per-wave `cbp-testing-qa-agent` spawns.
|
|
59
59
|
|
|
60
|
+
### Step 2a: Auto-Dev-Server (cmux)
|
|
61
|
+
|
|
62
|
+
Fire the dev-server hook at round-execution start. Self-no-ops outside cmux or when `auto_dev_server` is disabled in `.codebyplan/cmux.json`.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx codebyplan cmux-serve --files "<comma-separated approved_plan.files_to_modify[].path>"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The subcommand reads `.codebyplan/server.json` `port_allocations[]`, resolves which apps' source dirs intersect the round's files, probes each allocated port, and starts a cmux terminal split + browser pane for any app not already serving. Idempotent — if the port is already listening it only opens the browser pane (mitigating the multi-worktree port collision).
|
|
69
|
+
|
|
60
70
|
### Step 3: Route Execution Path
|
|
61
71
|
|
|
62
72
|
Inspect `approved_plan.files_to_modify[]` and `approved_plan.round_type`. Four execution paths exist; pick the one that matches BEFORE Step 3a/3b.
|
|
@@ -143,6 +153,14 @@ If the approved plan includes database schema changes, RLS policies, or type gen
|
|
|
143
153
|
- `status: 'blocked'` → present blocker to user via AskUserQuestion, resolve, re-spawn executor with remaining work
|
|
144
154
|
- Deliverables incomplete → re-spawn executor with remaining deliverables (max 3 re-triggers). After 3 re-triggers, save partial output and proceed.
|
|
145
155
|
|
|
156
|
+
### Step 3d: Push cmux QA Status
|
|
157
|
+
|
|
158
|
+
Push the round's QA outcome to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled. Status is one of: `completed`, `blocked`, or `re-triggering`.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx codebyplan cmux-status --qa "R{round_number} {status}"
|
|
162
|
+
```
|
|
163
|
+
|
|
146
164
|
### Step 4: Dev-Server Probe (rounds 2+, web/desktop profile)
|
|
147
165
|
|
|
148
166
|
When `round_number >= 2` AND `testing_profile` is `'web'` or `'desktop'` AND `files_changed` contains any UI file, probe the dev server BEFORE cbp-testing-qa-agent spawns (saves a full agent spawn when the server is down).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-setup-cmux
|
|
4
|
+
description: Configure .codebyplan/cmux.json — choose workspace color and toggle auto_status / auto_dev_server. Interactive, idempotent.
|
|
5
|
+
argument-hint: "[--force]"
|
|
6
|
+
model: sonnet
|
|
7
|
+
effort: xhigh
|
|
8
|
+
allowed-tools: Read, Write, Edit, Bash(cat *), Bash(jq *), Bash(test *), Bash(mv *), Bash(git check-ignore *), Bash(cmux *), AskUserQuestion
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# cmux Setup
|
|
12
|
+
|
|
13
|
+
Configure `.codebyplan/cmux.json` so the cmux workspace integration knows which color to
|
|
14
|
+
apply on session start and whether auto_status / auto_dev_server are enabled.
|
|
15
|
+
|
|
16
|
+
Invoke at any time. Existing values are preserved unless `--force` is passed.
|
|
17
|
+
Pass `--force` to re-ask all questions including already-configured fields.
|
|
18
|
+
|
|
19
|
+
## Arguments
|
|
20
|
+
|
|
21
|
+
Inspect `$ARGUMENTS` for `--force`. If present, set `force_mode = true`.
|
|
22
|
+
Absent: use idempotent mode — preserve existing configured values, skip re-asking
|
|
23
|
+
already-set fields.
|
|
24
|
+
|
|
25
|
+
## Step 1 — Parse --force and read existing config
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cat .codebyplan/cmux.json 2>/dev/null || echo '{}'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Capture the result as `EXISTING_JSON`. Extract existing values:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
EXISTING_COLOR=$(echo "$EXISTING_JSON" | jq -r '.workspace_color // empty')
|
|
35
|
+
EXISTING_AUTO_STATUS=$(echo "$EXISTING_JSON" | jq -r '.auto_status // empty')
|
|
36
|
+
EXISTING_AUTO_DEV=$(echo "$EXISTING_JSON" | jq -r '.auto_dev_server // empty')
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Step 2 — Ask for workspace color
|
|
40
|
+
|
|
41
|
+
**Idempotency gate**: skip this question if `force_mode = false` AND `EXISTING_COLOR`
|
|
42
|
+
is a non-empty string. Print the preserved value and continue to Step 3.
|
|
43
|
+
|
|
44
|
+
Otherwise, AskUserQuestion:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Workspace color for this cmux workspace
|
|
48
|
+
|
|
49
|
+
Choose a named color or enter a custom hex value (#RRGGBB).
|
|
50
|
+
|
|
51
|
+
Named colors:
|
|
52
|
+
A) Red B) Crimson C) Orange D) Amber
|
|
53
|
+
E) Olive F) Green G) Teal H) Aqua
|
|
54
|
+
I) Blue J) Navy K) Indigo L) Purple
|
|
55
|
+
M) Magenta N) Rose O) Brown P) Charcoal
|
|
56
|
+
Q) Custom hex — enter a value in the format #RRGGBB
|
|
57
|
+
|
|
58
|
+
Enter a letter (A-Q) or type a custom hex value directly:
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Validate the input:
|
|
62
|
+
- If a letter A-P: map to the corresponding color name (e.g. "A" → "Red").
|
|
63
|
+
- If "Q": prompt the user to type a custom hex value, then validate the typed value
|
|
64
|
+
against `^#[0-9A-Fa-f]{6}$`. If invalid, re-prompt once with an error. Never pass the
|
|
65
|
+
bare letter "Q" to set-color.
|
|
66
|
+
- If the raw input itself already looks like a hex value: validate it against
|
|
67
|
+
`^#[0-9A-Fa-f]{6}$` and accept on match.
|
|
68
|
+
- Empty input with no existing color: use no color (leave `workspace_color` as `null`).
|
|
69
|
+
|
|
70
|
+
Set `NEW_COLOR` to the chosen color name / hex, or `null` if skipped.
|
|
71
|
+
|
|
72
|
+
## Step 3 — Ask for auto_status and auto_dev_server toggles
|
|
73
|
+
|
|
74
|
+
**Idempotency gate**: skip questions for fields that are already set (non-empty in
|
|
75
|
+
existing config) when `force_mode = false`.
|
|
76
|
+
|
|
77
|
+
For each unset field (or all fields when `force_mode = true`), AskUserQuestion:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
cmux integration toggles
|
|
81
|
+
|
|
82
|
+
auto_status: automatically run `cmux status` in this workspace on session start?
|
|
83
|
+
(default: on)
|
|
84
|
+
A) On
|
|
85
|
+
B) Off
|
|
86
|
+
|
|
87
|
+
auto_dev_server: automatically start the dev server via cmux on session start?
|
|
88
|
+
(default: on)
|
|
89
|
+
A) On
|
|
90
|
+
B) Off
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Ask both in a single question when both are unset. Set `NEW_AUTO_STATUS` and
|
|
94
|
+
`NEW_AUTO_DEV` to `true` or `false` based on the answers.
|
|
95
|
+
|
|
96
|
+
## Step 4 — Write .codebyplan/cmux.json
|
|
97
|
+
|
|
98
|
+
Build the updated payload using jq deep-merge so sibling fields added by future
|
|
99
|
+
schema versions are preserved. Assemble variables first.
|
|
100
|
+
|
|
101
|
+
Backfill any field that the Step 2/3 idempotency gates skipped, so each `jq
|
|
102
|
+
--argjson` always receives valid JSON — an empty shell variable makes `jq` exit 1,
|
|
103
|
+
which would leave `NEW_PAYLOAD` empty and silently no-op the write. Fall back to the
|
|
104
|
+
existing value, then to the default (`true` for the toggles):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
NEW_COLOR="${NEW_COLOR:-$EXISTING_COLOR}"
|
|
108
|
+
NEW_AUTO_STATUS="${NEW_AUTO_STATUS:-${EXISTING_AUTO_STATUS:-true}}"
|
|
109
|
+
NEW_AUTO_DEV="${NEW_AUTO_DEV:-${EXISTING_AUTO_DEV:-true}}"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
NEW_COLOR_JSON=$(echo "$NEW_COLOR" | jq -R 'if . == "null" or . == "" then null else . end')
|
|
114
|
+
NEW_PAYLOAD=$(jq -n \
|
|
115
|
+
--argjson color "$NEW_COLOR_JSON" \
|
|
116
|
+
--argjson auto_status "$NEW_AUTO_STATUS" \
|
|
117
|
+
--argjson auto_dev_server "$NEW_AUTO_DEV" \
|
|
118
|
+
'{workspace_color: $color, auto_status: $auto_status, auto_dev_server: $auto_dev_server}')
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Atomic write via jq tmp+mv deep-merge (`. * $new` deep-merges, preserving any
|
|
122
|
+
sibling keys not in the new payload):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
jq --argjson new "$NEW_PAYLOAD" '. * $new' \
|
|
126
|
+
.codebyplan/cmux.json > .codebyplan/cmux.json.tmp \
|
|
127
|
+
&& mv .codebyplan/cmux.json.tmp .codebyplan/cmux.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Step 5 — Apply color immediately
|
|
131
|
+
|
|
132
|
+
If `NEW_COLOR` is a non-empty, non-null string AND cmux is available in the current
|
|
133
|
+
environment (`$CMUX_WORKSPACE_ID` is set), apply the color immediately so the change
|
|
134
|
+
is visible without needing to restart the session:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
if [ -n "$CMUX_WORKSPACE_ID" ] && [ -n "$NEW_COLOR" ] && [ "$NEW_COLOR" != "null" ]; then
|
|
138
|
+
CMUX_BIN="${CMUX_BUNDLED_CLI_PATH:-${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}}"
|
|
139
|
+
"$CMUX_BIN" workspace-action --action set-color --color "$NEW_COLOR" 2>/dev/null || true
|
|
140
|
+
fi
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The guard (`2>/dev/null || true`) ensures a missing or non-zero cmux never blocks
|
|
144
|
+
the skill — applying the color is best-effort.
|
|
145
|
+
|
|
146
|
+
## Step 6 — Verify and report
|
|
147
|
+
|
|
148
|
+
Re-read `.codebyplan/cmux.json` and emit a summary:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
cmux Setup — Complete
|
|
152
|
+
|
|
153
|
+
workspace_color : <value or "(none)">
|
|
154
|
+
auto_status : <true|false>
|
|
155
|
+
auto_dev_server : <true|false>
|
|
156
|
+
|
|
157
|
+
Config written to .codebyplan/cmux.json
|
|
158
|
+
|
|
159
|
+
Next: on the next SessionStart, codebyplan cmux-sync will apply the color
|
|
160
|
+
automatically. Run `/cbp-setup-cmux --force` at any time to reconfigure.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Key Rules
|
|
164
|
+
|
|
165
|
+
- Atomic write (tmp + mv) — never leaves cmux.json in a partial state
|
|
166
|
+
- Deep-merge with `. * $new` — sibling keys from future schema versions are preserved
|
|
167
|
+
- Color apply is best-effort — a missing cmux binary never causes an error
|
|
168
|
+
- `auto_status` and `auto_dev_server` default to `true` when absent (documented in
|
|
169
|
+
`packages/codebyplan-package/src/lib/types.ts` CmuxConfig)
|
|
170
|
+
- cmux.json is COMMITTED (not gitignored) — workspace color is shared across team members
|
|
@@ -138,6 +138,20 @@ Skip the push only when nothing was committed in Step 5 AND `/cbp-merge-main` re
|
|
|
138
138
|
|
|
139
139
|
Call `complete_task(task_id)`. The server resolves the caller's worktree identity from the JWT/ctx and enforces the mutate-lock (CHK-140 TASK-3 — `caller_worktree_id` input field removed). The server auto-clears `assigned_user_id` + `assigned_worktree_id` on the task; if this was the last sibling task, it also clears the parent checkpoint's assignment. (Per CHK-104 hard-lock.)
|
|
140
140
|
|
|
141
|
+
### Step 7.3: Push cmux Status (task done + progress)
|
|
142
|
+
|
|
143
|
+
Push completion status and checkpoint progress to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled.
|
|
144
|
+
|
|
145
|
+
Compute progress from the `get_tasks` data already loaded by the routing step: `completed = count of tasks with status 'completed'`, `total = total task count for the checkpoint`. For standalone tasks (no checkpoint), omit `--progress`.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Checkpoint-bound task:
|
|
149
|
+
npx codebyplan cmux-status --task "TASK-{N}: {task-title} done" --progress {completed}/{total}
|
|
150
|
+
|
|
151
|
+
# Standalone task:
|
|
152
|
+
npx codebyplan cmux-status --task "TASK-{N}: {task-title} done"
|
|
153
|
+
```
|
|
154
|
+
|
|
141
155
|
### Step 8: Run Cleanup + Migration (inline)
|
|
142
156
|
|
|
143
157
|
Apply the `cleanup` skill inline to remove orphan references to deleted/modified files. Then apply `migration` to propagate renames/moves to consumers. Both run without sub-agent spawns. Skip cleanup if no deletions/modifications; skip migration if cleanup handled everything.
|
|
@@ -228,6 +228,14 @@ Display context summary:
|
|
|
228
228
|
- **Previous rounds**: [count] completed
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
+
### Step 4.5: Push cmux Status
|
|
232
|
+
|
|
233
|
+
Push the active checkpoint and task context to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled in `.codebyplan/cmux.json`.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npx codebyplan cmux-status --checkpoint "CHK-{NNN}: {checkpoint-title}" --task "TASK-{N}: {task-title}"
|
|
237
|
+
```
|
|
238
|
+
|
|
231
239
|
### Step 5: Set Task Status
|
|
232
240
|
|
|
233
241
|
Use MCP `update_task(task_id, status: "in_progress")`.
|