codebyplan 1.13.0 → 1.13.4

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.
Files changed (28) hide show
  1. package/dist/cli.js +2337 -1701
  2. package/package.json +2 -2
  3. package/templates/github-workflows/publish.yml +207 -0
  4. package/templates/hooks/README.md +3 -19
  5. package/templates/hooks/cbp-test-hooks.sh +1 -9
  6. package/templates/hooks/hooks.json +0 -11
  7. package/templates/settings.project.base.json +154 -3
  8. package/templates/skills/cbp-build-cc-settings/SKILL.md +2 -0
  9. package/templates/skills/cbp-build-cc-settings/reference/cbp-permission-policy.md +48 -0
  10. package/templates/skills/cbp-checkpoint-end/SKILL.md +7 -0
  11. package/templates/skills/cbp-setup-eslint/SKILL.md +4 -3
  12. package/templates/skills/cbp-setup-eslint/reference/base.md +44 -55
  13. package/templates/skills/cbp-setup-eslint/reference/cli.md +43 -36
  14. package/templates/skills/cbp-setup-eslint/reference/e2e.md +57 -47
  15. package/templates/skills/cbp-setup-eslint/reference/jest.md +22 -38
  16. package/templates/skills/cbp-setup-eslint/reference/nestjs.md +39 -40
  17. package/templates/skills/cbp-setup-eslint/reference/nextjs.md +39 -40
  18. package/templates/skills/cbp-setup-eslint/reference/node.md +25 -54
  19. package/templates/skills/cbp-setup-eslint/reference/react-native.md +33 -37
  20. package/templates/skills/cbp-setup-eslint/reference/react.md +33 -58
  21. package/templates/skills/cbp-setup-eslint/reference/tailwind.md +45 -49
  22. package/templates/skills/cbp-setup-eslint/reference/testing-react.md +28 -37
  23. package/templates/skills/cbp-setup-eslint/reference/vitest.md +25 -45
  24. package/templates/skills/cbp-ship/reference/versioning.md +31 -3
  25. package/templates/skills/cbp-ship-configure/SKILL.md +16 -36
  26. package/templates/skills/cbp-ship-configure/reference/npm-package.md +15 -6
  27. package/templates/skills/cbp-ship-main/SKILL.md +4 -0
  28. package/templates/hooks/cbp-notify.sh +0 -68
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.13.0";
17
+ VERSION = "1.13.4";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -225,8 +225,8 @@ async function readLocalConfig(projectPath, onMigrationNotice) {
225
225
  }
226
226
  async function writeLocalConfig(projectPath, config) {
227
227
  const content = { device_id: config.device_id };
228
- const path7 = localConfigPath(projectPath);
229
- const dirPath = dirname(path7);
228
+ const path8 = localConfigPath(projectPath);
229
+ const dirPath = dirname(path8);
230
230
  let phase = "stat config directory";
231
231
  try {
232
232
  try {
@@ -246,7 +246,7 @@ async function writeLocalConfig(projectPath, config) {
246
246
  phase = "create config directory";
247
247
  await mkdir(dirPath, { recursive: true });
248
248
  phase = "write local config";
249
- await writeFile2(path7, JSON.stringify(content, null, 2) + "\n", "utf-8");
249
+ await writeFile2(path8, JSON.stringify(content, null, 2) + "\n", "utf-8");
250
250
  } catch (err) {
251
251
  const code = err.code;
252
252
  if (code === "LEGACY_FILE_BLOCKS_DIR") {
@@ -467,12 +467,12 @@ async function readFallback(filename) {
467
467
  }
468
468
  }
469
469
  async function writeFallback(filename, data) {
470
- const path7 = fallbackFile(filename);
471
- await mkdir3(dirname2(path7), { recursive: true });
472
- await writeFile4(path7, JSON.stringify(data, null, 2) + "\n", "utf-8");
470
+ const path8 = fallbackFile(filename);
471
+ await mkdir3(dirname2(path8), { recursive: true });
472
+ await writeFile4(path8, JSON.stringify(data, null, 2) + "\n", "utf-8");
473
473
  if (platform() !== "win32") {
474
474
  try {
475
- await chmod(path7, 384);
475
+ await chmod(path8, 384);
476
476
  } catch {
477
477
  }
478
478
  }
@@ -679,8 +679,8 @@ async function getAuthHeaders() {
679
679
  return { headers: { "x-api-key": key }, via: "api_key" };
680
680
  }
681
681
  }
682
- function buildUrl(path7, params) {
683
- const url = new URL(`${baseUrl()}/api${path7}`);
682
+ function buildUrl(path8, params) {
683
+ const url = new URL(`${baseUrl()}/api${path8}`);
684
684
  if (params) {
685
685
  for (const [key, value] of Object.entries(params)) {
686
686
  if (value !== void 0) {
@@ -697,10 +697,10 @@ function isRetryable(err) {
697
697
  return false;
698
698
  }
699
699
  function delay(ms) {
700
- return new Promise((resolve5) => setTimeout(resolve5, ms));
700
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
701
701
  }
702
- async function request(method, path7, options) {
703
- const url = buildUrl(path7, options?.params);
702
+ async function request(method, path8, options) {
703
+ const url = buildUrl(path8, options?.params);
704
704
  const auth = await getAuthHeaders();
705
705
  let lastError;
706
706
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -720,7 +720,7 @@ async function request(method, path7, options) {
720
720
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
721
721
  });
722
722
  if (!res.ok) {
723
- let message = `API ${method} ${path7} failed with status ${res.status}`;
723
+ let message = `API ${method} ${path8} failed with status ${res.status}`;
724
724
  let code;
725
725
  try {
726
726
  const body = await res.json();
@@ -754,14 +754,14 @@ async function request(method, path7, options) {
754
754
  }
755
755
  throw lastError;
756
756
  }
757
- async function apiGet(path7, params) {
758
- return request("GET", path7, { params });
757
+ async function apiGet(path8, params) {
758
+ return request("GET", path8, { params });
759
759
  }
760
- async function apiPost(path7, body) {
761
- return request("POST", path7, { body });
760
+ async function apiPost(path8, body) {
761
+ return request("POST", path8, { body });
762
762
  }
763
- async function apiPut(path7, body) {
764
- return request("PUT", path7, { body });
763
+ async function apiPut(path8, body) {
764
+ return request("PUT", path8, { body });
765
765
  }
766
766
  async function callMcpTool(toolName, params) {
767
767
  const url = mcpEndpoint();
@@ -1055,7 +1055,7 @@ var init_device_flow = __esm({
1055
1055
  this.name = "OAuthInvalidClientError";
1056
1056
  }
1057
1057
  };
1058
- defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
1058
+ defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1059
1059
  }
1060
1060
  });
1061
1061
 
@@ -1217,13 +1217,9 @@ import { createInterface } from "node:readline/promises";
1217
1217
  function getConfigPath(scope) {
1218
1218
  return scope === "user" ? join6(homedir2(), ".claude.json") : join6(process.cwd(), ".mcp.json");
1219
1219
  }
1220
- function legacyMcpUrl() {
1221
- const baseUrl2 = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
1222
- return `${baseUrl2.replace(/\/$/, "")}/mcp`;
1223
- }
1224
- async function readConfig(path7) {
1220
+ async function readConfig(path8) {
1225
1221
  try {
1226
- const raw = await readFile6(path7, "utf-8");
1222
+ const raw = await readFile6(path8, "utf-8");
1227
1223
  const parsed = JSON.parse(raw);
1228
1224
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1229
1225
  return parsed;
@@ -1233,19 +1229,16 @@ async function readConfig(path7) {
1233
1229
  return {};
1234
1230
  }
1235
1231
  }
1236
- function buildMcpEntry(auth) {
1237
- if (auth.kind === "oauth") {
1238
- return { url: mcpEndpoint() };
1239
- }
1240
- return { url: legacyMcpUrl(), headers: { "x-api-key": auth.apiKey } };
1232
+ function buildMcpEntry() {
1233
+ return { url: mcpEndpoint() };
1241
1234
  }
1242
- async function writeMcpConfig(scope, auth) {
1235
+ async function writeMcpConfig(scope) {
1243
1236
  const configPath = getConfigPath(scope);
1244
1237
  const config = await readConfig(configPath);
1245
1238
  if (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers)) {
1246
1239
  config.mcpServers = {};
1247
1240
  }
1248
- config.mcpServers.codebyplan = buildMcpEntry(auth);
1241
+ config.mcpServers.codebyplan = buildMcpEntry();
1249
1242
  await writeFile6(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1250
1243
  return configPath;
1251
1244
  }
@@ -1395,7 +1388,7 @@ async function runSetup() {
1395
1388
  const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
1396
1389
  const scope = scopeInput === "2" ? "project" : "user";
1397
1390
  console.log("\n Configuring MCP server...");
1398
- const configPath = await writeMcpConfig(scope, auth);
1391
+ const configPath = await writeMcpConfig(scope);
1399
1392
  console.log(` Done! Config written to ${configPath}
1400
1393
  `);
1401
1394
  if (auth.kind === "legacy" && scope === "project") {
@@ -1768,9 +1761,9 @@ import { join as join8 } from "node:path";
1768
1761
  function configPaths() {
1769
1762
  return [join8(homedir3(), ".claude.json"), join8(process.cwd(), ".mcp.json")];
1770
1763
  }
1771
- async function readConfig2(path7) {
1764
+ async function readConfig2(path8) {
1772
1765
  try {
1773
- const raw = await readFile8(path7, "utf-8");
1766
+ const raw = await readFile8(path8, "utf-8");
1774
1767
  const parsed = JSON.parse(raw);
1775
1768
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1776
1769
  return parsed;
@@ -1784,14 +1777,14 @@ function entryHasLegacyApiKey(entry) {
1784
1777
  if (!entry || !entry.headers) return false;
1785
1778
  return "x-api-key" in entry.headers;
1786
1779
  }
1787
- async function rewriteConfig(path7, config, newUrl) {
1780
+ async function rewriteConfig(path8, config, newUrl) {
1788
1781
  const servers = config.mcpServers;
1789
1782
  if (!servers) return false;
1790
1783
  const entry = servers.codebyplan;
1791
1784
  if (!entry) return false;
1792
1785
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1793
1786
  servers.codebyplan = { url: newUrl };
1794
- await writeFile7(path7, JSON.stringify(config, null, 2) + "\n", "utf-8");
1787
+ await writeFile7(path8, JSON.stringify(config, null, 2) + "\n", "utf-8");
1795
1788
  return true;
1796
1789
  }
1797
1790
  async function runUpgradeAuth() {
@@ -1799,12 +1792,12 @@ async function runUpgradeAuth() {
1799
1792
  await runLogin();
1800
1793
  const newUrl = mcpEndpoint();
1801
1794
  let migrated = 0;
1802
- for (const path7 of configPaths()) {
1803
- const config = await readConfig2(path7);
1795
+ for (const path8 of configPaths()) {
1796
+ const config = await readConfig2(path8);
1804
1797
  if (!config) continue;
1805
- const changed = await rewriteConfig(path7, config, newUrl);
1798
+ const changed = await rewriteConfig(path8, config, newUrl);
1806
1799
  if (changed) {
1807
- console.log(` Updated ${path7}`);
1800
+ console.log(` Updated ${path8}`);
1808
1801
  migrated++;
1809
1802
  }
1810
1803
  }
@@ -3176,17 +3169,16 @@ var init_eslint = __esm({
3176
3169
 
3177
3170
  // src/lib/mcp-client.ts
3178
3171
  async function mcpCall(toolName, args) {
3179
- if (!API_KEY) {
3180
- throw new McpError(
3181
- `Missing CODEBYPLAN_API_KEY environment variable.
3182
-
3183
- Quick setup:
3184
- npx ${PACKAGE_NAME} setup
3185
-
3186
- Or manually:
3187
- 1. Get your API key at https://codebyplan.com/settings/api-keys/
3188
- 2. claude mcp add codebyplan -e CODEBYPLAN_API_KEY=<key> -- npx -y ${PACKAGE_NAME}`
3189
- );
3172
+ let accessToken;
3173
+ try {
3174
+ accessToken = await getAccessToken();
3175
+ } catch (err) {
3176
+ if (err instanceof NoTokenError) {
3177
+ throw new McpError(
3178
+ "Not logged in. Run `codebyplan login` to authenticate."
3179
+ );
3180
+ }
3181
+ throw err;
3190
3182
  }
3191
3183
  const body = {
3192
3184
  jsonrpc: "2.0",
@@ -3196,12 +3188,12 @@ Or manually:
3196
3188
  };
3197
3189
  let res;
3198
3190
  try {
3199
- res = await fetch(`${BASE_URL}/mcp`, {
3191
+ res = await fetch(mcpEndpoint(), {
3200
3192
  method: "POST",
3201
3193
  headers: {
3202
3194
  "Content-Type": "application/json",
3203
3195
  Accept: "application/json, text/event-stream",
3204
- "x-api-key": API_KEY
3196
+ Authorization: `Bearer ${accessToken}`
3205
3197
  },
3206
3198
  body: JSON.stringify(body),
3207
3199
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
@@ -3212,7 +3204,7 @@ Or manually:
3212
3204
  }
3213
3205
  if (res.status === 401) {
3214
3206
  throw new McpError(
3215
- "Invalid API key. Generate a new one at https://codebyplan.com/settings/api-keys/",
3207
+ "Authentication failed. Run `codebyplan login` to refresh your session.",
3216
3208
  401
3217
3209
  );
3218
3210
  }
@@ -3271,13 +3263,12 @@ function parseSseEnvelope(text, toolName) {
3271
3263
  }
3272
3264
  throw new McpError(`mcp ${toolName} returned unparseable SSE body`);
3273
3265
  }
3274
- var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS2, McpError;
3266
+ var REQUEST_TIMEOUT_MS2, McpError;
3275
3267
  var init_mcp_client = __esm({
3276
3268
  "src/lib/mcp-client.ts"() {
3277
3269
  "use strict";
3278
- init_version();
3279
- API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
3280
- BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com").replace(/\/$/, "");
3270
+ init_token_refresh();
3271
+ init_urls();
3281
3272
  REQUEST_TIMEOUT_MS2 = 12e4;
3282
3273
  McpError = class extends Error {
3283
3274
  status;
@@ -3440,7 +3431,7 @@ function setRetryDelayMs(ms) {
3440
3431
  RETRY_DELAY_MS = ms;
3441
3432
  }
3442
3433
  function sleep(ms) {
3443
- return new Promise((resolve5) => setTimeout(resolve5, ms));
3434
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
3444
3435
  }
3445
3436
  function isTransientMcpError(err) {
3446
3437
  if (!(err instanceof McpError)) return false;
@@ -3917,17 +3908,10 @@ var init_branch = __esm({
3917
3908
  }
3918
3909
  });
3919
3910
 
3920
- // src/lib/ship.ts
3911
+ // src/lib/git-utils.ts
3921
3912
  import { readFile as readFile12 } from "node:fs/promises";
3922
3913
  import { join as join13 } from "node:path";
3923
- import { execSync as execSync4, spawnSync as spawnSync2 } from "node:child_process";
3924
- function assertValidBranchName2(branch, label) {
3925
- if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
3926
- throw new Error(
3927
- `Invalid branch name for ${label}: '${branch}' (refusing to pass shell metacharacters to git/gh)`
3928
- );
3929
- }
3930
- }
3914
+ import { spawnSync as spawnSync2 } from "node:child_process";
3931
3915
  async function readBaseBranch(cwd) {
3932
3916
  const found = await findCodebyplanConfig(cwd);
3933
3917
  let gitJsonPath;
@@ -3959,6 +3943,442 @@ async function readBaseBranch(cwd) {
3959
3943
  return "main";
3960
3944
  }
3961
3945
  }
3946
+ function resolveBaseRef(cwd, baseBranch) {
3947
+ if (baseBranch.startsWith("origin/")) return baseBranch;
3948
+ const remoteRef = `origin/${baseBranch}`;
3949
+ const check = spawnSync2(
3950
+ "git",
3951
+ ["rev-parse", "--verify", "--quiet", `${remoteRef}^{commit}`],
3952
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
3953
+ );
3954
+ if (check.status === 0 && (check.stdout ?? "").trim().length > 0) {
3955
+ return remoteRef;
3956
+ }
3957
+ return baseBranch;
3958
+ }
3959
+ var init_git_utils = __esm({
3960
+ "src/lib/git-utils.ts"() {
3961
+ "use strict";
3962
+ init_flags();
3963
+ }
3964
+ });
3965
+
3966
+ // src/lib/bump.ts
3967
+ import { readFile as readFile13, writeFile as writeFile10, access as access4, readdir as readdir3 } from "node:fs/promises";
3968
+ import { join as join14, relative as relative3, resolve as resolve2 } from "node:path";
3969
+ import { spawnSync as spawnSync3 } from "node:child_process";
3970
+ function parsePnpmWorkspaceGlobs(raw) {
3971
+ const lines = raw.split("\n");
3972
+ const globs = [];
3973
+ let inPackages = false;
3974
+ for (const line of lines) {
3975
+ const trimmed = line.trim();
3976
+ if (trimmed === "packages:") {
3977
+ inPackages = true;
3978
+ continue;
3979
+ }
3980
+ if (inPackages) {
3981
+ if (trimmed.length > 0 && !trimmed.startsWith("-")) {
3982
+ inPackages = false;
3983
+ continue;
3984
+ }
3985
+ if (trimmed.startsWith("-")) {
3986
+ const glob = trimmed.slice(1).trim().replace(/^['"]|['"]$/g, "");
3987
+ if (glob) globs.push(glob);
3988
+ }
3989
+ }
3990
+ }
3991
+ return globs;
3992
+ }
3993
+ async function expandGlob(cwd, glob) {
3994
+ const parts = glob.split("/");
3995
+ if (parts.length !== 2 || parts[1] !== "*") {
3996
+ return [];
3997
+ }
3998
+ const parentDir = join14(cwd, parts[0]);
3999
+ let dirs;
4000
+ try {
4001
+ const entries = await readdir3(parentDir, { withFileTypes: true });
4002
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join14(parentDir, e.name));
4003
+ } catch {
4004
+ return [];
4005
+ }
4006
+ const results = [];
4007
+ for (const dir of dirs) {
4008
+ try {
4009
+ await access4(join14(dir, "package.json"));
4010
+ results.push(dir);
4011
+ } catch {
4012
+ }
4013
+ }
4014
+ return results;
4015
+ }
4016
+ async function buildPackageMap(cwd) {
4017
+ const map = /* @__PURE__ */ new Map();
4018
+ let globs = [];
4019
+ try {
4020
+ const raw = await readFile13(join14(cwd, "pnpm-workspace.yaml"), "utf-8");
4021
+ globs = parsePnpmWorkspaceGlobs(raw);
4022
+ } catch {
4023
+ }
4024
+ for (const glob of globs) {
4025
+ const dirs = await expandGlob(cwd, glob);
4026
+ for (const dir of dirs) {
4027
+ try {
4028
+ const pkgRaw = await readFile13(join14(dir, "package.json"), "utf-8");
4029
+ const pkg = JSON.parse(pkgRaw);
4030
+ const name = pkg.name ?? relative3(cwd, dir);
4031
+ map.set(dir, { name, dir });
4032
+ } catch {
4033
+ }
4034
+ }
4035
+ }
4036
+ return map;
4037
+ }
4038
+ function findOwningPackage(filePath, packageDirs) {
4039
+ const sorted = [...packageDirs].sort((a, b) => b.length - a.length);
4040
+ for (const dir of sorted) {
4041
+ if (filePath.startsWith(dir + "/") || filePath === dir) {
4042
+ return dir;
4043
+ }
4044
+ }
4045
+ return null;
4046
+ }
4047
+ function patchBump(version) {
4048
+ const hasV = version.startsWith("v");
4049
+ const stripped = hasV ? version.slice(1) : version;
4050
+ const coreMatch = stripped.match(/^(\d+)\.(\d+)\.(\d+)/);
4051
+ if (!coreMatch) {
4052
+ return version;
4053
+ }
4054
+ const major = coreMatch[1];
4055
+ const minor = coreMatch[2];
4056
+ const patch = parseInt(coreMatch[3], 10);
4057
+ return `${hasV ? "v" : ""}${major}.${minor}.${patch + 1}`;
4058
+ }
4059
+ function compareSemver(a, b) {
4060
+ const parseCore = (v) => {
4061
+ const stripped = v.startsWith("v") ? v.slice(1) : v;
4062
+ const m = stripped.match(/^(\d+)\.(\d+)\.(\d+)/);
4063
+ if (!m) return [-1, -1, -1];
4064
+ return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
4065
+ };
4066
+ const hasPre = (v) => /^v?\d+\.\d+\.\d+-/.test(v);
4067
+ const [aMaj, aMin, aPat] = parseCore(a);
4068
+ const [bMaj, bMin, bPat] = parseCore(b);
4069
+ if (aMaj !== bMaj) return aMaj - bMaj;
4070
+ if (aMin !== bMin) return aMin - bMin;
4071
+ if (aPat !== bPat) return aPat - bPat;
4072
+ if (hasPre(a) && !hasPre(b)) return -1;
4073
+ if (!hasPre(a) && hasPre(b)) return 1;
4074
+ return 0;
4075
+ }
4076
+ function gitShowFile(cwd, ref, relPath) {
4077
+ const result = spawnSync3("git", ["show", `${ref}:${relPath}`], {
4078
+ cwd,
4079
+ encoding: "utf-8",
4080
+ stdio: ["pipe", "pipe", "pipe"]
4081
+ });
4082
+ if (result.status !== 0 || result.error) return null;
4083
+ return result.stdout ?? null;
4084
+ }
4085
+ function extractVersion(raw, filePath) {
4086
+ try {
4087
+ const parsed = JSON.parse(raw);
4088
+ if (typeof parsed !== "object" || parsed === null) return null;
4089
+ const obj = parsed;
4090
+ if (filePath.endsWith("app.json") || filePath.endsWith("app.config.json")) {
4091
+ const expo = obj["expo"];
4092
+ if (typeof expo === "object" && expo !== null) {
4093
+ const expoObj = expo;
4094
+ if (typeof expoObj["version"] === "string") return expoObj["version"];
4095
+ }
4096
+ }
4097
+ if (typeof obj["version"] === "string") return obj["version"];
4098
+ return null;
4099
+ } catch {
4100
+ return null;
4101
+ }
4102
+ }
4103
+ function injectVersion(raw, nextVersion, filePath) {
4104
+ const parsed = JSON.parse(raw);
4105
+ if (filePath.endsWith("app.json") || filePath.endsWith("app.config.json")) {
4106
+ const expo = parsed["expo"];
4107
+ if (typeof expo === "object" && expo !== null) {
4108
+ expo["version"] = nextVersion;
4109
+ return JSON.stringify(parsed, null, 2) + "\n";
4110
+ }
4111
+ }
4112
+ parsed["version"] = nextVersion;
4113
+ return JSON.stringify(parsed, null, 2) + "\n";
4114
+ }
4115
+ async function prependChangelog(changelogPath, packageName, nextVersion, now, dryRun) {
4116
+ let existing;
4117
+ try {
4118
+ existing = await readFile13(changelogPath, "utf-8");
4119
+ } catch {
4120
+ return false;
4121
+ }
4122
+ const entry = `## [${nextVersion}] - ${now}
4123
+
4124
+ ### Changed
4125
+
4126
+ - Version bump for ${packageName}
4127
+
4128
+ `;
4129
+ const updated = entry + existing;
4130
+ if (!dryRun) {
4131
+ await writeFile10(changelogPath, updated, "utf-8");
4132
+ }
4133
+ return true;
4134
+ }
4135
+ async function runBump(opts) {
4136
+ const cwd = resolve2(opts?.cwd ?? process.cwd());
4137
+ const dryRun = opts?.dryRun ?? false;
4138
+ const now = opts?.now ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4139
+ const baseBranch = await readBaseBranch(cwd);
4140
+ const baseRef = resolveBaseRef(cwd, baseBranch);
4141
+ const diffResult = spawnSync3(
4142
+ "git",
4143
+ ["diff", "--name-only", `${baseRef}...HEAD`],
4144
+ {
4145
+ cwd,
4146
+ encoding: "utf-8",
4147
+ stdio: ["pipe", "pipe", "pipe"]
4148
+ }
4149
+ );
4150
+ if (diffResult.status !== 0 || diffResult.error) {
4151
+ const errMsg = diffResult.stderr?.trim() || diffResult.error?.message || "git diff failed";
4152
+ throw new Error(`runBump: git diff failed: ${errMsg}`);
4153
+ }
4154
+ const changedFiles = (diffResult.stdout ?? "").trim().split("\n").filter(Boolean).map((f) => resolve2(cwd, f));
4155
+ const packageMap = await buildPackageMap(cwd);
4156
+ const packageDirs = Array.from(packageMap.keys());
4157
+ const rootPkgPath = join14(cwd, "package.json");
4158
+ const changedPackageDirs = /* @__PURE__ */ new Set();
4159
+ for (const absFile of changedFiles) {
4160
+ const owner = findOwningPackage(absFile, packageDirs);
4161
+ if (owner != null) {
4162
+ changedPackageDirs.add(owner);
4163
+ }
4164
+ }
4165
+ const entries = [];
4166
+ for (const pkgDir of changedPackageDirs) {
4167
+ const pkgInfo = packageMap.get(pkgDir);
4168
+ const pkgJsonPath = join14(pkgDir, "package.json");
4169
+ if (pkgJsonPath === rootPkgPath) continue;
4170
+ const versionFileCandidates = [
4171
+ { abs: pkgJsonPath, rel: relative3(cwd, pkgJsonPath).replace(/\\/g, "/") }
4172
+ ];
4173
+ const tauriConfPath = join14(pkgDir, "src-tauri", "tauri.conf.json");
4174
+ const tauriRelPath = relative3(cwd, tauriConfPath).replace(/\\/g, "/");
4175
+ try {
4176
+ await access4(tauriConfPath);
4177
+ versionFileCandidates.push({ abs: tauriConfPath, rel: tauriRelPath });
4178
+ } catch {
4179
+ }
4180
+ const appJsonPath = join14(pkgDir, "app.json");
4181
+ const appJsonRelPath = relative3(cwd, appJsonPath).replace(/\\/g, "/");
4182
+ try {
4183
+ await access4(appJsonPath);
4184
+ versionFileCandidates.push({ abs: appJsonPath, rel: appJsonRelPath });
4185
+ } catch {
4186
+ }
4187
+ let currentPkgJsonRaw;
4188
+ try {
4189
+ currentPkgJsonRaw = await readFile13(pkgJsonPath, "utf-8");
4190
+ } catch (err) {
4191
+ console.warn(
4192
+ `runBump: could not read ${pkgJsonPath}: ${err instanceof Error ? err.message : String(err)}`
4193
+ );
4194
+ continue;
4195
+ }
4196
+ const currentVersion = extractVersion(currentPkgJsonRaw, pkgJsonPath);
4197
+ if (currentVersion == null) {
4198
+ entries.push({
4199
+ name: pkgInfo.name,
4200
+ packageDir: pkgDir,
4201
+ currentVersion: "(none)",
4202
+ nextVersion: "(none)",
4203
+ versionFiles: [],
4204
+ changelogUpdated: false,
4205
+ skipped: true,
4206
+ skipReason: "No version field in package.json"
4207
+ });
4208
+ continue;
4209
+ }
4210
+ const pkgJsonRelPath = relative3(cwd, pkgJsonPath).replace(/\\/g, "/");
4211
+ const baseRaw = gitShowFile(cwd, baseRef, pkgJsonRelPath);
4212
+ if (baseRaw !== null) {
4213
+ const baseVersion = extractVersion(baseRaw, pkgJsonPath);
4214
+ if (baseVersion !== null && compareSemver(currentVersion, baseVersion) > 0) {
4215
+ entries.push({
4216
+ name: pkgInfo.name,
4217
+ packageDir: pkgDir,
4218
+ currentVersion,
4219
+ nextVersion: currentVersion,
4220
+ versionFiles: [],
4221
+ changelogUpdated: false,
4222
+ skipped: true,
4223
+ skipReason: `Already at ${currentVersion} > base ${baseVersion}`
4224
+ });
4225
+ continue;
4226
+ }
4227
+ }
4228
+ const nextVersion = patchBump(currentVersion);
4229
+ const updatedVersionFiles = [];
4230
+ const skippedVersionFiles = [];
4231
+ for (const { abs, rel } of versionFileCandidates) {
4232
+ let raw;
4233
+ try {
4234
+ raw = await readFile13(abs, "utf-8");
4235
+ } catch {
4236
+ continue;
4237
+ }
4238
+ const ver = abs === pkgJsonPath ? currentVersion : extractVersion(raw, abs);
4239
+ if (ver == null) {
4240
+ console.warn(
4241
+ `runBump: ${pkgInfo.name}: no version field in ${abs} \u2014 skipping update`
4242
+ );
4243
+ skippedVersionFiles.push(rel);
4244
+ continue;
4245
+ }
4246
+ const updated = injectVersion(raw, nextVersion, abs);
4247
+ if (!dryRun) {
4248
+ await writeFile10(abs, updated, "utf-8");
4249
+ }
4250
+ updatedVersionFiles.push(rel);
4251
+ }
4252
+ const changelogPath = join14(pkgDir, "CHANGELOG.md");
4253
+ const changelogUpdated = await prependChangelog(
4254
+ changelogPath,
4255
+ pkgInfo.name,
4256
+ nextVersion,
4257
+ now,
4258
+ dryRun
4259
+ );
4260
+ entries.push({
4261
+ name: pkgInfo.name,
4262
+ packageDir: pkgDir,
4263
+ currentVersion,
4264
+ nextVersion,
4265
+ versionFiles: updatedVersionFiles,
4266
+ ...skippedVersionFiles.length > 0 ? { skippedVersionFiles } : {},
4267
+ changelogUpdated,
4268
+ skipped: false
4269
+ });
4270
+ }
4271
+ return { baseBranch, baseRef, entries, dryRun };
4272
+ }
4273
+ var init_bump = __esm({
4274
+ "src/lib/bump.ts"() {
4275
+ "use strict";
4276
+ init_git_utils();
4277
+ }
4278
+ });
4279
+
4280
+ // src/cli/bump.ts
4281
+ var bump_exports = {};
4282
+ __export(bump_exports, {
4283
+ runBumpCommand: () => runBumpCommand
4284
+ });
4285
+ function parseFlagsFromArgs2(args) {
4286
+ const flags = {};
4287
+ const booleans = /* @__PURE__ */ new Set();
4288
+ for (let i = 0; i < args.length; i++) {
4289
+ const arg = args[i];
4290
+ if (arg.startsWith("--")) {
4291
+ const key = arg.slice(2);
4292
+ const next = args[i + 1];
4293
+ if (next !== void 0 && !next.startsWith("--")) {
4294
+ flags[key] = next;
4295
+ i++;
4296
+ } else {
4297
+ booleans.add(key);
4298
+ }
4299
+ }
4300
+ }
4301
+ return { flags, booleans };
4302
+ }
4303
+ function printBumpHelp() {
4304
+ process.stdout.write(
4305
+ "\n codebyplan bump\n\n Detect changed workspace packages and patch-bump their versions.\n Does NOT commit or push \u2014 pure version-file + changelog edits.\n\n Flags:\n --dry-run Preview planned bumps without writing any files\n --json Write structured JSON output to stdout\n\n"
4306
+ );
4307
+ }
4308
+ function printHumanResult(result) {
4309
+ const lines = [];
4310
+ const baseLabel = result.baseRef === result.baseBranch ? result.baseBranch : `${result.baseBranch} (compared against ${result.baseRef})`;
4311
+ if (result.dryRun) {
4312
+ lines.push(`[dry-run] Base: ${baseLabel}`);
4313
+ } else {
4314
+ lines.push(`Base: ${baseLabel}`);
4315
+ }
4316
+ const active = result.entries.filter((e) => !e.skipped);
4317
+ const skipped = result.entries.filter((e) => e.skipped);
4318
+ if (active.length === 0 && skipped.length === 0) {
4319
+ lines.push("No changed packages detected.");
4320
+ }
4321
+ for (const entry of active) {
4322
+ const action = result.dryRun ? "Would bump" : "Bumped";
4323
+ lines.push(
4324
+ ` ${action}: ${entry.name} ${entry.currentVersion} \u2192 ${entry.nextVersion}`
4325
+ );
4326
+ for (const f of entry.versionFiles) {
4327
+ lines.push(` - ${f}`);
4328
+ }
4329
+ if (entry.changelogUpdated) {
4330
+ lines.push(` - CHANGELOG.md`);
4331
+ }
4332
+ }
4333
+ for (const entry of skipped) {
4334
+ lines.push(
4335
+ ` Skipped: ${entry.name} (${entry.skipReason ?? "already bumped"})`
4336
+ );
4337
+ }
4338
+ process.stdout.write(lines.join("\n") + "\n");
4339
+ }
4340
+ async function runBumpCommand(args) {
4341
+ const firstArg = args[0];
4342
+ if (firstArg === "help" || firstArg === "--help" || firstArg === "-h") {
4343
+ printBumpHelp();
4344
+ process.exit(0);
4345
+ }
4346
+ const { booleans } = parseFlagsFromArgs2(args);
4347
+ const dryRun = booleans.has("dry-run");
4348
+ const jsonOutput = booleans.has("json");
4349
+ let result;
4350
+ try {
4351
+ result = await runBump({ dryRun });
4352
+ } catch (err) {
4353
+ process.stderr.write(
4354
+ `bump: ${err instanceof Error ? err.message : String(err)}
4355
+ `
4356
+ );
4357
+ process.exit(1);
4358
+ }
4359
+ if (jsonOutput) {
4360
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
4361
+ return;
4362
+ }
4363
+ printHumanResult(result);
4364
+ }
4365
+ var init_bump2 = __esm({
4366
+ "src/cli/bump.ts"() {
4367
+ "use strict";
4368
+ init_bump();
4369
+ }
4370
+ });
4371
+
4372
+ // src/lib/ship.ts
4373
+ import { execSync as execSync4, spawnSync as spawnSync4 } from "node:child_process";
4374
+ import { relative as relative4 } from "node:path";
4375
+ function assertValidBranchName2(branch, label) {
4376
+ if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
4377
+ throw new Error(
4378
+ `Invalid branch name for ${label}: '${branch}' (refusing to pass shell metacharacters to git/gh)`
4379
+ );
4380
+ }
4381
+ }
3962
4382
  function defaultPrBody(feat, base) {
3963
4383
  return `## Summary
3964
4384
 
@@ -3975,7 +4395,7 @@ async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Prom
3975
4395
  let totalGhErrors = 0;
3976
4396
  let iteration = 0;
3977
4397
  while (Date.now() < deadline) {
3978
- const ghResult = spawnSync2(
4398
+ const ghResult = spawnSync4(
3979
4399
  "gh",
3980
4400
  ["pr", "checks", feat, "--json", "name,state,conclusion"],
3981
4401
  {
@@ -4041,7 +4461,9 @@ async function runShip(opts) {
4041
4461
  const keepFeat = opts?.keepFeat ?? false;
4042
4462
  const bodyFile = opts?.bodyFile ?? null;
4043
4463
  const timeoutSeconds = opts?.timeoutSeconds ?? 600;
4464
+ const bump = opts?.bump ?? true;
4044
4465
  const sleepMs = opts?._sleepMs ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4466
+ let bumpEntries;
4045
4467
  let feat;
4046
4468
  try {
4047
4469
  feat = execSync4("git rev-parse --abbrev-ref HEAD", {
@@ -4102,15 +4524,66 @@ ${statusOut.trim()}`
4102
4524
  }
4103
4525
  } catch {
4104
4526
  }
4527
+ if (bump) {
4528
+ const bumpResult = await runBump({ cwd, dryRun: true });
4529
+ bumpEntries = bumpResult.entries;
4530
+ }
4105
4531
  return {
4106
4532
  pr_url: prUrl2,
4107
4533
  merge_commit: null,
4108
4534
  branch_deleted: false,
4109
4535
  dry_run: true,
4110
4536
  feat_branch: feat,
4111
- base_branch: base
4537
+ base_branch: base,
4538
+ ...bumpEntries !== void 0 ? { bumps: bumpEntries } : {}
4112
4539
  };
4113
4540
  }
4541
+ if (bump) {
4542
+ let bumpResult;
4543
+ try {
4544
+ bumpResult = await runBump({ cwd });
4545
+ } catch (err) {
4546
+ throw new Error(
4547
+ `Version bump failed before push (working tree may have been partially modified \u2014 run 'git status'): ${err instanceof Error ? err.message : String(err)}`
4548
+ );
4549
+ }
4550
+ bumpEntries = bumpResult.entries;
4551
+ const toStage = bumpResult.entries.filter((e) => !e.skipped).flatMap((e) => [
4552
+ ...e.versionFiles,
4553
+ ...e.changelogUpdated ? [relative4(cwd, e.packageDir).replace(/\\/g, "/") + "/CHANGELOG.md"] : []
4554
+ ]);
4555
+ if (toStage.length > 0) {
4556
+ const addResult = spawnSync4("git", ["add", "--", ...toStage], {
4557
+ cwd,
4558
+ encoding: "utf-8",
4559
+ stdio: ["pipe", "pipe", "pipe"]
4560
+ });
4561
+ if (addResult.status !== 0 || addResult.error) {
4562
+ throw new Error(
4563
+ `Failed to stage bump files: ${addResult.stderr?.trim() ?? addResult.error?.message}`
4564
+ );
4565
+ }
4566
+ const commitResult = spawnSync4(
4567
+ "git",
4568
+ ["commit", "-m", "chore(release): bump versions"],
4569
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
4570
+ );
4571
+ if (commitResult.status !== 0 || commitResult.error) {
4572
+ spawnSync4(
4573
+ "git",
4574
+ ["restore", "--staged", "--worktree", "--", ...toStage],
4575
+ {
4576
+ cwd,
4577
+ encoding: "utf-8",
4578
+ stdio: ["pipe", "pipe", "pipe"]
4579
+ }
4580
+ );
4581
+ throw new Error(
4582
+ `Failed to commit version bump: ${commitResult.stderr?.trim() ?? commitResult.error?.message}`
4583
+ );
4584
+ }
4585
+ }
4586
+ }
4114
4587
  execSync4(`git push -u origin HEAD`, {
4115
4588
  cwd,
4116
4589
  encoding: "utf-8",
@@ -4141,7 +4614,7 @@ ${statusOut.trim()}`
4141
4614
  } else {
4142
4615
  createArgs.push("--body", defaultPrBody(feat, base));
4143
4616
  }
4144
- const createResult = spawnSync2("gh", createArgs, {
4617
+ const createResult = spawnSync4("gh", createArgs, {
4145
4618
  cwd,
4146
4619
  encoding: "utf-8",
4147
4620
  stdio: ["pipe", "pipe", "pipe"]
@@ -4161,7 +4634,8 @@ ${statusOut.trim()}`
4161
4634
  checks_failed: true,
4162
4635
  checks_failure_reason: checksResult.reason,
4163
4636
  feat_branch: feat,
4164
- base_branch: base
4637
+ base_branch: base,
4638
+ ...bumpEntries !== void 0 ? { bumps: bumpEntries } : {}
4165
4639
  };
4166
4640
  }
4167
4641
  execSync4(`gh pr merge --merge ${feat}`, {
@@ -4171,7 +4645,7 @@ ${statusOut.trim()}`
4171
4645
  });
4172
4646
  let mergeCommit = null;
4173
4647
  try {
4174
- const prViewResult = spawnSync2(
4648
+ const prViewResult = spawnSync4(
4175
4649
  "gh",
4176
4650
  ["pr", "view", feat, "--json", "state,mergeCommit"],
4177
4651
  {
@@ -4215,13 +4689,15 @@ ${statusOut.trim()}`
4215
4689
  merge_commit: mergeCommit,
4216
4690
  branch_deleted: branchDeleted,
4217
4691
  feat_branch: feat,
4218
- base_branch: base
4692
+ base_branch: base,
4693
+ ...bumpEntries !== void 0 ? { bumps: bumpEntries } : {}
4219
4694
  };
4220
4695
  }
4221
4696
  var init_ship = __esm({
4222
4697
  "src/lib/ship.ts"() {
4223
4698
  "use strict";
4224
- init_flags();
4699
+ init_git_utils();
4700
+ init_bump();
4225
4701
  }
4226
4702
  });
4227
4703
 
@@ -4230,7 +4706,7 @@ var ship_exports = {};
4230
4706
  __export(ship_exports, {
4231
4707
  runShipCommand: () => runShipCommand
4232
4708
  });
4233
- function parseFlagsFromArgs2(args) {
4709
+ function parseFlagsFromArgs3(args) {
4234
4710
  const flags = {};
4235
4711
  const booleans = /* @__PURE__ */ new Set();
4236
4712
  for (let i = 0; i < args.length; i++) {
@@ -4250,7 +4726,7 @@ function parseFlagsFromArgs2(args) {
4250
4726
  }
4251
4727
  function printShipHelp() {
4252
4728
  process.stdout.write(
4253
- "\n codebyplan ship\n\n Ship the current feat branch to production via PR.\n\n Flags:\n --dry-run Preview without pushing, creating, or merging\n --json Write JSON result to stdout\n --keep-feat Keep the feat branch after merge (no auto-delete)\n --body-file <path> Path to a markdown file used as the PR body\n --timeout <seconds> Max seconds to poll required checks (default: 600)\n\n"
4729
+ "\n codebyplan ship\n\n Ship the current feat branch to production via PR.\n\n Flags:\n --dry-run Preview without pushing, creating, or merging\n --json Write JSON result to stdout\n --keep-feat Keep the feat branch after merge (no auto-delete)\n --body-file <path> Path to a markdown file used as the PR body\n --timeout <seconds> Max seconds to poll required checks (default: 600)\n --no-bump Skip automatic patch-version bump before push\n\n"
4254
4730
  );
4255
4731
  }
4256
4732
  async function runShipCommand(args) {
@@ -4262,10 +4738,11 @@ async function runShipCommand(args) {
4262
4738
  await runShipDefault(args);
4263
4739
  }
4264
4740
  async function runShipDefault(args) {
4265
- const { flags, booleans } = parseFlagsFromArgs2(args);
4741
+ const { flags, booleans } = parseFlagsFromArgs3(args);
4266
4742
  const dryRun = booleans.has("dry-run");
4267
4743
  const jsonOutput = booleans.has("json");
4268
4744
  const keepFeat = booleans.has("keep-feat");
4745
+ const noBump = booleans.has("no-bump");
4269
4746
  const bodyFile = flags["body-file"] ?? void 0;
4270
4747
  const timeoutRaw = flags["timeout"];
4271
4748
  let timeoutSeconds;
@@ -4282,7 +4759,13 @@ async function runShipDefault(args) {
4282
4759
  }
4283
4760
  let result;
4284
4761
  try {
4285
- result = await runShip({ dryRun, keepFeat, bodyFile, timeoutSeconds });
4762
+ result = await runShip({
4763
+ dryRun,
4764
+ keepFeat,
4765
+ bodyFile,
4766
+ timeoutSeconds,
4767
+ bump: !noBump
4768
+ });
4286
4769
  } catch (err) {
4287
4770
  process.stderr.write(
4288
4771
  `ship: ${err instanceof Error ? err.message : String(err)}
@@ -4319,6 +4802,13 @@ async function runShipDefault(args) {
4319
4802
  lines.push(`Branch deleted: ${result.feat_branch ?? ""}`);
4320
4803
  }
4321
4804
  }
4805
+ const bumpedEntries = result.bumps?.filter((e) => !e.skipped) ?? [];
4806
+ if (bumpedEntries.length > 0) {
4807
+ lines.push("Bumped:");
4808
+ for (const e of bumpedEntries) {
4809
+ lines.push(` ${e.name} ${e.currentVersion} \u2192 ${e.nextVersion}`);
4810
+ }
4811
+ }
4322
4812
  process.stdout.write(lines.join("\n") + "\n");
4323
4813
  }
4324
4814
  var init_ship2 = __esm({
@@ -4328,1753 +4818,1888 @@ var init_ship2 = __esm({
4328
4818
  }
4329
4819
  });
4330
4820
 
4331
- // src/cli/resolve-worktree.ts
4332
- var resolve_worktree_exports = {};
4333
- __export(resolve_worktree_exports, {
4334
- ProcessExitSignal: () => ProcessExitSignal,
4335
- runResolveWorktree: () => runResolveWorktree
4336
- });
4337
- import { execSync as execSync5 } from "node:child_process";
4338
- function distress(kind, message, jsonMode) {
4339
- if (jsonMode) return;
4340
- process.stderr.write(`resolve-worktree: ${kind}: ${message}
4341
- `);
4821
+ // src/lib/hash.ts
4822
+ import { createHash as createHash3 } from "node:crypto";
4823
+ function sha256(input) {
4824
+ const buf = typeof input === "string" ? Buffer.from(input, "utf8") : input;
4825
+ return `sha256:${createHash3("sha256").update(buf).digest("hex")}`;
4342
4826
  }
4343
- async function runResolveWorktree() {
4344
- const jsonMode = hasFlag("json", 3);
4345
- let errorContext = null;
4346
- const migrationNoticeCallback = (legacyPath, primaryPath) => {
4347
- if (!jsonMode) {
4348
- process.stderr.write(
4349
- `resolve-worktree: local_config_migration: ${legacyPath} is the legacy flat config; move device_id to ${primaryPath}
4350
- `
4351
- );
4352
- }
4353
- };
4354
- try {
4355
- const projectPath = process.cwd();
4356
- const found = await findCodebyplanConfig(projectPath);
4357
- if (!found?.contents.repo_id) {
4358
- emitAndExit(null, null, jsonMode);
4359
- }
4360
- const repoId = found.contents.repo_id;
4361
- try {
4362
- await readLocalConfig(projectPath);
4363
- } catch (readErr) {
4364
- const readErrCode = readErr.code;
4365
- errorContext = {
4366
- kind: readErrCode === "LEGACY_FILE_BLOCKS_DIR" ? "legacy_file_blocks_dir" : "local_config_read_failed",
4367
- message: readErr instanceof Error ? readErr.message : String(readErr)
4368
- };
4827
+ var init_hash = __esm({
4828
+ "src/lib/hash.ts"() {
4829
+ "use strict";
4830
+ }
4831
+ });
4832
+
4833
+ // src/lib/template-walker.ts
4834
+ import * as fs from "node:fs";
4835
+ import * as path2 from "node:path";
4836
+ function walkTemplates(templatesDir) {
4837
+ const absRoot = fs.realpathSync(templatesDir);
4838
+ const visited = /* @__PURE__ */ new Set();
4839
+ const out = [];
4840
+ const recurse = (absDir) => {
4841
+ const realDir = fs.realpathSync(absDir);
4842
+ if (visited.has(realDir)) {
4843
+ return;
4369
4844
  }
4370
- let deviceId;
4371
- try {
4372
- deviceId = await getOrCreateDeviceId(
4373
- projectPath,
4374
- migrationNoticeCallback
4375
- );
4376
- } catch (deviceErr) {
4377
- const code = deviceErr.code;
4378
- if (code === "LEGACY_FILE_BLOCKS_DIR") {
4379
- errorContext = {
4380
- kind: "legacy_file_blocks_dir",
4381
- message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
4382
- };
4383
- } else if (errorContext === null || errorContext.kind !== "local_config_read_failed" && errorContext.kind !== "legacy_file_blocks_dir") {
4384
- errorContext = {
4385
- kind: "local_config_write_failed",
4386
- message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
4387
- };
4845
+ visited.add(realDir);
4846
+ const entries = fs.readdirSync(absDir, { withFileTypes: true });
4847
+ for (const entry of entries) {
4848
+ const absPath = path2.join(absDir, entry.name);
4849
+ if (entry.isDirectory()) {
4850
+ recurse(absPath);
4851
+ continue;
4388
4852
  }
4389
- emitAndExit(null, errorContext, jsonMode);
4390
- }
4391
- let branch = "";
4392
- try {
4393
- branch = execSync5("git symbolic-ref --short HEAD", {
4394
- cwd: projectPath,
4395
- encoding: "utf-8"
4396
- }).trim();
4397
- } catch (gitErr) {
4398
- if (errorContext === null) {
4399
- errorContext = {
4400
- kind: "git_failed",
4401
- message: gitErr instanceof Error ? gitErr.message : String(gitErr)
4402
- };
4853
+ if (!entry.isFile()) {
4854
+ continue;
4403
4855
  }
4404
- }
4405
- const onResolverError = (kind, err) => {
4406
- if (errorContext === null) {
4407
- errorContext = { kind, message: err.message };
4856
+ if (entry.name === ".gitkeep") continue;
4857
+ const relPosix = path2.relative(absRoot, absPath).split(path2.sep).join("/");
4858
+ if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
4859
+ continue;
4408
4860
  }
4409
- };
4410
- const worktreeId = await resolveWorktreeId({
4411
- repoId,
4412
- repoPath: projectPath,
4413
- branch,
4414
- deviceId,
4415
- onError: onResolverError
4416
- });
4417
- if (worktreeId) {
4418
- emitAndExit(worktreeId, errorContext, jsonMode);
4419
- }
4420
- const useFallback = hasFlag("fallback-from-branch", 3);
4421
- if (useFallback) {
4422
- const fallbackId = await resolveWorktreeByBranch({
4423
- repoId,
4424
- deviceId,
4425
- branch,
4426
- onError: onResolverError
4861
+ const content = fs.readFileSync(absPath);
4862
+ out.push({
4863
+ src: relPosix,
4864
+ dest: relPosix,
4865
+ hash: sha256(content)
4427
4866
  });
4428
- if (fallbackId) {
4429
- emitAndExit(fallbackId, errorContext, jsonMode);
4430
- }
4431
- }
4432
- emitAndExit(null, errorContext, jsonMode);
4433
- } catch (err) {
4434
- if (err instanceof ProcessExitSignal) throw err;
4435
- const msg = err instanceof Error ? err.message : String(err);
4436
- errorContext = { kind: "unhandled", message: msg };
4437
- emitAndExit(null, errorContext, jsonMode);
4438
- }
4439
- }
4440
- function emitAndExit(worktreeId, errorContext, jsonMode) {
4441
- if (jsonMode) {
4442
- const errorKind = errorContext?.kind ?? (worktreeId === null ? "tuple_miss" : null);
4443
- process.stdout.write(
4444
- JSON.stringify({ worktree_id: worktreeId, error_kind: errorKind }) + "\n"
4445
- );
4446
- } else {
4447
- if (worktreeId !== null) {
4448
- process.stdout.write(worktreeId);
4449
- }
4450
- if (errorContext !== null) {
4451
- if (errorContext.kind !== "unhandled" || process.env.CODEBYPLAN_DEBUG === "1") {
4452
- distress(errorContext.kind, errorContext.message, jsonMode);
4453
- }
4454
4867
  }
4455
- }
4456
- process.exit(0);
4868
+ };
4869
+ recurse(absRoot);
4870
+ out.sort((a, b) => a.src.localeCompare(b.src));
4871
+ return out;
4457
4872
  }
4458
- var ProcessExitSignal;
4459
- var init_resolve_worktree2 = __esm({
4460
- "src/cli/resolve-worktree.ts"() {
4873
+ var EXCLUDED_RELATIVE_PATHS;
4874
+ var init_template_walker = __esm({
4875
+ "src/lib/template-walker.ts"() {
4461
4876
  "use strict";
4462
- init_flags();
4463
- init_local_config();
4464
- init_resolve_worktree();
4465
- ProcessExitSignal = class extends Error {
4466
- code;
4467
- constructor(code) {
4468
- super(`process.exit(${code})`);
4469
- this.name = "ProcessExitSignal";
4470
- this.code = code;
4471
- }
4472
- };
4877
+ init_hash();
4878
+ EXCLUDED_RELATIVE_PATHS = /* @__PURE__ */ new Set([
4879
+ // Meta files
4880
+ "hooks/hooks.json",
4881
+ "hooks/README.md",
4882
+ "rules/README.md",
4883
+ "settings.project.base.json",
4884
+ "settings.user.base.json",
4885
+ // .gitignore managed by ensureManagedGitignoreBlock; never copied into
4886
+ // consuming projects' .claude/ tree (it would overwrite the project root
4887
+ // .gitignore with a stale single-entry file).
4888
+ ".gitignore",
4889
+ // CBP-internal hooks — see templates/hooks/README.md "Hooks NOT included and why"
4890
+ "hooks/validate-structure.sh",
4891
+ "hooks/validate-structure-lib.sh",
4892
+ "hooks/validate-structure-patterns.sh",
4893
+ "hooks/validate-structure-templates.sh",
4894
+ "hooks/validate-structure-scope.sh",
4895
+ "hooks/validate-structure-lengths.sh",
4896
+ "hooks/validate-structure-smoke.sh",
4897
+ "hooks/validate-context-usage.sh",
4898
+ "hooks/validate-git-commit.sh"
4899
+ ]);
4473
4900
  }
4474
4901
  });
4475
4902
 
4476
- // src/lib/migrate-local-config.ts
4477
- import { mkdir as mkdir5, readFile as readFile13, unlink as unlink2, writeFile as writeFile10 } from "node:fs/promises";
4478
- import { join as join14 } from "node:path";
4479
- function legacySharedPath(projectPath) {
4480
- return join14(projectPath, ".codebyplan.json");
4481
- }
4482
- function legacyLocalPath(projectPath) {
4483
- return join14(projectPath, ".codebyplan.local.json");
4903
+ // src/lib/manifest.ts
4904
+ import * as fs2 from "node:fs";
4905
+ import * as os from "node:os";
4906
+ import * as path3 from "node:path";
4907
+ function manifestPath(projectDir) {
4908
+ return path3.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
4484
4909
  }
4485
- function newDirPath(projectPath) {
4486
- return join14(projectPath, ".codebyplan");
4910
+ function midManifestPath(projectDir) {
4911
+ return path3.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
4487
4912
  }
4488
- function sentinelPath(projectPath) {
4489
- return join14(projectPath, ".codebyplan", "repo.json");
4913
+ function oldManifestPath(projectDir) {
4914
+ return path3.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
4490
4915
  }
4491
- async function statSafe(p) {
4492
- const { stat: stat2 } = await import("node:fs/promises");
4493
- try {
4494
- return await stat2(p);
4495
- } catch {
4496
- return null;
4916
+ function readManifest(projectDir) {
4917
+ const newFile = manifestPath(projectDir);
4918
+ if (fs2.existsSync(newFile)) {
4919
+ const raw = fs2.readFileSync(newFile, "utf8");
4920
+ return JSON.parse(raw);
4497
4921
  }
4498
- }
4499
- async function needsLocalMigration(projectPath) {
4500
- try {
4501
- const legacyStat = await statSafe(legacySharedPath(projectPath));
4502
- if (!legacyStat) return false;
4503
- const sentinelStat = await statSafe(sentinelPath(projectPath));
4504
- if (sentinelStat) return false;
4505
- return true;
4506
- } catch {
4507
- return false;
4922
+ const midFile = midManifestPath(projectDir);
4923
+ if (fs2.existsSync(midFile)) {
4924
+ const raw = fs2.readFileSync(midFile, "utf8");
4925
+ return JSON.parse(raw);
4508
4926
  }
4509
- }
4510
- async function runLocalMigration(projectPath) {
4511
- const dirStat = await statSafe(newDirPath(projectPath));
4512
- if (dirStat && !dirStat.isDirectory()) {
4513
- throw new Error(
4514
- ".codebyplan exists as a file; remove or rename it before migrating."
4515
- );
4927
+ const oldFile = oldManifestPath(projectDir);
4928
+ if (fs2.existsSync(oldFile)) {
4929
+ const raw = fs2.readFileSync(oldFile, "utf8");
4930
+ return JSON.parse(raw);
4516
4931
  }
4517
- const sentinel = await statSafe(sentinelPath(projectPath));
4518
- if (sentinel) {
4519
- return {
4520
- migrated: true,
4521
- was_dirty: false,
4522
- files_changed: [],
4523
- summary: "already on new layout"
4524
- };
4932
+ return null;
4933
+ }
4934
+ function writeManifest(projectDir, manifest) {
4935
+ const file = manifestPath(projectDir);
4936
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
4937
+ fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
4938
+ const mid = midManifestPath(projectDir);
4939
+ if (fs2.existsSync(mid)) {
4940
+ fs2.rmSync(mid);
4525
4941
  }
4526
- let legacyRaw;
4527
- try {
4528
- legacyRaw = await readFile13(legacySharedPath(projectPath), "utf-8");
4529
- } catch {
4530
- return {
4531
- migrated: true,
4532
- was_dirty: false,
4533
- files_changed: [],
4534
- summary: "legacy .codebyplan.json absent; nothing to migrate"
4535
- };
4536
- }
4537
- let parsed;
4538
- try {
4539
- parsed = JSON.parse(legacyRaw);
4540
- } catch (err) {
4541
- const inner = err instanceof Error ? err.message : String(err);
4542
- throw new Error(
4543
- `.codebyplan.json contains invalid JSON \u2014 cannot migrate: ${inner}`
4544
- );
4545
- }
4546
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4547
- throw new Error(
4548
- ".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
4549
- );
4942
+ const legacy = oldManifestPath(projectDir);
4943
+ if (fs2.existsSync(legacy)) {
4944
+ fs2.rmSync(legacy);
4550
4945
  }
4551
- const cfg = parsed;
4552
- let deviceId;
4553
- let deviceWrittenByHelper = false;
4554
- try {
4555
- const localRaw = await readFile13(legacyLocalPath(projectPath), "utf-8");
4556
- const localParsed = JSON.parse(localRaw);
4557
- if (typeof localParsed.device_id === "string") {
4558
- deviceId = localParsed.device_id;
4946
+ }
4947
+ function defaultManifest() {
4948
+ return {
4949
+ version: VERSION,
4950
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
4951
+ files: []
4952
+ };
4953
+ }
4954
+ function userManifestPath(userDir) {
4955
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
4956
+ return path3.join(dir, NEW_MANIFEST_FILENAME);
4957
+ }
4958
+ function userMidManifestPath(userDir) {
4959
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
4960
+ return path3.join(dir, MID_MANIFEST_FILENAME);
4961
+ }
4962
+ function userOldManifestPath(userDir) {
4963
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
4964
+ return path3.join(dir, OLD_MANIFEST_FILENAME);
4965
+ }
4966
+ function readManifestForScope(scope, arg2) {
4967
+ if (scope === "user") {
4968
+ const newFile = userManifestPath(arg2);
4969
+ if (fs2.existsSync(newFile)) {
4970
+ const raw = fs2.readFileSync(newFile, "utf8");
4971
+ return JSON.parse(raw);
4559
4972
  }
4560
- } catch {
4561
- }
4562
- try {
4563
- await mkdir5(newDirPath(projectPath), { recursive: true });
4564
- } catch (err) {
4565
- const code = err.code;
4566
- if (code === "ENOTDIR" || code === "EEXIST") {
4567
- throw new Error(
4568
- ".codebyplan exists as a file; remove or rename it before migrating."
4569
- );
4973
+ const midFile = userMidManifestPath(arg2);
4974
+ if (fs2.existsSync(midFile)) {
4975
+ const raw = fs2.readFileSync(midFile, "utf8");
4976
+ return JSON.parse(raw);
4570
4977
  }
4571
- throw err;
4572
- }
4573
- if (!deviceId) {
4574
- deviceId = await getOrCreateDeviceId(projectPath);
4575
- deviceWrittenByHelper = true;
4576
- }
4577
- const filesChanged = [];
4578
- const repoJson = {};
4579
- if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
4580
- if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
4581
- if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
4582
- await writeFile10(
4583
- join14(projectPath, ".codebyplan", "repo.json"),
4584
- JSON.stringify(repoJson, null, 2) + "\n",
4585
- "utf-8"
4586
- );
4587
- filesChanged.push(".codebyplan/repo.json");
4588
- const serverJson = {};
4589
- if ("server_port" in cfg) serverJson.server_port = cfg.server_port;
4590
- if ("server_type" in cfg) serverJson.server_type = cfg.server_type;
4591
- if ("auto_push_enabled" in cfg)
4592
- serverJson.auto_push_enabled = cfg.auto_push_enabled;
4593
- if ("port_allocations" in cfg)
4594
- serverJson.port_allocations = cfg.port_allocations;
4595
- await writeFile10(
4596
- join14(projectPath, ".codebyplan", "server.json"),
4597
- JSON.stringify(serverJson, null, 2) + "\n",
4598
- "utf-8"
4599
- );
4600
- filesChanged.push(".codebyplan/server.json");
4601
- const gitJson = {};
4602
- if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
4603
- if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
4604
- await writeFile10(
4605
- join14(projectPath, ".codebyplan", "git.json"),
4606
- JSON.stringify(gitJson, null, 2) + "\n",
4607
- "utf-8"
4608
- );
4609
- filesChanged.push(".codebyplan/git.json");
4610
- const shipmentJson = {};
4611
- if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4612
- await writeFile10(
4613
- join14(projectPath, ".codebyplan", "shipment.json"),
4614
- JSON.stringify(shipmentJson, null, 2) + "\n",
4615
- "utf-8"
4616
- );
4617
- filesChanged.push(".codebyplan/shipment.json");
4618
- const vendorJson = {};
4619
- await writeFile10(
4620
- join14(projectPath, ".codebyplan", "vendor.json"),
4621
- JSON.stringify(vendorJson, null, 2) + "\n",
4622
- "utf-8"
4623
- );
4624
- filesChanged.push(".codebyplan/vendor.json");
4625
- const e2eJson = {};
4626
- await writeFile10(
4627
- join14(projectPath, ".codebyplan", "e2e.json"),
4628
- JSON.stringify(e2eJson, null, 2) + "\n",
4629
- "utf-8"
4630
- );
4631
- filesChanged.push(".codebyplan/e2e.json");
4632
- const eslintJson = {};
4633
- await writeFile10(
4634
- join14(projectPath, ".codebyplan", "eslint.json"),
4635
- JSON.stringify(eslintJson, null, 2) + "\n",
4636
- "utf-8"
4637
- );
4638
- filesChanged.push(".codebyplan/eslint.json");
4639
- if (!deviceWrittenByHelper) {
4640
- await writeFile10(
4641
- join14(projectPath, ".codebyplan", "device.local.json"),
4642
- JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4643
- "utf-8"
4644
- );
4645
- }
4646
- filesChanged.push(".codebyplan/device.local.json");
4647
- const writtenSentinel = await statSafe(sentinelPath(projectPath));
4648
- if (!writtenSentinel) {
4649
- throw new Error(
4650
- "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4651
- );
4978
+ const oldFile = userOldManifestPath(arg2);
4979
+ if (fs2.existsSync(oldFile)) {
4980
+ const raw = fs2.readFileSync(oldFile, "utf8");
4981
+ return JSON.parse(raw);
4982
+ }
4983
+ return null;
4652
4984
  }
4653
- const gitignorePath = join14(projectPath, ".gitignore");
4654
- try {
4655
- const gitignoreContent = await readFile13(gitignorePath, "utf-8");
4656
- const legacyLine = ".codebyplan.local.json";
4657
- const newLine = ".codebyplan/device.local.json";
4658
- const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
4659
- const hasNew = gitignoreContent.split("\n").some((l) => l.trimEnd() === newLine);
4660
- let updated;
4661
- if (hasLegacy && !hasNew) {
4662
- updated = gitignoreContent.split("\n").map((l) => {
4663
- const stripped = l.trimEnd();
4664
- return stripped === legacyLine ? newLine + (l.endsWith("\r") ? "\r" : "") : l;
4665
- }).join("\n");
4666
- } else if (hasLegacy && hasNew) {
4667
- updated = gitignoreContent.split("\n").filter((l) => l.trimEnd() !== legacyLine).join("\n");
4668
- } else if (!hasLegacy && !hasNew) {
4669
- updated = gitignoreContent.endsWith("\n") ? gitignoreContent + newLine + "\n" : gitignoreContent + "\n" + newLine + "\n";
4670
- } else {
4671
- updated = gitignoreContent;
4985
+ return readManifest(arg2);
4986
+ }
4987
+ function writeManifestForScope(scope, manifest, arg3) {
4988
+ if (scope === "user") {
4989
+ const file = userManifestPath(arg3);
4990
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
4991
+ fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
4992
+ const mid = userMidManifestPath(arg3);
4993
+ if (fs2.existsSync(mid)) {
4994
+ fs2.rmSync(mid);
4672
4995
  }
4673
- if (updated !== gitignoreContent) {
4674
- await writeFile10(gitignorePath, updated, "utf-8");
4675
- filesChanged.push(".gitignore");
4996
+ const legacy = userOldManifestPath(arg3);
4997
+ if (fs2.existsSync(legacy)) {
4998
+ fs2.rmSync(legacy);
4676
4999
  }
4677
- } catch {
4678
- }
4679
- try {
4680
- await unlink2(legacySharedPath(projectPath));
4681
- filesChanged.push(".codebyplan.json (deleted)");
4682
- } catch {
4683
- }
4684
- try {
4685
- await unlink2(legacyLocalPath(projectPath));
4686
- filesChanged.push(".codebyplan.local.json (deleted)");
4687
- } catch {
5000
+ return;
4688
5001
  }
4689
- return {
4690
- migrated: true,
4691
- was_dirty: true,
4692
- files_changed: filesChanged,
4693
- summary: `migrated legacy .codebyplan.json to .codebyplan/ layout (device_id=${deviceId.slice(0, 8)})`
4694
- };
5002
+ writeManifest(arg3, manifest);
4695
5003
  }
4696
- var init_migrate_local_config = __esm({
4697
- "src/lib/migrate-local-config.ts"() {
5004
+ var NEW_MANIFEST_FILENAME, MID_MANIFEST_FILENAME, OLD_MANIFEST_FILENAME;
5005
+ var init_manifest = __esm({
5006
+ "src/lib/manifest.ts"() {
4698
5007
  "use strict";
4699
- init_local_config();
5008
+ init_version();
5009
+ NEW_MANIFEST_FILENAME = ".cbp.manifest.json";
5010
+ MID_MANIFEST_FILENAME = ".cbp-claude.manifest.json";
5011
+ OLD_MANIFEST_FILENAME = ".codebyplan-claude.manifest.json";
4700
5012
  }
4701
5013
  });
4702
5014
 
4703
- // src/cli/config.ts
4704
- var config_exports = {};
4705
- __export(config_exports, {
4706
- readE2eConfig: () => readE2eConfig,
4707
- readGitConfig: () => readGitConfig,
4708
- readRepoConfig: () => readRepoConfig,
4709
- readServerConfig: () => readServerConfig,
4710
- readShipmentConfig: () => readShipmentConfig,
4711
- readVendorConfig: () => readVendorConfig,
4712
- runConfig: () => runConfig
4713
- });
4714
- import { mkdir as mkdir6, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
4715
- import { join as join15 } from "node:path";
4716
- async function runConfig() {
4717
- const flags = parseFlags(3);
4718
- const dryRun = hasFlag("dry-run", 3);
4719
- validateApiKey();
4720
- const config = await resolveConfig(flags);
4721
- const { repoId, projectPath } = config;
4722
- console.log(`
4723
- CodeByPlan Config`);
4724
- console.log(` Repo: ${repoId}`);
4725
- console.log(` Path: ${projectPath}`);
4726
- if (dryRun) console.log(` Mode: dry-run`);
4727
- console.log();
4728
- if (!dryRun && await needsLocalMigration(projectPath)) {
4729
- console.log(
4730
- " Migrating legacy .codebyplan.json to .codebyplan/ layout..."
5015
+ // src/lib/settings-merge.ts
5016
+ function extractHookId(command) {
5017
+ const match = /([^\s/]+\.sh)(?:\s|$)/.exec(command);
5018
+ const id = match?.[1];
5019
+ if (!id) {
5020
+ throw new Error(
5021
+ `Cannot derive _hook_id from command (no .sh script segment): ${command}`
4731
5022
  );
4732
- try {
4733
- const result = await runLocalMigration(projectPath);
4734
- if (result.was_dirty) {
4735
- console.log(" Migration complete.");
4736
- }
4737
- } catch (err) {
4738
- const msg = err instanceof Error ? err.message : String(err);
4739
- console.warn(
4740
- ` Warning: migration failed (${msg}); continuing with config sync.`
4741
- );
4742
- }
4743
5023
  }
4744
- await syncConfigToFile(repoId, projectPath, dryRun);
4745
- console.log("\n Config complete.\n");
5024
+ return id;
4746
5025
  }
4747
- async function syncConfigToFile(repoId, projectPath, dryRun) {
4748
- const codebyplanDir = join15(projectPath, ".codebyplan");
4749
- let resolvedWorktreeId;
4750
- try {
4751
- const deviceId = await getOrCreateDeviceId(projectPath);
4752
- let branch = "main";
4753
- try {
4754
- const { execSync: execSync6 } = await import("node:child_process");
4755
- branch = execSync6("git symbolic-ref --short HEAD", {
4756
- cwd: projectPath,
4757
- encoding: "utf-8"
4758
- }).trim();
4759
- } catch {
4760
- }
4761
- const tupleId = await resolveWorktreeId({
4762
- repoId,
4763
- repoPath: projectPath,
4764
- branch,
4765
- deviceId
4766
- });
4767
- if (tupleId) {
4768
- resolvedWorktreeId = tupleId;
4769
- } else {
4770
- resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
4771
- }
4772
- } catch (err) {
4773
- const msg = err instanceof Error ? err.message : String(err);
4774
- console.warn(
4775
- ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
4776
- );
5026
+ function rewriteCommand(command) {
5027
+ return command.replace(PLACEHOLDER_RE, REPLACEMENT);
5028
+ }
5029
+ function mergeHooksIntoSettings(settings, hooksJson) {
5030
+ if (!settings.hooks) {
5031
+ settings.hooks = {};
4777
5032
  }
4778
- let repoRes;
4779
- try {
4780
- repoRes = await apiGet(`/repos/${repoId}`);
4781
- } catch (err) {
4782
- let message;
4783
- if (err instanceof ApiError) {
4784
- if (err.status === 401) {
4785
- message = "Session expired. Run `codebyplan login` and try again.";
4786
- } else if (err.status === 403 || err.status === 404) {
4787
- message = "Repo not found or not accessible to your account. Confirm the repo_id in .codebyplan/repo.json and that you are logged in as the right user.";
4788
- } else if (err.status >= 500) {
4789
- message = `CodeByPlan server error (status ${err.status}). Please try again shortly.`;
4790
- } else {
4791
- message = `Unexpected API error (status ${err.status}). Please try again.`;
4792
- }
4793
- } else {
4794
- message = "Failed to reach the CodeByPlan API. Check your network connection and try again.";
5033
+ const target = settings.hooks;
5034
+ for (const [event, sourceMatchers] of Object.entries(hooksJson.hooks)) {
5035
+ if (!target[event]) {
5036
+ target[event] = [];
4795
5037
  }
4796
- process.stderr.write(message + "\n");
4797
- process.exit(1);
4798
- }
4799
- const repo = repoRes.data;
4800
- let portAllocations = [];
4801
- try {
4802
- const portsRes = await apiGet(
4803
- `/port-allocations`,
4804
- { repo_id: repoId }
4805
- );
4806
- const allAllocations = portsRes.data ?? [];
4807
- const filtered = resolvedWorktreeId ? allAllocations.filter((a) => a.worktree_id === resolvedWorktreeId) : allAllocations.filter((a) => !a.worktree_id);
4808
- const ALLOWED_FIELDS = [
4809
- "id",
4810
- "repo_id",
4811
- "port",
4812
- "label",
4813
- "server_type",
4814
- "auto_start",
4815
- "command",
4816
- "working_dir",
4817
- "env_vars",
4818
- "external_refs",
4819
- "worktree_id",
4820
- "created_at",
4821
- "updated_at"
4822
- ];
4823
- portAllocations = filtered.map((a) => {
4824
- const clean = {};
4825
- for (const key of ALLOWED_FIELDS) {
4826
- if (key in a) clean[key] = a[key];
5038
+ const eventBlocks = target[event];
5039
+ for (const sourceBlock of sourceMatchers) {
5040
+ let destBlock = eventBlocks.find(
5041
+ (b) => b.matcher === sourceBlock.matcher
5042
+ );
5043
+ if (!destBlock) {
5044
+ destBlock = { matcher: sourceBlock.matcher, hooks: [] };
5045
+ eventBlocks.push(destBlock);
4827
5046
  }
4828
- return clean;
4829
- });
4830
- } catch (err) {
4831
- console.warn(
4832
- ` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
4833
- );
4834
- }
4835
- const matchingAlloc = portAllocations[0];
4836
- const defaultBranchConfig = {
4837
- protected: ["main"],
4838
- integration: null,
4839
- production: "main",
4840
- staging: null
4841
- };
4842
- const branchConfig = repo.branch_config ?? defaultBranchConfig;
4843
- if (!legacyBranchConfigWarned && typeof branchConfig["integration"] === "string") {
4844
- legacyBranchConfigWarned = true;
4845
- process.stderr.write(
4846
- `warning: legacy 3-branch branch_config detected (integration: '${String(branchConfig["integration"])}'). Run 'npx codebyplan branch migrate' to consolidate to main-only.
4847
- `
4848
- );
5047
+ for (const sourceCmd of sourceBlock.hooks) {
5048
+ const taggedEntry = {
5049
+ _owner: OWNER,
5050
+ _hook_id: extractHookId(sourceCmd.command),
5051
+ type: sourceCmd.type,
5052
+ command: rewriteCommand(sourceCmd.command)
5053
+ };
5054
+ const existingIdx = destBlock.hooks.findIndex(
5055
+ (e) => e._owner === OWNER && e._hook_id === taggedEntry._hook_id
5056
+ );
5057
+ if (existingIdx >= 0) {
5058
+ destBlock.hooks[existingIdx] = taggedEntry;
5059
+ } else {
5060
+ destBlock.hooks.push(taggedEntry);
5061
+ }
5062
+ }
5063
+ }
4849
5064
  }
4850
- const repoPayload = { repo_id: repoId };
4851
- const repoAny = repo;
4852
- if (typeof repoAny.organization_id === "string") {
4853
- repoPayload.organization_id = repoAny.organization_id;
5065
+ return settings;
5066
+ }
5067
+ function mergeBaseSettingsIntoSettings(settings, base) {
5068
+ for (const key of SCALAR_BASE_KEYS) {
5069
+ if (base[key] !== void 0 && settings[key] === void 0) {
5070
+ settings[key] = base[key];
5071
+ }
4854
5072
  }
4855
- if (typeof repoAny.project_id === "string") {
4856
- repoPayload.project_id = repoAny.project_id;
5073
+ if (base.statusLine !== void 0 && settings.statusLine === void 0) {
5074
+ settings.statusLine = { ...base.statusLine };
4857
5075
  }
4858
- const serverPayload = {
4859
- server_port: resolvedWorktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
4860
- server_type: resolvedWorktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
4861
- auto_push_enabled: repo.auto_push_enabled,
4862
- port_allocations: portAllocations
4863
- };
4864
- const gitPayload = {
4865
- git_branch: repo.git_branch ?? "main",
4866
- branch_config: branchConfig
4867
- };
4868
- const shipmentPayload = {};
4869
- if (typeof repoAny.shipment !== "undefined") {
4870
- shipmentPayload.shipment = repoAny.shipment;
5076
+ if (base.subagentStatusLine !== void 0 && settings.subagentStatusLine === void 0) {
5077
+ settings.subagentStatusLine = { ...base.subagentStatusLine };
4871
5078
  }
4872
- const vendorPayload = {};
4873
- const e2ePayload = {};
4874
- const eslintPayload = {};
4875
- if (dryRun) {
4876
- console.log(" Config would be updated (dry-run).");
4877
- return;
5079
+ if (base.attribution !== void 0 && settings.attribution === void 0) {
5080
+ settings.attribution = { ...base.attribution };
4878
5081
  }
4879
- await mkdir6(codebyplanDir, { recursive: true });
4880
- const files = [
4881
- { name: "repo.json", payload: repoPayload },
4882
- { name: "server.json", payload: serverPayload },
4883
- { name: "git.json", payload: gitPayload },
4884
- { name: "shipment.json", payload: shipmentPayload },
4885
- { name: "vendor.json", payload: vendorPayload },
4886
- { name: "e2e.json", payload: e2ePayload, createOnly: true },
4887
- { name: "eslint.json", payload: eslintPayload, createOnly: true }
4888
- ];
4889
- let anyUpdated = false;
4890
- for (const { name, payload, createOnly } of files) {
4891
- const filePath = join15(codebyplanDir, name);
4892
- const newJson = JSON.stringify(payload, null, 2) + "\n";
4893
- let currentJson = "";
4894
- try {
4895
- currentJson = await readFile14(filePath, "utf-8");
4896
- } catch {
5082
+ if (base.worktree !== void 0) {
5083
+ if (settings.worktree === void 0) {
5084
+ settings.worktree = { ...base.worktree };
5085
+ } else {
5086
+ const sw = settings.worktree;
5087
+ for (const [k, v] of Object.entries(base.worktree)) {
5088
+ if (sw[k] === void 0) {
5089
+ sw[k] = v;
5090
+ }
5091
+ }
4897
5092
  }
4898
- if (createOnly && currentJson !== "") continue;
4899
- if (currentJson === newJson) continue;
4900
- await writeFile11(filePath, newJson, "utf-8");
4901
- console.log(` Updated .codebyplan/${name}`);
4902
- anyUpdated = true;
4903
5093
  }
4904
- if (!anyUpdated) {
4905
- console.log(" Config up to date.");
5094
+ if (base.permissions !== void 0) {
5095
+ const existing = settings.permissions ?? {};
5096
+ if (base.permissions.defaultMode !== void 0 && existing.defaultMode === void 0) {
5097
+ existing.defaultMode = base.permissions.defaultMode;
5098
+ }
5099
+ if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && existing.skipDangerousModePermissionPrompt === void 0) {
5100
+ existing.skipDangerousModePermissionPrompt = base.permissions.skipDangerousModePermissionPrompt;
5101
+ }
5102
+ for (const key of ["deny", "ask", "allow"]) {
5103
+ const incoming = base.permissions[key];
5104
+ if (!incoming || incoming.length === 0) continue;
5105
+ const current = existing[key] ?? [];
5106
+ const seen = new Set(current);
5107
+ const merged = [...current];
5108
+ for (const item of incoming) {
5109
+ if (!seen.has(item)) {
5110
+ merged.push(item);
5111
+ seen.add(item);
5112
+ }
5113
+ }
5114
+ existing[key] = merged;
5115
+ }
5116
+ if (settings.permissions !== void 0 || Object.keys(existing).length > 0) {
5117
+ settings.permissions = existing;
5118
+ }
4906
5119
  }
5120
+ return settings;
4907
5121
  }
4908
- async function readRepoConfig(projectPath) {
4909
- try {
4910
- const raw = await readFile14(
4911
- join15(projectPath, ".codebyplan", "repo.json"),
4912
- "utf-8"
4913
- );
4914
- return JSON.parse(raw);
4915
- } catch {
4916
- return null;
5122
+ function stripBaseSettingsFromSettings(settings, base) {
5123
+ for (const key of SCALAR_BASE_KEYS) {
5124
+ if (base[key] !== void 0 && settings[key] === base[key]) {
5125
+ delete settings[key];
5126
+ }
4917
5127
  }
4918
- }
4919
- async function readServerConfig(projectPath) {
4920
- try {
4921
- const raw = await readFile14(
4922
- join15(projectPath, ".codebyplan", "server.json"),
4923
- "utf-8"
4924
- );
4925
- return JSON.parse(raw);
4926
- } catch {
4927
- return null;
5128
+ if (base.statusLine !== void 0 && settings.statusLine !== void 0 && JSON.stringify(settings.statusLine) === JSON.stringify(base.statusLine)) {
5129
+ delete settings.statusLine;
4928
5130
  }
4929
- }
4930
- async function readGitConfig(projectPath) {
4931
- try {
4932
- const raw = await readFile14(
4933
- join15(projectPath, ".codebyplan", "git.json"),
4934
- "utf-8"
4935
- );
4936
- return JSON.parse(raw);
4937
- } catch {
4938
- return null;
5131
+ if (base.subagentStatusLine !== void 0 && settings.subagentStatusLine !== void 0 && JSON.stringify(settings.subagentStatusLine) === JSON.stringify(base.subagentStatusLine)) {
5132
+ delete settings.subagentStatusLine;
4939
5133
  }
4940
- }
4941
- async function readShipmentConfig(projectPath) {
4942
- try {
4943
- const raw = await readFile14(
4944
- join15(projectPath, ".codebyplan", "shipment.json"),
4945
- "utf-8"
4946
- );
4947
- return JSON.parse(raw);
4948
- } catch {
4949
- return null;
4950
- }
4951
- }
4952
- async function readVendorConfig(projectPath) {
4953
- try {
4954
- const raw = await readFile14(
4955
- join15(projectPath, ".codebyplan", "vendor.json"),
4956
- "utf-8"
4957
- );
4958
- return JSON.parse(raw);
4959
- } catch {
4960
- return null;
4961
- }
4962
- }
4963
- async function readE2eConfig(projectPath) {
4964
- try {
4965
- const raw = await readFile14(
4966
- join15(projectPath, ".codebyplan", "e2e.json"),
4967
- "utf-8"
4968
- );
4969
- return JSON.parse(raw);
4970
- } catch {
4971
- return null;
4972
- }
4973
- }
4974
- var legacyBranchConfigWarned;
4975
- var init_config = __esm({
4976
- "src/cli/config.ts"() {
4977
- "use strict";
4978
- init_flags();
4979
- init_api();
4980
- init_resolve_worktree();
4981
- init_local_config();
4982
- init_migrate_local_config();
4983
- legacyBranchConfigWarned = false;
5134
+ if (base.attribution !== void 0 && settings.attribution !== void 0 && JSON.stringify(settings.attribution) === JSON.stringify(base.attribution)) {
5135
+ delete settings.attribution;
4984
5136
  }
4985
- });
4986
-
4987
- // src/lib/server-detect.ts
4988
- function detectFramework(pkg) {
4989
- const deps = pkg.dependencies ?? {};
4990
- const devDeps = pkg.devDependencies ?? {};
4991
- const hasDep = (name) => name in deps || name in devDeps;
4992
- if (hasDep("next")) return "nextjs";
4993
- if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
4994
- if (hasDep("expo")) return "expo";
4995
- if (hasDep("vite")) return "vite";
4996
- if (hasDep("express")) return "express";
4997
- if (hasDep("@nestjs/core")) return "nestjs";
4998
- return "custom";
4999
- }
5000
- function detectPortFromScripts(pkg) {
5001
- const scripts = pkg.scripts;
5002
- if (!scripts?.dev) return null;
5003
- const parts = scripts.dev.split(/\s+/);
5004
- for (let i = 0; i < parts.length - 1; i++) {
5005
- if (parts[i] === "--port" || parts[i] === "-p") {
5006
- const next = parts[i + 1];
5007
- if (next) {
5008
- const port = parseInt(next, 10);
5009
- if (!isNaN(port)) return port;
5137
+ if (base.worktree !== void 0 && settings.worktree !== void 0) {
5138
+ const sw = settings.worktree;
5139
+ for (const [k, v] of Object.entries(base.worktree)) {
5140
+ if (sw[k] === v) {
5141
+ delete sw[k];
5010
5142
  }
5011
5143
  }
5144
+ if (Object.keys(sw).length === 0) {
5145
+ delete settings.worktree;
5146
+ }
5012
5147
  }
5013
- return null;
5014
- }
5015
- var init_server_detect = __esm({
5016
- "src/lib/server-detect.ts"() {
5017
- "use strict";
5018
- }
5019
- });
5020
-
5021
- // src/lib/port-verify.ts
5022
- import { readFile as readFile15 } from "node:fs/promises";
5023
- async function verifyPorts(projectPath, portAllocations) {
5024
- const mismatches = [];
5025
- const allocatedPorts = new Set(portAllocations.map((a) => a.port));
5026
- const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
5027
- for (const pkgPath of packageJsonPaths) {
5028
- try {
5029
- const raw = await readFile15(pkgPath, "utf-8");
5030
- const pkg = JSON.parse(raw);
5031
- const scriptPort = detectPortFromScripts(pkg);
5032
- if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
5033
- const relativePath = pkgPath.replace(projectPath + "/", "");
5034
- const matchingAlloc = portAllocations.find(
5035
- (a) => a.label === getAppLabel(relativePath)
5036
- );
5037
- mismatches.push({
5038
- packageJsonPath: relativePath,
5039
- scriptPort,
5040
- allocation: matchingAlloc ?? null,
5041
- reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
5042
- });
5148
+ if (base.permissions !== void 0 && settings.permissions !== void 0) {
5149
+ const perms = settings.permissions;
5150
+ if (base.permissions.defaultMode !== void 0 && perms.defaultMode === base.permissions.defaultMode) {
5151
+ delete perms.defaultMode;
5152
+ }
5153
+ if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && perms.skipDangerousModePermissionPrompt === base.permissions.skipDangerousModePermissionPrompt) {
5154
+ delete perms.skipDangerousModePermissionPrompt;
5155
+ }
5156
+ for (const key of ["deny", "ask", "allow"]) {
5157
+ const baseList = base.permissions[key];
5158
+ if (!baseList || baseList.length === 0) continue;
5159
+ const current = perms[key];
5160
+ if (!current) continue;
5161
+ const baseSet = new Set(baseList);
5162
+ const filtered = current.filter((x) => !baseSet.has(x));
5163
+ if (filtered.length === 0) {
5164
+ delete perms[key];
5165
+ } else {
5166
+ perms[key] = filtered;
5043
5167
  }
5044
- } catch {
5168
+ }
5169
+ if (Object.keys(perms).length === 0) {
5170
+ delete settings.permissions;
5045
5171
  }
5046
5172
  }
5047
- return mismatches;
5048
- }
5049
- function isDevServerScript(pkg) {
5050
- const scripts = pkg.scripts;
5051
- const raw = scripts?.dev;
5052
- if (!raw || typeof raw !== "string") return false;
5053
- const script = raw.trim().toLowerCase();
5054
- if (!script) return false;
5055
- for (const pattern of DEV_SERVER_BIN_PATTERNS) {
5056
- if (pattern.test(script)) return true;
5057
- }
5058
- const tokens = script.split(/\s+/);
5059
- for (const token of tokens) {
5060
- if (token === "--port" || token === "-p") return true;
5061
- if (token.startsWith("--port=")) return true;
5062
- }
5063
- return false;
5064
- }
5065
- function labelMatchesAppName(label, appName) {
5066
- if (!label || !appName) return false;
5067
- const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
5068
- const labelTokens = normalize(label).split(" ").filter(Boolean);
5069
- const appToken = normalize(appName);
5070
- if (!appToken) return false;
5071
- const appTokens = appToken.split(" ").filter(Boolean);
5072
- if (appTokens.length === 1) {
5073
- return labelTokens.includes(appTokens[0]);
5074
- }
5075
- for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
5076
- if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
5077
- }
5078
- return false;
5173
+ return settings;
5079
5174
  }
5080
- async function findUnallocatedApps(projectPath, portAllocations) {
5081
- const apps = await discoverMonorepoApps(projectPath);
5082
- if (apps.length === 0) {
5083
- return [];
5175
+ function stripOwnedHooksFromSettings(settings) {
5176
+ if (!settings.hooks) {
5177
+ return settings;
5084
5178
  }
5085
- const unallocated = [];
5086
- for (const app of apps) {
5087
- if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
5179
+ for (const event of Object.keys(settings.hooks)) {
5180
+ const eventBlocks = settings.hooks[event];
5181
+ if (!eventBlocks) {
5088
5182
  continue;
5089
5183
  }
5090
- let pkg;
5091
- try {
5092
- const raw = await readFile15(`${app.absPath}/package.json`, "utf-8");
5093
- pkg = JSON.parse(raw);
5094
- } catch {
5095
- continue;
5184
+ const survivingBlocks = [];
5185
+ for (const block of eventBlocks) {
5186
+ block.hooks = block.hooks.filter((e) => e._owner !== OWNER);
5187
+ if (block.hooks.length > 0) {
5188
+ survivingBlocks.push(block);
5189
+ }
5190
+ }
5191
+ if (survivingBlocks.length > 0) {
5192
+ settings.hooks[event] = survivingBlocks;
5193
+ } else {
5194
+ delete settings.hooks[event];
5096
5195
  }
5097
- if (!isDevServerScript(pkg)) continue;
5098
- const framework = detectFramework(pkg);
5099
- const detectedPort = detectPortFromScripts(pkg);
5100
- const command = `pnpm --filter ${app.name} dev`;
5101
- unallocated.push({
5102
- name: app.name,
5103
- path: app.path,
5104
- framework,
5105
- detectedPort,
5106
- command
5107
- });
5108
5196
  }
5109
- return unallocated;
5110
- }
5111
- function getAppLabel(relativePath) {
5112
- const parts = relativePath.split("/");
5113
- if (parts.length >= 3 && parts[0] === "apps") {
5114
- return parts[1];
5197
+ if (Object.keys(settings.hooks).length === 0) {
5198
+ delete settings.hooks;
5115
5199
  }
5116
- return "root";
5200
+ return settings;
5117
5201
  }
5118
- var DEV_SERVER_BIN_PATTERNS;
5119
- var init_port_verify = __esm({
5120
- "src/lib/port-verify.ts"() {
5202
+ var OWNER, PLACEHOLDER_RE, REPLACEMENT, SCALAR_BASE_KEYS;
5203
+ var init_settings_merge = __esm({
5204
+ "src/lib/settings-merge.ts"() {
5121
5205
  "use strict";
5122
- init_tech_detect();
5123
- init_server_detect();
5124
- DEV_SERVER_BIN_PATTERNS = [
5125
- /\bnext\s+dev\b/,
5126
- /\bnest\s+start\b/,
5127
- /\bvite\s+(?:dev|serve)\b/,
5128
- /\bvite\s+preview\b/,
5129
- /\bnuxt\s+dev\b/,
5130
- /\b(?:svelte-kit|sveltekit)\s+dev\b/,
5131
- /\bexpo\s+start\b/
5206
+ OWNER = "codebyplan-claude";
5207
+ PLACEHOLDER_RE = /\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\//g;
5208
+ REPLACEMENT = "./.claude/hooks/";
5209
+ SCALAR_BASE_KEYS = [
5210
+ "alwaysThinkingEnabled",
5211
+ "autoUpdatesChannel",
5212
+ "awaySummaryEnabled",
5213
+ "disableAgentView",
5214
+ "disableRemoteControl",
5215
+ "editorMode",
5216
+ "outputStyle",
5217
+ "preferredNotifChannel",
5218
+ "prefersReducedMotion",
5219
+ "respectGitignore",
5220
+ "showTurnDuration",
5221
+ "spinnerTipsEnabled",
5222
+ "terminalProgressBarEnabled",
5223
+ "viewMode",
5224
+ "autoScrollEnabled",
5225
+ "cleanupPeriodDays",
5226
+ "includeGitInstructions",
5227
+ "showThinkingSummaries",
5228
+ "disableSkillShellExecution",
5229
+ "skipWebFetchPreflight",
5230
+ "fastModePerSessionOptIn",
5231
+ "effortLevel",
5232
+ "showClearContextOnPlanAccept",
5233
+ "syntaxHighlightingDisabled"
5132
5234
  ];
5133
5235
  }
5134
5236
  });
5135
5237
 
5136
- // src/cli/ports.ts
5137
- var ports_exports = {};
5138
- __export(ports_exports, {
5139
- runPorts: () => runPorts
5140
- });
5141
- async function runPorts() {
5142
- const flags = parseFlags(3);
5143
- const dryRun = hasFlag("dry-run", 3);
5144
- const fix = hasFlag("fix", 3);
5145
- validateApiKey();
5146
- const config = await resolveConfig(flags);
5147
- const { repoId, projectPath } = config;
5148
- console.log(`
5149
- CodeByPlan Ports`);
5150
- console.log(` Repo: ${repoId}`);
5151
- console.log(` Path: ${projectPath}`);
5152
- if (dryRun) console.log(` Mode: dry-run`);
5153
- if (fix) console.log(` Mode: fix`);
5154
- console.log();
5155
- try {
5156
- const portsRes = await apiGet(
5157
- `/port-allocations`,
5158
- { repo_id: repoId }
5159
- );
5160
- const allocations = portsRes.data ?? [];
5161
- if (allocations.length === 0) {
5162
- console.log(" No port allocations found \u2014 skipping verification.");
5163
- console.log("\n Ports complete.\n");
5164
- return;
5238
+ // src/cli/claude/install.ts
5239
+ var install_exports = {};
5240
+ __export(install_exports, {
5241
+ resolveTemplatesDir: () => resolveTemplatesDir,
5242
+ runInstall: () => runInstall
5243
+ });
5244
+ import * as fs3 from "node:fs";
5245
+ import * as os2 from "node:os";
5246
+ import * as path4 from "node:path";
5247
+ import { fileURLToPath } from "node:url";
5248
+ function resolveTemplatesDir() {
5249
+ const here = path4.dirname(fileURLToPath(import.meta.url));
5250
+ const candidates = [
5251
+ path4.resolve(here, "..", "templates"),
5252
+ path4.resolve(here, "..", "..", "templates"),
5253
+ path4.resolve(here, "..", "..", "..", "templates")
5254
+ ];
5255
+ for (const c of candidates) {
5256
+ if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
5257
+ return c;
5165
5258
  }
5166
- const mismatches = await verifyPorts(projectPath, allocations);
5167
- if (mismatches.length > 0) {
5168
- console.log(` Port mismatches: ${mismatches.length}`);
5169
- for (const m of mismatches) {
5170
- console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
5171
- }
5259
+ }
5260
+ throw new Error(
5261
+ `codebyplan: could not locate templates/ directory. Probed:
5262
+ ${candidates.join(
5263
+ "\n "
5264
+ )}`
5265
+ );
5266
+ }
5267
+ async function runInstall(opts, deps = {}) {
5268
+ await Promise.resolve();
5269
+ const scope = opts.scope ?? "project";
5270
+ if (scope === "user") {
5271
+ if (opts.renderer) {
5272
+ console.warn(
5273
+ "codebyplan claude install: --bash/--node/--python is ignored for --scope user (no project root for statusline.local.json)."
5274
+ );
5172
5275
  }
5173
- const unallocated = await findUnallocatedApps(projectPath, allocations);
5174
- if (unallocated.length > 0) {
5175
- console.log(` Unallocated apps: ${unallocated.length}`);
5176
- for (const app of unallocated) {
5177
- console.log(
5178
- ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
5179
- );
5180
- }
5181
- if (fix && !dryRun) {
5182
- const maxPort = Math.max(...allocations.map((a) => a.port), 2999);
5183
- let nextPort = maxPort + 1;
5184
- for (const app of unallocated) {
5185
- const port = app.detectedPort ?? nextPort++;
5186
- try {
5187
- await apiPost("/port-allocations", {
5188
- repo_id: repoId,
5189
- port,
5190
- label: app.name,
5191
- server_type: app.framework,
5192
- auto_start: "manual",
5193
- command: app.command,
5194
- working_dir: app.path
5195
- });
5196
- console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
5197
- } catch (err) {
5198
- const msg = err instanceof Error ? err.message : String(err);
5199
- console.log(
5200
- ` Failed to create allocation for ${app.name}: ${msg}`
5201
- );
5202
- }
5203
- if (app.detectedPort && app.detectedPort >= nextPort) {
5204
- nextPort = app.detectedPort + 1;
5205
- }
5276
+ runInstallUser(opts, deps);
5277
+ return;
5278
+ }
5279
+ const projectDir = deps.projectDir ?? process.cwd();
5280
+ let templatesDir;
5281
+ try {
5282
+ templatesDir = deps.templatesDir ?? resolveTemplatesDir();
5283
+ } catch (err) {
5284
+ console.error(
5285
+ err instanceof Error ? err.message : `codebyplan claude install: ${String(err)}`
5286
+ );
5287
+ process.exitCode = 1;
5288
+ return;
5289
+ }
5290
+ try {
5291
+ const files = walkTemplates(templatesDir);
5292
+ const manifestEntries = [];
5293
+ for (const f of files) {
5294
+ const absDest = path4.join(projectDir, ".claude", f.dest);
5295
+ const absSrc = path4.join(templatesDir, f.src);
5296
+ if (opts.dryRun) {
5297
+ if (opts.verbose) {
5298
+ console.log(`[dry-run] would copy ${f.src} \u2192 .claude/${f.dest}`);
5206
5299
  }
5207
- } else if (fix && dryRun) {
5208
- console.log(" (dry-run \u2014 would create allocations with --fix)");
5209
5300
  } else {
5210
- console.log(" Run with --fix to auto-create allocations.");
5301
+ fs3.mkdirSync(path4.dirname(absDest), { recursive: true });
5302
+ fs3.copyFileSync(absSrc, absDest);
5303
+ if (opts.verbose) {
5304
+ console.log(`copied ${f.src} \u2192 .claude/${f.dest}`);
5305
+ }
5211
5306
  }
5307
+ manifestEntries.push({ src: f.src, dest: f.dest, hash: f.hash });
5212
5308
  }
5213
- if (mismatches.length === 0 && unallocated.length === 0) {
5214
- console.log(" Ports verified.");
5215
- }
5216
- } catch (err) {
5217
- console.warn(
5218
- ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
5309
+ const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
5310
+ const baseSettingsPath = path4.join(
5311
+ templatesDir,
5312
+ "settings.project.base.json"
5219
5313
  );
5220
- }
5221
- console.log("\n Ports complete.\n");
5222
- }
5223
- var init_ports = __esm({
5224
- "src/cli/ports.ts"() {
5225
- "use strict";
5226
- init_flags();
5227
- init_api();
5228
- init_port_verify();
5229
- }
5230
- });
5231
-
5232
- // src/cli/tech-stack.ts
5233
- var tech_stack_exports = {};
5234
- __export(tech_stack_exports, {
5235
- runFullTechStack: () => runFullTechStack,
5236
- runTechStack: () => runTechStack
5237
- });
5238
- import { existsSync } from "node:fs";
5239
- async function runTechStack() {
5240
- const flags = parseFlags(3);
5241
- const dryRun = hasFlag("dry-run", 3);
5242
- if (hasFlag("full-tech-stack", 3)) {
5243
- await runFullTechStack(dryRun);
5244
- return;
5245
- }
5246
- validateApiKey();
5247
- const config = await resolveConfig(flags);
5248
- const { repoId, projectPath } = config;
5249
- console.log(`
5250
- CodeByPlan Tech Stack`);
5251
- console.log(` Repo: ${repoId}`);
5252
- console.log(` Path: ${projectPath}`);
5253
- if (dryRun) console.log(` Mode: dry-run`);
5254
- console.log();
5255
- if (dryRun) {
5256
- try {
5257
- if (await needsLocalMigration(projectPath)) {
5258
- console.log(
5259
- ` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
5314
+ const hasHooks = fs3.existsSync(hooksJsonPath);
5315
+ const hasBase = fs3.existsSync(baseSettingsPath);
5316
+ if (hasHooks || hasBase) {
5317
+ const settingsPath = path4.join(projectDir, ".claude", "settings.json");
5318
+ const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5319
+ if (hasBase) {
5320
+ const base = JSON.parse(
5321
+ fs3.readFileSync(baseSettingsPath, "utf8")
5260
5322
  );
5323
+ mergeBaseSettingsIntoSettings(existingSettings, base);
5261
5324
  }
5262
- } catch {
5263
- }
5264
- } else {
5265
- try {
5266
- if (await needsLocalMigration(projectPath)) {
5267
- const result = await runLocalMigration(projectPath);
5268
- console.log(
5269
- ` Migrated .codebyplan.json to .codebyplan/ layout: ${result.summary}`
5325
+ if (hasHooks) {
5326
+ const hooksJson = JSON.parse(
5327
+ fs3.readFileSync(hooksJsonPath, "utf8")
5328
+ );
5329
+ mergeHooksIntoSettings(existingSettings, hooksJson);
5330
+ }
5331
+ if (!opts.dryRun) {
5332
+ fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5333
+ fs3.writeFileSync(
5334
+ settingsPath,
5335
+ JSON.stringify(existingSettings, null, 2) + "\n",
5336
+ "utf8"
5270
5337
  );
5338
+ } else if (opts.verbose) {
5271
5339
  console.log(
5272
- ` Suggest /cbp-git-commit to stage the cleaned shared file.`
5340
+ `[dry-run] would merge settings into ${path4.relative(projectDir, settingsPath)}`
5273
5341
  );
5274
5342
  }
5275
- } catch (err) {
5276
- console.warn(
5277
- ` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
5343
+ }
5344
+ const gitignoreAction = await ensureManagedGitignoreBlock(
5345
+ projectDir,
5346
+ opts.dryRun
5347
+ );
5348
+ if (opts.verbose && gitignoreAction !== "unchanged") {
5349
+ console.log(
5350
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path4.relative(projectDir, path4.join(projectDir, ".gitignore"))}`
5278
5351
  );
5279
5352
  }
5280
- }
5281
- try {
5282
- const { dependencies } = await scanAllDependencies(projectPath);
5283
- if (dependencies.length === 0) {
5284
- console.log(" No dependencies found.");
5285
- console.log("\n Tech stack complete.\n");
5286
- return;
5353
+ if (!opts.dryRun) {
5354
+ const manifest = defaultManifest();
5355
+ manifest.files = manifestEntries;
5356
+ writeManifest(projectDir, manifest);
5287
5357
  }
5288
- const sourcePaths = new Set(dependencies.map((d) => d.source_path));
5289
5358
  console.log(
5290
- ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
5359
+ `codebyplan claude install${opts.dryRun ? " (dry-run)" : ""}: ${manifestEntries.length} files, ${countHookEntries(templatesDir)} hook entries.`
5291
5360
  );
5292
- if (!dryRun) {
5293
- const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
5294
- if (result.data.stale_removed > 0) {
5295
- console.log(
5296
- ` ${result.data.stale_removed} stale dependencies removed`
5297
- );
5298
- }
5299
- try {
5300
- const { execSync: execSync6 } = await import("node:child_process");
5301
- let branch = "main";
5302
- try {
5303
- branch = execSync6("git symbolic-ref --short HEAD", {
5304
- cwd: projectPath,
5305
- encoding: "utf-8"
5306
- }).trim();
5307
- } catch {
5308
- }
5309
- const deviceId = await getOrCreateDeviceId(projectPath);
5310
- const tupleId = await resolveWorktreeId({
5311
- repoId,
5312
- repoPath: projectPath,
5313
- branch,
5314
- deviceId
5315
- });
5316
- await callMcpTool("enqueue_todo_job", {
5317
- repo_id: repoId,
5318
- ...tupleId ? { worktree_id: tupleId } : {},
5319
- reason: "CLI_SYNC"
5320
- });
5321
- } catch {
5322
- }
5323
- }
5324
- const detected = await detectTechStack(projectPath);
5325
- if (detected.flat.length > 0) {
5326
- const repoRes = await apiGet(`/repos/${repoId}`);
5327
- const remote = parseTechStackResult(repoRes.data.tech_stack);
5328
- const { merged, added } = mergeTechStack(remote, detected);
5329
- if (added.length > 0) {
5330
- console.log(` ${added.length} new tech entries`);
5331
- if (!dryRun) {
5332
- await apiPut(`/repos/${repoId}`, { tech_stack: merged });
5333
- }
5334
- }
5361
+ if (opts.renderer && !opts.dryRun) {
5362
+ await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
5335
5363
  }
5336
5364
  } catch (err) {
5337
- console.warn(
5338
- ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
5365
+ console.error(
5366
+ `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
5339
5367
  );
5368
+ process.exitCode = 1;
5340
5369
  }
5341
- console.log("\n Tech stack complete.\n");
5342
5370
  }
5343
- async function syncTechStackForPath(repoId, projectPath, dryRun) {
5371
+ function runInstallUser(opts, deps) {
5372
+ let templatesDir;
5344
5373
  try {
5345
- const { dependencies } = await scanAllDependencies(projectPath);
5346
- if (dependencies.length === 0) {
5347
- console.log(" No dependencies found.");
5374
+ templatesDir = deps.templatesDir ?? resolveTemplatesDir();
5375
+ } catch (err) {
5376
+ console.error(
5377
+ err instanceof Error ? err.message : `codebyplan claude install: ${String(err)}`
5378
+ );
5379
+ process.exitCode = 1;
5380
+ return;
5381
+ }
5382
+ try {
5383
+ const userDir = deps.userDir ?? path4.join(os2.homedir(), ".claude");
5384
+ const settingsPath = path4.join(userDir, "settings.json");
5385
+ const userBaseSettingsPath = path4.join(
5386
+ templatesDir,
5387
+ "settings.user.base.json"
5388
+ );
5389
+ if (!fs3.existsSync(userBaseSettingsPath)) {
5390
+ console.error(
5391
+ "codebyplan claude install: settings.user.base.json not found in templates."
5392
+ );
5393
+ process.exitCode = 1;
5348
5394
  return;
5349
5395
  }
5350
- const sourcePaths = new Set(dependencies.map((d) => d.source_path));
5351
- console.log(
5352
- ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
5396
+ const userBase = JSON.parse(
5397
+ fs3.readFileSync(userBaseSettingsPath, "utf8")
5353
5398
  );
5354
- if (!dryRun) {
5355
- const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
5356
- if (result.data.stale_removed > 0) {
5357
- console.log(
5358
- ` ${result.data.stale_removed} stale dependencies removed`
5359
- );
5360
- }
5361
- }
5362
- const detected = await detectTechStack(projectPath);
5363
- if (detected.flat.length > 0) {
5364
- const repoRes = await apiGet(`/repos/${repoId}`);
5365
- const remote = parseTechStackResult(repoRes.data.tech_stack);
5366
- const { merged, added } = mergeTechStack(remote, detected);
5367
- if (added.length > 0) {
5368
- console.log(` ${added.length} new tech entries`);
5369
- if (!dryRun) {
5370
- await apiPut(`/repos/${repoId}`, { tech_stack: merged });
5371
- }
5372
- }
5399
+ const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5400
+ mergeBaseSettingsIntoSettings(existingSettings, userBase);
5401
+ if (!opts.dryRun) {
5402
+ fs3.mkdirSync(userDir, { recursive: true });
5403
+ fs3.writeFileSync(
5404
+ settingsPath,
5405
+ JSON.stringify(existingSettings, null, 2) + "\n",
5406
+ "utf8"
5407
+ );
5408
+ const manifest = defaultManifest();
5409
+ manifest.files = [];
5410
+ writeManifestForScope("user", manifest, userDir);
5411
+ } else if (opts.verbose) {
5412
+ console.log(
5413
+ `[dry-run] would merge user base settings into ${settingsPath}`
5414
+ );
5373
5415
  }
5416
+ console.log(
5417
+ `codebyplan claude install --scope user${opts.dryRun ? " (dry-run)" : ""}: settings.json updated, 0 template files copied.`
5418
+ );
5374
5419
  } catch (err) {
5375
- console.warn(
5376
- ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
5420
+ console.error(
5421
+ `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
5377
5422
  );
5423
+ process.exitCode = 1;
5378
5424
  }
5379
5425
  }
5380
- async function runFullTechStack(dryRun) {
5381
- validateApiKey();
5382
- const localConfig = await readLocalConfig(process.cwd());
5383
- if (!localConfig?.device_id) {
5426
+ function countHookEntries(templatesDir) {
5427
+ const p = path4.join(templatesDir, "hooks", "hooks.json");
5428
+ if (!fs3.existsSync(p)) return 0;
5429
+ try {
5430
+ const j = JSON.parse(fs3.readFileSync(p, "utf8"));
5431
+ let n = 0;
5432
+ for (const blocks of Object.values(j.hooks)) {
5433
+ for (const b of blocks) n += b.hooks.length;
5434
+ }
5435
+ return n;
5436
+ } catch (err) {
5384
5437
  console.error(
5385
- " --full-tech-stack requires a device_id in .codebyplan/device.local.json.\n Run `npx codebyplan setup` in this directory first to register the device."
5438
+ `codebyplan: could not count hook entries in hooks.json: ${err instanceof Error ? err.message : String(err)}`
5386
5439
  );
5387
- process.exitCode = 1;
5388
- return;
5440
+ return 0;
5389
5441
  }
5390
- console.log("\n CodeByPlan Tech Stack \u2014 Full (all local worktrees)");
5391
- if (dryRun) console.log(" Mode: dry-run");
5392
- console.log();
5393
- const reposRes = await apiGet("/repos");
5394
- const repos = reposRes.data ?? [];
5395
- let synced = 0;
5396
- let skipped = 0;
5397
- for (const repo of repos) {
5398
- let worktrees = [];
5399
- try {
5400
- const wtRes = await apiGet(
5401
- `/worktrees?repo_id=${repo.id}`
5402
- );
5403
- worktrees = wtRes.data ?? [];
5404
- } catch (err) {
5405
- console.warn(
5406
- ` Warning: failed to fetch worktrees for ${repo.name}: ${err instanceof Error ? err.message : String(err)}`
5407
- );
5408
- continue;
5409
- }
5410
- const localWorktrees = worktrees.filter(
5411
- (wt) => wt.path ? existsSync(wt.path) : false
5442
+ }
5443
+ var init_install = __esm({
5444
+ "src/cli/claude/install.ts"() {
5445
+ "use strict";
5446
+ init_template_walker();
5447
+ init_gitignore_block();
5448
+ init_manifest();
5449
+ init_settings_merge();
5450
+ init_statusline_config();
5451
+ }
5452
+ });
5453
+
5454
+ // src/lib/scaffold-publish-workflow.ts
5455
+ import * as fs4 from "node:fs";
5456
+ import * as path5 from "node:path";
5457
+ async function runScaffoldPublishWorkflow(opts) {
5458
+ await Promise.resolve();
5459
+ const dryRun = opts?.dryRun ?? false;
5460
+ const force = opts?.force ?? false;
5461
+ const projectDir = path5.resolve(opts?.projectDir ?? process.cwd());
5462
+ const templatesDir = opts?.templatesDir ?? resolveTemplatesDir();
5463
+ const templatePath = path5.join(
5464
+ templatesDir,
5465
+ "github-workflows",
5466
+ "publish.yml"
5467
+ );
5468
+ if (!fs4.existsSync(templatePath)) {
5469
+ throw new Error(
5470
+ `scaffold-publish-workflow: template not found at ${templatePath}`
5412
5471
  );
5413
- if (localWorktrees.length === 0) {
5414
- console.log(` skipping ${repo.name} \u2014 no local worktree on this device`);
5415
- skipped++;
5416
- continue;
5472
+ }
5473
+ const templateContent = fs4.readFileSync(templatePath, "utf-8");
5474
+ const targetPath = path5.join(
5475
+ projectDir,
5476
+ ".github",
5477
+ "workflows",
5478
+ "publish.yml"
5479
+ );
5480
+ if (dryRun) {
5481
+ return { status: "dry_run", path: targetPath };
5482
+ }
5483
+ if (fs4.existsSync(targetPath)) {
5484
+ const existingContent = fs4.readFileSync(targetPath, "utf-8");
5485
+ if (existingContent === templateContent) {
5486
+ return {
5487
+ status: "skipped",
5488
+ path: targetPath,
5489
+ reason: "already up to date"
5490
+ };
5417
5491
  }
5418
- for (const wt of localWorktrees) {
5419
- console.log(` ==> Syncing ${repo.name} @ ${wt.path}`);
5420
- try {
5421
- await syncTechStackForPath(repo.id, wt.path, dryRun);
5422
- synced++;
5423
- } catch (err) {
5424
- console.error(
5425
- ` Error syncing tech stack for ${repo.name} @ ${wt.path}: ${err instanceof Error ? err.message : String(err)}`
5426
- );
5427
- }
5492
+ if (!force) {
5493
+ throw new Error(
5494
+ `scaffold-publish-workflow: ${targetPath} already exists and differs from the template. Pass --force to overwrite.`
5495
+ );
5428
5496
  }
5429
5497
  }
5430
- console.log(
5431
- `
5432
- Walked ${repos.length} repos, synced ${synced} local worktrees, skipped ${skipped} remote.
5433
- `
5434
- );
5498
+ const targetDir = path5.dirname(targetPath);
5499
+ fs4.mkdirSync(targetDir, { recursive: true });
5500
+ fs4.writeFileSync(targetPath, templateContent, "utf-8");
5501
+ return { status: "written", path: targetPath };
5435
5502
  }
5436
- var init_tech_stack = __esm({
5437
- "src/cli/tech-stack.ts"() {
5503
+ var init_scaffold_publish_workflow = __esm({
5504
+ "src/lib/scaffold-publish-workflow.ts"() {
5438
5505
  "use strict";
5439
- init_flags();
5440
- init_api();
5441
- init_tech_detect();
5442
- init_migrate_local_config();
5443
- init_local_config();
5444
- init_resolve_worktree();
5506
+ init_install();
5445
5507
  }
5446
5508
  });
5447
5509
 
5448
- // src/lib/hash.ts
5449
- import { createHash as createHash3 } from "node:crypto";
5450
- function sha256(input) {
5451
- const buf = typeof input === "string" ? Buffer.from(input, "utf8") : input;
5452
- return `sha256:${createHash3("sha256").update(buf).digest("hex")}`;
5510
+ // src/cli/scaffold-publish-workflow.ts
5511
+ var scaffold_publish_workflow_exports = {};
5512
+ __export(scaffold_publish_workflow_exports, {
5513
+ runScaffoldPublishWorkflowCommand: () => runScaffoldPublishWorkflowCommand
5514
+ });
5515
+ function parseFlagsFromArgs4(args) {
5516
+ const flags = {};
5517
+ const booleans = /* @__PURE__ */ new Set();
5518
+ for (let i = 0; i < args.length; i++) {
5519
+ const arg = args[i];
5520
+ if (arg.startsWith("--")) {
5521
+ const key = arg.slice(2);
5522
+ const next = args[i + 1];
5523
+ if (next !== void 0 && !next.startsWith("--")) {
5524
+ flags[key] = next;
5525
+ i++;
5526
+ } else {
5527
+ booleans.add(key);
5528
+ }
5529
+ }
5530
+ }
5531
+ return { flags, booleans };
5453
5532
  }
5454
- var init_hash = __esm({
5455
- "src/lib/hash.ts"() {
5533
+ function printHelp() {
5534
+ process.stdout.write(
5535
+ "\n codebyplan scaffold-publish-workflow\n\n Write the publish-on-main GitHub Actions workflow into\n ./.github/workflows/publish.yml. Idempotent \u2014 safe to re-run.\n\n Flags:\n --dry-run Preview the operation without writing any files\n --force Overwrite an existing file that differs from the template\n --project-dir <p> Target project root (default: current directory)\n --json Write structured JSON to stdout\n\n"
5536
+ );
5537
+ }
5538
+ function printHumanResult2(result) {
5539
+ switch (result.status) {
5540
+ case "written":
5541
+ process.stdout.write(`Written: ${result.path}
5542
+ `);
5543
+ break;
5544
+ case "skipped":
5545
+ process.stdout.write(`Skipped: ${result.path} (${result.reason})
5546
+ `);
5547
+ break;
5548
+ case "dry_run":
5549
+ process.stdout.write(`[dry-run] Would write: ${result.path}
5550
+ `);
5551
+ break;
5552
+ }
5553
+ }
5554
+ async function runScaffoldPublishWorkflowCommand(args) {
5555
+ const firstArg = args[0];
5556
+ if (firstArg === "help" || firstArg === "--help" || firstArg === "-h") {
5557
+ printHelp();
5558
+ process.exit(0);
5559
+ }
5560
+ const { flags, booleans } = parseFlagsFromArgs4(args);
5561
+ const dryRun = booleans.has("dry-run");
5562
+ const force = booleans.has("force");
5563
+ const jsonOutput = booleans.has("json");
5564
+ const projectDir = flags["project-dir"];
5565
+ let result;
5566
+ try {
5567
+ result = await runScaffoldPublishWorkflow({ dryRun, force, projectDir });
5568
+ } catch (err) {
5569
+ process.stderr.write(
5570
+ `scaffold-publish-workflow: ${err instanceof Error ? err.message : String(err)}
5571
+ `
5572
+ );
5573
+ process.exitCode = 1;
5574
+ return;
5575
+ }
5576
+ if (jsonOutput) {
5577
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
5578
+ return;
5579
+ }
5580
+ printHumanResult2(result);
5581
+ }
5582
+ var init_scaffold_publish_workflow2 = __esm({
5583
+ "src/cli/scaffold-publish-workflow.ts"() {
5456
5584
  "use strict";
5585
+ init_scaffold_publish_workflow();
5457
5586
  }
5458
5587
  });
5459
5588
 
5460
- // src/lib/template-walker.ts
5461
- import * as fs from "node:fs";
5462
- import * as path2 from "node:path";
5463
- function walkTemplates(templatesDir) {
5464
- const absRoot = fs.realpathSync(templatesDir);
5465
- const visited = /* @__PURE__ */ new Set();
5466
- const out = [];
5467
- const recurse = (absDir) => {
5468
- const realDir = fs.realpathSync(absDir);
5469
- if (visited.has(realDir)) {
5470
- return;
5589
+ // src/cli/resolve-worktree.ts
5590
+ var resolve_worktree_exports = {};
5591
+ __export(resolve_worktree_exports, {
5592
+ ProcessExitSignal: () => ProcessExitSignal,
5593
+ runResolveWorktree: () => runResolveWorktree
5594
+ });
5595
+ import { execSync as execSync5 } from "node:child_process";
5596
+ function distress(kind, message, jsonMode) {
5597
+ if (jsonMode) return;
5598
+ process.stderr.write(`resolve-worktree: ${kind}: ${message}
5599
+ `);
5600
+ }
5601
+ async function runResolveWorktree() {
5602
+ const jsonMode = hasFlag("json", 3);
5603
+ let errorContext = null;
5604
+ const migrationNoticeCallback = (legacyPath, primaryPath) => {
5605
+ if (!jsonMode) {
5606
+ process.stderr.write(
5607
+ `resolve-worktree: local_config_migration: ${legacyPath} is the legacy flat config; move device_id to ${primaryPath}
5608
+ `
5609
+ );
5471
5610
  }
5472
- visited.add(realDir);
5473
- const entries = fs.readdirSync(absDir, { withFileTypes: true });
5474
- for (const entry of entries) {
5475
- const absPath = path2.join(absDir, entry.name);
5476
- if (entry.isDirectory()) {
5477
- recurse(absPath);
5478
- continue;
5611
+ };
5612
+ try {
5613
+ const projectPath = process.cwd();
5614
+ const found = await findCodebyplanConfig(projectPath);
5615
+ if (!found?.contents.repo_id) {
5616
+ emitAndExit(null, null, jsonMode);
5617
+ }
5618
+ const repoId = found.contents.repo_id;
5619
+ try {
5620
+ await readLocalConfig(projectPath);
5621
+ } catch (readErr) {
5622
+ const readErrCode = readErr.code;
5623
+ errorContext = {
5624
+ kind: readErrCode === "LEGACY_FILE_BLOCKS_DIR" ? "legacy_file_blocks_dir" : "local_config_read_failed",
5625
+ message: readErr instanceof Error ? readErr.message : String(readErr)
5626
+ };
5627
+ }
5628
+ let deviceId;
5629
+ try {
5630
+ deviceId = await getOrCreateDeviceId(
5631
+ projectPath,
5632
+ migrationNoticeCallback
5633
+ );
5634
+ } catch (deviceErr) {
5635
+ const code = deviceErr.code;
5636
+ if (code === "LEGACY_FILE_BLOCKS_DIR") {
5637
+ errorContext = {
5638
+ kind: "legacy_file_blocks_dir",
5639
+ message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
5640
+ };
5641
+ } else if (errorContext === null || errorContext.kind !== "local_config_read_failed" && errorContext.kind !== "legacy_file_blocks_dir") {
5642
+ errorContext = {
5643
+ kind: "local_config_write_failed",
5644
+ message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
5645
+ };
5479
5646
  }
5480
- if (!entry.isFile()) {
5481
- continue;
5647
+ emitAndExit(null, errorContext, jsonMode);
5648
+ }
5649
+ let branch = "";
5650
+ try {
5651
+ branch = execSync5("git symbolic-ref --short HEAD", {
5652
+ cwd: projectPath,
5653
+ encoding: "utf-8"
5654
+ }).trim();
5655
+ } catch (gitErr) {
5656
+ if (errorContext === null) {
5657
+ errorContext = {
5658
+ kind: "git_failed",
5659
+ message: gitErr instanceof Error ? gitErr.message : String(gitErr)
5660
+ };
5482
5661
  }
5483
- if (entry.name === ".gitkeep") continue;
5484
- const relPosix = path2.relative(absRoot, absPath).split(path2.sep).join("/");
5485
- if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
5486
- continue;
5662
+ }
5663
+ const onResolverError = (kind, err) => {
5664
+ if (errorContext === null) {
5665
+ errorContext = { kind, message: err.message };
5487
5666
  }
5488
- const content = fs.readFileSync(absPath);
5489
- out.push({
5490
- src: relPosix,
5491
- dest: relPosix,
5492
- hash: sha256(content)
5667
+ };
5668
+ const worktreeId = await resolveWorktreeId({
5669
+ repoId,
5670
+ repoPath: projectPath,
5671
+ branch,
5672
+ deviceId,
5673
+ onError: onResolverError
5674
+ });
5675
+ if (worktreeId) {
5676
+ emitAndExit(worktreeId, errorContext, jsonMode);
5677
+ }
5678
+ const useFallback = hasFlag("fallback-from-branch", 3);
5679
+ if (useFallback) {
5680
+ const fallbackId = await resolveWorktreeByBranch({
5681
+ repoId,
5682
+ deviceId,
5683
+ branch,
5684
+ onError: onResolverError
5493
5685
  });
5686
+ if (fallbackId) {
5687
+ emitAndExit(fallbackId, errorContext, jsonMode);
5688
+ }
5494
5689
  }
5495
- };
5496
- recurse(absRoot);
5497
- out.sort((a, b) => a.src.localeCompare(b.src));
5498
- return out;
5690
+ emitAndExit(null, errorContext, jsonMode);
5691
+ } catch (err) {
5692
+ if (err instanceof ProcessExitSignal) throw err;
5693
+ const msg = err instanceof Error ? err.message : String(err);
5694
+ errorContext = { kind: "unhandled", message: msg };
5695
+ emitAndExit(null, errorContext, jsonMode);
5696
+ }
5499
5697
  }
5500
- var EXCLUDED_RELATIVE_PATHS;
5501
- var init_template_walker = __esm({
5502
- "src/lib/template-walker.ts"() {
5698
+ function emitAndExit(worktreeId, errorContext, jsonMode) {
5699
+ if (jsonMode) {
5700
+ const errorKind = errorContext?.kind ?? (worktreeId === null ? "tuple_miss" : null);
5701
+ process.stdout.write(
5702
+ JSON.stringify({ worktree_id: worktreeId, error_kind: errorKind }) + "\n"
5703
+ );
5704
+ } else {
5705
+ if (worktreeId !== null) {
5706
+ process.stdout.write(worktreeId);
5707
+ }
5708
+ if (errorContext !== null) {
5709
+ if (errorContext.kind !== "unhandled" || process.env.CODEBYPLAN_DEBUG === "1") {
5710
+ distress(errorContext.kind, errorContext.message, jsonMode);
5711
+ }
5712
+ }
5713
+ }
5714
+ process.exit(0);
5715
+ }
5716
+ var ProcessExitSignal;
5717
+ var init_resolve_worktree2 = __esm({
5718
+ "src/cli/resolve-worktree.ts"() {
5503
5719
  "use strict";
5504
- init_hash();
5505
- EXCLUDED_RELATIVE_PATHS = /* @__PURE__ */ new Set([
5506
- // Meta files
5507
- "hooks/hooks.json",
5508
- "hooks/README.md",
5509
- "rules/README.md",
5510
- "settings.project.base.json",
5511
- "settings.user.base.json",
5512
- // .gitignore managed by ensureManagedGitignoreBlock; never copied into
5513
- // consuming projects' .claude/ tree (it would overwrite the project root
5514
- // .gitignore with a stale single-entry file).
5515
- ".gitignore",
5516
- // CBP-internal hooks — see templates/hooks/README.md "Hooks NOT included and why"
5517
- "hooks/validate-structure.sh",
5518
- "hooks/validate-structure-lib.sh",
5519
- "hooks/validate-structure-patterns.sh",
5520
- "hooks/validate-structure-templates.sh",
5521
- "hooks/validate-structure-scope.sh",
5522
- "hooks/validate-structure-lengths.sh",
5523
- "hooks/validate-structure-smoke.sh",
5524
- "hooks/validate-context-usage.sh",
5525
- "hooks/validate-git-commit.sh"
5526
- ]);
5720
+ init_flags();
5721
+ init_local_config();
5722
+ init_resolve_worktree();
5723
+ ProcessExitSignal = class extends Error {
5724
+ code;
5725
+ constructor(code) {
5726
+ super(`process.exit(${code})`);
5727
+ this.name = "ProcessExitSignal";
5728
+ this.code = code;
5729
+ }
5730
+ };
5527
5731
  }
5528
5732
  });
5529
5733
 
5530
- // src/lib/manifest.ts
5531
- import * as fs2 from "node:fs";
5532
- import * as os from "node:os";
5533
- import * as path3 from "node:path";
5534
- function manifestPath(projectDir) {
5535
- return path3.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5734
+ // src/lib/migrate-local-config.ts
5735
+ import { mkdir as mkdir5, readFile as readFile14, unlink as unlink2, writeFile as writeFile11 } from "node:fs/promises";
5736
+ import { join as join19 } from "node:path";
5737
+ function legacySharedPath(projectPath) {
5738
+ return join19(projectPath, ".codebyplan.json");
5536
5739
  }
5537
- function midManifestPath(projectDir) {
5538
- return path3.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5740
+ function legacyLocalPath(projectPath) {
5741
+ return join19(projectPath, ".codebyplan.local.json");
5539
5742
  }
5540
- function oldManifestPath(projectDir) {
5541
- return path3.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5743
+ function newDirPath(projectPath) {
5744
+ return join19(projectPath, ".codebyplan");
5542
5745
  }
5543
- function readManifest(projectDir) {
5544
- const newFile = manifestPath(projectDir);
5545
- if (fs2.existsSync(newFile)) {
5546
- const raw = fs2.readFileSync(newFile, "utf8");
5547
- return JSON.parse(raw);
5548
- }
5549
- const midFile = midManifestPath(projectDir);
5550
- if (fs2.existsSync(midFile)) {
5551
- const raw = fs2.readFileSync(midFile, "utf8");
5552
- return JSON.parse(raw);
5553
- }
5554
- const oldFile = oldManifestPath(projectDir);
5555
- if (fs2.existsSync(oldFile)) {
5556
- const raw = fs2.readFileSync(oldFile, "utf8");
5557
- return JSON.parse(raw);
5558
- }
5559
- return null;
5746
+ function sentinelPath(projectPath) {
5747
+ return join19(projectPath, ".codebyplan", "repo.json");
5560
5748
  }
5561
- function writeManifest(projectDir, manifest) {
5562
- const file = manifestPath(projectDir);
5563
- fs2.mkdirSync(path3.dirname(file), { recursive: true });
5564
- fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5565
- const mid = midManifestPath(projectDir);
5566
- if (fs2.existsSync(mid)) {
5567
- fs2.rmSync(mid);
5749
+ async function statSafe(p) {
5750
+ const { stat: stat2 } = await import("node:fs/promises");
5751
+ try {
5752
+ return await stat2(p);
5753
+ } catch {
5754
+ return null;
5568
5755
  }
5569
- const legacy = oldManifestPath(projectDir);
5570
- if (fs2.existsSync(legacy)) {
5571
- fs2.rmSync(legacy);
5756
+ }
5757
+ async function needsLocalMigration(projectPath) {
5758
+ try {
5759
+ const legacyStat = await statSafe(legacySharedPath(projectPath));
5760
+ if (!legacyStat) return false;
5761
+ const sentinelStat = await statSafe(sentinelPath(projectPath));
5762
+ if (sentinelStat) return false;
5763
+ return true;
5764
+ } catch {
5765
+ return false;
5572
5766
  }
5573
5767
  }
5574
- function defaultManifest() {
5575
- return {
5576
- version: VERSION,
5577
- installed_at: (/* @__PURE__ */ new Date()).toISOString(),
5578
- files: []
5579
- };
5580
- }
5581
- function userManifestPath(userDir) {
5582
- const dir = userDir ?? path3.join(os.homedir(), ".claude");
5583
- return path3.join(dir, NEW_MANIFEST_FILENAME);
5584
- }
5585
- function userMidManifestPath(userDir) {
5586
- const dir = userDir ?? path3.join(os.homedir(), ".claude");
5587
- return path3.join(dir, MID_MANIFEST_FILENAME);
5588
- }
5589
- function userOldManifestPath(userDir) {
5590
- const dir = userDir ?? path3.join(os.homedir(), ".claude");
5591
- return path3.join(dir, OLD_MANIFEST_FILENAME);
5592
- }
5593
- function readManifestForScope(scope, arg2) {
5594
- if (scope === "user") {
5595
- const newFile = userManifestPath(arg2);
5596
- if (fs2.existsSync(newFile)) {
5597
- const raw = fs2.readFileSync(newFile, "utf8");
5598
- return JSON.parse(raw);
5599
- }
5600
- const midFile = userMidManifestPath(arg2);
5601
- if (fs2.existsSync(midFile)) {
5602
- const raw = fs2.readFileSync(midFile, "utf8");
5603
- return JSON.parse(raw);
5768
+ async function runLocalMigration(projectPath) {
5769
+ const dirStat = await statSafe(newDirPath(projectPath));
5770
+ if (dirStat && !dirStat.isDirectory()) {
5771
+ throw new Error(
5772
+ ".codebyplan exists as a file; remove or rename it before migrating."
5773
+ );
5774
+ }
5775
+ const sentinel = await statSafe(sentinelPath(projectPath));
5776
+ if (sentinel) {
5777
+ return {
5778
+ migrated: true,
5779
+ was_dirty: false,
5780
+ files_changed: [],
5781
+ summary: "already on new layout"
5782
+ };
5783
+ }
5784
+ let legacyRaw;
5785
+ try {
5786
+ legacyRaw = await readFile14(legacySharedPath(projectPath), "utf-8");
5787
+ } catch {
5788
+ return {
5789
+ migrated: true,
5790
+ was_dirty: false,
5791
+ files_changed: [],
5792
+ summary: "legacy .codebyplan.json absent; nothing to migrate"
5793
+ };
5794
+ }
5795
+ let parsed;
5796
+ try {
5797
+ parsed = JSON.parse(legacyRaw);
5798
+ } catch (err) {
5799
+ const inner = err instanceof Error ? err.message : String(err);
5800
+ throw new Error(
5801
+ `.codebyplan.json contains invalid JSON \u2014 cannot migrate: ${inner}`
5802
+ );
5803
+ }
5804
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5805
+ throw new Error(
5806
+ ".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
5807
+ );
5808
+ }
5809
+ const cfg = parsed;
5810
+ let deviceId;
5811
+ let deviceWrittenByHelper = false;
5812
+ try {
5813
+ const localRaw = await readFile14(legacyLocalPath(projectPath), "utf-8");
5814
+ const localParsed = JSON.parse(localRaw);
5815
+ if (typeof localParsed.device_id === "string") {
5816
+ deviceId = localParsed.device_id;
5604
5817
  }
5605
- const oldFile = userOldManifestPath(arg2);
5606
- if (fs2.existsSync(oldFile)) {
5607
- const raw = fs2.readFileSync(oldFile, "utf8");
5608
- return JSON.parse(raw);
5818
+ } catch {
5819
+ }
5820
+ try {
5821
+ await mkdir5(newDirPath(projectPath), { recursive: true });
5822
+ } catch (err) {
5823
+ const code = err.code;
5824
+ if (code === "ENOTDIR" || code === "EEXIST") {
5825
+ throw new Error(
5826
+ ".codebyplan exists as a file; remove or rename it before migrating."
5827
+ );
5609
5828
  }
5610
- return null;
5829
+ throw err;
5611
5830
  }
5612
- return readManifest(arg2);
5613
- }
5614
- function writeManifestForScope(scope, manifest, arg3) {
5615
- if (scope === "user") {
5616
- const file = userManifestPath(arg3);
5617
- fs2.mkdirSync(path3.dirname(file), { recursive: true });
5618
- fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5619
- const mid = userMidManifestPath(arg3);
5620
- if (fs2.existsSync(mid)) {
5621
- fs2.rmSync(mid);
5831
+ if (!deviceId) {
5832
+ deviceId = await getOrCreateDeviceId(projectPath);
5833
+ deviceWrittenByHelper = true;
5834
+ }
5835
+ const filesChanged = [];
5836
+ const repoJson = {};
5837
+ if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
5838
+ if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
5839
+ if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
5840
+ await writeFile11(
5841
+ join19(projectPath, ".codebyplan", "repo.json"),
5842
+ JSON.stringify(repoJson, null, 2) + "\n",
5843
+ "utf-8"
5844
+ );
5845
+ filesChanged.push(".codebyplan/repo.json");
5846
+ const serverJson = {};
5847
+ if ("server_port" in cfg) serverJson.server_port = cfg.server_port;
5848
+ if ("server_type" in cfg) serverJson.server_type = cfg.server_type;
5849
+ if ("auto_push_enabled" in cfg)
5850
+ serverJson.auto_push_enabled = cfg.auto_push_enabled;
5851
+ if ("port_allocations" in cfg)
5852
+ serverJson.port_allocations = cfg.port_allocations;
5853
+ await writeFile11(
5854
+ join19(projectPath, ".codebyplan", "server.json"),
5855
+ JSON.stringify(serverJson, null, 2) + "\n",
5856
+ "utf-8"
5857
+ );
5858
+ filesChanged.push(".codebyplan/server.json");
5859
+ const gitJson = {};
5860
+ if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
5861
+ if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
5862
+ await writeFile11(
5863
+ join19(projectPath, ".codebyplan", "git.json"),
5864
+ JSON.stringify(gitJson, null, 2) + "\n",
5865
+ "utf-8"
5866
+ );
5867
+ filesChanged.push(".codebyplan/git.json");
5868
+ const shipmentJson = {};
5869
+ if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
5870
+ await writeFile11(
5871
+ join19(projectPath, ".codebyplan", "shipment.json"),
5872
+ JSON.stringify(shipmentJson, null, 2) + "\n",
5873
+ "utf-8"
5874
+ );
5875
+ filesChanged.push(".codebyplan/shipment.json");
5876
+ const vendorJson = {};
5877
+ await writeFile11(
5878
+ join19(projectPath, ".codebyplan", "vendor.json"),
5879
+ JSON.stringify(vendorJson, null, 2) + "\n",
5880
+ "utf-8"
5881
+ );
5882
+ filesChanged.push(".codebyplan/vendor.json");
5883
+ const e2eJson = {};
5884
+ await writeFile11(
5885
+ join19(projectPath, ".codebyplan", "e2e.json"),
5886
+ JSON.stringify(e2eJson, null, 2) + "\n",
5887
+ "utf-8"
5888
+ );
5889
+ filesChanged.push(".codebyplan/e2e.json");
5890
+ const eslintJson = {};
5891
+ await writeFile11(
5892
+ join19(projectPath, ".codebyplan", "eslint.json"),
5893
+ JSON.stringify(eslintJson, null, 2) + "\n",
5894
+ "utf-8"
5895
+ );
5896
+ filesChanged.push(".codebyplan/eslint.json");
5897
+ if (!deviceWrittenByHelper) {
5898
+ await writeFile11(
5899
+ join19(projectPath, ".codebyplan", "device.local.json"),
5900
+ JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
5901
+ "utf-8"
5902
+ );
5903
+ }
5904
+ filesChanged.push(".codebyplan/device.local.json");
5905
+ const writtenSentinel = await statSafe(sentinelPath(projectPath));
5906
+ if (!writtenSentinel) {
5907
+ throw new Error(
5908
+ "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
5909
+ );
5910
+ }
5911
+ const gitignorePath = join19(projectPath, ".gitignore");
5912
+ try {
5913
+ const gitignoreContent = await readFile14(gitignorePath, "utf-8");
5914
+ const legacyLine = ".codebyplan.local.json";
5915
+ const newLine = ".codebyplan/device.local.json";
5916
+ const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
5917
+ const hasNew = gitignoreContent.split("\n").some((l) => l.trimEnd() === newLine);
5918
+ let updated;
5919
+ if (hasLegacy && !hasNew) {
5920
+ updated = gitignoreContent.split("\n").map((l) => {
5921
+ const stripped = l.trimEnd();
5922
+ return stripped === legacyLine ? newLine + (l.endsWith("\r") ? "\r" : "") : l;
5923
+ }).join("\n");
5924
+ } else if (hasLegacy && hasNew) {
5925
+ updated = gitignoreContent.split("\n").filter((l) => l.trimEnd() !== legacyLine).join("\n");
5926
+ } else if (!hasLegacy && !hasNew) {
5927
+ updated = gitignoreContent.endsWith("\n") ? gitignoreContent + newLine + "\n" : gitignoreContent + "\n" + newLine + "\n";
5928
+ } else {
5929
+ updated = gitignoreContent;
5622
5930
  }
5623
- const legacy = userOldManifestPath(arg3);
5624
- if (fs2.existsSync(legacy)) {
5625
- fs2.rmSync(legacy);
5931
+ if (updated !== gitignoreContent) {
5932
+ await writeFile11(gitignorePath, updated, "utf-8");
5933
+ filesChanged.push(".gitignore");
5626
5934
  }
5627
- return;
5935
+ } catch {
5628
5936
  }
5629
- writeManifest(arg3, manifest);
5937
+ try {
5938
+ await unlink2(legacySharedPath(projectPath));
5939
+ filesChanged.push(".codebyplan.json (deleted)");
5940
+ } catch {
5941
+ }
5942
+ try {
5943
+ await unlink2(legacyLocalPath(projectPath));
5944
+ filesChanged.push(".codebyplan.local.json (deleted)");
5945
+ } catch {
5946
+ }
5947
+ return {
5948
+ migrated: true,
5949
+ was_dirty: true,
5950
+ files_changed: filesChanged,
5951
+ summary: `migrated legacy .codebyplan.json to .codebyplan/ layout (device_id=${deviceId.slice(0, 8)})`
5952
+ };
5630
5953
  }
5631
- var NEW_MANIFEST_FILENAME, MID_MANIFEST_FILENAME, OLD_MANIFEST_FILENAME;
5632
- var init_manifest = __esm({
5633
- "src/lib/manifest.ts"() {
5954
+ var init_migrate_local_config = __esm({
5955
+ "src/lib/migrate-local-config.ts"() {
5634
5956
  "use strict";
5635
- init_version();
5636
- NEW_MANIFEST_FILENAME = ".cbp.manifest.json";
5637
- MID_MANIFEST_FILENAME = ".cbp-claude.manifest.json";
5638
- OLD_MANIFEST_FILENAME = ".codebyplan-claude.manifest.json";
5957
+ init_local_config();
5639
5958
  }
5640
5959
  });
5641
5960
 
5642
- // src/lib/settings-merge.ts
5643
- function extractHookId(command) {
5644
- const match = /([^\s/]+\.sh)(?:\s|$)/.exec(command);
5645
- const id = match?.[1];
5646
- if (!id) {
5647
- throw new Error(
5648
- `Cannot derive _hook_id from command (no .sh script segment): ${command}`
5961
+ // src/cli/config.ts
5962
+ var config_exports = {};
5963
+ __export(config_exports, {
5964
+ readE2eConfig: () => readE2eConfig,
5965
+ readGitConfig: () => readGitConfig,
5966
+ readRepoConfig: () => readRepoConfig,
5967
+ readServerConfig: () => readServerConfig,
5968
+ readShipmentConfig: () => readShipmentConfig,
5969
+ readVendorConfig: () => readVendorConfig,
5970
+ runConfig: () => runConfig
5971
+ });
5972
+ import { mkdir as mkdir6, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
5973
+ import { join as join20 } from "node:path";
5974
+ async function runConfig() {
5975
+ const flags = parseFlags(3);
5976
+ const dryRun = hasFlag("dry-run", 3);
5977
+ validateApiKey();
5978
+ const config = await resolveConfig(flags);
5979
+ const { repoId, projectPath } = config;
5980
+ console.log(`
5981
+ CodeByPlan Config`);
5982
+ console.log(` Repo: ${repoId}`);
5983
+ console.log(` Path: ${projectPath}`);
5984
+ if (dryRun) console.log(` Mode: dry-run`);
5985
+ console.log();
5986
+ if (!dryRun && await needsLocalMigration(projectPath)) {
5987
+ console.log(
5988
+ " Migrating legacy .codebyplan.json to .codebyplan/ layout..."
5649
5989
  );
5990
+ try {
5991
+ const result = await runLocalMigration(projectPath);
5992
+ if (result.was_dirty) {
5993
+ console.log(" Migration complete.");
5994
+ }
5995
+ } catch (err) {
5996
+ const msg = err instanceof Error ? err.message : String(err);
5997
+ console.warn(
5998
+ ` Warning: migration failed (${msg}); continuing with config sync.`
5999
+ );
6000
+ }
5650
6001
  }
5651
- return id;
5652
- }
5653
- function rewriteCommand(command) {
5654
- return command.replace(PLACEHOLDER_RE, REPLACEMENT);
6002
+ await syncConfigToFile(repoId, projectPath, dryRun);
6003
+ console.log("\n Config complete.\n");
5655
6004
  }
5656
- function mergeHooksIntoSettings(settings, hooksJson) {
5657
- if (!settings.hooks) {
5658
- settings.hooks = {};
5659
- }
5660
- const target = settings.hooks;
5661
- for (const [event, sourceMatchers] of Object.entries(hooksJson.hooks)) {
5662
- if (!target[event]) {
5663
- target[event] = [];
6005
+ async function syncConfigToFile(repoId, projectPath, dryRun) {
6006
+ const codebyplanDir = join20(projectPath, ".codebyplan");
6007
+ let resolvedWorktreeId;
6008
+ try {
6009
+ const deviceId = await getOrCreateDeviceId(projectPath);
6010
+ let branch = "main";
6011
+ try {
6012
+ const { execSync: execSync6 } = await import("node:child_process");
6013
+ branch = execSync6("git symbolic-ref --short HEAD", {
6014
+ cwd: projectPath,
6015
+ encoding: "utf-8"
6016
+ }).trim();
6017
+ } catch {
5664
6018
  }
5665
- const eventBlocks = target[event];
5666
- for (const sourceBlock of sourceMatchers) {
5667
- let destBlock = eventBlocks.find(
5668
- (b) => b.matcher === sourceBlock.matcher
5669
- );
5670
- if (!destBlock) {
5671
- destBlock = { matcher: sourceBlock.matcher, hooks: [] };
5672
- eventBlocks.push(destBlock);
6019
+ const tupleId = await resolveWorktreeId({
6020
+ repoId,
6021
+ repoPath: projectPath,
6022
+ branch,
6023
+ deviceId
6024
+ });
6025
+ if (tupleId) {
6026
+ resolvedWorktreeId = tupleId;
6027
+ } else {
6028
+ resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
6029
+ }
6030
+ } catch (err) {
6031
+ const msg = err instanceof Error ? err.message : String(err);
6032
+ console.warn(
6033
+ ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
6034
+ );
6035
+ }
6036
+ let repoRes;
6037
+ try {
6038
+ repoRes = await apiGet(`/repos/${repoId}`);
6039
+ } catch (err) {
6040
+ let message;
6041
+ if (err instanceof ApiError) {
6042
+ if (err.status === 401) {
6043
+ message = "Session expired. Run `codebyplan login` and try again.";
6044
+ } else if (err.status === 403 || err.status === 404) {
6045
+ message = "Repo not found or not accessible to your account. Confirm the repo_id in .codebyplan/repo.json and that you are logged in as the right user.";
6046
+ } else if (err.status >= 500) {
6047
+ message = `CodeByPlan server error (status ${err.status}). Please try again shortly.`;
6048
+ } else {
6049
+ message = `Unexpected API error (status ${err.status}). Please try again.`;
5673
6050
  }
5674
- for (const sourceCmd of sourceBlock.hooks) {
5675
- const taggedEntry = {
5676
- _owner: OWNER,
5677
- _hook_id: extractHookId(sourceCmd.command),
5678
- type: sourceCmd.type,
5679
- command: rewriteCommand(sourceCmd.command)
5680
- };
5681
- const existingIdx = destBlock.hooks.findIndex(
5682
- (e) => e._owner === OWNER && e._hook_id === taggedEntry._hook_id
5683
- );
5684
- if (existingIdx >= 0) {
5685
- destBlock.hooks[existingIdx] = taggedEntry;
5686
- } else {
5687
- destBlock.hooks.push(taggedEntry);
5688
- }
6051
+ } else {
6052
+ message = "Failed to reach the CodeByPlan API. Check your network connection and try again.";
6053
+ }
6054
+ process.stderr.write(message + "\n");
6055
+ process.exit(1);
6056
+ }
6057
+ const repo = repoRes.data;
6058
+ let portAllocations = [];
6059
+ try {
6060
+ const portsRes = await apiGet(
6061
+ `/port-allocations`,
6062
+ { repo_id: repoId }
6063
+ );
6064
+ const allAllocations = portsRes.data ?? [];
6065
+ const filtered = resolvedWorktreeId ? allAllocations.filter((a) => a.worktree_id === resolvedWorktreeId) : allAllocations.filter((a) => !a.worktree_id);
6066
+ const ALLOWED_FIELDS = [
6067
+ "id",
6068
+ "repo_id",
6069
+ "port",
6070
+ "label",
6071
+ "server_type",
6072
+ "auto_start",
6073
+ "command",
6074
+ "working_dir",
6075
+ "env_vars",
6076
+ "external_refs",
6077
+ "worktree_id",
6078
+ "created_at",
6079
+ "updated_at"
6080
+ ];
6081
+ portAllocations = filtered.map((a) => {
6082
+ const clean = {};
6083
+ for (const key of ALLOWED_FIELDS) {
6084
+ if (key in a) clean[key] = a[key];
5689
6085
  }
6086
+ return clean;
6087
+ });
6088
+ } catch (err) {
6089
+ console.warn(
6090
+ ` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
6091
+ );
6092
+ }
6093
+ const matchingAlloc = portAllocations[0];
6094
+ const defaultBranchConfig = {
6095
+ protected: ["main"],
6096
+ integration: null,
6097
+ production: "main",
6098
+ staging: null
6099
+ };
6100
+ const branchConfig = repo.branch_config ?? defaultBranchConfig;
6101
+ if (!legacyBranchConfigWarned && typeof branchConfig["integration"] === "string") {
6102
+ legacyBranchConfigWarned = true;
6103
+ process.stderr.write(
6104
+ `warning: legacy 3-branch branch_config detected (integration: '${String(branchConfig["integration"])}'). Run 'npx codebyplan branch migrate' to consolidate to main-only.
6105
+ `
6106
+ );
6107
+ }
6108
+ const repoPayload = { repo_id: repoId };
6109
+ const repoAny = repo;
6110
+ if (typeof repoAny.organization_id === "string") {
6111
+ repoPayload.organization_id = repoAny.organization_id;
6112
+ }
6113
+ if (typeof repoAny.project_id === "string") {
6114
+ repoPayload.project_id = repoAny.project_id;
6115
+ }
6116
+ const serverPayload = {
6117
+ server_port: resolvedWorktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
6118
+ server_type: resolvedWorktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
6119
+ auto_push_enabled: repo.auto_push_enabled,
6120
+ port_allocations: portAllocations
6121
+ };
6122
+ const gitPayload = {
6123
+ git_branch: repo.git_branch ?? "main",
6124
+ branch_config: branchConfig
6125
+ };
6126
+ const shipmentPayload = {};
6127
+ if (typeof repoAny.shipment !== "undefined") {
6128
+ shipmentPayload.shipment = repoAny.shipment;
6129
+ }
6130
+ const vendorPayload = {};
6131
+ const e2ePayload = {};
6132
+ const eslintPayload = {};
6133
+ if (dryRun) {
6134
+ console.log(" Config would be updated (dry-run).");
6135
+ return;
6136
+ }
6137
+ await mkdir6(codebyplanDir, { recursive: true });
6138
+ const files = [
6139
+ { name: "repo.json", payload: repoPayload },
6140
+ { name: "server.json", payload: serverPayload },
6141
+ { name: "git.json", payload: gitPayload },
6142
+ { name: "shipment.json", payload: shipmentPayload },
6143
+ { name: "vendor.json", payload: vendorPayload },
6144
+ { name: "e2e.json", payload: e2ePayload, createOnly: true },
6145
+ { name: "eslint.json", payload: eslintPayload, createOnly: true }
6146
+ ];
6147
+ let anyUpdated = false;
6148
+ for (const { name, payload, createOnly } of files) {
6149
+ const filePath = join20(codebyplanDir, name);
6150
+ const newJson = JSON.stringify(payload, null, 2) + "\n";
6151
+ let currentJson = "";
6152
+ try {
6153
+ currentJson = await readFile15(filePath, "utf-8");
6154
+ } catch {
5690
6155
  }
6156
+ if (createOnly && currentJson !== "") continue;
6157
+ if (currentJson === newJson) continue;
6158
+ await writeFile12(filePath, newJson, "utf-8");
6159
+ console.log(` Updated .codebyplan/${name}`);
6160
+ anyUpdated = true;
6161
+ }
6162
+ if (!anyUpdated) {
6163
+ console.log(" Config up to date.");
6164
+ }
6165
+ }
6166
+ async function readRepoConfig(projectPath) {
6167
+ try {
6168
+ const raw = await readFile15(
6169
+ join20(projectPath, ".codebyplan", "repo.json"),
6170
+ "utf-8"
6171
+ );
6172
+ return JSON.parse(raw);
6173
+ } catch {
6174
+ return null;
6175
+ }
6176
+ }
6177
+ async function readServerConfig(projectPath) {
6178
+ try {
6179
+ const raw = await readFile15(
6180
+ join20(projectPath, ".codebyplan", "server.json"),
6181
+ "utf-8"
6182
+ );
6183
+ return JSON.parse(raw);
6184
+ } catch {
6185
+ return null;
6186
+ }
6187
+ }
6188
+ async function readGitConfig(projectPath) {
6189
+ try {
6190
+ const raw = await readFile15(
6191
+ join20(projectPath, ".codebyplan", "git.json"),
6192
+ "utf-8"
6193
+ );
6194
+ return JSON.parse(raw);
6195
+ } catch {
6196
+ return null;
5691
6197
  }
5692
- return settings;
5693
6198
  }
5694
- function mergeBaseSettingsIntoSettings(settings, base) {
5695
- for (const key of SCALAR_BASE_KEYS) {
5696
- if (base[key] !== void 0 && settings[key] === void 0) {
5697
- settings[key] = base[key];
5698
- }
6199
+ async function readShipmentConfig(projectPath) {
6200
+ try {
6201
+ const raw = await readFile15(
6202
+ join20(projectPath, ".codebyplan", "shipment.json"),
6203
+ "utf-8"
6204
+ );
6205
+ return JSON.parse(raw);
6206
+ } catch {
6207
+ return null;
5699
6208
  }
5700
- if (base.statusLine !== void 0 && settings.statusLine === void 0) {
5701
- settings.statusLine = { ...base.statusLine };
6209
+ }
6210
+ async function readVendorConfig(projectPath) {
6211
+ try {
6212
+ const raw = await readFile15(
6213
+ join20(projectPath, ".codebyplan", "vendor.json"),
6214
+ "utf-8"
6215
+ );
6216
+ return JSON.parse(raw);
6217
+ } catch {
6218
+ return null;
5702
6219
  }
5703
- if (base.subagentStatusLine !== void 0 && settings.subagentStatusLine === void 0) {
5704
- settings.subagentStatusLine = { ...base.subagentStatusLine };
6220
+ }
6221
+ async function readE2eConfig(projectPath) {
6222
+ try {
6223
+ const raw = await readFile15(
6224
+ join20(projectPath, ".codebyplan", "e2e.json"),
6225
+ "utf-8"
6226
+ );
6227
+ return JSON.parse(raw);
6228
+ } catch {
6229
+ return null;
5705
6230
  }
5706
- if (base.attribution !== void 0 && settings.attribution === void 0) {
5707
- settings.attribution = { ...base.attribution };
6231
+ }
6232
+ var legacyBranchConfigWarned;
6233
+ var init_config = __esm({
6234
+ "src/cli/config.ts"() {
6235
+ "use strict";
6236
+ init_flags();
6237
+ init_api();
6238
+ init_resolve_worktree();
6239
+ init_local_config();
6240
+ init_migrate_local_config();
6241
+ legacyBranchConfigWarned = false;
5708
6242
  }
5709
- if (base.worktree !== void 0) {
5710
- if (settings.worktree === void 0) {
5711
- settings.worktree = { ...base.worktree };
5712
- } else {
5713
- const sw = settings.worktree;
5714
- for (const [k, v] of Object.entries(base.worktree)) {
5715
- if (sw[k] === void 0) {
5716
- sw[k] = v;
5717
- }
6243
+ });
6244
+
6245
+ // src/lib/server-detect.ts
6246
+ function detectFramework(pkg) {
6247
+ const deps = pkg.dependencies ?? {};
6248
+ const devDeps = pkg.devDependencies ?? {};
6249
+ const hasDep = (name) => name in deps || name in devDeps;
6250
+ if (hasDep("next")) return "nextjs";
6251
+ if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
6252
+ if (hasDep("expo")) return "expo";
6253
+ if (hasDep("vite")) return "vite";
6254
+ if (hasDep("express")) return "express";
6255
+ if (hasDep("@nestjs/core")) return "nestjs";
6256
+ return "custom";
6257
+ }
6258
+ function detectPortFromScripts(pkg) {
6259
+ const scripts = pkg.scripts;
6260
+ if (!scripts?.dev) return null;
6261
+ const parts = scripts.dev.split(/\s+/);
6262
+ for (let i = 0; i < parts.length - 1; i++) {
6263
+ if (parts[i] === "--port" || parts[i] === "-p") {
6264
+ const next = parts[i + 1];
6265
+ if (next) {
6266
+ const port = parseInt(next, 10);
6267
+ if (!isNaN(port)) return port;
5718
6268
  }
5719
6269
  }
5720
6270
  }
5721
- if (base.permissions !== void 0) {
5722
- const existing = settings.permissions ?? {};
5723
- if (base.permissions.defaultMode !== void 0 && existing.defaultMode === void 0) {
5724
- existing.defaultMode = base.permissions.defaultMode;
5725
- }
5726
- if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && existing.skipDangerousModePermissionPrompt === void 0) {
5727
- existing.skipDangerousModePermissionPrompt = base.permissions.skipDangerousModePermissionPrompt;
5728
- }
5729
- for (const key of ["deny", "ask", "allow"]) {
5730
- const incoming = base.permissions[key];
5731
- if (!incoming || incoming.length === 0) continue;
5732
- const current = existing[key] ?? [];
5733
- const seen = new Set(current);
5734
- const merged = [...current];
5735
- for (const item of incoming) {
5736
- if (!seen.has(item)) {
5737
- merged.push(item);
5738
- seen.add(item);
5739
- }
6271
+ return null;
6272
+ }
6273
+ var init_server_detect = __esm({
6274
+ "src/lib/server-detect.ts"() {
6275
+ "use strict";
6276
+ }
6277
+ });
6278
+
6279
+ // src/lib/port-verify.ts
6280
+ import { readFile as readFile16 } from "node:fs/promises";
6281
+ async function verifyPorts(projectPath, portAllocations) {
6282
+ const mismatches = [];
6283
+ const allocatedPorts = new Set(portAllocations.map((a) => a.port));
6284
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
6285
+ for (const pkgPath of packageJsonPaths) {
6286
+ try {
6287
+ const raw = await readFile16(pkgPath, "utf-8");
6288
+ const pkg = JSON.parse(raw);
6289
+ const scriptPort = detectPortFromScripts(pkg);
6290
+ if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
6291
+ const relativePath = pkgPath.replace(projectPath + "/", "");
6292
+ const matchingAlloc = portAllocations.find(
6293
+ (a) => a.label === getAppLabel(relativePath)
6294
+ );
6295
+ mismatches.push({
6296
+ packageJsonPath: relativePath,
6297
+ scriptPort,
6298
+ allocation: matchingAlloc ?? null,
6299
+ reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
6300
+ });
5740
6301
  }
5741
- existing[key] = merged;
5742
- }
5743
- if (settings.permissions !== void 0 || Object.keys(existing).length > 0) {
5744
- settings.permissions = existing;
6302
+ } catch {
5745
6303
  }
5746
6304
  }
5747
- return settings;
6305
+ return mismatches;
5748
6306
  }
5749
- function stripBaseSettingsFromSettings(settings, base) {
5750
- for (const key of SCALAR_BASE_KEYS) {
5751
- if (base[key] !== void 0 && settings[key] === base[key]) {
5752
- delete settings[key];
5753
- }
5754
- }
5755
- if (base.statusLine !== void 0 && settings.statusLine !== void 0 && JSON.stringify(settings.statusLine) === JSON.stringify(base.statusLine)) {
5756
- delete settings.statusLine;
5757
- }
5758
- if (base.subagentStatusLine !== void 0 && settings.subagentStatusLine !== void 0 && JSON.stringify(settings.subagentStatusLine) === JSON.stringify(base.subagentStatusLine)) {
5759
- delete settings.subagentStatusLine;
6307
+ function isDevServerScript(pkg) {
6308
+ const scripts = pkg.scripts;
6309
+ const raw = scripts?.dev;
6310
+ if (!raw || typeof raw !== "string") return false;
6311
+ const script = raw.trim().toLowerCase();
6312
+ if (!script) return false;
6313
+ for (const pattern of DEV_SERVER_BIN_PATTERNS) {
6314
+ if (pattern.test(script)) return true;
5760
6315
  }
5761
- if (base.attribution !== void 0 && settings.attribution !== void 0 && JSON.stringify(settings.attribution) === JSON.stringify(base.attribution)) {
5762
- delete settings.attribution;
6316
+ const tokens = script.split(/\s+/);
6317
+ for (const token of tokens) {
6318
+ if (token === "--port" || token === "-p") return true;
6319
+ if (token.startsWith("--port=")) return true;
5763
6320
  }
5764
- if (base.worktree !== void 0 && settings.worktree !== void 0) {
5765
- const sw = settings.worktree;
5766
- for (const [k, v] of Object.entries(base.worktree)) {
5767
- if (sw[k] === v) {
5768
- delete sw[k];
5769
- }
5770
- }
5771
- if (Object.keys(sw).length === 0) {
5772
- delete settings.worktree;
5773
- }
6321
+ return false;
6322
+ }
6323
+ function labelMatchesAppName(label, appName) {
6324
+ if (!label || !appName) return false;
6325
+ const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
6326
+ const labelTokens = normalize(label).split(" ").filter(Boolean);
6327
+ const appToken = normalize(appName);
6328
+ if (!appToken) return false;
6329
+ const appTokens = appToken.split(" ").filter(Boolean);
6330
+ if (appTokens.length === 1) {
6331
+ return labelTokens.includes(appTokens[0]);
5774
6332
  }
5775
- if (base.permissions !== void 0 && settings.permissions !== void 0) {
5776
- const perms = settings.permissions;
5777
- if (base.permissions.defaultMode !== void 0 && perms.defaultMode === base.permissions.defaultMode) {
5778
- delete perms.defaultMode;
5779
- }
5780
- if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && perms.skipDangerousModePermissionPrompt === base.permissions.skipDangerousModePermissionPrompt) {
5781
- delete perms.skipDangerousModePermissionPrompt;
5782
- }
5783
- for (const key of ["deny", "ask", "allow"]) {
5784
- const baseList = base.permissions[key];
5785
- if (!baseList || baseList.length === 0) continue;
5786
- const current = perms[key];
5787
- if (!current) continue;
5788
- const baseSet = new Set(baseList);
5789
- const filtered = current.filter((x) => !baseSet.has(x));
5790
- if (filtered.length === 0) {
5791
- delete perms[key];
5792
- } else {
5793
- perms[key] = filtered;
5794
- }
5795
- }
5796
- if (Object.keys(perms).length === 0) {
5797
- delete settings.permissions;
5798
- }
6333
+ for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
6334
+ if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
5799
6335
  }
5800
- return settings;
6336
+ return false;
5801
6337
  }
5802
- function stripOwnedHooksFromSettings(settings) {
5803
- if (!settings.hooks) {
5804
- return settings;
6338
+ async function findUnallocatedApps(projectPath, portAllocations) {
6339
+ const apps = await discoverMonorepoApps(projectPath);
6340
+ if (apps.length === 0) {
6341
+ return [];
5805
6342
  }
5806
- for (const event of Object.keys(settings.hooks)) {
5807
- const eventBlocks = settings.hooks[event];
5808
- if (!eventBlocks) {
6343
+ const unallocated = [];
6344
+ for (const app of apps) {
6345
+ if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
5809
6346
  continue;
5810
6347
  }
5811
- const survivingBlocks = [];
5812
- for (const block of eventBlocks) {
5813
- block.hooks = block.hooks.filter((e) => e._owner !== OWNER);
5814
- if (block.hooks.length > 0) {
5815
- survivingBlocks.push(block);
5816
- }
5817
- }
5818
- if (survivingBlocks.length > 0) {
5819
- settings.hooks[event] = survivingBlocks;
5820
- } else {
5821
- delete settings.hooks[event];
6348
+ let pkg;
6349
+ try {
6350
+ const raw = await readFile16(`${app.absPath}/package.json`, "utf-8");
6351
+ pkg = JSON.parse(raw);
6352
+ } catch {
6353
+ continue;
5822
6354
  }
6355
+ if (!isDevServerScript(pkg)) continue;
6356
+ const framework = detectFramework(pkg);
6357
+ const detectedPort = detectPortFromScripts(pkg);
6358
+ const command = `pnpm --filter ${app.name} dev`;
6359
+ unallocated.push({
6360
+ name: app.name,
6361
+ path: app.path,
6362
+ framework,
6363
+ detectedPort,
6364
+ command
6365
+ });
5823
6366
  }
5824
- if (Object.keys(settings.hooks).length === 0) {
5825
- delete settings.hooks;
6367
+ return unallocated;
6368
+ }
6369
+ function getAppLabel(relativePath) {
6370
+ const parts = relativePath.split("/");
6371
+ if (parts.length >= 3 && parts[0] === "apps") {
6372
+ return parts[1];
5826
6373
  }
5827
- return settings;
6374
+ return "root";
5828
6375
  }
5829
- var OWNER, PLACEHOLDER_RE, REPLACEMENT, SCALAR_BASE_KEYS;
5830
- var init_settings_merge = __esm({
5831
- "src/lib/settings-merge.ts"() {
6376
+ var DEV_SERVER_BIN_PATTERNS;
6377
+ var init_port_verify = __esm({
6378
+ "src/lib/port-verify.ts"() {
5832
6379
  "use strict";
5833
- OWNER = "codebyplan-claude";
5834
- PLACEHOLDER_RE = /\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\//g;
5835
- REPLACEMENT = "./.claude/hooks/";
5836
- SCALAR_BASE_KEYS = [
5837
- "alwaysThinkingEnabled",
5838
- "autoUpdatesChannel",
5839
- "awaySummaryEnabled",
5840
- "disableAgentView",
5841
- "disableRemoteControl",
5842
- "editorMode",
5843
- "outputStyle",
5844
- "preferredNotifChannel",
5845
- "prefersReducedMotion",
5846
- "respectGitignore",
5847
- "showTurnDuration",
5848
- "spinnerTipsEnabled",
5849
- "terminalProgressBarEnabled",
5850
- "viewMode",
5851
- "autoScrollEnabled",
5852
- "cleanupPeriodDays",
5853
- "includeGitInstructions",
5854
- "showThinkingSummaries",
5855
- "disableSkillShellExecution",
5856
- "skipWebFetchPreflight",
5857
- "fastModePerSessionOptIn",
5858
- "effortLevel",
5859
- "showClearContextOnPlanAccept",
5860
- "syntaxHighlightingDisabled"
6380
+ init_tech_detect();
6381
+ init_server_detect();
6382
+ DEV_SERVER_BIN_PATTERNS = [
6383
+ /\bnext\s+dev\b/,
6384
+ /\bnest\s+start\b/,
6385
+ /\bvite\s+(?:dev|serve)\b/,
6386
+ /\bvite\s+preview\b/,
6387
+ /\bnuxt\s+dev\b/,
6388
+ /\b(?:svelte-kit|sveltekit)\s+dev\b/,
6389
+ /\bexpo\s+start\b/
5861
6390
  ];
5862
6391
  }
5863
6392
  });
5864
6393
 
5865
- // src/cli/claude/install.ts
5866
- var install_exports = {};
5867
- __export(install_exports, {
5868
- resolveTemplatesDir: () => resolveTemplatesDir,
5869
- runInstall: () => runInstall
6394
+ // src/cli/ports.ts
6395
+ var ports_exports = {};
6396
+ __export(ports_exports, {
6397
+ runPorts: () => runPorts
5870
6398
  });
5871
- import * as fs3 from "node:fs";
5872
- import * as os2 from "node:os";
5873
- import * as path4 from "node:path";
5874
- import { fileURLToPath } from "node:url";
5875
- function resolveTemplatesDir() {
5876
- const here = path4.dirname(fileURLToPath(import.meta.url));
5877
- const candidates = [
5878
- path4.resolve(here, "..", "templates"),
5879
- path4.resolve(here, "..", "..", "templates"),
5880
- path4.resolve(here, "..", "..", "..", "templates")
5881
- ];
5882
- for (const c of candidates) {
5883
- if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
5884
- return c;
5885
- }
5886
- }
5887
- throw new Error(
5888
- `codebyplan: could not locate templates/ directory. Probed:
5889
- ${candidates.join(
5890
- "\n "
5891
- )}`
5892
- );
5893
- }
5894
- async function runInstall(opts, deps = {}) {
5895
- await Promise.resolve();
5896
- const scope = opts.scope ?? "project";
5897
- if (scope === "user") {
5898
- if (opts.renderer) {
5899
- console.warn(
5900
- "codebyplan claude install: --bash/--node/--python is ignored for --scope user (no project root for statusline.local.json)."
5901
- );
5902
- }
5903
- runInstallUser(opts, deps);
5904
- return;
5905
- }
5906
- const projectDir = deps.projectDir ?? process.cwd();
5907
- let templatesDir;
6399
+ async function runPorts() {
6400
+ const flags = parseFlags(3);
6401
+ const dryRun = hasFlag("dry-run", 3);
6402
+ const fix = hasFlag("fix", 3);
6403
+ validateApiKey();
6404
+ const config = await resolveConfig(flags);
6405
+ const { repoId, projectPath } = config;
6406
+ console.log(`
6407
+ CodeByPlan Ports`);
6408
+ console.log(` Repo: ${repoId}`);
6409
+ console.log(` Path: ${projectPath}`);
6410
+ if (dryRun) console.log(` Mode: dry-run`);
6411
+ if (fix) console.log(` Mode: fix`);
6412
+ console.log();
5908
6413
  try {
5909
- templatesDir = deps.templatesDir ?? resolveTemplatesDir();
5910
- } catch (err) {
5911
- console.error(
5912
- err instanceof Error ? err.message : `codebyplan claude install: ${String(err)}`
6414
+ const portsRes = await apiGet(
6415
+ `/port-allocations`,
6416
+ { repo_id: repoId }
5913
6417
  );
5914
- process.exitCode = 1;
5915
- return;
5916
- }
5917
- try {
5918
- const files = walkTemplates(templatesDir);
5919
- const manifestEntries = [];
5920
- for (const f of files) {
5921
- const absDest = path4.join(projectDir, ".claude", f.dest);
5922
- const absSrc = path4.join(templatesDir, f.src);
5923
- if (opts.dryRun) {
5924
- if (opts.verbose) {
5925
- console.log(`[dry-run] would copy ${f.src} \u2192 .claude/${f.dest}`);
5926
- }
5927
- } else {
5928
- fs3.mkdirSync(path4.dirname(absDest), { recursive: true });
5929
- fs3.copyFileSync(absSrc, absDest);
5930
- if (opts.verbose) {
5931
- console.log(`copied ${f.src} \u2192 .claude/${f.dest}`);
5932
- }
6418
+ const allocations = portsRes.data ?? [];
6419
+ if (allocations.length === 0) {
6420
+ console.log(" No port allocations found \u2014 skipping verification.");
6421
+ console.log("\n Ports complete.\n");
6422
+ return;
6423
+ }
6424
+ const mismatches = await verifyPorts(projectPath, allocations);
6425
+ if (mismatches.length > 0) {
6426
+ console.log(` Port mismatches: ${mismatches.length}`);
6427
+ for (const m of mismatches) {
6428
+ console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
5933
6429
  }
5934
- manifestEntries.push({ src: f.src, dest: f.dest, hash: f.hash });
5935
6430
  }
5936
- const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
5937
- const baseSettingsPath = path4.join(
5938
- templatesDir,
5939
- "settings.project.base.json"
5940
- );
5941
- const hasHooks = fs3.existsSync(hooksJsonPath);
5942
- const hasBase = fs3.existsSync(baseSettingsPath);
5943
- if (hasHooks || hasBase) {
5944
- const settingsPath = path4.join(projectDir, ".claude", "settings.json");
5945
- const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5946
- if (hasBase) {
5947
- const base = JSON.parse(
5948
- fs3.readFileSync(baseSettingsPath, "utf8")
6431
+ const unallocated = await findUnallocatedApps(projectPath, allocations);
6432
+ if (unallocated.length > 0) {
6433
+ console.log(` Unallocated apps: ${unallocated.length}`);
6434
+ for (const app of unallocated) {
6435
+ console.log(
6436
+ ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
5949
6437
  );
5950
- mergeBaseSettingsIntoSettings(existingSettings, base);
5951
6438
  }
5952
- if (hasHooks) {
5953
- const hooksJson = JSON.parse(
5954
- fs3.readFileSync(hooksJsonPath, "utf8")
5955
- );
5956
- mergeHooksIntoSettings(existingSettings, hooksJson);
6439
+ if (fix && !dryRun) {
6440
+ const maxPort = Math.max(...allocations.map((a) => a.port), 2999);
6441
+ let nextPort = maxPort + 1;
6442
+ for (const app of unallocated) {
6443
+ const port = app.detectedPort ?? nextPort++;
6444
+ try {
6445
+ await apiPost("/port-allocations", {
6446
+ repo_id: repoId,
6447
+ port,
6448
+ label: app.name,
6449
+ server_type: app.framework,
6450
+ auto_start: "manual",
6451
+ command: app.command,
6452
+ working_dir: app.path
6453
+ });
6454
+ console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
6455
+ } catch (err) {
6456
+ const msg = err instanceof Error ? err.message : String(err);
6457
+ console.log(
6458
+ ` Failed to create allocation for ${app.name}: ${msg}`
6459
+ );
6460
+ }
6461
+ if (app.detectedPort && app.detectedPort >= nextPort) {
6462
+ nextPort = app.detectedPort + 1;
6463
+ }
6464
+ }
6465
+ } else if (fix && dryRun) {
6466
+ console.log(" (dry-run \u2014 would create allocations with --fix)");
6467
+ } else {
6468
+ console.log(" Run with --fix to auto-create allocations.");
5957
6469
  }
5958
- if (!opts.dryRun) {
5959
- fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5960
- fs3.writeFileSync(
5961
- settingsPath,
5962
- JSON.stringify(existingSettings, null, 2) + "\n",
5963
- "utf8"
5964
- );
5965
- } else if (opts.verbose) {
6470
+ }
6471
+ if (mismatches.length === 0 && unallocated.length === 0) {
6472
+ console.log(" Ports verified.");
6473
+ }
6474
+ } catch (err) {
6475
+ console.warn(
6476
+ ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
6477
+ );
6478
+ }
6479
+ console.log("\n Ports complete.\n");
6480
+ }
6481
+ var init_ports = __esm({
6482
+ "src/cli/ports.ts"() {
6483
+ "use strict";
6484
+ init_flags();
6485
+ init_api();
6486
+ init_port_verify();
6487
+ }
6488
+ });
6489
+
6490
+ // src/cli/tech-stack.ts
6491
+ var tech_stack_exports = {};
6492
+ __export(tech_stack_exports, {
6493
+ runFullTechStack: () => runFullTechStack,
6494
+ runTechStack: () => runTechStack
6495
+ });
6496
+ import { existsSync as existsSync4 } from "node:fs";
6497
+ async function runTechStack() {
6498
+ const flags = parseFlags(3);
6499
+ const dryRun = hasFlag("dry-run", 3);
6500
+ if (hasFlag("full-tech-stack", 3)) {
6501
+ await runFullTechStack(dryRun);
6502
+ return;
6503
+ }
6504
+ validateApiKey();
6505
+ const config = await resolveConfig(flags);
6506
+ const { repoId, projectPath } = config;
6507
+ console.log(`
6508
+ CodeByPlan Tech Stack`);
6509
+ console.log(` Repo: ${repoId}`);
6510
+ console.log(` Path: ${projectPath}`);
6511
+ if (dryRun) console.log(` Mode: dry-run`);
6512
+ console.log();
6513
+ if (dryRun) {
6514
+ try {
6515
+ if (await needsLocalMigration(projectPath)) {
5966
6516
  console.log(
5967
- `[dry-run] would merge settings into ${path4.relative(projectDir, settingsPath)}`
6517
+ ` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
5968
6518
  );
5969
6519
  }
6520
+ } catch {
5970
6521
  }
5971
- const gitignoreAction = await ensureManagedGitignoreBlock(
5972
- projectDir,
5973
- opts.dryRun
5974
- );
5975
- if (opts.verbose && gitignoreAction !== "unchanged") {
5976
- console.log(
5977
- `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path4.relative(projectDir, path4.join(projectDir, ".gitignore"))}`
6522
+ } else {
6523
+ try {
6524
+ if (await needsLocalMigration(projectPath)) {
6525
+ const result = await runLocalMigration(projectPath);
6526
+ console.log(
6527
+ ` Migrated .codebyplan.json to .codebyplan/ layout: ${result.summary}`
6528
+ );
6529
+ console.log(
6530
+ ` Suggest /cbp-git-commit to stage the cleaned shared file.`
6531
+ );
6532
+ }
6533
+ } catch (err) {
6534
+ console.warn(
6535
+ ` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
5978
6536
  );
5979
6537
  }
5980
- if (!opts.dryRun) {
5981
- const manifest = defaultManifest();
5982
- manifest.files = manifestEntries;
5983
- writeManifest(projectDir, manifest);
6538
+ }
6539
+ try {
6540
+ const { dependencies } = await scanAllDependencies(projectPath);
6541
+ if (dependencies.length === 0) {
6542
+ console.log(" No dependencies found.");
6543
+ console.log("\n Tech stack complete.\n");
6544
+ return;
5984
6545
  }
6546
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
5985
6547
  console.log(
5986
- `codebyplan claude install${opts.dryRun ? " (dry-run)" : ""}: ${manifestEntries.length} files, ${countHookEntries(templatesDir)} hook entries.`
6548
+ ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
5987
6549
  );
5988
- if (opts.renderer && !opts.dryRun) {
5989
- await writeStatuslineLocalConfig(projectDir, { renderer: opts.renderer });
6550
+ if (!dryRun) {
6551
+ const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
6552
+ if (result.data.stale_removed > 0) {
6553
+ console.log(
6554
+ ` ${result.data.stale_removed} stale dependencies removed`
6555
+ );
6556
+ }
6557
+ try {
6558
+ const { execSync: execSync6 } = await import("node:child_process");
6559
+ let branch = "main";
6560
+ try {
6561
+ branch = execSync6("git symbolic-ref --short HEAD", {
6562
+ cwd: projectPath,
6563
+ encoding: "utf-8"
6564
+ }).trim();
6565
+ } catch {
6566
+ }
6567
+ const deviceId = await getOrCreateDeviceId(projectPath);
6568
+ const tupleId = await resolveWorktreeId({
6569
+ repoId,
6570
+ repoPath: projectPath,
6571
+ branch,
6572
+ deviceId
6573
+ });
6574
+ await callMcpTool("enqueue_todo_job", {
6575
+ repo_id: repoId,
6576
+ ...tupleId ? { worktree_id: tupleId } : {},
6577
+ reason: "CLI_SYNC"
6578
+ });
6579
+ } catch {
6580
+ }
6581
+ }
6582
+ const detected = await detectTechStack(projectPath);
6583
+ if (detected.flat.length > 0) {
6584
+ const repoRes = await apiGet(`/repos/${repoId}`);
6585
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
6586
+ const { merged, added } = mergeTechStack(remote, detected);
6587
+ if (added.length > 0) {
6588
+ console.log(` ${added.length} new tech entries`);
6589
+ if (!dryRun) {
6590
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
6591
+ }
6592
+ }
5990
6593
  }
5991
6594
  } catch (err) {
5992
- console.error(
5993
- `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
6595
+ console.warn(
6596
+ ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
5994
6597
  );
5995
- process.exitCode = 1;
5996
6598
  }
6599
+ console.log("\n Tech stack complete.\n");
5997
6600
  }
5998
- function runInstallUser(opts, deps) {
5999
- let templatesDir;
6000
- try {
6001
- templatesDir = deps.templatesDir ?? resolveTemplatesDir();
6002
- } catch (err) {
6003
- console.error(
6004
- err instanceof Error ? err.message : `codebyplan claude install: ${String(err)}`
6005
- );
6006
- process.exitCode = 1;
6007
- return;
6008
- }
6601
+ async function syncTechStackForPath(repoId, projectPath, dryRun) {
6009
6602
  try {
6010
- const userDir = deps.userDir ?? path4.join(os2.homedir(), ".claude");
6011
- const settingsPath = path4.join(userDir, "settings.json");
6012
- const userBaseSettingsPath = path4.join(
6013
- templatesDir,
6014
- "settings.user.base.json"
6015
- );
6016
- if (!fs3.existsSync(userBaseSettingsPath)) {
6017
- console.error(
6018
- "codebyplan claude install: settings.user.base.json not found in templates."
6019
- );
6020
- process.exitCode = 1;
6603
+ const { dependencies } = await scanAllDependencies(projectPath);
6604
+ if (dependencies.length === 0) {
6605
+ console.log(" No dependencies found.");
6021
6606
  return;
6022
6607
  }
6023
- const userBase = JSON.parse(
6024
- fs3.readFileSync(userBaseSettingsPath, "utf8")
6025
- );
6026
- const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
6027
- mergeBaseSettingsIntoSettings(existingSettings, userBase);
6028
- if (!opts.dryRun) {
6029
- fs3.mkdirSync(userDir, { recursive: true });
6030
- fs3.writeFileSync(
6031
- settingsPath,
6032
- JSON.stringify(existingSettings, null, 2) + "\n",
6033
- "utf8"
6034
- );
6035
- const manifest = defaultManifest();
6036
- manifest.files = [];
6037
- writeManifestForScope("user", manifest, userDir);
6038
- } else if (opts.verbose) {
6039
- console.log(
6040
- `[dry-run] would merge user base settings into ${settingsPath}`
6041
- );
6042
- }
6608
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
6043
6609
  console.log(
6044
- `codebyplan claude install --scope user${opts.dryRun ? " (dry-run)" : ""}: settings.json updated, 0 template files copied.`
6610
+ ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
6045
6611
  );
6612
+ if (!dryRun) {
6613
+ const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
6614
+ if (result.data.stale_removed > 0) {
6615
+ console.log(
6616
+ ` ${result.data.stale_removed} stale dependencies removed`
6617
+ );
6618
+ }
6619
+ }
6620
+ const detected = await detectTechStack(projectPath);
6621
+ if (detected.flat.length > 0) {
6622
+ const repoRes = await apiGet(`/repos/${repoId}`);
6623
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
6624
+ const { merged, added } = mergeTechStack(remote, detected);
6625
+ if (added.length > 0) {
6626
+ console.log(` ${added.length} new tech entries`);
6627
+ if (!dryRun) {
6628
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
6629
+ }
6630
+ }
6631
+ }
6046
6632
  } catch (err) {
6633
+ console.warn(
6634
+ ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
6635
+ );
6636
+ }
6637
+ }
6638
+ async function runFullTechStack(dryRun) {
6639
+ validateApiKey();
6640
+ const localConfig = await readLocalConfig(process.cwd());
6641
+ if (!localConfig?.device_id) {
6047
6642
  console.error(
6048
- `codebyplan claude install failed: ${err instanceof Error ? err.message : String(err)}`
6643
+ " --full-tech-stack requires a device_id in .codebyplan/device.local.json.\n Run `npx codebyplan setup` in this directory first to register the device."
6049
6644
  );
6050
6645
  process.exitCode = 1;
6646
+ return;
6051
6647
  }
6052
- }
6053
- function countHookEntries(templatesDir) {
6054
- const p = path4.join(templatesDir, "hooks", "hooks.json");
6055
- if (!fs3.existsSync(p)) return 0;
6056
- try {
6057
- const j = JSON.parse(fs3.readFileSync(p, "utf8"));
6058
- let n = 0;
6059
- for (const blocks of Object.values(j.hooks)) {
6060
- for (const b of blocks) n += b.hooks.length;
6648
+ console.log("\n CodeByPlan Tech Stack \u2014 Full (all local worktrees)");
6649
+ if (dryRun) console.log(" Mode: dry-run");
6650
+ console.log();
6651
+ const reposRes = await apiGet("/repos");
6652
+ const repos = reposRes.data ?? [];
6653
+ let synced = 0;
6654
+ let skipped = 0;
6655
+ for (const repo of repos) {
6656
+ let worktrees = [];
6657
+ try {
6658
+ const wtRes = await apiGet(
6659
+ `/worktrees?repo_id=${repo.id}`
6660
+ );
6661
+ worktrees = wtRes.data ?? [];
6662
+ } catch (err) {
6663
+ console.warn(
6664
+ ` Warning: failed to fetch worktrees for ${repo.name}: ${err instanceof Error ? err.message : String(err)}`
6665
+ );
6666
+ continue;
6061
6667
  }
6062
- return n;
6063
- } catch (err) {
6064
- console.error(
6065
- `codebyplan: could not count hook entries in hooks.json: ${err instanceof Error ? err.message : String(err)}`
6668
+ const localWorktrees = worktrees.filter(
6669
+ (wt) => wt.path ? existsSync4(wt.path) : false
6066
6670
  );
6067
- return 0;
6671
+ if (localWorktrees.length === 0) {
6672
+ console.log(` skipping ${repo.name} \u2014 no local worktree on this device`);
6673
+ skipped++;
6674
+ continue;
6675
+ }
6676
+ for (const wt of localWorktrees) {
6677
+ console.log(` ==> Syncing ${repo.name} @ ${wt.path}`);
6678
+ try {
6679
+ await syncTechStackForPath(repo.id, wt.path, dryRun);
6680
+ synced++;
6681
+ } catch (err) {
6682
+ console.error(
6683
+ ` Error syncing tech stack for ${repo.name} @ ${wt.path}: ${err instanceof Error ? err.message : String(err)}`
6684
+ );
6685
+ }
6686
+ }
6068
6687
  }
6688
+ console.log(
6689
+ `
6690
+ Walked ${repos.length} repos, synced ${synced} local worktrees, skipped ${skipped} remote.
6691
+ `
6692
+ );
6069
6693
  }
6070
- var init_install = __esm({
6071
- "src/cli/claude/install.ts"() {
6694
+ var init_tech_stack = __esm({
6695
+ "src/cli/tech-stack.ts"() {
6072
6696
  "use strict";
6073
- init_template_walker();
6074
- init_gitignore_block();
6075
- init_manifest();
6076
- init_settings_merge();
6077
- init_statusline_config();
6697
+ init_flags();
6698
+ init_api();
6699
+ init_tech_detect();
6700
+ init_migrate_local_config();
6701
+ init_local_config();
6702
+ init_resolve_worktree();
6078
6703
  }
6079
6704
  });
6080
6705
 
@@ -6096,11 +6721,11 @@ async function ask(q, opts) {
6096
6721
  try {
6097
6722
  while (true) {
6098
6723
  const choices = q.choices.map((c) => `[${c.key}] ${c.label}`).join(" ");
6099
- const answer = await new Promise((resolve5) => {
6724
+ const answer = await new Promise((resolve7) => {
6100
6725
  rl.question(`${q.message}
6101
6726
  ${choices}
6102
6727
  > `, (input) => {
6103
- resolve5(input.trim().toLowerCase());
6728
+ resolve7(input.trim().toLowerCase());
6104
6729
  });
6105
6730
  });
6106
6731
  const match = q.choices.find(
@@ -6194,9 +6819,9 @@ var update_exports = {};
6194
6819
  __export(update_exports, {
6195
6820
  runUpdate: () => runUpdate
6196
6821
  });
6197
- import * as fs4 from "node:fs";
6822
+ import * as fs5 from "node:fs";
6198
6823
  import * as os3 from "node:os";
6199
- import * as path5 from "node:path";
6824
+ import * as path6 from "node:path";
6200
6825
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6201
6826
  async function runUpdate(opts, deps = {}) {
6202
6827
  await Promise.resolve();
@@ -6236,10 +6861,10 @@ async function runUpdate(opts, deps = {}) {
6236
6861
  finalManifestEntries.push(e);
6237
6862
  }
6238
6863
  for (const { packaged, absSrc } of plan.overwriteSafe) {
6239
- const absDest = path5.join(projectDir, ".claude", packaged.dest);
6864
+ const absDest = path6.join(projectDir, ".claude", packaged.dest);
6240
6865
  if (!opts.dryRun) {
6241
- fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6242
- fs4.copyFileSync(absSrc, absDest);
6866
+ fs5.mkdirSync(path6.dirname(absDest), { recursive: true });
6867
+ fs5.copyFileSync(absSrc, absDest);
6243
6868
  if (opts.verbose) console.log(`updated ${packaged.dest}`);
6244
6869
  } else if (opts.verbose) {
6245
6870
  console.log(`[dry-run] would update ${packaged.dest}`);
@@ -6251,8 +6876,8 @@ async function runUpdate(opts, deps = {}) {
6251
6876
  absSrc,
6252
6877
  onDiskContent
6253
6878
  } of plan.overwriteHandEdited) {
6254
- const absDest = path5.join(projectDir, ".claude", packaged.dest);
6255
- const newContent = fs4.readFileSync(absSrc);
6879
+ const absDest = path6.join(projectDir, ".claude", packaged.dest);
6880
+ const newContent = fs5.readFileSync(absSrc);
6256
6881
  const showDiff = () => {
6257
6882
  console.log(
6258
6883
  renderDiff(
@@ -6264,8 +6889,8 @@ async function runUpdate(opts, deps = {}) {
6264
6889
  const answer = await promptOverwrite(packaged.dest, opts, showDiff);
6265
6890
  if (answer === "overwrite") {
6266
6891
  if (!opts.dryRun) {
6267
- fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6268
- fs4.copyFileSync(absSrc, absDest);
6892
+ fs5.mkdirSync(path6.dirname(absDest), { recursive: true });
6893
+ fs5.copyFileSync(absSrc, absDest);
6269
6894
  }
6270
6895
  finalManifestEntries.push(packaged);
6271
6896
  } else {
@@ -6280,10 +6905,10 @@ async function runUpdate(opts, deps = {}) {
6280
6905
  for (const { packaged, absSrc } of plan.newOptIn) {
6281
6906
  const answer = await promptOptIn(packaged.dest, opts);
6282
6907
  if (answer === "opt-in") {
6283
- const absDest = path5.join(projectDir, ".claude", packaged.dest);
6908
+ const absDest = path6.join(projectDir, ".claude", packaged.dest);
6284
6909
  if (!opts.dryRun) {
6285
- fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6286
- fs4.copyFileSync(absSrc, absDest);
6910
+ fs5.mkdirSync(path6.dirname(absDest), { recursive: true });
6911
+ fs5.copyFileSync(absSrc, absDest);
6287
6912
  }
6288
6913
  finalManifestEntries.push(packaged);
6289
6914
  if (opts.verbose) console.log(`installed new file ${packaged.dest}`);
@@ -6294,25 +6919,25 @@ async function runUpdate(opts, deps = {}) {
6294
6919
  for (const e of plan.removedFromPackage) {
6295
6920
  const answer = await promptRemove(e.dest, opts);
6296
6921
  if (answer === "remove") {
6297
- const absDest = path5.join(projectDir, ".claude", e.dest);
6298
- if (!opts.dryRun && fs4.existsSync(absDest)) {
6299
- fs4.rmSync(absDest);
6300
- const claudeDir = path5.join(projectDir, ".claude");
6301
- let cur = path5.dirname(absDest);
6302
- while (cur !== claudeDir && cur !== path5.dirname(cur)) {
6303
- if (path5.dirname(cur) === claudeDir) break;
6922
+ const absDest = path6.join(projectDir, ".claude", e.dest);
6923
+ if (!opts.dryRun && fs5.existsSync(absDest)) {
6924
+ fs5.rmSync(absDest);
6925
+ const claudeDir = path6.join(projectDir, ".claude");
6926
+ let cur = path6.dirname(absDest);
6927
+ while (cur !== claudeDir && cur !== path6.dirname(cur)) {
6928
+ if (path6.dirname(cur) === claudeDir) break;
6304
6929
  try {
6305
- fs4.rmdirSync(cur);
6930
+ fs5.rmdirSync(cur);
6306
6931
  if (opts.verbose)
6307
6932
  console.log(
6308
- `pruned empty dir ${path5.relative(claudeDir, cur)}`
6933
+ `pruned empty dir ${path6.relative(claudeDir, cur)}`
6309
6934
  );
6310
- cur = path5.dirname(cur);
6935
+ cur = path6.dirname(cur);
6311
6936
  } catch (err) {
6312
6937
  const code = err.code;
6313
6938
  if (code !== "ENOTEMPTY" && code !== "ENOENT") {
6314
6939
  console.warn(
6315
- `codebyplan claude: could not prune empty dir ${path5.relative(claudeDir, cur)}: ${err.message}`
6940
+ `codebyplan claude: could not prune empty dir ${path6.relative(claudeDir, cur)}: ${err.message}`
6316
6941
  );
6317
6942
  }
6318
6943
  break;
@@ -6324,17 +6949,17 @@ async function runUpdate(opts, deps = {}) {
6324
6949
  if (opts.verbose) console.log(`kept (untracked) ${e.dest}`);
6325
6950
  }
6326
6951
  }
6327
- const hooksJsonPath = path5.join(templatesDir, "hooks", "hooks.json");
6328
- if (fs4.existsSync(hooksJsonPath)) {
6952
+ const hooksJsonPath = path6.join(templatesDir, "hooks", "hooks.json");
6953
+ if (fs5.existsSync(hooksJsonPath)) {
6329
6954
  const hooksJson = JSON.parse(
6330
- fs4.readFileSync(hooksJsonPath, "utf8")
6955
+ fs5.readFileSync(hooksJsonPath, "utf8")
6331
6956
  );
6332
- const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6333
- const existingSettings = fs4.existsSync(settingsPath) ? JSON.parse(fs4.readFileSync(settingsPath, "utf8")) : {};
6957
+ const settingsPath = path6.join(projectDir, ".claude", "settings.json");
6958
+ const existingSettings = fs5.existsSync(settingsPath) ? JSON.parse(fs5.readFileSync(settingsPath, "utf8")) : {};
6334
6959
  mergeHooksIntoSettings(existingSettings, hooksJson);
6335
6960
  if (!opts.dryRun) {
6336
- fs4.mkdirSync(path5.dirname(settingsPath), { recursive: true });
6337
- fs4.writeFileSync(
6961
+ fs5.mkdirSync(path6.dirname(settingsPath), { recursive: true });
6962
+ fs5.writeFileSync(
6338
6963
  settingsPath,
6339
6964
  JSON.stringify(existingSettings, null, 2) + "\n",
6340
6965
  "utf8"
@@ -6347,7 +6972,7 @@ async function runUpdate(opts, deps = {}) {
6347
6972
  );
6348
6973
  if (opts.verbose && gitignoreAction !== "unchanged") {
6349
6974
  console.log(
6350
- `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path5.relative(projectDir, path5.join(projectDir, ".gitignore"))}`
6975
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path6.relative(projectDir, path6.join(projectDir, ".gitignore"))}`
6351
6976
  );
6352
6977
  }
6353
6978
  if (!opts.dryRun) {
@@ -6382,9 +7007,9 @@ function runUpdateUser(opts, deps) {
6382
7007
  return;
6383
7008
  }
6384
7009
  try {
6385
- const userDir = deps.userDir ?? path5.join(os3.homedir(), ".claude");
6386
- const settingsPath = path5.join(userDir, "settings.json");
6387
- const userBaseSettingsPath = path5.join(
7010
+ const userDir = deps.userDir ?? path6.join(os3.homedir(), ".claude");
7011
+ const settingsPath = path6.join(userDir, "settings.json");
7012
+ const userBaseSettingsPath = path6.join(
6388
7013
  templatesDir,
6389
7014
  "settings.user.base.json"
6390
7015
  );
@@ -6396,7 +7021,7 @@ function runUpdateUser(opts, deps) {
6396
7021
  process.exitCode = 1;
6397
7022
  return;
6398
7023
  }
6399
- if (!fs4.existsSync(userBaseSettingsPath)) {
7024
+ if (!fs5.existsSync(userBaseSettingsPath)) {
6400
7025
  console.error(
6401
7026
  "codebyplan claude update: settings.user.base.json not found in templates."
6402
7027
  );
@@ -6404,13 +7029,13 @@ function runUpdateUser(opts, deps) {
6404
7029
  return;
6405
7030
  }
6406
7031
  const userBase = JSON.parse(
6407
- fs4.readFileSync(userBaseSettingsPath, "utf8")
7032
+ fs5.readFileSync(userBaseSettingsPath, "utf8")
6408
7033
  );
6409
- const existingSettings = fs4.existsSync(settingsPath) ? JSON.parse(fs4.readFileSync(settingsPath, "utf8")) : {};
7034
+ const existingSettings = fs5.existsSync(settingsPath) ? JSON.parse(fs5.readFileSync(settingsPath, "utf8")) : {};
6410
7035
  mergeBaseSettingsIntoSettings(existingSettings, userBase);
6411
7036
  if (!opts.dryRun) {
6412
- fs4.mkdirSync(userDir, { recursive: true });
6413
- fs4.writeFileSync(
7037
+ fs5.mkdirSync(userDir, { recursive: true });
7038
+ fs5.writeFileSync(
6414
7039
  settingsPath,
6415
7040
  JSON.stringify(existingSettings, null, 2) + "\n",
6416
7041
  "utf8"
@@ -6446,8 +7071,8 @@ function buildPlan(projectDir, templatesDir, manifest) {
6446
7071
  };
6447
7072
  for (const pkg of packaged) {
6448
7073
  const inManifest = manifestBySrc.get(pkg.src);
6449
- const absDest = path5.join(projectDir, ".claude", pkg.dest);
6450
- const absSrc = path5.join(templatesDir, pkg.src);
7074
+ const absDest = path6.join(projectDir, ".claude", pkg.dest);
7075
+ const absSrc = path6.join(templatesDir, pkg.src);
6451
7076
  if (!inManifest) {
6452
7077
  plan.newOptIn.push({
6453
7078
  packaged: { src: pkg.src, dest: pkg.dest, hash: pkg.hash },
@@ -6455,8 +7080,8 @@ function buildPlan(projectDir, templatesDir, manifest) {
6455
7080
  });
6456
7081
  continue;
6457
7082
  }
6458
- const onDiskExists = fs4.existsSync(absDest);
6459
- const onDiskContent = onDiskExists ? fs4.readFileSync(absDest) : Buffer.alloc(0);
7083
+ const onDiskExists = fs5.existsSync(absDest);
7084
+ const onDiskContent = onDiskExists ? fs5.readFileSync(absDest) : Buffer.alloc(0);
6460
7085
  const onDiskHash = onDiskExists ? sha256(onDiskContent) : null;
6461
7086
  if (pkg.hash === inManifest.hash && onDiskHash === inManifest.hash) {
6462
7087
  plan.unchanged.push(inManifest);
@@ -6483,14 +7108,14 @@ function buildPlan(projectDir, templatesDir, manifest) {
6483
7108
  return plan;
6484
7109
  }
6485
7110
  function resolveTemplatesDirFromInstall() {
6486
- const here = path5.dirname(fileURLToPath2(import.meta.url));
7111
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
6487
7112
  const candidates = [
6488
- path5.resolve(here, "..", "templates"),
6489
- path5.resolve(here, "..", "..", "templates"),
6490
- path5.resolve(here, "..", "..", "..", "templates")
7113
+ path6.resolve(here, "..", "templates"),
7114
+ path6.resolve(here, "..", "..", "templates"),
7115
+ path6.resolve(here, "..", "..", "..", "templates")
6491
7116
  ];
6492
7117
  for (const c of candidates) {
6493
- if (fs4.existsSync(c) && fs4.statSync(c).isDirectory()) {
7118
+ if (fs5.existsSync(c) && fs5.statSync(c).isDirectory()) {
6494
7119
  return c;
6495
7120
  }
6496
7121
  }
@@ -6519,9 +7144,9 @@ var uninstall_exports = {};
6519
7144
  __export(uninstall_exports, {
6520
7145
  runUninstall: () => runUninstall
6521
7146
  });
6522
- import * as fs5 from "node:fs";
7147
+ import * as fs6 from "node:fs";
6523
7148
  import * as os4 from "node:os";
6524
- import * as path6 from "node:path";
7149
+ import * as path7 from "node:path";
6525
7150
  async function runUninstall(opts, deps = {}) {
6526
7151
  await Promise.resolve();
6527
7152
  const scope = opts.scope ?? "project";
@@ -6550,15 +7175,15 @@ async function runUninstall(opts, deps = {}) {
6550
7175
  let removed = 0;
6551
7176
  let warnings = 0;
6552
7177
  for (const entry of manifest.files) {
6553
- const abs = path6.join(projectDir, ".claude", entry.dest);
6554
- if (!fs5.existsSync(abs)) {
7178
+ const abs = path7.join(projectDir, ".claude", entry.dest);
7179
+ if (!fs6.existsSync(abs)) {
6555
7180
  console.warn(
6556
7181
  `codebyplan claude uninstall: ${entry.dest} already absent (skipping).`
6557
7182
  );
6558
7183
  warnings += 1;
6559
7184
  continue;
6560
7185
  }
6561
- const onDiskHash = sha256(fs5.readFileSync(abs));
7186
+ const onDiskHash = sha256(fs6.readFileSync(abs));
6562
7187
  if (onDiskHash !== entry.hash) {
6563
7188
  console.warn(
6564
7189
  `codebyplan claude uninstall: ${entry.dest} has been modified locally; removing anyway.`
@@ -6566,7 +7191,7 @@ async function runUninstall(opts, deps = {}) {
6566
7191
  warnings += 1;
6567
7192
  }
6568
7193
  if (!opts.dryRun) {
6569
- fs5.rmSync(abs);
7194
+ fs6.rmSync(abs);
6570
7195
  }
6571
7196
  removed += 1;
6572
7197
  if (opts.verbose) console.log(`removed ${entry.dest}`);
@@ -6574,15 +7199,15 @@ async function runUninstall(opts, deps = {}) {
6574
7199
  if (!opts.dryRun) {
6575
7200
  pruneEmptyManagedDirs(projectDir);
6576
7201
  }
6577
- const settingsPath = path6.join(projectDir, ".claude", "settings.json");
6578
- if (fs5.existsSync(settingsPath)) {
7202
+ const settingsPath = path7.join(projectDir, ".claude", "settings.json");
7203
+ if (fs6.existsSync(settingsPath)) {
6579
7204
  const settings = JSON.parse(
6580
- fs5.readFileSync(settingsPath, "utf8")
7205
+ fs6.readFileSync(settingsPath, "utf8")
6581
7206
  );
6582
- const baseSettingsPath = templatesDir ? path6.join(templatesDir, "settings.project.base.json") : null;
6583
- if (baseSettingsPath && fs5.existsSync(baseSettingsPath)) {
7207
+ const baseSettingsPath = templatesDir ? path7.join(templatesDir, "settings.project.base.json") : null;
7208
+ if (baseSettingsPath && fs6.existsSync(baseSettingsPath)) {
6584
7209
  const base = JSON.parse(
6585
- fs5.readFileSync(baseSettingsPath, "utf8")
7210
+ fs6.readFileSync(baseSettingsPath, "utf8")
6586
7211
  );
6587
7212
  stripBaseSettingsFromSettings(settings, base);
6588
7213
  }
@@ -6590,9 +7215,9 @@ async function runUninstall(opts, deps = {}) {
6590
7215
  if (!opts.dryRun) {
6591
7216
  const isEmpty = Object.keys(settings).length === 0;
6592
7217
  if (isEmpty) {
6593
- fs5.rmSync(settingsPath);
7218
+ fs6.rmSync(settingsPath);
6594
7219
  } else {
6595
- fs5.writeFileSync(
7220
+ fs6.writeFileSync(
6596
7221
  settingsPath,
6597
7222
  JSON.stringify(settings, null, 2) + "\n",
6598
7223
  "utf8"
@@ -6611,11 +7236,11 @@ async function runUninstall(opts, deps = {}) {
6611
7236
  }
6612
7237
  if (!opts.dryRun) {
6613
7238
  const m = manifestPath(projectDir);
6614
- if (fs5.existsSync(m)) fs5.rmSync(m);
7239
+ if (fs6.existsSync(m)) fs6.rmSync(m);
6615
7240
  const mid = midManifestPath(projectDir);
6616
- if (fs5.existsSync(mid)) fs5.rmSync(mid);
7241
+ if (fs6.existsSync(mid)) fs6.rmSync(mid);
6617
7242
  const legacy = oldManifestPath(projectDir);
6618
- if (fs5.existsSync(legacy)) fs5.rmSync(legacy);
7243
+ if (fs6.existsSync(legacy)) fs6.rmSync(legacy);
6619
7244
  }
6620
7245
  console.log(
6621
7246
  `codebyplan claude uninstall${opts.dryRun ? " (dry-run)" : ""}: removed ${removed} files${warnings > 0 ? ` (${warnings} warnings)` : ""}.`
@@ -6637,7 +7262,7 @@ function runUninstallUser(opts, deps) {
6637
7262
  }
6638
7263
  }
6639
7264
  try {
6640
- const userDir = deps.userDir ?? path6.join(os4.homedir(), ".claude");
7265
+ const userDir = deps.userDir ?? path7.join(os4.homedir(), ".claude");
6641
7266
  const existingManifest = readManifestForScope("user", userDir);
6642
7267
  if (!existingManifest) {
6643
7268
  console.error(
@@ -6646,24 +7271,24 @@ function runUninstallUser(opts, deps) {
6646
7271
  process.exitCode = 1;
6647
7272
  return;
6648
7273
  }
6649
- const settingsPath = path6.join(userDir, "settings.json");
6650
- if (fs5.existsSync(settingsPath)) {
7274
+ const settingsPath = path7.join(userDir, "settings.json");
7275
+ if (fs6.existsSync(settingsPath)) {
6651
7276
  const settings = JSON.parse(
6652
- fs5.readFileSync(settingsPath, "utf8")
7277
+ fs6.readFileSync(settingsPath, "utf8")
6653
7278
  );
6654
- const userBaseSettingsPath = templatesDir != null ? path6.join(templatesDir, "settings.user.base.json") : null;
6655
- if (userBaseSettingsPath && fs5.existsSync(userBaseSettingsPath)) {
7279
+ const userBaseSettingsPath = templatesDir != null ? path7.join(templatesDir, "settings.user.base.json") : null;
7280
+ if (userBaseSettingsPath && fs6.existsSync(userBaseSettingsPath)) {
6656
7281
  const userBase = JSON.parse(
6657
- fs5.readFileSync(userBaseSettingsPath, "utf8")
7282
+ fs6.readFileSync(userBaseSettingsPath, "utf8")
6658
7283
  );
6659
7284
  stripBaseSettingsFromSettings(settings, userBase);
6660
7285
  }
6661
7286
  if (!opts.dryRun) {
6662
7287
  const isEmpty = Object.keys(settings).length === 0;
6663
7288
  if (isEmpty) {
6664
- fs5.rmSync(settingsPath);
7289
+ fs6.rmSync(settingsPath);
6665
7290
  } else {
6666
- fs5.writeFileSync(
7291
+ fs6.writeFileSync(
6667
7292
  settingsPath,
6668
7293
  JSON.stringify(settings, null, 2) + "\n",
6669
7294
  "utf8"
@@ -6673,11 +7298,11 @@ function runUninstallUser(opts, deps) {
6673
7298
  }
6674
7299
  if (!opts.dryRun) {
6675
7300
  const m = userManifestPath(userDir);
6676
- if (fs5.existsSync(m)) fs5.rmSync(m);
7301
+ if (fs6.existsSync(m)) fs6.rmSync(m);
6677
7302
  const midUser = userMidManifestPath(userDir);
6678
- if (fs5.existsSync(midUser)) fs5.rmSync(midUser);
7303
+ if (fs6.existsSync(midUser)) fs6.rmSync(midUser);
6679
7304
  const oldUser = userOldManifestPath(userDir);
6680
- if (fs5.existsSync(oldUser)) fs5.rmSync(oldUser);
7305
+ if (fs6.existsSync(oldUser)) fs6.rmSync(oldUser);
6681
7306
  }
6682
7307
  console.log(
6683
7308
  `codebyplan claude uninstall --scope user${opts.dryRun ? " (dry-run)" : ""}: user base settings stripped.`
@@ -6692,23 +7317,23 @@ function runUninstallUser(opts, deps) {
6692
7317
  function pruneEmptyManagedDirs(projectDir) {
6693
7318
  const managedRoots = ["skills", "agents", "hooks", "rules"];
6694
7319
  for (const root of managedRoots) {
6695
- const abs = path6.join(projectDir, ".claude", root);
6696
- if (!fs5.existsSync(abs)) continue;
7320
+ const abs = path7.join(projectDir, ".claude", root);
7321
+ if (!fs6.existsSync(abs)) continue;
6697
7322
  pruneLeafFirst(abs);
6698
7323
  }
6699
7324
  }
6700
7325
  function pruneLeafFirst(dir) {
6701
- if (!fs5.existsSync(dir)) return;
6702
- const stat2 = fs5.statSync(dir);
7326
+ if (!fs6.existsSync(dir)) return;
7327
+ const stat2 = fs6.statSync(dir);
6703
7328
  if (!stat2.isDirectory()) return;
6704
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
7329
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
6705
7330
  if (entry.isDirectory()) {
6706
- pruneLeafFirst(path6.join(dir, entry.name));
7331
+ pruneLeafFirst(path7.join(dir, entry.name));
6707
7332
  }
6708
7333
  }
6709
- const remaining = fs5.readdirSync(dir);
7334
+ const remaining = fs6.readdirSync(dir);
6710
7335
  if (remaining.length === 0) {
6711
- fs5.rmdirSync(dir);
7336
+ fs6.rmdirSync(dir);
6712
7337
  }
6713
7338
  }
6714
7339
  var init_uninstall = __esm({
@@ -6724,13 +7349,13 @@ var init_uninstall = __esm({
6724
7349
 
6725
7350
  // src/index.ts
6726
7351
  init_version();
6727
- import { readFileSync as readFileSync6 } from "node:fs";
6728
- import { resolve as resolve4 } from "node:path";
7352
+ import { readFileSync as readFileSync7 } from "node:fs";
7353
+ import { resolve as resolve6 } from "node:path";
6729
7354
  void (async () => {
6730
7355
  if (!process.env.CODEBYPLAN_API_KEY) {
6731
7356
  try {
6732
- const envPath = resolve4(process.cwd(), ".env.local");
6733
- const content = readFileSync6(envPath, "utf-8");
7357
+ const envPath = resolve6(process.cwd(), ".env.local");
7358
+ const content = readFileSync7(envPath, "utf-8");
6734
7359
  for (const line of content.split("\n")) {
6735
7360
  const trimmed = line.trim();
6736
7361
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -6820,12 +7445,23 @@ void (async () => {
6820
7445
  await runBranchCommand2(rest);
6821
7446
  process.exit(0);
6822
7447
  }
7448
+ if (arg === "bump") {
7449
+ const { runBumpCommand: runBumpCommand2 } = await Promise.resolve().then(() => (init_bump2(), bump_exports));
7450
+ const rest = process.argv.slice(3);
7451
+ await runBumpCommand2(rest);
7452
+ process.exit(0);
7453
+ }
6823
7454
  if (arg === "ship") {
6824
7455
  const { runShipCommand: runShipCommand2 } = await Promise.resolve().then(() => (init_ship2(), ship_exports));
6825
7456
  const rest = process.argv.slice(3);
6826
7457
  await runShipCommand2(rest);
6827
7458
  process.exit(0);
6828
7459
  }
7460
+ if (arg === "scaffold-publish-workflow") {
7461
+ const { runScaffoldPublishWorkflowCommand: runScaffoldPublishWorkflowCommand2 } = await Promise.resolve().then(() => (init_scaffold_publish_workflow2(), scaffold_publish_workflow_exports));
7462
+ await runScaffoldPublishWorkflowCommand2(process.argv.slice(3));
7463
+ process.exit(process.exitCode ?? 0);
7464
+ }
6829
7465
  if (arg === "resolve-worktree") {
6830
7466
  const { runResolveWorktree: runResolveWorktree2 } = await Promise.resolve().then(() => (init_resolve_worktree2(), resolve_worktree_exports));
6831
7467
  await runResolveWorktree2();
@@ -6922,7 +7558,9 @@ void (async () => {
6922
7558
  (--full-tech-stack: sync every local worktree on this device)
6923
7559
  codebyplan eslint ESLint config management (init)
6924
7560
  codebyplan round sync-approvals Sync git diff and approvals with round/task state
7561
+ codebyplan bump Detect changed packages and patch-bump versions
6925
7562
  codebyplan ship Ship current feat branch to production via PR
7563
+ codebyplan scaffold-publish-workflow Write the publish-on-main GitHub workflow into ./.github/workflows/
6926
7564
  codebyplan branch migrate Rewrite branch_config from 3-branch to 2-tier model
6927
7565
  codebyplan claude Claude asset management (install/update/uninstall)
6928
7566
  codebyplan statusline Show or set the statusline renderer (bash/node/python)
@@ -6969,8 +7607,6 @@ void (async () => {
6969
7607
  URL: https://mcp.codebyplan.com/mcp
6970
7608
  Auth: OAuth 2.1 (configure via \`codebyplan login\`)
6971
7609
 
6972
- Legacy x-api-key at https://www.codebyplan.com/mcp is supported until 2026-06-30.
6973
-
6974
7610
  Learn more: https://codebyplan.com
6975
7611
  `);
6976
7612
  process.exit(0);