codebyplan 1.10.3 → 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.3";
17
+ VERSION = "1.11.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -151,6 +151,122 @@ var init_local_config = __esm({
151
151
  }
152
152
  });
153
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
+
154
270
  // src/oauth/jwt-decode.ts
155
271
  function base64UrlDecode(input) {
156
272
  const padded = input + "=".repeat((4 - input.length % 4) % 4);
@@ -181,9 +297,9 @@ var init_jwt_decode = __esm({
181
297
  });
182
298
 
183
299
  // src/oauth/keychain.ts
184
- 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";
185
301
  import { homedir, platform } from "node:os";
186
- import { dirname as dirname2, join as join2 } from "node:path";
302
+ import { dirname as dirname2, join as join3 } from "node:path";
187
303
  async function loadKeyring() {
188
304
  if (keyringOverride !== void 0) return keyringOverride;
189
305
  try {
@@ -195,18 +311,18 @@ async function loadKeyring() {
195
311
  }
196
312
  function fallbackDir() {
197
313
  if (platform() === "win32") {
198
- const appData = process.env.APPDATA ?? join2(homedir(), "AppData", "Roaming");
199
- return join2(appData, "codebyplan");
314
+ const appData = process.env.APPDATA ?? join3(homedir(), "AppData", "Roaming");
315
+ return join3(appData, "codebyplan");
200
316
  }
201
- const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config");
202
- return join2(xdg, "codebyplan");
317
+ const xdg = process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config");
318
+ return join3(xdg, "codebyplan");
203
319
  }
204
320
  function fallbackFile(filename) {
205
- return join2(fallbackDir(), filename);
321
+ return join3(fallbackDir(), filename);
206
322
  }
207
323
  async function readFallback(filename) {
208
324
  try {
209
- const raw = await readFile2(fallbackFile(filename), "utf-8");
325
+ const raw = await readFile3(fallbackFile(filename), "utf-8");
210
326
  return JSON.parse(raw);
211
327
  } catch {
212
328
  return null;
@@ -214,8 +330,8 @@ async function readFallback(filename) {
214
330
  }
215
331
  async function writeFallback(filename, data) {
216
332
  const path6 = fallbackFile(filename);
217
- await mkdir2(dirname2(path6), { recursive: true });
218
- 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");
219
335
  if (platform() !== "win32") {
220
336
  try {
221
337
  await chmod(path6, 384);
@@ -566,8 +682,8 @@ var init_api = __esm({
566
682
  });
567
683
 
568
684
  // src/lib/resolve-worktree.ts
569
- import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
570
- 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";
571
687
  async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
572
688
  let worktreeId;
573
689
  try {
@@ -589,10 +705,10 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
589
705
  if (options?.skipWrite) {
590
706
  return worktreeId;
591
707
  }
592
- const codebyplanPath = join3(projectPath, ".codebyplan.json");
708
+ const codebyplanPath = join4(projectPath, ".codebyplan.json");
593
709
  let currentConfig = {};
594
710
  try {
595
- const raw = await readFile3(codebyplanPath, "utf-8");
711
+ const raw = await readFile4(codebyplanPath, "utf-8");
596
712
  const parsed = JSON.parse(raw);
597
713
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
598
714
  currentConfig = parsed;
@@ -607,7 +723,7 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
607
723
  worktree_id: worktreeId
608
724
  };
609
725
  try {
610
- await writeFile3(
726
+ await writeFile4(
611
727
  codebyplanPath,
612
728
  JSON.stringify(merged, null, 2) + "\n",
613
729
  "utf-8"
@@ -941,13 +1057,13 @@ var setup_exports = {};
941
1057
  __export(setup_exports, {
942
1058
  runSetup: () => runSetup
943
1059
  });
944
- 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";
945
1061
  import { homedir as homedir2 } from "node:os";
946
- import { join as join4 } from "node:path";
1062
+ import { join as join5 } from "node:path";
947
1063
  import { stdin, stdout as stdout2 } from "node:process";
948
1064
  import { createInterface } from "node:readline/promises";
949
1065
  function getConfigPath(scope) {
950
- 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");
951
1067
  }
952
1068
  function legacyMcpUrl() {
953
1069
  const baseUrl2 = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
@@ -955,7 +1071,7 @@ function legacyMcpUrl() {
955
1071
  }
956
1072
  async function readConfig(path6) {
957
1073
  try {
958
- const raw = await readFile4(path6, "utf-8");
1074
+ const raw = await readFile5(path6, "utf-8");
959
1075
  const parsed = JSON.parse(raw);
960
1076
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
961
1077
  return parsed;
@@ -978,7 +1094,7 @@ async function writeMcpConfig(scope, auth) {
978
1094
  config.mcpServers = {};
979
1095
  }
980
1096
  config.mcpServers.codebyplan = buildMcpEntry(auth);
981
- await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1097
+ await writeFile5(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
982
1098
  return configPath;
983
1099
  }
984
1100
  async function fetchRepos(auth) {
@@ -1033,8 +1149,8 @@ async function chooseAuthMode(rl) {
1033
1149
  return { kind: "legacy", apiKey };
1034
1150
  }
1035
1151
  async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1036
- const codebyplanDir = join4(projectPath, ".codebyplan");
1037
- await mkdir3(codebyplanDir, { recursive: true });
1152
+ const codebyplanDir = join5(projectPath, ".codebyplan");
1153
+ await mkdir4(codebyplanDir, { recursive: true });
1038
1154
  const repoJson = {
1039
1155
  repo_id: selectedRepo.id
1040
1156
  };
@@ -1045,13 +1161,13 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1045
1161
  if (typeof repoAny.project_id === "string") {
1046
1162
  repoJson.project_id = repoAny.project_id;
1047
1163
  }
1048
- await writeFile4(
1049
- join4(codebyplanDir, "repo.json"),
1164
+ await writeFile5(
1165
+ join5(codebyplanDir, "repo.json"),
1050
1166
  JSON.stringify(repoJson, null, 2) + "\n",
1051
1167
  "utf-8"
1052
1168
  );
1053
- await writeFile4(
1054
- join4(codebyplanDir, "server.json"),
1169
+ await writeFile5(
1170
+ join5(codebyplanDir, "server.json"),
1055
1171
  JSON.stringify(
1056
1172
  {
1057
1173
  server_port: null,
@@ -1064,40 +1180,68 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1064
1180
  ) + "\n",
1065
1181
  "utf-8"
1066
1182
  );
1067
- await writeFile4(
1068
- join4(codebyplanDir, "git.json"),
1183
+ await writeFile5(
1184
+ join5(codebyplanDir, "git.json"),
1069
1185
  JSON.stringify({ git_branch: null, branch_config: null }, null, 2) + "\n",
1070
1186
  "utf-8"
1071
1187
  );
1072
- await writeFile4(
1073
- join4(codebyplanDir, "shipment.json"),
1188
+ await writeFile5(
1189
+ join5(codebyplanDir, "shipment.json"),
1074
1190
  JSON.stringify({ shipment: null }, null, 2) + "\n",
1075
1191
  "utf-8"
1076
1192
  );
1077
- await writeFile4(
1078
- join4(codebyplanDir, "vendor.json"),
1193
+ await writeFile5(
1194
+ join5(codebyplanDir, "vendor.json"),
1079
1195
  JSON.stringify({}, null, 2) + "\n",
1080
1196
  "utf-8"
1081
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
+ }
1082
1212
  await writeLocalConfig(projectPath, { device_id: deviceId });
1083
1213
  console.log(` Created ${codebyplanDir}/`);
1084
1214
  console.log(
1085
- ` repo.json, server.json, git.json, shipment.json, vendor.json`
1215
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, statusline.json`
1086
1216
  );
1087
1217
  console.log(` device.local.json (gitignored)`);
1088
- const gitignorePath = join4(projectPath, ".gitignore");
1218
+ const gitignorePath = join5(projectPath, ".gitignore");
1089
1219
  const gitignoreEntry = ".codebyplan/device.local.json";
1220
+ const statuslineLocalEntry = ".codebyplan/statusline.local.json";
1090
1221
  try {
1091
- const existing = await readFile4(gitignorePath, "utf-8");
1222
+ const existing = await readFile5(gitignorePath, "utf-8");
1092
1223
  const lines = existing.split("\n");
1224
+ let content = existing;
1093
1225
  if (!lines.some((l) => l.trimEnd() === gitignoreEntry)) {
1094
- const appended = (existing.endsWith("\n") ? existing : existing + "\n") + gitignoreEntry + "\n";
1095
- await writeFile4(gitignorePath, appended, "utf-8");
1226
+ content = (content.endsWith("\n") ? content : content + "\n") + gitignoreEntry + "\n";
1096
1227
  console.log(` Added '${gitignoreEntry}' to .gitignore`);
1097
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
+ }
1098
1236
  } catch {
1099
- await writeFile4(gitignorePath, gitignoreEntry + "\n", "utf-8");
1100
- 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
+ );
1101
1245
  }
1102
1246
  }
1103
1247
  async function runSetup() {
@@ -1190,6 +1334,31 @@ async function runSetup() {
1190
1334
  const _worktreeId = tupleId ?? pathBasedId;
1191
1335
  void _worktreeId;
1192
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
+ }
1193
1362
  console.log(
1194
1363
  "\n Run `npx codebyplan config` to sync repo config from the DB."
1195
1364
  );
@@ -1204,6 +1373,7 @@ var init_setup = __esm({
1204
1373
  "src/cli/setup.ts"() {
1205
1374
  "use strict";
1206
1375
  init_local_config();
1376
+ init_statusline_config();
1207
1377
  init_resolve_worktree();
1208
1378
  init_token_refresh();
1209
1379
  init_urls();
@@ -1211,6 +1381,190 @@ var init_setup = __esm({
1211
1381
  }
1212
1382
  });
1213
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
+
1214
1568
  // src/cli/logout.ts
1215
1569
  var logout_exports = {};
1216
1570
  __export(logout_exports, {
@@ -1269,15 +1623,15 @@ var upgrade_auth_exports = {};
1269
1623
  __export(upgrade_auth_exports, {
1270
1624
  runUpgradeAuth: () => runUpgradeAuth
1271
1625
  });
1272
- import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1626
+ import { readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1273
1627
  import { homedir as homedir3 } from "node:os";
1274
- import { join as join5 } from "node:path";
1628
+ import { join as join7 } from "node:path";
1275
1629
  function configPaths() {
1276
- return [join5(homedir3(), ".claude.json"), join5(process.cwd(), ".mcp.json")];
1630
+ return [join7(homedir3(), ".claude.json"), join7(process.cwd(), ".mcp.json")];
1277
1631
  }
1278
1632
  async function readConfig2(path6) {
1279
1633
  try {
1280
- const raw = await readFile5(path6, "utf-8");
1634
+ const raw = await readFile7(path6, "utf-8");
1281
1635
  const parsed = JSON.parse(raw);
1282
1636
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1283
1637
  return parsed;
@@ -1298,7 +1652,7 @@ async function rewriteConfig(path6, config, newUrl) {
1298
1652
  if (!entry) return false;
1299
1653
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1300
1654
  servers.codebyplan = { url: newUrl };
1301
- await writeFile5(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1655
+ await writeFile6(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1302
1656
  return true;
1303
1657
  }
1304
1658
  async function runUpgradeAuth() {
@@ -1335,77 +1689,6 @@ var init_upgrade_auth = __esm({
1335
1689
  }
1336
1690
  });
1337
1691
 
1338
- // src/lib/flags.ts
1339
- import { readFile as readFile6 } from "node:fs/promises";
1340
- import { join as join6, resolve } from "node:path";
1341
- async function findCodebyplanConfig(startDir, maxDepth = 20) {
1342
- let cursor = resolve(startDir);
1343
- for (let depth = 0; depth < maxDepth; depth++) {
1344
- const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1345
- try {
1346
- const raw = await readFile6(sentinelPath2, "utf-8");
1347
- const parsed = JSON.parse(raw);
1348
- return { path: sentinelPath2, contents: parsed };
1349
- } catch {
1350
- }
1351
- const legacyPath = join6(cursor, ".codebyplan.json");
1352
- try {
1353
- const raw = await readFile6(legacyPath, "utf-8");
1354
- const parsed = JSON.parse(raw);
1355
- return { path: legacyPath, contents: parsed };
1356
- } catch {
1357
- }
1358
- const parent = resolve(cursor, "..");
1359
- if (parent === cursor) return null;
1360
- cursor = parent;
1361
- }
1362
- return null;
1363
- }
1364
- function parseFlags(startIndex) {
1365
- const flags = {};
1366
- const args = process.argv.slice(startIndex);
1367
- for (let i = 0; i < args.length; i++) {
1368
- const arg = args[i];
1369
- if (arg.startsWith("--") && i + 1 < args.length) {
1370
- const key = arg.slice(2);
1371
- flags[key] = args[++i];
1372
- }
1373
- }
1374
- return flags;
1375
- }
1376
- function hasFlag(name, startIndex) {
1377
- return process.argv.slice(startIndex).includes(`--${name}`);
1378
- }
1379
- async function resolveConfig(flags) {
1380
- const projectPath = flags["path"] ?? process.cwd();
1381
- let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
1382
- let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
1383
- if (!repoId) {
1384
- const found = await findCodebyplanConfig(projectPath);
1385
- if (found) {
1386
- repoId = found.contents.repo_id;
1387
- if (!worktreeId) worktreeId = found.contents.worktree_id;
1388
- }
1389
- }
1390
- if (!repoId) {
1391
- throw new Error(
1392
- `Could not determine repo_id.
1393
-
1394
- Provide it via one of:
1395
- --repo-id <uuid> CLI flag
1396
- CODEBYPLAN_REPO_ID=<uuid> environment variable
1397
- .codebyplan/repo.json { "repo_id": "<uuid>" } in project root
1398
- Run 'codebyplan setup' to initialize this project`
1399
- );
1400
- }
1401
- return { repoId, worktreeId, projectPath };
1402
- }
1403
- var init_flags = __esm({
1404
- "src/lib/flags.ts"() {
1405
- "use strict";
1406
- }
1407
- });
1408
-
1409
1692
  // src/cli/confirm.ts
1410
1693
  var confirm_exports = {};
1411
1694
  __export(confirm_exports, {
@@ -1450,8 +1733,8 @@ var init_confirm = __esm({
1450
1733
  });
1451
1734
 
1452
1735
  // src/lib/tech-detect.ts
1453
- import { readFile as readFile7, access, readdir } from "node:fs/promises";
1454
- 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";
1455
1738
  async function fileExists(filePath) {
1456
1739
  try {
1457
1740
  await access(filePath);
@@ -1464,8 +1747,8 @@ async function discoverMonorepoApps(projectPath) {
1464
1747
  const apps = [];
1465
1748
  const patterns = [];
1466
1749
  try {
1467
- const raw = await readFile7(
1468
- join7(projectPath, "pnpm-workspace.yaml"),
1750
+ const raw = await readFile8(
1751
+ join8(projectPath, "pnpm-workspace.yaml"),
1469
1752
  "utf-8"
1470
1753
  );
1471
1754
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1479,7 +1762,7 @@ async function discoverMonorepoApps(projectPath) {
1479
1762
  }
1480
1763
  if (patterns.length === 0) {
1481
1764
  try {
1482
- const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
1765
+ const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1483
1766
  const pkg = JSON.parse(raw);
1484
1767
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1485
1768
  if (ws) patterns.push(...ws);
@@ -1489,14 +1772,14 @@ async function discoverMonorepoApps(projectPath) {
1489
1772
  for (const pattern of patterns) {
1490
1773
  if (pattern.endsWith("/*")) {
1491
1774
  const dir = pattern.slice(0, -2);
1492
- const absDir = join7(projectPath, dir);
1775
+ const absDir = join8(projectPath, dir);
1493
1776
  try {
1494
1777
  const entries = await readdir(absDir, { withFileTypes: true });
1495
1778
  for (const entry of entries) {
1496
1779
  if (entry.isDirectory()) {
1497
- const relPath = join7(dir, entry.name);
1498
- const absPath = join7(absDir, entry.name);
1499
- 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"))) {
1500
1783
  apps.push({ name: entry.name, path: relPath, absPath });
1501
1784
  }
1502
1785
  }
@@ -1515,7 +1798,7 @@ async function hasJsxFile(dir, depth = 0) {
1515
1798
  const name = entry.name;
1516
1799
  if (entry.isDirectory()) {
1517
1800
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1518
- if (await hasJsxFile(join7(dir, name), depth + 1)) return true;
1801
+ if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1519
1802
  } else if (entry.isFile()) {
1520
1803
  if (JSX_TEST_PATTERN.test(name)) continue;
1521
1804
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1534,7 +1817,7 @@ async function hasJsxFile(dir, depth = 0) {
1534
1817
  async function detectCapabilities(dirPath, pkgJson) {
1535
1818
  const caps = /* @__PURE__ */ new Set();
1536
1819
  for (const sub of JSX_SCAN_DIRS) {
1537
- if (await hasJsxFile(join7(dirPath, sub))) {
1820
+ if (await hasJsxFile(join8(dirPath, sub))) {
1538
1821
  caps.add("jsx");
1539
1822
  break;
1540
1823
  }
@@ -1556,7 +1839,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1556
1839
  }
1557
1840
  }
1558
1841
  }
1559
- 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"))) {
1560
1843
  caps.add("node-server");
1561
1844
  }
1562
1845
  if (pkgJson && pkgJson.bin) {
@@ -1572,7 +1855,7 @@ async function detectFromDirectory(dirPath) {
1572
1855
  const seen = /* @__PURE__ */ new Map();
1573
1856
  let pkgJson = null;
1574
1857
  try {
1575
- const raw = await readFile7(join7(dirPath, "package.json"), "utf-8");
1858
+ const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1576
1859
  pkgJson = JSON.parse(raw);
1577
1860
  const allDeps = {
1578
1861
  ...pkgJson.dependencies ?? {},
@@ -1604,7 +1887,7 @@ async function detectFromDirectory(dirPath) {
1604
1887
  }
1605
1888
  for (const { file, rule } of CONFIG_FILE_MAP) {
1606
1889
  const key = rule.name.toLowerCase();
1607
- if (!seen.has(key) && await fileExists(join7(dirPath, file))) {
1890
+ if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
1608
1891
  seen.set(key, { name: rule.name, category: rule.category });
1609
1892
  }
1610
1893
  }
@@ -1782,7 +2065,7 @@ function categorizeDependency(depName) {
1782
2065
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1783
2066
  if (depth > 4) return [];
1784
2067
  const results = [];
1785
- const pkgPath = join7(dir, "package.json");
2068
+ const pkgPath = join8(dir, "package.json");
1786
2069
  if (await fileExists(pkgPath)) {
1787
2070
  results.push(pkgPath);
1788
2071
  }
@@ -1791,7 +2074,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1791
2074
  for (const entry of entries) {
1792
2075
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1793
2076
  const subResults = await findPackageJsonFiles(
1794
- join7(dir, entry.name),
2077
+ join8(dir, entry.name),
1795
2078
  projectPath,
1796
2079
  depth + 1
1797
2080
  );
@@ -1806,7 +2089,7 @@ async function scanAllDependencies(projectPath) {
1806
2089
  const dependencies = [];
1807
2090
  for (const pkgPath of packageJsonPaths) {
1808
2091
  try {
1809
- const raw = await readFile7(pkgPath, "utf-8");
2092
+ const raw = await readFile8(pkgPath, "utf-8");
1810
2093
  const pkg = JSON.parse(raw);
1811
2094
  const sourcePath = relative(projectPath, pkgPath);
1812
2095
  const depSections = [
@@ -2406,8 +2689,8 @@ __export(eslint_exports, {
2406
2689
  eslintInit: () => eslintInit,
2407
2690
  runEslint: () => runEslint
2408
2691
  });
2409
- import { readFile as readFile8, writeFile as writeFile6, access as access2, readdir as readdir2 } from "node:fs/promises";
2410
- 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";
2411
2694
  async function fileExists2(filePath) {
2412
2695
  try {
2413
2696
  await access2(filePath);
@@ -2418,7 +2701,7 @@ async function fileExists2(filePath) {
2418
2701
  }
2419
2702
  async function autoDetectIgnorePatterns(absPath) {
2420
2703
  const patterns = [];
2421
- if (await fileExists2(join8(absPath, "esbuild.js"))) {
2704
+ if (await fileExists2(join9(absPath, "esbuild.js"))) {
2422
2705
  patterns.push("esbuild.js");
2423
2706
  }
2424
2707
  let entries = [];
@@ -2438,19 +2721,19 @@ async function autoDetectIgnorePatterns(absPath) {
2438
2721
  }
2439
2722
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2440
2723
  const candidate = `vitest.config.${ext}`;
2441
- if (await fileExists2(join8(absPath, candidate))) {
2724
+ if (await fileExists2(join9(absPath, candidate))) {
2442
2725
  patterns.push(candidate);
2443
2726
  break;
2444
2727
  }
2445
2728
  }
2446
2729
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2447
2730
  const candidate = `vite.config.${ext}`;
2448
- if (await fileExists2(join8(absPath, candidate))) {
2731
+ if (await fileExists2(join9(absPath, candidate))) {
2449
2732
  patterns.push(candidate);
2450
2733
  break;
2451
2734
  }
2452
2735
  }
2453
- if (await fileExists2(join8(absPath, "tauri.conf.json"))) {
2736
+ if (await fileExists2(join9(absPath, "tauri.conf.json"))) {
2454
2737
  patterns.push("src-tauri/**");
2455
2738
  patterns.push("**/*.d.ts");
2456
2739
  }
@@ -2458,14 +2741,14 @@ async function autoDetectIgnorePatterns(absPath) {
2458
2741
  }
2459
2742
  function detectPackageManager(projectPath) {
2460
2743
  return (async () => {
2461
- if (await fileExists2(join8(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2462
- 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";
2463
2746
  return "npm";
2464
2747
  })();
2465
2748
  }
2466
2749
  async function getInstalledDeps(pkgJsonPath) {
2467
2750
  try {
2468
- const raw = await readFile8(pkgJsonPath, "utf-8");
2751
+ const raw = await readFile9(pkgJsonPath, "utf-8");
2469
2752
  const pkg = JSON.parse(raw);
2470
2753
  const all = /* @__PURE__ */ new Set();
2471
2754
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2578,7 +2861,7 @@ async function eslintInit(repoId, projectPath) {
2578
2861
  ignorePatterns: detectedIgnores
2579
2862
  });
2580
2863
  const hash = hashConfig(content);
2581
- const configPath = join8(target.absPath, "eslint.config.mjs");
2864
+ const configPath = join9(target.absPath, "eslint.config.mjs");
2582
2865
  configsToWrite.push({
2583
2866
  target,
2584
2867
  presets,
@@ -2600,11 +2883,11 @@ async function eslintInit(repoId, projectPath) {
2600
2883
  return;
2601
2884
  }
2602
2885
  const pm = await detectPackageManager(projectPath);
2603
- const rootPkgJsonPath = join8(projectPath, "package.json");
2886
+ const rootPkgJsonPath = join9(projectPath, "package.json");
2604
2887
  const installed = await getInstalledDeps(rootPkgJsonPath);
2605
2888
  if (isMonorepo) {
2606
2889
  for (const { target } of configsToWrite) {
2607
- const appPkgJson = join8(target.absPath, "package.json");
2890
+ const appPkgJson = join9(target.absPath, "package.json");
2608
2891
  const appDeps = await getInstalledDeps(appPkgJson);
2609
2892
  for (const dep of appDeps) {
2610
2893
  installed.add(dep);
@@ -2656,7 +2939,7 @@ async function eslintInit(repoId, projectPath) {
2656
2939
  } of configsToWrite) {
2657
2940
  if (await fileExists2(configPath)) {
2658
2941
  try {
2659
- const existing = await readFile8(configPath, "utf-8");
2942
+ const existing = await readFile9(configPath, "utf-8");
2660
2943
  const existingHash = hashConfig(existing);
2661
2944
  if (existingHash === hash) {
2662
2945
  console.log(
@@ -2676,7 +2959,7 @@ async function eslintInit(repoId, projectPath) {
2676
2959
  }
2677
2960
  }
2678
2961
  try {
2679
- await writeFile6(configPath, content, "utf-8");
2962
+ await writeFile7(configPath, content, "utf-8");
2680
2963
  } catch (err) {
2681
2964
  console.error(
2682
2965
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -2984,7 +3267,7 @@ __export(round_exports, {
2984
3267
  runRoundSyncApprovals: () => runRoundSyncApprovals
2985
3268
  });
2986
3269
  import { access as access3 } from "node:fs/promises";
2987
- import { join as join9 } from "node:path";
3270
+ import { join as join10 } from "node:path";
2988
3271
  import { execSync as execSync2 } from "node:child_process";
2989
3272
  async function runRoundCommand(args) {
2990
3273
  const subcommand = args[0];
@@ -3078,7 +3361,7 @@ async function runRoundSyncApprovals(args) {
3078
3361
  "sync-approvals: git status failed; proceeding with empty diff\n"
3079
3362
  );
3080
3363
  }
3081
- const hookPath = join9(
3364
+ const hookPath = join10(
3082
3365
  repoRoot,
3083
3366
  ".claude",
3084
3367
  "hooks",
@@ -3200,8 +3483,8 @@ var init_round = __esm({
3200
3483
  });
3201
3484
 
3202
3485
  // src/lib/migrate-branch-model.ts
3203
- import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
3204
- 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";
3205
3488
  import { execSync as execSync3 } from "node:child_process";
3206
3489
  function assertValidBranchName(branch) {
3207
3490
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3211,7 +3494,7 @@ function assertValidBranchName(branch) {
3211
3494
  }
3212
3495
  }
3213
3496
  async function readJsonFile(filePath) {
3214
- const raw = await readFile9(filePath, "utf-8");
3497
+ const raw = await readFile10(filePath, "utf-8");
3215
3498
  const parsed = JSON.parse(raw);
3216
3499
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3217
3500
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3280,12 +3563,12 @@ async function runBranchMigration(opts) {
3280
3563
  if (found) {
3281
3564
  if (found.path.endsWith("/repo.json")) {
3282
3565
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3283
- configPath = join10(dir, "git.json");
3566
+ configPath = join11(dir, "git.json");
3284
3567
  } else {
3285
3568
  configPath = found.path;
3286
3569
  }
3287
3570
  } else {
3288
- configPath = join10(cwd, ".codebyplan", "git.json");
3571
+ configPath = join11(cwd, ".codebyplan", "git.json");
3289
3572
  }
3290
3573
  let fileRaw;
3291
3574
  let fileParsed;
@@ -3359,7 +3642,7 @@ async function runBranchMigration(opts) {
3359
3642
  const updatedParsed = { ...fileParsed, branch_config: after };
3360
3643
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3361
3644
  if (newJson !== fileRaw) {
3362
- await writeFile7(configPath, newJson, "utf-8");
3645
+ await writeFile8(configPath, newJson, "utf-8");
3363
3646
  }
3364
3647
  }
3365
3648
  return {
@@ -3465,9 +3748,9 @@ var init_branch = __esm({
3465
3748
  });
3466
3749
 
3467
3750
  // src/lib/ship.ts
3468
- import { readFile as readFile10 } from "node:fs/promises";
3469
- import { join as join11 } from "node:path";
3470
- 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";
3471
3754
  function assertValidBranchName2(branch, label) {
3472
3755
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
3473
3756
  throw new Error(
@@ -3481,15 +3764,15 @@ async function readBaseBranch(cwd) {
3481
3764
  if (found) {
3482
3765
  if (found.path.endsWith("/repo.json")) {
3483
3766
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3484
- gitJsonPath = join11(dir, "git.json");
3767
+ gitJsonPath = join12(dir, "git.json");
3485
3768
  } else {
3486
3769
  gitJsonPath = found.path;
3487
3770
  }
3488
3771
  } else {
3489
- gitJsonPath = join11(cwd, ".codebyplan", "git.json");
3772
+ gitJsonPath = join12(cwd, ".codebyplan", "git.json");
3490
3773
  }
3491
3774
  try {
3492
- const raw = await readFile10(gitJsonPath, "utf-8");
3775
+ const raw = await readFile11(gitJsonPath, "utf-8");
3493
3776
  const parsed = JSON.parse(raw);
3494
3777
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3495
3778
  return "main";
@@ -3522,7 +3805,7 @@ async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Prom
3522
3805
  let totalGhErrors = 0;
3523
3806
  let iteration = 0;
3524
3807
  while (Date.now() < deadline) {
3525
- const ghResult = spawnSync(
3808
+ const ghResult = spawnSync2(
3526
3809
  "gh",
3527
3810
  ["pr", "checks", feat, "--json", "name,state,conclusion"],
3528
3811
  {
@@ -3688,7 +3971,7 @@ ${statusOut.trim()}`
3688
3971
  } else {
3689
3972
  createArgs.push("--body", defaultPrBody(feat, base));
3690
3973
  }
3691
- const createResult = spawnSync("gh", createArgs, {
3974
+ const createResult = spawnSync2("gh", createArgs, {
3692
3975
  cwd,
3693
3976
  encoding: "utf-8",
3694
3977
  stdio: ["pipe", "pipe", "pipe"]
@@ -3718,7 +4001,7 @@ ${statusOut.trim()}`
3718
4001
  });
3719
4002
  let mergeCommit = null;
3720
4003
  try {
3721
- const prViewResult = spawnSync(
4004
+ const prViewResult = spawnSync2(
3722
4005
  "gh",
3723
4006
  ["pr", "view", feat, "--json", "state,mergeCommit"],
3724
4007
  {
@@ -4021,19 +4304,19 @@ var init_resolve_worktree2 = __esm({
4021
4304
  });
4022
4305
 
4023
4306
  // src/lib/migrate-local-config.ts
4024
- import { mkdir as mkdir4, readFile as readFile11, unlink as unlink2, writeFile as writeFile8 } from "node:fs/promises";
4025
- 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";
4026
4309
  function legacySharedPath(projectPath) {
4027
- return join12(projectPath, ".codebyplan.json");
4310
+ return join13(projectPath, ".codebyplan.json");
4028
4311
  }
4029
4312
  function legacyLocalPath(projectPath) {
4030
- return join12(projectPath, ".codebyplan.local.json");
4313
+ return join13(projectPath, ".codebyplan.local.json");
4031
4314
  }
4032
4315
  function newDirPath(projectPath) {
4033
- return join12(projectPath, ".codebyplan");
4316
+ return join13(projectPath, ".codebyplan");
4034
4317
  }
4035
4318
  function sentinelPath(projectPath) {
4036
- return join12(projectPath, ".codebyplan", "repo.json");
4319
+ return join13(projectPath, ".codebyplan", "repo.json");
4037
4320
  }
4038
4321
  async function statSafe(p) {
4039
4322
  const { stat: stat2 } = await import("node:fs/promises");
@@ -4072,7 +4355,7 @@ async function runLocalMigration(projectPath) {
4072
4355
  }
4073
4356
  let legacyRaw;
4074
4357
  try {
4075
- legacyRaw = await readFile11(legacySharedPath(projectPath), "utf-8");
4358
+ legacyRaw = await readFile12(legacySharedPath(projectPath), "utf-8");
4076
4359
  } catch {
4077
4360
  return {
4078
4361
  migrated: true,
@@ -4099,7 +4382,7 @@ async function runLocalMigration(projectPath) {
4099
4382
  let deviceId;
4100
4383
  let deviceWrittenByHelper = false;
4101
4384
  try {
4102
- const localRaw = await readFile11(legacyLocalPath(projectPath), "utf-8");
4385
+ const localRaw = await readFile12(legacyLocalPath(projectPath), "utf-8");
4103
4386
  const localParsed = JSON.parse(localRaw);
4104
4387
  if (typeof localParsed.device_id === "string") {
4105
4388
  deviceId = localParsed.device_id;
@@ -4107,7 +4390,7 @@ async function runLocalMigration(projectPath) {
4107
4390
  } catch {
4108
4391
  }
4109
4392
  try {
4110
- await mkdir4(newDirPath(projectPath), { recursive: true });
4393
+ await mkdir5(newDirPath(projectPath), { recursive: true });
4111
4394
  } catch (err) {
4112
4395
  const code = err.code;
4113
4396
  if (code === "ENOTDIR" || code === "EEXIST") {
@@ -4126,8 +4409,8 @@ async function runLocalMigration(projectPath) {
4126
4409
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
4127
4410
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
4128
4411
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
4129
- await writeFile8(
4130
- join12(projectPath, ".codebyplan", "repo.json"),
4412
+ await writeFile9(
4413
+ join13(projectPath, ".codebyplan", "repo.json"),
4131
4414
  JSON.stringify(repoJson, null, 2) + "\n",
4132
4415
  "utf-8"
4133
4416
  );
@@ -4139,8 +4422,8 @@ async function runLocalMigration(projectPath) {
4139
4422
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
4140
4423
  if ("port_allocations" in cfg)
4141
4424
  serverJson.port_allocations = cfg.port_allocations;
4142
- await writeFile8(
4143
- join12(projectPath, ".codebyplan", "server.json"),
4425
+ await writeFile9(
4426
+ join13(projectPath, ".codebyplan", "server.json"),
4144
4427
  JSON.stringify(serverJson, null, 2) + "\n",
4145
4428
  "utf-8"
4146
4429
  );
@@ -4148,30 +4431,30 @@ async function runLocalMigration(projectPath) {
4148
4431
  const gitJson = {};
4149
4432
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
4150
4433
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
4151
- await writeFile8(
4152
- join12(projectPath, ".codebyplan", "git.json"),
4434
+ await writeFile9(
4435
+ join13(projectPath, ".codebyplan", "git.json"),
4153
4436
  JSON.stringify(gitJson, null, 2) + "\n",
4154
4437
  "utf-8"
4155
4438
  );
4156
4439
  filesChanged.push(".codebyplan/git.json");
4157
4440
  const shipmentJson = {};
4158
4441
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4159
- await writeFile8(
4160
- join12(projectPath, ".codebyplan", "shipment.json"),
4442
+ await writeFile9(
4443
+ join13(projectPath, ".codebyplan", "shipment.json"),
4161
4444
  JSON.stringify(shipmentJson, null, 2) + "\n",
4162
4445
  "utf-8"
4163
4446
  );
4164
4447
  filesChanged.push(".codebyplan/shipment.json");
4165
4448
  const vendorJson = {};
4166
- await writeFile8(
4167
- join12(projectPath, ".codebyplan", "vendor.json"),
4449
+ await writeFile9(
4450
+ join13(projectPath, ".codebyplan", "vendor.json"),
4168
4451
  JSON.stringify(vendorJson, null, 2) + "\n",
4169
4452
  "utf-8"
4170
4453
  );
4171
4454
  filesChanged.push(".codebyplan/vendor.json");
4172
4455
  if (!deviceWrittenByHelper) {
4173
- await writeFile8(
4174
- join12(projectPath, ".codebyplan", "device.local.json"),
4456
+ await writeFile9(
4457
+ join13(projectPath, ".codebyplan", "device.local.json"),
4175
4458
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4176
4459
  "utf-8"
4177
4460
  );
@@ -4183,9 +4466,9 @@ async function runLocalMigration(projectPath) {
4183
4466
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4184
4467
  );
4185
4468
  }
4186
- const gitignorePath = join12(projectPath, ".gitignore");
4469
+ const gitignorePath = join13(projectPath, ".gitignore");
4187
4470
  try {
4188
- const gitignoreContent = await readFile11(gitignorePath, "utf-8");
4471
+ const gitignoreContent = await readFile12(gitignorePath, "utf-8");
4189
4472
  const legacyLine = ".codebyplan.local.json";
4190
4473
  const newLine = ".codebyplan/device.local.json";
4191
4474
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -4204,7 +4487,7 @@ async function runLocalMigration(projectPath) {
4204
4487
  updated = gitignoreContent;
4205
4488
  }
4206
4489
  if (updated !== gitignoreContent) {
4207
- await writeFile8(gitignorePath, updated, "utf-8");
4490
+ await writeFile9(gitignorePath, updated, "utf-8");
4208
4491
  filesChanged.push(".gitignore");
4209
4492
  }
4210
4493
  } catch {
@@ -4243,8 +4526,8 @@ __export(config_exports, {
4243
4526
  readVendorConfig: () => readVendorConfig,
4244
4527
  runConfig: () => runConfig
4245
4528
  });
4246
- import { mkdir as mkdir5, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
4247
- 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";
4248
4531
  async function runConfig() {
4249
4532
  const flags = parseFlags(3);
4250
4533
  const dryRun = hasFlag("dry-run", 3);
@@ -4277,7 +4560,7 @@ async function runConfig() {
4277
4560
  console.log("\n Config complete.\n");
4278
4561
  }
4279
4562
  async function syncConfigToFile(repoId, projectPath, dryRun) {
4280
- const codebyplanDir = join13(projectPath, ".codebyplan");
4563
+ const codebyplanDir = join14(projectPath, ".codebyplan");
4281
4564
  let resolvedWorktreeId;
4282
4565
  try {
4283
4566
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4406,7 +4689,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4406
4689
  console.log(" Config would be updated (dry-run).");
4407
4690
  return;
4408
4691
  }
4409
- await mkdir5(codebyplanDir, { recursive: true });
4692
+ await mkdir6(codebyplanDir, { recursive: true });
4410
4693
  const files = [
4411
4694
  { name: "repo.json", payload: repoPayload },
4412
4695
  { name: "server.json", payload: serverPayload },
@@ -4416,15 +4699,15 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4416
4699
  ];
4417
4700
  let anyUpdated = false;
4418
4701
  for (const { name, payload } of files) {
4419
- const filePath = join13(codebyplanDir, name);
4702
+ const filePath = join14(codebyplanDir, name);
4420
4703
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4421
4704
  let currentJson = "";
4422
4705
  try {
4423
- currentJson = await readFile12(filePath, "utf-8");
4706
+ currentJson = await readFile13(filePath, "utf-8");
4424
4707
  } catch {
4425
4708
  }
4426
4709
  if (currentJson === newJson) continue;
4427
- await writeFile9(filePath, newJson, "utf-8");
4710
+ await writeFile10(filePath, newJson, "utf-8");
4428
4711
  console.log(` Updated .codebyplan/${name}`);
4429
4712
  anyUpdated = true;
4430
4713
  }
@@ -4434,8 +4717,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4434
4717
  }
4435
4718
  async function readRepoConfig(projectPath) {
4436
4719
  try {
4437
- const raw = await readFile12(
4438
- join13(projectPath, ".codebyplan", "repo.json"),
4720
+ const raw = await readFile13(
4721
+ join14(projectPath, ".codebyplan", "repo.json"),
4439
4722
  "utf-8"
4440
4723
  );
4441
4724
  return JSON.parse(raw);
@@ -4445,8 +4728,8 @@ async function readRepoConfig(projectPath) {
4445
4728
  }
4446
4729
  async function readServerConfig(projectPath) {
4447
4730
  try {
4448
- const raw = await readFile12(
4449
- join13(projectPath, ".codebyplan", "server.json"),
4731
+ const raw = await readFile13(
4732
+ join14(projectPath, ".codebyplan", "server.json"),
4450
4733
  "utf-8"
4451
4734
  );
4452
4735
  return JSON.parse(raw);
@@ -4456,8 +4739,8 @@ async function readServerConfig(projectPath) {
4456
4739
  }
4457
4740
  async function readGitConfig(projectPath) {
4458
4741
  try {
4459
- const raw = await readFile12(
4460
- join13(projectPath, ".codebyplan", "git.json"),
4742
+ const raw = await readFile13(
4743
+ join14(projectPath, ".codebyplan", "git.json"),
4461
4744
  "utf-8"
4462
4745
  );
4463
4746
  return JSON.parse(raw);
@@ -4467,8 +4750,8 @@ async function readGitConfig(projectPath) {
4467
4750
  }
4468
4751
  async function readShipmentConfig(projectPath) {
4469
4752
  try {
4470
- const raw = await readFile12(
4471
- join13(projectPath, ".codebyplan", "shipment.json"),
4753
+ const raw = await readFile13(
4754
+ join14(projectPath, ".codebyplan", "shipment.json"),
4472
4755
  "utf-8"
4473
4756
  );
4474
4757
  return JSON.parse(raw);
@@ -4478,8 +4761,8 @@ async function readShipmentConfig(projectPath) {
4478
4761
  }
4479
4762
  async function readVendorConfig(projectPath) {
4480
4763
  try {
4481
- const raw = await readFile12(
4482
- join13(projectPath, ".codebyplan", "vendor.json"),
4764
+ const raw = await readFile13(
4765
+ join14(projectPath, ".codebyplan", "vendor.json"),
4483
4766
  "utf-8"
4484
4767
  );
4485
4768
  return JSON.parse(raw);
@@ -4535,14 +4818,14 @@ var init_server_detect = __esm({
4535
4818
  });
4536
4819
 
4537
4820
  // src/lib/port-verify.ts
4538
- import { readFile as readFile13 } from "node:fs/promises";
4821
+ import { readFile as readFile14 } from "node:fs/promises";
4539
4822
  async function verifyPorts(projectPath, portAllocations) {
4540
4823
  const mismatches = [];
4541
4824
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
4542
4825
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
4543
4826
  for (const pkgPath of packageJsonPaths) {
4544
4827
  try {
4545
- const raw = await readFile13(pkgPath, "utf-8");
4828
+ const raw = await readFile14(pkgPath, "utf-8");
4546
4829
  const pkg = JSON.parse(raw);
4547
4830
  const scriptPort = detectPortFromScripts(pkg);
4548
4831
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -4605,7 +4888,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
4605
4888
  }
4606
4889
  let pkg;
4607
4890
  try {
4608
- const raw = await readFile13(`${app.absPath}/package.json`, "utf-8");
4891
+ const raw = await readFile14(`${app.absPath}/package.json`, "utf-8");
4609
4892
  pkg = JSON.parse(raw);
4610
4893
  } catch {
4611
4894
  continue;
@@ -5407,6 +5690,11 @@ async function runInstall(opts, deps = {}) {
5407
5690
  await Promise.resolve();
5408
5691
  const scope = opts.scope ?? "project";
5409
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
+ }
5410
5698
  runInstallUser(opts, deps);
5411
5699
  return;
5412
5700
  }
@@ -5483,6 +5771,9 @@ async function runInstall(opts, deps = {}) {
5483
5771
  console.log(
5484
5772
  `codebyplan claude install${opts.dryRun ? " (dry-run)" : ""}: ${manifestEntries.length} files, ${countHookEntries(templatesDir)} hook entries.`
5485
5773
  );
5774
+ if (opts.renderer && !opts.dryRun) {
5775
+ await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
5776
+ }
5486
5777
  } catch (err) {
5487
5778
  console.error(
5488
5779
  `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
@@ -5568,6 +5859,7 @@ var init_install = __esm({
5568
5859
  init_template_walker();
5569
5860
  init_manifest();
5570
5861
  init_settings_merge();
5862
+ init_statusline_config();
5571
5863
  }
5572
5864
  });
5573
5865
 
@@ -5695,6 +5987,11 @@ async function runUpdate(opts, deps = {}) {
5695
5987
  await Promise.resolve();
5696
5988
  const scope = opts.scope ?? "project";
5697
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
+ }
5698
5995
  runUpdateUser(opts, deps);
5699
5996
  return;
5700
5997
  }
@@ -5839,6 +6136,9 @@ async function runUpdate(opts, deps = {}) {
5839
6136
  console.log(
5840
6137
  `codebyplan claude update${opts.dryRun ? " (dry-run)" : ""}: ${finalManifestEntries.length} files tracked.`
5841
6138
  );
6139
+ if (opts.renderer && !opts.dryRun) {
6140
+ await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
6141
+ }
5842
6142
  } catch (err) {
5843
6143
  console.error(
5844
6144
  `codebyplan claude update failed: ${err instanceof Error ? err.message : String(err)}`
@@ -5985,6 +6285,7 @@ var init_update = __esm({
5985
6285
  init_manifest();
5986
6286
  init_settings_merge();
5987
6287
  init_prompt();
6288
+ init_statusline_config();
5988
6289
  }
5989
6290
  });
5990
6291
 
@@ -6222,6 +6523,12 @@ void (async () => {
6222
6523
  await runSetup2();
6223
6524
  process.exit(0);
6224
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
+ }
6225
6532
  if (arg === "login") {
6226
6533
  const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
6227
6534
  try {
@@ -6357,6 +6664,9 @@ void (async () => {
6357
6664
  user \u2014 writes only owned base settings into ~/.claude/settings.json;
6358
6665
  never copies templates or hooks
6359
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
6360
6670
  `);
6361
6671
  process.exit(0);
6362
6672
  }
@@ -6379,6 +6689,7 @@ void (async () => {
6379
6689
  codebyplan ship Ship current feat branch to production via PR
6380
6690
  codebyplan branch migrate Rewrite branch_config from 3-branch to 2-tier model
6381
6691
  codebyplan claude Claude asset management (install/update/uninstall)
6692
+ codebyplan statusline Show or set the statusline renderer (bash/node/python)
6382
6693
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
6383
6694
  codebyplan help Show this help message
6384
6695
  codebyplan --version Print version
@@ -6413,6 +6724,9 @@ void (async () => {
6413
6724
  --verbose Log every file operation
6414
6725
  --scope <user|project> Target scope (default: project)
6415
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
6416
6730
 
6417
6731
  MCP Server:
6418
6732
  Claude Code connects to CodeByPlan via remote MCP:
@@ -6463,8 +6777,19 @@ function parseClaudeFlags(rest) {
6463
6777
  );
6464
6778
  return null;
6465
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;
6466
6791
  return {
6467
- opts: { yes, dryRun, verbose, scope },
6792
+ opts: { yes, dryRun, verbose, scope, renderer },
6468
6793
  projectDir
6469
6794
  };
6470
6795
  }