codebyplan 1.10.2 → 1.11.0

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/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.10.2";
17
+ VERSION = "1.11.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -22,13 +22,16 @@ var init_version = __esm({
22
22
  // src/lib/local-config.ts
23
23
  import { execSync } from "node:child_process";
24
24
  import { createHash } from "node:crypto";
25
- import { mkdir, readFile, writeFile } from "node:fs/promises";
25
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
26
26
  import { hostname } from "node:os";
27
27
  import { dirname, join } from "node:path";
28
28
  function localConfigPath(projectPath) {
29
29
  return join(projectPath, ".codebyplan", "device.local.json");
30
30
  }
31
- async function readLocalConfig(projectPath) {
31
+ function legacyLocalConfigPath(projectPath) {
32
+ return join(projectPath, ".codebyplan.local.json");
33
+ }
34
+ async function readLocalConfig(projectPath, onMigrationNotice) {
32
35
  try {
33
36
  const raw = await readFile(localConfigPath(projectPath), "utf-8");
34
37
  const parsed = JSON.parse(raw);
@@ -38,22 +41,81 @@ async function readLocalConfig(projectPath) {
38
41
  console.error("Failed to read local config: invalid shape");
39
42
  return null;
40
43
  } catch (err) {
41
- console.error(
42
- `Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
43
- );
44
+ const code = err.code;
45
+ if (code === "ENOENT") {
46
+ } else if (code === "ENOTDIR") {
47
+ try {
48
+ const dirPath = dirname(localConfigPath(projectPath));
49
+ const st = await stat(dirPath);
50
+ if (!st.isDirectory()) {
51
+ throw Object.assign(
52
+ new Error(`${dirPath} is a file, expected directory`),
53
+ { code: "LEGACY_FILE_BLOCKS_DIR" }
54
+ );
55
+ }
56
+ } catch (statErr) {
57
+ if (statErr.code === "LEGACY_FILE_BLOCKS_DIR")
58
+ throw statErr;
59
+ }
60
+ throw err;
61
+ } else if (typeof code === "string") {
62
+ throw err;
63
+ } else {
64
+ console.error(
65
+ `Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
66
+ );
67
+ return null;
68
+ }
69
+ }
70
+ try {
71
+ const raw = await readFile(legacyLocalConfigPath(projectPath), "utf-8");
72
+ const parsed = JSON.parse(raw);
73
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
74
+ onMigrationNotice?.(
75
+ legacyLocalConfigPath(projectPath),
76
+ localConfigPath(projectPath)
77
+ );
78
+ return parsed;
79
+ }
44
80
  return null;
81
+ } catch (err) {
82
+ const code = err.code;
83
+ if (code === "ENOENT") {
84
+ return null;
85
+ }
86
+ throw err;
45
87
  }
46
88
  }
47
89
  async function writeLocalConfig(projectPath, config) {
48
90
  const content = { device_id: config.device_id };
49
91
  const path6 = localConfigPath(projectPath);
92
+ const dirPath = dirname(path6);
93
+ let phase = "stat config directory";
50
94
  try {
51
- await mkdir(dirname(path6), { recursive: true });
95
+ try {
96
+ const st = await stat(dirPath);
97
+ if (!st.isDirectory()) {
98
+ const err = Object.assign(
99
+ new Error(`${dirPath} is a file, expected directory`),
100
+ { code: "LEGACY_FILE_BLOCKS_DIR" }
101
+ );
102
+ throw err;
103
+ }
104
+ } catch (statErr) {
105
+ const code = statErr.code;
106
+ if (code === "LEGACY_FILE_BLOCKS_DIR") throw statErr;
107
+ if (code !== "ENOENT") throw statErr;
108
+ }
109
+ phase = "create config directory";
110
+ await mkdir(dirPath, { recursive: true });
111
+ phase = "write local config";
52
112
  await writeFile(path6, JSON.stringify(content, null, 2) + "\n", "utf-8");
53
113
  } catch (err) {
54
- console.error(
55
- `Failed to write local config: ${err instanceof Error ? err.message : String(err)}`
56
- );
114
+ const code = err.code;
115
+ if (code === "LEGACY_FILE_BLOCKS_DIR") {
116
+ throw err;
117
+ }
118
+ console.error(`Failed to ${phase}: ${err.message}`);
57
119
  throw err;
58
120
  }
59
121
  }
@@ -73,8 +135,8 @@ async function resolveMachineSeed() {
73
135
  }
74
136
  return hostname();
75
137
  }
76
- async function getOrCreateDeviceId(projectPath) {
77
- const existing = await readLocalConfig(projectPath);
138
+ async function getOrCreateDeviceId(projectPath, onMigrationNotice) {
139
+ const existing = await readLocalConfig(projectPath, onMigrationNotice);
78
140
  if (existing?.device_id) {
79
141
  return existing.device_id;
80
142
  }
@@ -89,6 +151,122 @@ var init_local_config = __esm({
89
151
  }
90
152
  });
91
153
 
154
+ // src/lib/statusline-config.ts
155
+ import { spawnSync } from "node:child_process";
156
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
157
+ import { join as join2 } from "node:path";
158
+ function statuslineConfigPath(projectPath) {
159
+ return join2(projectPath, ".codebyplan", "statusline.json");
160
+ }
161
+ function statuslineLocalConfigPath(projectPath) {
162
+ return join2(projectPath, ".codebyplan", "statusline.local.json");
163
+ }
164
+ function isValidRenderer(value) {
165
+ return typeof value === "string" && VALID_RENDERERS.has(value);
166
+ }
167
+ function mergeOverDefaults(raw) {
168
+ const result = {
169
+ lines: { ...STATUSLINE_DEFAULTS.lines },
170
+ no_color: STATUSLINE_DEFAULTS.no_color
171
+ };
172
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
173
+ return result;
174
+ }
175
+ const obj = raw;
176
+ if (typeof obj.no_color === "boolean") {
177
+ result.no_color = obj.no_color;
178
+ }
179
+ if (typeof obj.lines === "object" && obj.lines !== null && !Array.isArray(obj.lines)) {
180
+ const lines = obj.lines;
181
+ for (const key of Object.keys(STATUSLINE_DEFAULTS.lines)) {
182
+ if (typeof lines[key] === "boolean") {
183
+ result.lines[key] = lines[key];
184
+ }
185
+ }
186
+ }
187
+ return result;
188
+ }
189
+ async function readStatuslineConfig(projectPath) {
190
+ try {
191
+ const raw = await readFile2(statuslineConfigPath(projectPath), "utf-8");
192
+ const parsed = JSON.parse(raw);
193
+ return mergeOverDefaults(parsed);
194
+ } catch {
195
+ return mergeOverDefaults(void 0);
196
+ }
197
+ }
198
+ async function readStatuslineLocalConfig(projectPath) {
199
+ try {
200
+ const raw = await readFile2(statuslineLocalConfigPath(projectPath), "utf-8");
201
+ const parsed = JSON.parse(raw);
202
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
203
+ const obj = parsed;
204
+ if (isValidRenderer(obj.renderer)) {
205
+ return { renderer: obj.renderer };
206
+ }
207
+ }
208
+ return null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ async function writeStatuslineLocalConfig(projectPath, localConfig) {
214
+ if (!isValidRenderer(localConfig.renderer)) {
215
+ throw new TypeError(
216
+ `Unknown renderer: "${String(localConfig.renderer)}". Must be one of: bash, node, python.`
217
+ );
218
+ }
219
+ const filePath = statuslineLocalConfigPath(projectPath);
220
+ await mkdir2(join2(projectPath, ".codebyplan"), { recursive: true });
221
+ await writeFile2(
222
+ filePath,
223
+ JSON.stringify(localConfig, null, 2) + "\n",
224
+ "utf-8"
225
+ );
226
+ }
227
+ async function resolveRenderer(projectPath) {
228
+ const local = await readStatuslineLocalConfig(projectPath);
229
+ return local?.renderer ?? DEFAULT_RENDERER;
230
+ }
231
+ function detectAvailableRenderers() {
232
+ function probe(cmd, args) {
233
+ try {
234
+ const result = spawnSync(cmd, args, {
235
+ timeout: 1e3,
236
+ stdio: "ignore",
237
+ windowsHide: true
238
+ });
239
+ return result.status === 0;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+ return {
245
+ bash: true,
246
+ node: probe("node", ["--version"]),
247
+ python: probe("python3", ["--version"])
248
+ };
249
+ }
250
+ var STATUSLINE_DEFAULTS, DEFAULT_RENDERER, VALID_RENDERERS;
251
+ var init_statusline_config = __esm({
252
+ "src/lib/statusline-config.ts"() {
253
+ "use strict";
254
+ STATUSLINE_DEFAULTS = {
255
+ lines: {
256
+ identity: true,
257
+ context: true,
258
+ cost: true,
259
+ rate_limits: true,
260
+ repo_pr: true,
261
+ worktree: true
262
+ },
263
+ no_color: false
264
+ };
265
+ DEFAULT_RENDERER = "bash";
266
+ VALID_RENDERERS = /* @__PURE__ */ new Set(["bash", "node", "python"]);
267
+ }
268
+ });
269
+
92
270
  // src/oauth/jwt-decode.ts
93
271
  function base64UrlDecode(input) {
94
272
  const padded = input + "=".repeat((4 - input.length % 4) % 4);
@@ -119,9 +297,9 @@ var init_jwt_decode = __esm({
119
297
  });
120
298
 
121
299
  // src/oauth/keychain.ts
122
- import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
300
+ import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "node:fs/promises";
123
301
  import { homedir, platform } from "node:os";
124
- import { dirname as dirname2, join as join2 } from "node:path";
302
+ import { dirname as dirname2, join as join3 } from "node:path";
125
303
  async function loadKeyring() {
126
304
  if (keyringOverride !== void 0) return keyringOverride;
127
305
  try {
@@ -133,18 +311,18 @@ async function loadKeyring() {
133
311
  }
134
312
  function fallbackDir() {
135
313
  if (platform() === "win32") {
136
- const appData = process.env.APPDATA ?? join2(homedir(), "AppData", "Roaming");
137
- return join2(appData, "codebyplan");
314
+ const appData = process.env.APPDATA ?? join3(homedir(), "AppData", "Roaming");
315
+ return join3(appData, "codebyplan");
138
316
  }
139
- const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config");
140
- return join2(xdg, "codebyplan");
317
+ const xdg = process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config");
318
+ return join3(xdg, "codebyplan");
141
319
  }
142
320
  function fallbackFile(filename) {
143
- return join2(fallbackDir(), filename);
321
+ return join3(fallbackDir(), filename);
144
322
  }
145
323
  async function readFallback(filename) {
146
324
  try {
147
- const raw = await readFile2(fallbackFile(filename), "utf-8");
325
+ const raw = await readFile3(fallbackFile(filename), "utf-8");
148
326
  return JSON.parse(raw);
149
327
  } catch {
150
328
  return null;
@@ -152,8 +330,8 @@ async function readFallback(filename) {
152
330
  }
153
331
  async function writeFallback(filename, data) {
154
332
  const path6 = fallbackFile(filename);
155
- await mkdir2(dirname2(path6), { recursive: true });
156
- await writeFile2(path6, JSON.stringify(data, null, 2) + "\n", "utf-8");
333
+ await mkdir3(dirname2(path6), { recursive: true });
334
+ await writeFile3(path6, JSON.stringify(data, null, 2) + "\n", "utf-8");
157
335
  if (platform() !== "win32") {
158
336
  try {
159
337
  await chmod(path6, 384);
@@ -504,8 +682,8 @@ var init_api = __esm({
504
682
  });
505
683
 
506
684
  // src/lib/resolve-worktree.ts
507
- import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
508
- import { join as join3 } from "node:path";
685
+ import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
686
+ import { join as join4 } from "node:path";
509
687
  async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
510
688
  let worktreeId;
511
689
  try {
@@ -527,10 +705,10 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
527
705
  if (options?.skipWrite) {
528
706
  return worktreeId;
529
707
  }
530
- const codebyplanPath = join3(projectPath, ".codebyplan.json");
708
+ const codebyplanPath = join4(projectPath, ".codebyplan.json");
531
709
  let currentConfig = {};
532
710
  try {
533
- const raw = await readFile3(codebyplanPath, "utf-8");
711
+ const raw = await readFile4(codebyplanPath, "utf-8");
534
712
  const parsed = JSON.parse(raw);
535
713
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
536
714
  currentConfig = parsed;
@@ -545,7 +723,7 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
545
723
  worktree_id: worktreeId
546
724
  };
547
725
  try {
548
- await writeFile3(
726
+ await writeFile4(
549
727
  codebyplanPath,
550
728
  JSON.stringify(merged, null, 2) + "\n",
551
729
  "utf-8"
@@ -561,7 +739,8 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
561
739
  async function resolveWorktreeByBranch({
562
740
  repoId,
563
741
  deviceId,
564
- branch
742
+ branch,
743
+ onError
565
744
  }) {
566
745
  if (!deviceId || !branch) {
567
746
  return null;
@@ -588,6 +767,10 @@ async function resolveWorktreeByBranch({
588
767
  console.warn(
589
768
  `Worktree self-heal skipped (non-fatal): branch worktree resolve failed: ${err instanceof Error ? err.message : String(err)}. Continuing.`
590
769
  );
770
+ onError?.(
771
+ "api_failed",
772
+ err instanceof Error ? err : new Error(String(err))
773
+ );
591
774
  return null;
592
775
  }
593
776
  }
@@ -595,7 +778,8 @@ async function resolveWorktreeId({
595
778
  repoId,
596
779
  repoPath,
597
780
  branch,
598
- deviceId
781
+ deviceId,
782
+ onError
599
783
  }) {
600
784
  try {
601
785
  const res = await apiPost(
@@ -607,6 +791,10 @@ async function resolveWorktreeId({
607
791
  console.warn(
608
792
  `Worktree self-heal skipped (non-fatal): ${err instanceof Error ? err.message : String(err)}. Continuing \u2014 run \`codebyplan config\` again later to retry.`
609
793
  );
794
+ onError?.(
795
+ "api_failed",
796
+ err instanceof Error ? err : new Error(String(err))
797
+ );
610
798
  return null;
611
799
  }
612
800
  }
@@ -869,13 +1057,13 @@ var setup_exports = {};
869
1057
  __export(setup_exports, {
870
1058
  runSetup: () => runSetup
871
1059
  });
872
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
1060
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
873
1061
  import { homedir as homedir2 } from "node:os";
874
- import { join as join4 } from "node:path";
1062
+ import { join as join5 } from "node:path";
875
1063
  import { stdin, stdout as stdout2 } from "node:process";
876
1064
  import { createInterface } from "node:readline/promises";
877
1065
  function getConfigPath(scope) {
878
- return scope === "user" ? join4(homedir2(), ".claude.json") : join4(process.cwd(), ".mcp.json");
1066
+ return scope === "user" ? join5(homedir2(), ".claude.json") : join5(process.cwd(), ".mcp.json");
879
1067
  }
880
1068
  function legacyMcpUrl() {
881
1069
  const baseUrl2 = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
@@ -883,7 +1071,7 @@ function legacyMcpUrl() {
883
1071
  }
884
1072
  async function readConfig(path6) {
885
1073
  try {
886
- const raw = await readFile4(path6, "utf-8");
1074
+ const raw = await readFile5(path6, "utf-8");
887
1075
  const parsed = JSON.parse(raw);
888
1076
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
889
1077
  return parsed;
@@ -906,7 +1094,7 @@ async function writeMcpConfig(scope, auth) {
906
1094
  config.mcpServers = {};
907
1095
  }
908
1096
  config.mcpServers.codebyplan = buildMcpEntry(auth);
909
- await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1097
+ await writeFile5(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
910
1098
  return configPath;
911
1099
  }
912
1100
  async function fetchRepos(auth) {
@@ -961,8 +1149,8 @@ async function chooseAuthMode(rl) {
961
1149
  return { kind: "legacy", apiKey };
962
1150
  }
963
1151
  async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
964
- const codebyplanDir = join4(projectPath, ".codebyplan");
965
- await mkdir3(codebyplanDir, { recursive: true });
1152
+ const codebyplanDir = join5(projectPath, ".codebyplan");
1153
+ await mkdir4(codebyplanDir, { recursive: true });
966
1154
  const repoJson = {
967
1155
  repo_id: selectedRepo.id
968
1156
  };
@@ -973,13 +1161,13 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
973
1161
  if (typeof repoAny.project_id === "string") {
974
1162
  repoJson.project_id = repoAny.project_id;
975
1163
  }
976
- await writeFile4(
977
- join4(codebyplanDir, "repo.json"),
1164
+ await writeFile5(
1165
+ join5(codebyplanDir, "repo.json"),
978
1166
  JSON.stringify(repoJson, null, 2) + "\n",
979
1167
  "utf-8"
980
1168
  );
981
- await writeFile4(
982
- join4(codebyplanDir, "server.json"),
1169
+ await writeFile5(
1170
+ join5(codebyplanDir, "server.json"),
983
1171
  JSON.stringify(
984
1172
  {
985
1173
  server_port: null,
@@ -992,40 +1180,68 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
992
1180
  ) + "\n",
993
1181
  "utf-8"
994
1182
  );
995
- await writeFile4(
996
- join4(codebyplanDir, "git.json"),
1183
+ await writeFile5(
1184
+ join5(codebyplanDir, "git.json"),
997
1185
  JSON.stringify({ git_branch: null, branch_config: null }, null, 2) + "\n",
998
1186
  "utf-8"
999
1187
  );
1000
- await writeFile4(
1001
- join4(codebyplanDir, "shipment.json"),
1188
+ await writeFile5(
1189
+ join5(codebyplanDir, "shipment.json"),
1002
1190
  JSON.stringify({ shipment: null }, null, 2) + "\n",
1003
1191
  "utf-8"
1004
1192
  );
1005
- await writeFile4(
1006
- join4(codebyplanDir, "vendor.json"),
1193
+ await writeFile5(
1194
+ join5(codebyplanDir, "vendor.json"),
1007
1195
  JSON.stringify({}, null, 2) + "\n",
1008
1196
  "utf-8"
1009
1197
  );
1198
+ const statuslinePath = join5(codebyplanDir, "statusline.json");
1199
+ let statuslineExists = false;
1200
+ try {
1201
+ await readFile5(statuslinePath, "utf-8");
1202
+ statuslineExists = true;
1203
+ } catch {
1204
+ }
1205
+ if (!statuslineExists) {
1206
+ await writeFile5(
1207
+ statuslinePath,
1208
+ JSON.stringify(STATUSLINE_DEFAULTS, null, 2) + "\n",
1209
+ "utf-8"
1210
+ );
1211
+ }
1010
1212
  await writeLocalConfig(projectPath, { device_id: deviceId });
1011
1213
  console.log(` Created ${codebyplanDir}/`);
1012
1214
  console.log(
1013
- ` repo.json, server.json, git.json, shipment.json, vendor.json`
1215
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, statusline.json`
1014
1216
  );
1015
1217
  console.log(` device.local.json (gitignored)`);
1016
- const gitignorePath = join4(projectPath, ".gitignore");
1218
+ const gitignorePath = join5(projectPath, ".gitignore");
1017
1219
  const gitignoreEntry = ".codebyplan/device.local.json";
1220
+ const statuslineLocalEntry = ".codebyplan/statusline.local.json";
1018
1221
  try {
1019
- const existing = await readFile4(gitignorePath, "utf-8");
1222
+ const existing = await readFile5(gitignorePath, "utf-8");
1020
1223
  const lines = existing.split("\n");
1224
+ let content = existing;
1021
1225
  if (!lines.some((l) => l.trimEnd() === gitignoreEntry)) {
1022
- const appended = (existing.endsWith("\n") ? existing : existing + "\n") + gitignoreEntry + "\n";
1023
- await writeFile4(gitignorePath, appended, "utf-8");
1226
+ content = (content.endsWith("\n") ? content : content + "\n") + gitignoreEntry + "\n";
1024
1227
  console.log(` Added '${gitignoreEntry}' to .gitignore`);
1025
1228
  }
1229
+ if (!lines.some((l) => l.trimEnd() === statuslineLocalEntry)) {
1230
+ content = (content.endsWith("\n") ? content : content + "\n") + statuslineLocalEntry + "\n";
1231
+ console.log(` Added '${statuslineLocalEntry}' to .gitignore`);
1232
+ }
1233
+ if (content !== existing) {
1234
+ await writeFile5(gitignorePath, content, "utf-8");
1235
+ }
1026
1236
  } catch {
1027
- await writeFile4(gitignorePath, gitignoreEntry + "\n", "utf-8");
1028
- console.log(` Created .gitignore with '${gitignoreEntry}'`);
1237
+ await writeFile5(
1238
+ gitignorePath,
1239
+ gitignoreEntry + "\n" + statuslineLocalEntry + "\n",
1240
+ "utf-8"
1241
+ );
1242
+ console.log(
1243
+ ` Created .gitignore with '${gitignoreEntry}' and '${statuslineLocalEntry}'`
1244
+ );
1029
1245
  }
1030
1246
  }
1031
1247
  async function runSetup() {
@@ -1118,6 +1334,31 @@ async function runSetup() {
1118
1334
  const _worktreeId = tupleId ?? pathBasedId;
1119
1335
  void _worktreeId;
1120
1336
  await writeCodebyplanDirectory(projectPath, selectedRepo, deviceId);
1337
+ const existingRenderer = await readStatuslineLocalConfig(projectPath);
1338
+ const isInteractive = process.stdin.isTTY === true;
1339
+ if (!existingRenderer && isInteractive) {
1340
+ const availability = detectAvailableRenderers();
1341
+ const available = Object.entries(availability).filter(([, v]) => v).map(([k]) => k).join(", ");
1342
+ console.log(
1343
+ `
1344
+ Which statusline renderer would you like to use? (available: ${available})`
1345
+ );
1346
+ console.log(" bash \u2014 shell script, zero dependencies (default)");
1347
+ console.log(" node \u2014 Node.js renderer");
1348
+ console.log(" python \u2014 Python 3 renderer");
1349
+ const rendererInput = (await rl.question(
1350
+ " Select renderer (bash/node/python, default: bash): "
1351
+ )).trim().toLowerCase();
1352
+ const chosen = rendererInput === "node" ? "node" : rendererInput === "python" ? "python" : "bash";
1353
+ await writeStatuslineLocalConfig(projectPath, { renderer: chosen });
1354
+ if (!availability[chosen]) {
1355
+ console.log(
1356
+ ` Warning: '${chosen}' was not detected on this machine. You can change it later with \`codebyplan statusline\`.`
1357
+ );
1358
+ } else {
1359
+ console.log(` Renderer set to '${chosen}'.`);
1360
+ }
1361
+ }
1121
1362
  console.log(
1122
1363
  "\n Run `npx codebyplan config` to sync repo config from the DB."
1123
1364
  );
@@ -1132,6 +1373,7 @@ var init_setup = __esm({
1132
1373
  "src/cli/setup.ts"() {
1133
1374
  "use strict";
1134
1375
  init_local_config();
1376
+ init_statusline_config();
1135
1377
  init_resolve_worktree();
1136
1378
  init_token_refresh();
1137
1379
  init_urls();
@@ -1139,6 +1381,190 @@ var init_setup = __esm({
1139
1381
  }
1140
1382
  });
1141
1383
 
1384
+ // src/lib/flags.ts
1385
+ import { readFile as readFile6 } from "node:fs/promises";
1386
+ import { join as join6, resolve } from "node:path";
1387
+ async function findCodebyplanConfig(startDir, maxDepth = 20) {
1388
+ let cursor = resolve(startDir);
1389
+ for (let depth = 0; depth < maxDepth; depth++) {
1390
+ const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1391
+ try {
1392
+ const raw = await readFile6(sentinelPath2, "utf-8");
1393
+ const parsed = JSON.parse(raw);
1394
+ return { path: sentinelPath2, contents: parsed };
1395
+ } catch {
1396
+ }
1397
+ const legacyPath = join6(cursor, ".codebyplan.json");
1398
+ try {
1399
+ const raw = await readFile6(legacyPath, "utf-8");
1400
+ const parsed = JSON.parse(raw);
1401
+ return { path: legacyPath, contents: parsed };
1402
+ } catch {
1403
+ }
1404
+ const parent = resolve(cursor, "..");
1405
+ if (parent === cursor) return null;
1406
+ cursor = parent;
1407
+ }
1408
+ return null;
1409
+ }
1410
+ function parseFlags(startIndex) {
1411
+ const flags = {};
1412
+ const args = process.argv.slice(startIndex);
1413
+ for (let i = 0; i < args.length; i++) {
1414
+ const arg = args[i];
1415
+ if (arg.startsWith("--") && i + 1 < args.length) {
1416
+ const key = arg.slice(2);
1417
+ flags[key] = args[++i];
1418
+ }
1419
+ }
1420
+ return flags;
1421
+ }
1422
+ function hasFlag(name, startIndex) {
1423
+ return process.argv.slice(startIndex).includes(`--${name}`);
1424
+ }
1425
+ async function resolveConfig(flags) {
1426
+ const projectPath = flags["path"] ?? process.cwd();
1427
+ let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
1428
+ let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
1429
+ if (!repoId) {
1430
+ const found = await findCodebyplanConfig(projectPath);
1431
+ if (found) {
1432
+ repoId = found.contents.repo_id;
1433
+ if (!worktreeId) worktreeId = found.contents.worktree_id;
1434
+ }
1435
+ }
1436
+ if (!repoId) {
1437
+ throw new Error(
1438
+ `Could not determine repo_id.
1439
+
1440
+ Provide it via one of:
1441
+ --repo-id <uuid> CLI flag
1442
+ CODEBYPLAN_REPO_ID=<uuid> environment variable
1443
+ .codebyplan/repo.json { "repo_id": "<uuid>" } in project root
1444
+ Run 'codebyplan setup' to initialize this project`
1445
+ );
1446
+ }
1447
+ return { repoId, worktreeId, projectPath };
1448
+ }
1449
+ var init_flags = __esm({
1450
+ "src/lib/flags.ts"() {
1451
+ "use strict";
1452
+ }
1453
+ });
1454
+
1455
+ // src/cli/statusline.ts
1456
+ var statusline_exports = {};
1457
+ __export(statusline_exports, {
1458
+ runStatusline: () => runStatusline
1459
+ });
1460
+ import { dirname as dirname3 } from "node:path";
1461
+ async function resolveProjectPath() {
1462
+ const found = await findCodebyplanConfig(process.cwd());
1463
+ if (found) {
1464
+ return dirname3(dirname3(found.path));
1465
+ }
1466
+ return process.cwd();
1467
+ }
1468
+ function parseRendererArg(arg) {
1469
+ const normalised = arg.startsWith("--") ? arg.slice(2) : arg;
1470
+ if (VALID_RENDERERS2.has(normalised)) {
1471
+ return normalised;
1472
+ }
1473
+ return null;
1474
+ }
1475
+ async function runStatusline(args) {
1476
+ const positional = args.filter((a) => !a.startsWith("-"));
1477
+ const flagArgs = args.filter((a) => a.startsWith("--"));
1478
+ const rendererTokens = [...positional, ...flagArgs].filter(
1479
+ (a) => parseRendererArg(a) !== null
1480
+ );
1481
+ if (rendererTokens.length > 1) {
1482
+ process.stderr.write(
1483
+ "codebyplan statusline: bash, node, and python are mutually exclusive; pass only one.\n"
1484
+ );
1485
+ process.exitCode = 1;
1486
+ return;
1487
+ }
1488
+ let rendererArg;
1489
+ for (const a of [...positional, ...flagArgs]) {
1490
+ const parsed = parseRendererArg(a);
1491
+ if (parsed !== null) {
1492
+ rendererArg = a;
1493
+ break;
1494
+ }
1495
+ }
1496
+ const unknownPositional = args.filter(
1497
+ (a) => !a.startsWith("-") && !VALID_RENDERERS2.has(a)
1498
+ );
1499
+ if (unknownPositional.length > 0) {
1500
+ process.stderr.write(
1501
+ `codebyplan statusline: unknown argument '${unknownPositional[0]}'.
1502
+
1503
+ Usage:
1504
+ codebyplan statusline Show current renderer and line toggles
1505
+ codebyplan statusline bash|node|python Set the renderer
1506
+ codebyplan statusline --bash|--node|--python Set the renderer (flag form)
1507
+ `
1508
+ );
1509
+ process.exitCode = 1;
1510
+ return;
1511
+ }
1512
+ const unknownFlags = flagArgs.filter((a) => parseRendererArg(a) === null);
1513
+ if (unknownFlags.length > 0) {
1514
+ process.stderr.write(
1515
+ `codebyplan statusline: unknown flag '${unknownFlags[0]}'.
1516
+
1517
+ Usage:
1518
+ codebyplan statusline Show current renderer and line toggles
1519
+ codebyplan statusline bash|node|python Set the renderer
1520
+ codebyplan statusline --bash|--node|--python Set the renderer (flag form)
1521
+ `
1522
+ );
1523
+ process.exitCode = 1;
1524
+ return;
1525
+ }
1526
+ const projectPath = await resolveProjectPath();
1527
+ if (rendererArg !== void 0) {
1528
+ const chosen = parseRendererArg(rendererArg);
1529
+ await writeStatuslineLocalConfig(projectPath, { renderer: chosen });
1530
+ const availability2 = detectAvailableRenderers();
1531
+ if (!availability2[chosen]) {
1532
+ process.stderr.write(
1533
+ `Warning: '${chosen}' was not detected on this machine. The renderer is saved but may not run.
1534
+ `
1535
+ );
1536
+ }
1537
+ console.log(`Statusline renderer set to '${chosen}'.`);
1538
+ return;
1539
+ }
1540
+ const availability = detectAvailableRenderers();
1541
+ const [renderer, config] = await Promise.all([
1542
+ resolveRenderer(projectPath),
1543
+ readStatuslineConfig(projectPath)
1544
+ ]);
1545
+ const availStr = Object.entries(availability).map(([k, v]) => `${k}: ${v ? "yes" : "no"}`).join(", ");
1546
+ console.log(`
1547
+ Statusline configuration`);
1548
+ console.log(` Renderer: ${renderer}`);
1549
+ console.log(` Available: ${availStr}`);
1550
+ console.log(` no_color: ${config.no_color}`);
1551
+ console.log(`
1552
+ Line toggles:`);
1553
+ for (const [key, val] of Object.entries(config.lines)) {
1554
+ console.log(` ${key.padEnd(14)} ${val ? "on" : "off"}`);
1555
+ }
1556
+ console.log();
1557
+ }
1558
+ var VALID_RENDERERS2;
1559
+ var init_statusline = __esm({
1560
+ "src/cli/statusline.ts"() {
1561
+ "use strict";
1562
+ init_flags();
1563
+ init_statusline_config();
1564
+ VALID_RENDERERS2 = /* @__PURE__ */ new Set(["bash", "node", "python"]);
1565
+ }
1566
+ });
1567
+
1142
1568
  // src/cli/logout.ts
1143
1569
  var logout_exports = {};
1144
1570
  __export(logout_exports, {
@@ -1197,15 +1623,15 @@ var upgrade_auth_exports = {};
1197
1623
  __export(upgrade_auth_exports, {
1198
1624
  runUpgradeAuth: () => runUpgradeAuth
1199
1625
  });
1200
- import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1626
+ import { readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1201
1627
  import { homedir as homedir3 } from "node:os";
1202
- import { join as join5 } from "node:path";
1628
+ import { join as join7 } from "node:path";
1203
1629
  function configPaths() {
1204
- return [join5(homedir3(), ".claude.json"), join5(process.cwd(), ".mcp.json")];
1630
+ return [join7(homedir3(), ".claude.json"), join7(process.cwd(), ".mcp.json")];
1205
1631
  }
1206
1632
  async function readConfig2(path6) {
1207
1633
  try {
1208
- const raw = await readFile5(path6, "utf-8");
1634
+ const raw = await readFile7(path6, "utf-8");
1209
1635
  const parsed = JSON.parse(raw);
1210
1636
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1211
1637
  return parsed;
@@ -1226,7 +1652,7 @@ async function rewriteConfig(path6, config, newUrl) {
1226
1652
  if (!entry) return false;
1227
1653
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1228
1654
  servers.codebyplan = { url: newUrl };
1229
- await writeFile5(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1655
+ await writeFile6(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1230
1656
  return true;
1231
1657
  }
1232
1658
  async function runUpgradeAuth() {
@@ -1263,77 +1689,6 @@ var init_upgrade_auth = __esm({
1263
1689
  }
1264
1690
  });
1265
1691
 
1266
- // src/lib/flags.ts
1267
- import { readFile as readFile6 } from "node:fs/promises";
1268
- import { join as join6, resolve } from "node:path";
1269
- async function findCodebyplanConfig(startDir, maxDepth = 20) {
1270
- let cursor = resolve(startDir);
1271
- for (let depth = 0; depth < maxDepth; depth++) {
1272
- const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1273
- try {
1274
- const raw = await readFile6(sentinelPath2, "utf-8");
1275
- const parsed = JSON.parse(raw);
1276
- return { path: sentinelPath2, contents: parsed };
1277
- } catch {
1278
- }
1279
- const legacyPath = join6(cursor, ".codebyplan.json");
1280
- try {
1281
- const raw = await readFile6(legacyPath, "utf-8");
1282
- const parsed = JSON.parse(raw);
1283
- return { path: legacyPath, contents: parsed };
1284
- } catch {
1285
- }
1286
- const parent = resolve(cursor, "..");
1287
- if (parent === cursor) return null;
1288
- cursor = parent;
1289
- }
1290
- return null;
1291
- }
1292
- function parseFlags(startIndex) {
1293
- const flags = {};
1294
- const args = process.argv.slice(startIndex);
1295
- for (let i = 0; i < args.length; i++) {
1296
- const arg = args[i];
1297
- if (arg.startsWith("--") && i + 1 < args.length) {
1298
- const key = arg.slice(2);
1299
- flags[key] = args[++i];
1300
- }
1301
- }
1302
- return flags;
1303
- }
1304
- function hasFlag(name, startIndex) {
1305
- return process.argv.slice(startIndex).includes(`--${name}`);
1306
- }
1307
- async function resolveConfig(flags) {
1308
- const projectPath = flags["path"] ?? process.cwd();
1309
- let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
1310
- let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
1311
- if (!repoId) {
1312
- const found = await findCodebyplanConfig(projectPath);
1313
- if (found) {
1314
- repoId = found.contents.repo_id;
1315
- if (!worktreeId) worktreeId = found.contents.worktree_id;
1316
- }
1317
- }
1318
- if (!repoId) {
1319
- throw new Error(
1320
- `Could not determine repo_id.
1321
-
1322
- Provide it via one of:
1323
- --repo-id <uuid> CLI flag
1324
- CODEBYPLAN_REPO_ID=<uuid> environment variable
1325
- .codebyplan/repo.json { "repo_id": "<uuid>" } in project root
1326
- Run 'codebyplan setup' to initialize this project`
1327
- );
1328
- }
1329
- return { repoId, worktreeId, projectPath };
1330
- }
1331
- var init_flags = __esm({
1332
- "src/lib/flags.ts"() {
1333
- "use strict";
1334
- }
1335
- });
1336
-
1337
1692
  // src/cli/confirm.ts
1338
1693
  var confirm_exports = {};
1339
1694
  __export(confirm_exports, {
@@ -1378,8 +1733,8 @@ var init_confirm = __esm({
1378
1733
  });
1379
1734
 
1380
1735
  // src/lib/tech-detect.ts
1381
- import { readFile as readFile7, access, readdir } from "node:fs/promises";
1382
- import { join as join7, relative } from "node:path";
1736
+ import { readFile as readFile8, access, readdir } from "node:fs/promises";
1737
+ import { join as join8, relative } from "node:path";
1383
1738
  async function fileExists(filePath) {
1384
1739
  try {
1385
1740
  await access(filePath);
@@ -1392,8 +1747,8 @@ async function discoverMonorepoApps(projectPath) {
1392
1747
  const apps = [];
1393
1748
  const patterns = [];
1394
1749
  try {
1395
- const raw = await readFile7(
1396
- join7(projectPath, "pnpm-workspace.yaml"),
1750
+ const raw = await readFile8(
1751
+ join8(projectPath, "pnpm-workspace.yaml"),
1397
1752
  "utf-8"
1398
1753
  );
1399
1754
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1407,7 +1762,7 @@ async function discoverMonorepoApps(projectPath) {
1407
1762
  }
1408
1763
  if (patterns.length === 0) {
1409
1764
  try {
1410
- const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
1765
+ const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1411
1766
  const pkg = JSON.parse(raw);
1412
1767
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1413
1768
  if (ws) patterns.push(...ws);
@@ -1417,14 +1772,14 @@ async function discoverMonorepoApps(projectPath) {
1417
1772
  for (const pattern of patterns) {
1418
1773
  if (pattern.endsWith("/*")) {
1419
1774
  const dir = pattern.slice(0, -2);
1420
- const absDir = join7(projectPath, dir);
1775
+ const absDir = join8(projectPath, dir);
1421
1776
  try {
1422
1777
  const entries = await readdir(absDir, { withFileTypes: true });
1423
1778
  for (const entry of entries) {
1424
1779
  if (entry.isDirectory()) {
1425
- const relPath = join7(dir, entry.name);
1426
- const absPath = join7(absDir, entry.name);
1427
- if (await fileExists(join7(absPath, "package.json"))) {
1780
+ const relPath = join8(dir, entry.name);
1781
+ const absPath = join8(absDir, entry.name);
1782
+ if (await fileExists(join8(absPath, "package.json"))) {
1428
1783
  apps.push({ name: entry.name, path: relPath, absPath });
1429
1784
  }
1430
1785
  }
@@ -1443,7 +1798,7 @@ async function hasJsxFile(dir, depth = 0) {
1443
1798
  const name = entry.name;
1444
1799
  if (entry.isDirectory()) {
1445
1800
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1446
- if (await hasJsxFile(join7(dir, name), depth + 1)) return true;
1801
+ if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1447
1802
  } else if (entry.isFile()) {
1448
1803
  if (JSX_TEST_PATTERN.test(name)) continue;
1449
1804
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1462,7 +1817,7 @@ async function hasJsxFile(dir, depth = 0) {
1462
1817
  async function detectCapabilities(dirPath, pkgJson) {
1463
1818
  const caps = /* @__PURE__ */ new Set();
1464
1819
  for (const sub of JSX_SCAN_DIRS) {
1465
- if (await hasJsxFile(join7(dirPath, sub))) {
1820
+ if (await hasJsxFile(join8(dirPath, sub))) {
1466
1821
  caps.add("jsx");
1467
1822
  break;
1468
1823
  }
@@ -1484,7 +1839,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1484
1839
  }
1485
1840
  }
1486
1841
  }
1487
- if (!caps.has("node-server") && await fileExists(join7(dirPath, "src", "main.ts"))) {
1842
+ if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
1488
1843
  caps.add("node-server");
1489
1844
  }
1490
1845
  if (pkgJson && pkgJson.bin) {
@@ -1500,7 +1855,7 @@ async function detectFromDirectory(dirPath) {
1500
1855
  const seen = /* @__PURE__ */ new Map();
1501
1856
  let pkgJson = null;
1502
1857
  try {
1503
- const raw = await readFile7(join7(dirPath, "package.json"), "utf-8");
1858
+ const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1504
1859
  pkgJson = JSON.parse(raw);
1505
1860
  const allDeps = {
1506
1861
  ...pkgJson.dependencies ?? {},
@@ -1532,7 +1887,7 @@ async function detectFromDirectory(dirPath) {
1532
1887
  }
1533
1888
  for (const { file, rule } of CONFIG_FILE_MAP) {
1534
1889
  const key = rule.name.toLowerCase();
1535
- if (!seen.has(key) && await fileExists(join7(dirPath, file))) {
1890
+ if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
1536
1891
  seen.set(key, { name: rule.name, category: rule.category });
1537
1892
  }
1538
1893
  }
@@ -1710,7 +2065,7 @@ function categorizeDependency(depName) {
1710
2065
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1711
2066
  if (depth > 4) return [];
1712
2067
  const results = [];
1713
- const pkgPath = join7(dir, "package.json");
2068
+ const pkgPath = join8(dir, "package.json");
1714
2069
  if (await fileExists(pkgPath)) {
1715
2070
  results.push(pkgPath);
1716
2071
  }
@@ -1719,7 +2074,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1719
2074
  for (const entry of entries) {
1720
2075
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1721
2076
  const subResults = await findPackageJsonFiles(
1722
- join7(dir, entry.name),
2077
+ join8(dir, entry.name),
1723
2078
  projectPath,
1724
2079
  depth + 1
1725
2080
  );
@@ -1734,7 +2089,7 @@ async function scanAllDependencies(projectPath) {
1734
2089
  const dependencies = [];
1735
2090
  for (const pkgPath of packageJsonPaths) {
1736
2091
  try {
1737
- const raw = await readFile7(pkgPath, "utf-8");
2092
+ const raw = await readFile8(pkgPath, "utf-8");
1738
2093
  const pkg = JSON.parse(raw);
1739
2094
  const sourcePath = relative(projectPath, pkgPath);
1740
2095
  const depSections = [
@@ -2334,8 +2689,8 @@ __export(eslint_exports, {
2334
2689
  eslintInit: () => eslintInit,
2335
2690
  runEslint: () => runEslint
2336
2691
  });
2337
- import { readFile as readFile8, writeFile as writeFile6, access as access2, readdir as readdir2 } from "node:fs/promises";
2338
- import { join as join8, relative as relative2 } from "node:path";
2692
+ import { readFile as readFile9, writeFile as writeFile7, access as access2, readdir as readdir2 } from "node:fs/promises";
2693
+ import { join as join9, relative as relative2 } from "node:path";
2339
2694
  async function fileExists2(filePath) {
2340
2695
  try {
2341
2696
  await access2(filePath);
@@ -2346,7 +2701,7 @@ async function fileExists2(filePath) {
2346
2701
  }
2347
2702
  async function autoDetectIgnorePatterns(absPath) {
2348
2703
  const patterns = [];
2349
- if (await fileExists2(join8(absPath, "esbuild.js"))) {
2704
+ if (await fileExists2(join9(absPath, "esbuild.js"))) {
2350
2705
  patterns.push("esbuild.js");
2351
2706
  }
2352
2707
  let entries = [];
@@ -2366,19 +2721,19 @@ async function autoDetectIgnorePatterns(absPath) {
2366
2721
  }
2367
2722
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2368
2723
  const candidate = `vitest.config.${ext}`;
2369
- if (await fileExists2(join8(absPath, candidate))) {
2724
+ if (await fileExists2(join9(absPath, candidate))) {
2370
2725
  patterns.push(candidate);
2371
2726
  break;
2372
2727
  }
2373
2728
  }
2374
2729
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2375
2730
  const candidate = `vite.config.${ext}`;
2376
- if (await fileExists2(join8(absPath, candidate))) {
2731
+ if (await fileExists2(join9(absPath, candidate))) {
2377
2732
  patterns.push(candidate);
2378
2733
  break;
2379
2734
  }
2380
2735
  }
2381
- if (await fileExists2(join8(absPath, "tauri.conf.json"))) {
2736
+ if (await fileExists2(join9(absPath, "tauri.conf.json"))) {
2382
2737
  patterns.push("src-tauri/**");
2383
2738
  patterns.push("**/*.d.ts");
2384
2739
  }
@@ -2386,14 +2741,14 @@ async function autoDetectIgnorePatterns(absPath) {
2386
2741
  }
2387
2742
  function detectPackageManager(projectPath) {
2388
2743
  return (async () => {
2389
- if (await fileExists2(join8(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2390
- if (await fileExists2(join8(projectPath, "yarn.lock"))) return "yarn";
2744
+ if (await fileExists2(join9(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2745
+ if (await fileExists2(join9(projectPath, "yarn.lock"))) return "yarn";
2391
2746
  return "npm";
2392
2747
  })();
2393
2748
  }
2394
2749
  async function getInstalledDeps(pkgJsonPath) {
2395
2750
  try {
2396
- const raw = await readFile8(pkgJsonPath, "utf-8");
2751
+ const raw = await readFile9(pkgJsonPath, "utf-8");
2397
2752
  const pkg = JSON.parse(raw);
2398
2753
  const all = /* @__PURE__ */ new Set();
2399
2754
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2506,7 +2861,7 @@ async function eslintInit(repoId, projectPath) {
2506
2861
  ignorePatterns: detectedIgnores
2507
2862
  });
2508
2863
  const hash = hashConfig(content);
2509
- const configPath = join8(target.absPath, "eslint.config.mjs");
2864
+ const configPath = join9(target.absPath, "eslint.config.mjs");
2510
2865
  configsToWrite.push({
2511
2866
  target,
2512
2867
  presets,
@@ -2528,11 +2883,11 @@ async function eslintInit(repoId, projectPath) {
2528
2883
  return;
2529
2884
  }
2530
2885
  const pm = await detectPackageManager(projectPath);
2531
- const rootPkgJsonPath = join8(projectPath, "package.json");
2886
+ const rootPkgJsonPath = join9(projectPath, "package.json");
2532
2887
  const installed = await getInstalledDeps(rootPkgJsonPath);
2533
2888
  if (isMonorepo) {
2534
2889
  for (const { target } of configsToWrite) {
2535
- const appPkgJson = join8(target.absPath, "package.json");
2890
+ const appPkgJson = join9(target.absPath, "package.json");
2536
2891
  const appDeps = await getInstalledDeps(appPkgJson);
2537
2892
  for (const dep of appDeps) {
2538
2893
  installed.add(dep);
@@ -2584,7 +2939,7 @@ async function eslintInit(repoId, projectPath) {
2584
2939
  } of configsToWrite) {
2585
2940
  if (await fileExists2(configPath)) {
2586
2941
  try {
2587
- const existing = await readFile8(configPath, "utf-8");
2942
+ const existing = await readFile9(configPath, "utf-8");
2588
2943
  const existingHash = hashConfig(existing);
2589
2944
  if (existingHash === hash) {
2590
2945
  console.log(
@@ -2604,7 +2959,7 @@ async function eslintInit(repoId, projectPath) {
2604
2959
  }
2605
2960
  }
2606
2961
  try {
2607
- await writeFile6(configPath, content, "utf-8");
2962
+ await writeFile7(configPath, content, "utf-8");
2608
2963
  } catch (err) {
2609
2964
  console.error(
2610
2965
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -2912,7 +3267,7 @@ __export(round_exports, {
2912
3267
  runRoundSyncApprovals: () => runRoundSyncApprovals
2913
3268
  });
2914
3269
  import { access as access3 } from "node:fs/promises";
2915
- import { join as join9 } from "node:path";
3270
+ import { join as join10 } from "node:path";
2916
3271
  import { execSync as execSync2 } from "node:child_process";
2917
3272
  async function runRoundCommand(args) {
2918
3273
  const subcommand = args[0];
@@ -3006,7 +3361,7 @@ async function runRoundSyncApprovals(args) {
3006
3361
  "sync-approvals: git status failed; proceeding with empty diff\n"
3007
3362
  );
3008
3363
  }
3009
- const hookPath = join9(
3364
+ const hookPath = join10(
3010
3365
  repoRoot,
3011
3366
  ".claude",
3012
3367
  "hooks",
@@ -3128,8 +3483,8 @@ var init_round = __esm({
3128
3483
  });
3129
3484
 
3130
3485
  // src/lib/migrate-branch-model.ts
3131
- import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
3132
- import { join as join10 } from "node:path";
3486
+ import { readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
3487
+ import { join as join11 } from "node:path";
3133
3488
  import { execSync as execSync3 } from "node:child_process";
3134
3489
  function assertValidBranchName(branch) {
3135
3490
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3139,7 +3494,7 @@ function assertValidBranchName(branch) {
3139
3494
  }
3140
3495
  }
3141
3496
  async function readJsonFile(filePath) {
3142
- const raw = await readFile9(filePath, "utf-8");
3497
+ const raw = await readFile10(filePath, "utf-8");
3143
3498
  const parsed = JSON.parse(raw);
3144
3499
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3145
3500
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3208,12 +3563,12 @@ async function runBranchMigration(opts) {
3208
3563
  if (found) {
3209
3564
  if (found.path.endsWith("/repo.json")) {
3210
3565
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3211
- configPath = join10(dir, "git.json");
3566
+ configPath = join11(dir, "git.json");
3212
3567
  } else {
3213
3568
  configPath = found.path;
3214
3569
  }
3215
3570
  } else {
3216
- configPath = join10(cwd, ".codebyplan", "git.json");
3571
+ configPath = join11(cwd, ".codebyplan", "git.json");
3217
3572
  }
3218
3573
  let fileRaw;
3219
3574
  let fileParsed;
@@ -3287,7 +3642,7 @@ async function runBranchMigration(opts) {
3287
3642
  const updatedParsed = { ...fileParsed, branch_config: after };
3288
3643
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3289
3644
  if (newJson !== fileRaw) {
3290
- await writeFile7(configPath, newJson, "utf-8");
3645
+ await writeFile8(configPath, newJson, "utf-8");
3291
3646
  }
3292
3647
  }
3293
3648
  return {
@@ -3393,9 +3748,9 @@ var init_branch = __esm({
3393
3748
  });
3394
3749
 
3395
3750
  // src/lib/ship.ts
3396
- import { readFile as readFile10 } from "node:fs/promises";
3397
- import { join as join11 } from "node:path";
3398
- import { execSync as execSync4, spawnSync } from "node:child_process";
3751
+ import { readFile as readFile11 } from "node:fs/promises";
3752
+ import { join as join12 } from "node:path";
3753
+ import { execSync as execSync4, spawnSync as spawnSync2 } from "node:child_process";
3399
3754
  function assertValidBranchName2(branch, label) {
3400
3755
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
3401
3756
  throw new Error(
@@ -3409,15 +3764,15 @@ async function readBaseBranch(cwd) {
3409
3764
  if (found) {
3410
3765
  if (found.path.endsWith("/repo.json")) {
3411
3766
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3412
- gitJsonPath = join11(dir, "git.json");
3767
+ gitJsonPath = join12(dir, "git.json");
3413
3768
  } else {
3414
3769
  gitJsonPath = found.path;
3415
3770
  }
3416
3771
  } else {
3417
- gitJsonPath = join11(cwd, ".codebyplan", "git.json");
3772
+ gitJsonPath = join12(cwd, ".codebyplan", "git.json");
3418
3773
  }
3419
3774
  try {
3420
- const raw = await readFile10(gitJsonPath, "utf-8");
3775
+ const raw = await readFile11(gitJsonPath, "utf-8");
3421
3776
  const parsed = JSON.parse(raw);
3422
3777
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3423
3778
  return "main";
@@ -3450,7 +3805,7 @@ async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Prom
3450
3805
  let totalGhErrors = 0;
3451
3806
  let iteration = 0;
3452
3807
  while (Date.now() < deadline) {
3453
- const ghResult = spawnSync(
3808
+ const ghResult = spawnSync2(
3454
3809
  "gh",
3455
3810
  ["pr", "checks", feat, "--json", "name,state,conclusion"],
3456
3811
  {
@@ -3616,7 +3971,7 @@ ${statusOut.trim()}`
3616
3971
  } else {
3617
3972
  createArgs.push("--body", defaultPrBody(feat, base));
3618
3973
  }
3619
- const createResult = spawnSync("gh", createArgs, {
3974
+ const createResult = spawnSync2("gh", createArgs, {
3620
3975
  cwd,
3621
3976
  encoding: "utf-8",
3622
3977
  stdio: ["pipe", "pipe", "pipe"]
@@ -3646,7 +4001,7 @@ ${statusOut.trim()}`
3646
4001
  });
3647
4002
  let mergeCommit = null;
3648
4003
  try {
3649
- const prViewResult = spawnSync(
4004
+ const prViewResult = spawnSync2(
3650
4005
  "gh",
3651
4006
  ["pr", "view", feat, "--json", "state,mergeCommit"],
3652
4007
  {
@@ -3806,85 +4161,167 @@ var init_ship2 = __esm({
3806
4161
  // src/cli/resolve-worktree.ts
3807
4162
  var resolve_worktree_exports = {};
3808
4163
  __export(resolve_worktree_exports, {
4164
+ ProcessExitSignal: () => ProcessExitSignal,
3809
4165
  runResolveWorktree: () => runResolveWorktree
3810
4166
  });
3811
4167
  import { execSync as execSync5 } from "node:child_process";
4168
+ function distress(kind, message, jsonMode) {
4169
+ if (jsonMode) return;
4170
+ process.stderr.write(`resolve-worktree: ${kind}: ${message}
4171
+ `);
4172
+ }
3812
4173
  async function runResolveWorktree() {
4174
+ const jsonMode = hasFlag("json", 3);
4175
+ let errorContext = null;
4176
+ const migrationNoticeCallback = (legacyPath, primaryPath) => {
4177
+ if (!jsonMode) {
4178
+ process.stderr.write(
4179
+ `resolve-worktree: local_config_migration: ${legacyPath} is the legacy flat config; move device_id to ${primaryPath}
4180
+ `
4181
+ );
4182
+ }
4183
+ };
3813
4184
  try {
3814
4185
  const projectPath = process.cwd();
3815
4186
  const found = await findCodebyplanConfig(projectPath);
3816
4187
  if (!found?.contents.repo_id) {
3817
- process.exit(0);
4188
+ emitAndExit(null, null, jsonMode);
3818
4189
  }
3819
4190
  const repoId = found.contents.repo_id;
3820
- const deviceId = await getOrCreateDeviceId(projectPath);
4191
+ try {
4192
+ await readLocalConfig(projectPath);
4193
+ } catch (readErr) {
4194
+ const readErrCode = readErr.code;
4195
+ errorContext = {
4196
+ kind: readErrCode === "LEGACY_FILE_BLOCKS_DIR" ? "legacy_file_blocks_dir" : "local_config_read_failed",
4197
+ message: readErr instanceof Error ? readErr.message : String(readErr)
4198
+ };
4199
+ }
4200
+ let deviceId;
4201
+ try {
4202
+ deviceId = await getOrCreateDeviceId(
4203
+ projectPath,
4204
+ migrationNoticeCallback
4205
+ );
4206
+ } catch (deviceErr) {
4207
+ const code = deviceErr.code;
4208
+ if (code === "LEGACY_FILE_BLOCKS_DIR") {
4209
+ errorContext = {
4210
+ kind: "legacy_file_blocks_dir",
4211
+ message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
4212
+ };
4213
+ } else if (errorContext === null || errorContext.kind !== "local_config_read_failed" && errorContext.kind !== "legacy_file_blocks_dir") {
4214
+ errorContext = {
4215
+ kind: "local_config_write_failed",
4216
+ message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
4217
+ };
4218
+ }
4219
+ emitAndExit(null, errorContext, jsonMode);
4220
+ }
3821
4221
  let branch = "";
3822
4222
  try {
3823
4223
  branch = execSync5("git symbolic-ref --short HEAD", {
3824
4224
  cwd: projectPath,
3825
4225
  encoding: "utf-8"
3826
4226
  }).trim();
3827
- } catch {
4227
+ } catch (gitErr) {
4228
+ if (errorContext === null) {
4229
+ errorContext = {
4230
+ kind: "git_failed",
4231
+ message: gitErr instanceof Error ? gitErr.message : String(gitErr)
4232
+ };
4233
+ }
3828
4234
  }
4235
+ const onResolverError = (kind, err) => {
4236
+ if (errorContext === null) {
4237
+ errorContext = { kind, message: err.message };
4238
+ }
4239
+ };
3829
4240
  const worktreeId = await resolveWorktreeId({
3830
4241
  repoId,
3831
4242
  repoPath: projectPath,
3832
4243
  branch,
3833
- deviceId
4244
+ deviceId,
4245
+ onError: onResolverError
3834
4246
  });
3835
4247
  if (worktreeId) {
3836
- process.stdout.write(worktreeId);
3837
- process.exit(0);
4248
+ emitAndExit(worktreeId, errorContext, jsonMode);
3838
4249
  }
3839
4250
  const useFallback = hasFlag("fallback-from-branch", 3);
3840
4251
  if (useFallback) {
3841
4252
  const fallbackId = await resolveWorktreeByBranch({
3842
4253
  repoId,
3843
4254
  deviceId,
3844
- branch
4255
+ branch,
4256
+ onError: onResolverError
3845
4257
  });
3846
4258
  if (fallbackId) {
3847
- process.stdout.write(fallbackId);
4259
+ emitAndExit(fallbackId, errorContext, jsonMode);
3848
4260
  }
3849
4261
  }
3850
- process.exit(0);
4262
+ emitAndExit(null, errorContext, jsonMode);
3851
4263
  } catch (err) {
3852
- if (process.env.CODEBYPLAN_DEBUG === "1") {
3853
- const msg = err instanceof Error ? err.message : String(err);
3854
- process.stderr.write(`resolve-worktree: ${msg}
3855
- `);
4264
+ if (err instanceof ProcessExitSignal) throw err;
4265
+ const msg = err instanceof Error ? err.message : String(err);
4266
+ errorContext = { kind: "unhandled", message: msg };
4267
+ emitAndExit(null, errorContext, jsonMode);
4268
+ }
4269
+ }
4270
+ function emitAndExit(worktreeId, errorContext, jsonMode) {
4271
+ if (jsonMode) {
4272
+ const errorKind = errorContext?.kind ?? (worktreeId === null ? "tuple_miss" : null);
4273
+ process.stdout.write(
4274
+ JSON.stringify({ worktree_id: worktreeId, error_kind: errorKind }) + "\n"
4275
+ );
4276
+ } else {
4277
+ if (worktreeId !== null) {
4278
+ process.stdout.write(worktreeId);
4279
+ }
4280
+ if (errorContext !== null) {
4281
+ if (errorContext.kind !== "unhandled" || process.env.CODEBYPLAN_DEBUG === "1") {
4282
+ distress(errorContext.kind, errorContext.message, jsonMode);
4283
+ }
3856
4284
  }
3857
- process.exit(0);
3858
4285
  }
4286
+ process.exit(0);
3859
4287
  }
4288
+ var ProcessExitSignal;
3860
4289
  var init_resolve_worktree2 = __esm({
3861
4290
  "src/cli/resolve-worktree.ts"() {
3862
4291
  "use strict";
3863
4292
  init_flags();
3864
4293
  init_local_config();
3865
4294
  init_resolve_worktree();
4295
+ ProcessExitSignal = class extends Error {
4296
+ code;
4297
+ constructor(code) {
4298
+ super(`process.exit(${code})`);
4299
+ this.name = "ProcessExitSignal";
4300
+ this.code = code;
4301
+ }
4302
+ };
3866
4303
  }
3867
4304
  });
3868
4305
 
3869
4306
  // src/lib/migrate-local-config.ts
3870
- import { mkdir as mkdir4, readFile as readFile11, unlink as unlink2, writeFile as writeFile8 } from "node:fs/promises";
3871
- import { join as join12 } from "node:path";
4307
+ import { mkdir as mkdir5, readFile as readFile12, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
4308
+ import { join as join13 } from "node:path";
3872
4309
  function legacySharedPath(projectPath) {
3873
- return join12(projectPath, ".codebyplan.json");
4310
+ return join13(projectPath, ".codebyplan.json");
3874
4311
  }
3875
4312
  function legacyLocalPath(projectPath) {
3876
- return join12(projectPath, ".codebyplan.local.json");
4313
+ return join13(projectPath, ".codebyplan.local.json");
3877
4314
  }
3878
4315
  function newDirPath(projectPath) {
3879
- return join12(projectPath, ".codebyplan");
4316
+ return join13(projectPath, ".codebyplan");
3880
4317
  }
3881
4318
  function sentinelPath(projectPath) {
3882
- return join12(projectPath, ".codebyplan", "repo.json");
4319
+ return join13(projectPath, ".codebyplan", "repo.json");
3883
4320
  }
3884
4321
  async function statSafe(p) {
3885
- const { stat } = await import("node:fs/promises");
4322
+ const { stat: stat2 } = await import("node:fs/promises");
3886
4323
  try {
3887
- return await stat(p);
4324
+ return await stat2(p);
3888
4325
  } catch {
3889
4326
  return null;
3890
4327
  }
@@ -3918,7 +4355,7 @@ async function runLocalMigration(projectPath) {
3918
4355
  }
3919
4356
  let legacyRaw;
3920
4357
  try {
3921
- legacyRaw = await readFile11(legacySharedPath(projectPath), "utf-8");
4358
+ legacyRaw = await readFile12(legacySharedPath(projectPath), "utf-8");
3922
4359
  } catch {
3923
4360
  return {
3924
4361
  migrated: true,
@@ -3945,7 +4382,7 @@ async function runLocalMigration(projectPath) {
3945
4382
  let deviceId;
3946
4383
  let deviceWrittenByHelper = false;
3947
4384
  try {
3948
- const localRaw = await readFile11(legacyLocalPath(projectPath), "utf-8");
4385
+ const localRaw = await readFile12(legacyLocalPath(projectPath), "utf-8");
3949
4386
  const localParsed = JSON.parse(localRaw);
3950
4387
  if (typeof localParsed.device_id === "string") {
3951
4388
  deviceId = localParsed.device_id;
@@ -3953,7 +4390,7 @@ async function runLocalMigration(projectPath) {
3953
4390
  } catch {
3954
4391
  }
3955
4392
  try {
3956
- await mkdir4(newDirPath(projectPath), { recursive: true });
4393
+ await mkdir5(newDirPath(projectPath), { recursive: true });
3957
4394
  } catch (err) {
3958
4395
  const code = err.code;
3959
4396
  if (code === "ENOTDIR" || code === "EEXIST") {
@@ -3972,8 +4409,8 @@ async function runLocalMigration(projectPath) {
3972
4409
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
3973
4410
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
3974
4411
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
3975
- await writeFile8(
3976
- join12(projectPath, ".codebyplan", "repo.json"),
4412
+ await writeFile9(
4413
+ join13(projectPath, ".codebyplan", "repo.json"),
3977
4414
  JSON.stringify(repoJson, null, 2) + "\n",
3978
4415
  "utf-8"
3979
4416
  );
@@ -3985,8 +4422,8 @@ async function runLocalMigration(projectPath) {
3985
4422
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
3986
4423
  if ("port_allocations" in cfg)
3987
4424
  serverJson.port_allocations = cfg.port_allocations;
3988
- await writeFile8(
3989
- join12(projectPath, ".codebyplan", "server.json"),
4425
+ await writeFile9(
4426
+ join13(projectPath, ".codebyplan", "server.json"),
3990
4427
  JSON.stringify(serverJson, null, 2) + "\n",
3991
4428
  "utf-8"
3992
4429
  );
@@ -3994,30 +4431,30 @@ async function runLocalMigration(projectPath) {
3994
4431
  const gitJson = {};
3995
4432
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
3996
4433
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
3997
- await writeFile8(
3998
- join12(projectPath, ".codebyplan", "git.json"),
4434
+ await writeFile9(
4435
+ join13(projectPath, ".codebyplan", "git.json"),
3999
4436
  JSON.stringify(gitJson, null, 2) + "\n",
4000
4437
  "utf-8"
4001
4438
  );
4002
4439
  filesChanged.push(".codebyplan/git.json");
4003
4440
  const shipmentJson = {};
4004
4441
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4005
- await writeFile8(
4006
- join12(projectPath, ".codebyplan", "shipment.json"),
4442
+ await writeFile9(
4443
+ join13(projectPath, ".codebyplan", "shipment.json"),
4007
4444
  JSON.stringify(shipmentJson, null, 2) + "\n",
4008
4445
  "utf-8"
4009
4446
  );
4010
4447
  filesChanged.push(".codebyplan/shipment.json");
4011
4448
  const vendorJson = {};
4012
- await writeFile8(
4013
- join12(projectPath, ".codebyplan", "vendor.json"),
4449
+ await writeFile9(
4450
+ join13(projectPath, ".codebyplan", "vendor.json"),
4014
4451
  JSON.stringify(vendorJson, null, 2) + "\n",
4015
4452
  "utf-8"
4016
4453
  );
4017
4454
  filesChanged.push(".codebyplan/vendor.json");
4018
4455
  if (!deviceWrittenByHelper) {
4019
- await writeFile8(
4020
- join12(projectPath, ".codebyplan", "device.local.json"),
4456
+ await writeFile9(
4457
+ join13(projectPath, ".codebyplan", "device.local.json"),
4021
4458
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4022
4459
  "utf-8"
4023
4460
  );
@@ -4029,9 +4466,9 @@ async function runLocalMigration(projectPath) {
4029
4466
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4030
4467
  );
4031
4468
  }
4032
- const gitignorePath = join12(projectPath, ".gitignore");
4469
+ const gitignorePath = join13(projectPath, ".gitignore");
4033
4470
  try {
4034
- const gitignoreContent = await readFile11(gitignorePath, "utf-8");
4471
+ const gitignoreContent = await readFile12(gitignorePath, "utf-8");
4035
4472
  const legacyLine = ".codebyplan.local.json";
4036
4473
  const newLine = ".codebyplan/device.local.json";
4037
4474
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -4050,7 +4487,7 @@ async function runLocalMigration(projectPath) {
4050
4487
  updated = gitignoreContent;
4051
4488
  }
4052
4489
  if (updated !== gitignoreContent) {
4053
- await writeFile8(gitignorePath, updated, "utf-8");
4490
+ await writeFile9(gitignorePath, updated, "utf-8");
4054
4491
  filesChanged.push(".gitignore");
4055
4492
  }
4056
4493
  } catch {
@@ -4089,8 +4526,8 @@ __export(config_exports, {
4089
4526
  readVendorConfig: () => readVendorConfig,
4090
4527
  runConfig: () => runConfig
4091
4528
  });
4092
- import { mkdir as mkdir5, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
4093
- import { join as join13 } from "node:path";
4529
+ import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "node:fs/promises";
4530
+ import { join as join14 } from "node:path";
4094
4531
  async function runConfig() {
4095
4532
  const flags = parseFlags(3);
4096
4533
  const dryRun = hasFlag("dry-run", 3);
@@ -4123,7 +4560,7 @@ async function runConfig() {
4123
4560
  console.log("\n Config complete.\n");
4124
4561
  }
4125
4562
  async function syncConfigToFile(repoId, projectPath, dryRun) {
4126
- const codebyplanDir = join13(projectPath, ".codebyplan");
4563
+ const codebyplanDir = join14(projectPath, ".codebyplan");
4127
4564
  let resolvedWorktreeId;
4128
4565
  try {
4129
4566
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4252,7 +4689,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4252
4689
  console.log(" Config would be updated (dry-run).");
4253
4690
  return;
4254
4691
  }
4255
- await mkdir5(codebyplanDir, { recursive: true });
4692
+ await mkdir6(codebyplanDir, { recursive: true });
4256
4693
  const files = [
4257
4694
  { name: "repo.json", payload: repoPayload },
4258
4695
  { name: "server.json", payload: serverPayload },
@@ -4262,15 +4699,15 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4262
4699
  ];
4263
4700
  let anyUpdated = false;
4264
4701
  for (const { name, payload } of files) {
4265
- const filePath = join13(codebyplanDir, name);
4702
+ const filePath = join14(codebyplanDir, name);
4266
4703
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4267
4704
  let currentJson = "";
4268
4705
  try {
4269
- currentJson = await readFile12(filePath, "utf-8");
4706
+ currentJson = await readFile13(filePath, "utf-8");
4270
4707
  } catch {
4271
4708
  }
4272
4709
  if (currentJson === newJson) continue;
4273
- await writeFile9(filePath, newJson, "utf-8");
4710
+ await writeFile10(filePath, newJson, "utf-8");
4274
4711
  console.log(` Updated .codebyplan/${name}`);
4275
4712
  anyUpdated = true;
4276
4713
  }
@@ -4280,8 +4717,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4280
4717
  }
4281
4718
  async function readRepoConfig(projectPath) {
4282
4719
  try {
4283
- const raw = await readFile12(
4284
- join13(projectPath, ".codebyplan", "repo.json"),
4720
+ const raw = await readFile13(
4721
+ join14(projectPath, ".codebyplan", "repo.json"),
4285
4722
  "utf-8"
4286
4723
  );
4287
4724
  return JSON.parse(raw);
@@ -4291,8 +4728,8 @@ async function readRepoConfig(projectPath) {
4291
4728
  }
4292
4729
  async function readServerConfig(projectPath) {
4293
4730
  try {
4294
- const raw = await readFile12(
4295
- join13(projectPath, ".codebyplan", "server.json"),
4731
+ const raw = await readFile13(
4732
+ join14(projectPath, ".codebyplan", "server.json"),
4296
4733
  "utf-8"
4297
4734
  );
4298
4735
  return JSON.parse(raw);
@@ -4302,8 +4739,8 @@ async function readServerConfig(projectPath) {
4302
4739
  }
4303
4740
  async function readGitConfig(projectPath) {
4304
4741
  try {
4305
- const raw = await readFile12(
4306
- join13(projectPath, ".codebyplan", "git.json"),
4742
+ const raw = await readFile13(
4743
+ join14(projectPath, ".codebyplan", "git.json"),
4307
4744
  "utf-8"
4308
4745
  );
4309
4746
  return JSON.parse(raw);
@@ -4313,8 +4750,8 @@ async function readGitConfig(projectPath) {
4313
4750
  }
4314
4751
  async function readShipmentConfig(projectPath) {
4315
4752
  try {
4316
- const raw = await readFile12(
4317
- join13(projectPath, ".codebyplan", "shipment.json"),
4753
+ const raw = await readFile13(
4754
+ join14(projectPath, ".codebyplan", "shipment.json"),
4318
4755
  "utf-8"
4319
4756
  );
4320
4757
  return JSON.parse(raw);
@@ -4324,8 +4761,8 @@ async function readShipmentConfig(projectPath) {
4324
4761
  }
4325
4762
  async function readVendorConfig(projectPath) {
4326
4763
  try {
4327
- const raw = await readFile12(
4328
- join13(projectPath, ".codebyplan", "vendor.json"),
4764
+ const raw = await readFile13(
4765
+ join14(projectPath, ".codebyplan", "vendor.json"),
4329
4766
  "utf-8"
4330
4767
  );
4331
4768
  return JSON.parse(raw);
@@ -4381,14 +4818,14 @@ var init_server_detect = __esm({
4381
4818
  });
4382
4819
 
4383
4820
  // src/lib/port-verify.ts
4384
- import { readFile as readFile13 } from "node:fs/promises";
4821
+ import { readFile as readFile14 } from "node:fs/promises";
4385
4822
  async function verifyPorts(projectPath, portAllocations) {
4386
4823
  const mismatches = [];
4387
4824
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
4388
4825
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
4389
4826
  for (const pkgPath of packageJsonPaths) {
4390
4827
  try {
4391
- const raw = await readFile13(pkgPath, "utf-8");
4828
+ const raw = await readFile14(pkgPath, "utf-8");
4392
4829
  const pkg = JSON.parse(raw);
4393
4830
  const scriptPort = detectPortFromScripts(pkg);
4394
4831
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -4451,7 +4888,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
4451
4888
  }
4452
4889
  let pkg;
4453
4890
  try {
4454
- const raw = await readFile13(`${app.absPath}/package.json`, "utf-8");
4891
+ const raw = await readFile14(`${app.absPath}/package.json`, "utf-8");
4455
4892
  pkg = JSON.parse(raw);
4456
4893
  } catch {
4457
4894
  continue;
@@ -5253,6 +5690,11 @@ async function runInstall(opts, deps = {}) {
5253
5690
  await Promise.resolve();
5254
5691
  const scope = opts.scope ?? "project";
5255
5692
  if (scope === "user") {
5693
+ if (opts.renderer) {
5694
+ console.warn(
5695
+ "codebyplan claude install: --bash/--node/--python is ignored for --scope user (no project root for statusline.local.json)."
5696
+ );
5697
+ }
5256
5698
  runInstallUser(opts, deps);
5257
5699
  return;
5258
5700
  }
@@ -5329,6 +5771,9 @@ async function runInstall(opts, deps = {}) {
5329
5771
  console.log(
5330
5772
  `codebyplan claude install${opts.dryRun ? " (dry-run)" : ""}: ${manifestEntries.length} files, ${countHookEntries(templatesDir)} hook entries.`
5331
5773
  );
5774
+ if (opts.renderer && !opts.dryRun) {
5775
+ await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
5776
+ }
5332
5777
  } catch (err) {
5333
5778
  console.error(
5334
5779
  `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
@@ -5414,6 +5859,7 @@ var init_install = __esm({
5414
5859
  init_template_walker();
5415
5860
  init_manifest();
5416
5861
  init_settings_merge();
5862
+ init_statusline_config();
5417
5863
  }
5418
5864
  });
5419
5865
 
@@ -5541,6 +5987,11 @@ async function runUpdate(opts, deps = {}) {
5541
5987
  await Promise.resolve();
5542
5988
  const scope = opts.scope ?? "project";
5543
5989
  if (scope === "user") {
5990
+ if (opts.renderer) {
5991
+ console.warn(
5992
+ "codebyplan claude update: --bash/--node/--python is ignored for --scope user (no project root for statusline.local.json)."
5993
+ );
5994
+ }
5544
5995
  runUpdateUser(opts, deps);
5545
5996
  return;
5546
5997
  }
@@ -5685,6 +6136,9 @@ async function runUpdate(opts, deps = {}) {
5685
6136
  console.log(
5686
6137
  `codebyplan claude update${opts.dryRun ? " (dry-run)" : ""}: ${finalManifestEntries.length} files tracked.`
5687
6138
  );
6139
+ if (opts.renderer && !opts.dryRun) {
6140
+ await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
6141
+ }
5688
6142
  } catch (err) {
5689
6143
  console.error(
5690
6144
  `codebyplan claude update failed: ${err instanceof Error ? err.message : String(err)}`
@@ -5831,6 +6285,7 @@ var init_update = __esm({
5831
6285
  init_manifest();
5832
6286
  init_settings_merge();
5833
6287
  init_prompt();
6288
+ init_statusline_config();
5834
6289
  }
5835
6290
  });
5836
6291
 
@@ -6010,8 +6465,8 @@ function pruneEmptyManagedDirs(projectDir) {
6010
6465
  }
6011
6466
  function pruneLeafFirst(dir) {
6012
6467
  if (!fs5.existsSync(dir)) return;
6013
- const stat = fs5.statSync(dir);
6014
- if (!stat.isDirectory()) return;
6468
+ const stat2 = fs5.statSync(dir);
6469
+ if (!stat2.isDirectory()) return;
6015
6470
  for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6016
6471
  if (entry.isDirectory()) {
6017
6472
  pruneLeafFirst(path5.join(dir, entry.name));
@@ -6068,6 +6523,12 @@ void (async () => {
6068
6523
  await runSetup2();
6069
6524
  process.exit(0);
6070
6525
  }
6526
+ if (arg === "statusline") {
6527
+ const { runStatusline: runStatusline2 } = await Promise.resolve().then(() => (init_statusline(), statusline_exports));
6528
+ const rest = process.argv.slice(3);
6529
+ await runStatusline2(rest);
6530
+ process.exit(process.exitCode ?? 0);
6531
+ }
6071
6532
  if (arg === "login") {
6072
6533
  const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
6073
6534
  try {
@@ -6203,6 +6664,9 @@ void (async () => {
6203
6664
  user \u2014 writes only owned base settings into ~/.claude/settings.json;
6204
6665
  never copies templates or hooks
6205
6666
  --project-dir <path> Override the project root (default: cwd). Cannot be combined with --scope user.
6667
+ --bash Set statusline renderer to bash after install/update
6668
+ --node Set statusline renderer to node after install/update
6669
+ --python Set statusline renderer to python after install/update
6206
6670
  `);
6207
6671
  process.exit(0);
6208
6672
  }
@@ -6225,6 +6689,7 @@ void (async () => {
6225
6689
  codebyplan ship Ship current feat branch to production via PR
6226
6690
  codebyplan branch migrate Rewrite branch_config from 3-branch to 2-tier model
6227
6691
  codebyplan claude Claude asset management (install/update/uninstall)
6692
+ codebyplan statusline Show or set the statusline renderer (bash/node/python)
6228
6693
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
6229
6694
  codebyplan help Show this help message
6230
6695
  codebyplan --version Print version
@@ -6259,6 +6724,9 @@ void (async () => {
6259
6724
  --verbose Log every file operation
6260
6725
  --scope <user|project> Target scope (default: project)
6261
6726
  --project-dir <path> Override the project root (default: cwd)
6727
+ --bash Set statusline renderer to bash after install/update
6728
+ --node Set statusline renderer to node after install/update
6729
+ --python Set statusline renderer to python after install/update
6262
6730
 
6263
6731
  MCP Server:
6264
6732
  Claude Code connects to CodeByPlan via remote MCP:
@@ -6309,8 +6777,19 @@ function parseClaudeFlags(rest) {
6309
6777
  );
6310
6778
  return null;
6311
6779
  }
6780
+ const hasBash = rest.includes("--bash");
6781
+ const hasNode = rest.includes("--node");
6782
+ const hasPython = rest.includes("--python");
6783
+ const rendererCount = [hasBash, hasNode, hasPython].filter(Boolean).length;
6784
+ if (rendererCount > 1) {
6785
+ process.stderr.write(
6786
+ "error: --bash, --node, and --python are mutually exclusive; pass only one.\n"
6787
+ );
6788
+ return null;
6789
+ }
6790
+ const renderer = hasBash ? "bash" : hasNode ? "node" : hasPython ? "python" : void 0;
6312
6791
  return {
6313
- opts: { yes, dryRun, verbose, scope },
6792
+ opts: { yes, dryRun, verbose, scope, renderer },
6314
6793
  projectDir
6315
6794
  };
6316
6795
  }