codebyplan 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +509 -157
  3. package/package.json +2 -1
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.4.2";
17
+ VERSION = "1.5.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -143,6 +143,159 @@ var init_api = __esm({
143
143
  }
144
144
  });
145
145
 
146
+ // src/lib/resolve-worktree.ts
147
+ import { readFile, writeFile } from "node:fs/promises";
148
+ import { join } from "node:path";
149
+ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
150
+ let worktreeId;
151
+ try {
152
+ const worktreesRes = await apiGet(`/worktrees?repo_id=${repoId}`);
153
+ const match = worktreesRes.data.find((wt) => {
154
+ const wtPath = wt.path.endsWith("/") ? wt.path.slice(0, -1) : wt.path;
155
+ return projectPath === wtPath || projectPath.startsWith(wtPath + "/");
156
+ });
157
+ if (match) worktreeId = match.id;
158
+ } catch (err) {
159
+ console.error(
160
+ `Worktree lookup failed: ${err instanceof Error ? err.message : String(err)}`
161
+ );
162
+ return void 0;
163
+ }
164
+ if (!worktreeId) {
165
+ return void 0;
166
+ }
167
+ if (options?.skipWrite) {
168
+ return worktreeId;
169
+ }
170
+ const codebyplanPath = join(projectPath, ".codebyplan.json");
171
+ let currentConfig = {};
172
+ try {
173
+ const raw = await readFile(codebyplanPath, "utf-8");
174
+ const parsed = JSON.parse(raw);
175
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
176
+ currentConfig = parsed;
177
+ }
178
+ } catch {
179
+ }
180
+ if (currentConfig.worktree_id === worktreeId) {
181
+ return worktreeId;
182
+ }
183
+ const merged = {
184
+ ...currentConfig,
185
+ worktree_id: worktreeId
186
+ };
187
+ try {
188
+ await writeFile(
189
+ codebyplanPath,
190
+ JSON.stringify(merged, null, 2) + "\n",
191
+ "utf-8"
192
+ );
193
+ } catch (err) {
194
+ console.error(
195
+ `Failed to cache worktree_id in ${codebyplanPath}: ${err instanceof Error ? err.message : String(err)}`
196
+ );
197
+ throw err;
198
+ }
199
+ return worktreeId;
200
+ }
201
+ async function resolveWorktreeId({
202
+ repoId,
203
+ repoPath,
204
+ branch,
205
+ deviceId
206
+ }) {
207
+ try {
208
+ const res = await apiPost(
209
+ "/worktrees/resolve",
210
+ { repo_id: repoId, device_id: deviceId, repo_path: repoPath, branch }
211
+ );
212
+ return res.worktree_id ?? null;
213
+ } catch (err) {
214
+ console.error(
215
+ `Tuple worktree resolve failed: ${err instanceof Error ? err.message : String(err)}`
216
+ );
217
+ return null;
218
+ }
219
+ }
220
+ var init_resolve_worktree = __esm({
221
+ "src/lib/resolve-worktree.ts"() {
222
+ "use strict";
223
+ init_api();
224
+ }
225
+ });
226
+
227
+ // src/lib/local-config.ts
228
+ import { execSync } from "node:child_process";
229
+ import { createHash } from "node:crypto";
230
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
231
+ import { hostname } from "node:os";
232
+ import { join as join2 } from "node:path";
233
+ function localConfigPath(projectPath) {
234
+ return join2(projectPath, ".codebyplan.local.json");
235
+ }
236
+ async function readLocalConfig(projectPath) {
237
+ try {
238
+ const raw = await readFile2(localConfigPath(projectPath), "utf-8");
239
+ const parsed = JSON.parse(raw);
240
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
241
+ return parsed;
242
+ }
243
+ console.error("Failed to read local config: invalid shape");
244
+ return null;
245
+ } catch (err) {
246
+ console.error(
247
+ `Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
248
+ );
249
+ return null;
250
+ }
251
+ }
252
+ async function writeLocalConfig(projectPath, config) {
253
+ const content = { device_id: config.device_id };
254
+ try {
255
+ await writeFile2(
256
+ localConfigPath(projectPath),
257
+ JSON.stringify(content, null, 2) + "\n",
258
+ "utf-8"
259
+ );
260
+ } catch (err) {
261
+ console.error(
262
+ `Failed to write local config: ${err instanceof Error ? err.message : String(err)}`
263
+ );
264
+ throw err;
265
+ }
266
+ }
267
+ async function resolveMachineSeed() {
268
+ try {
269
+ const raw = await readFile2("/etc/machine-id", "utf-8");
270
+ const trimmed = raw.trim();
271
+ if (trimmed) return trimmed;
272
+ } catch {
273
+ }
274
+ if (process.platform === "darwin") {
275
+ try {
276
+ const out = execSync("sysctl -n kern.uuid", { encoding: "utf-8" }).trim();
277
+ if (out) return out;
278
+ } catch {
279
+ }
280
+ }
281
+ return hostname();
282
+ }
283
+ async function getOrCreateDeviceId(projectPath) {
284
+ const existing = await readLocalConfig(projectPath);
285
+ if (existing?.device_id) {
286
+ return existing.device_id;
287
+ }
288
+ const seed = await resolveMachineSeed();
289
+ const deviceId = createHash("sha256").update(seed).digest("hex").slice(0, 16);
290
+ await writeLocalConfig(projectPath, { device_id: deviceId });
291
+ return deviceId;
292
+ }
293
+ var init_local_config = __esm({
294
+ "src/lib/local-config.ts"() {
295
+ "use strict";
296
+ }
297
+ });
298
+
146
299
  // src/lib/settings-merge.ts
147
300
  function mergeSettings(template, local) {
148
301
  const merged = { ...local };
@@ -208,8 +361,8 @@ var init_settings_merge = __esm({
208
361
  });
209
362
 
210
363
  // src/lib/hook-registry.ts
211
- import { readdir, readFile } from "node:fs/promises";
212
- import { join } from "node:path";
364
+ import { readdir, readFile as readFile3 } from "node:fs/promises";
365
+ import { join as join3 } from "node:path";
213
366
  function parseHookMeta(content) {
214
367
  const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
215
368
  if (!lineMatch) return null;
@@ -231,7 +384,7 @@ async function discoverHooks(hooksDir) {
231
384
  return discovered;
232
385
  }
233
386
  for (const filename of filenames) {
234
- const content = await readFile(join(hooksDir, filename), "utf-8");
387
+ const content = await readFile3(join3(hooksDir, filename), "utf-8");
235
388
  const meta = parseHookMeta(content);
236
389
  if (meta) {
237
390
  discovered.set(filename.replace(/\.sh$/, ""), meta);
@@ -378,45 +531,45 @@ __export(sync_engine_exports, {
378
531
  });
379
532
  import {
380
533
  readdir as readdir2,
381
- readFile as readFile2,
382
- writeFile,
534
+ readFile as readFile4,
535
+ writeFile as writeFile3,
383
536
  unlink,
384
537
  mkdir,
385
538
  rmdir,
386
539
  chmod,
387
540
  stat
388
541
  } from "node:fs/promises";
389
- import { join as join2, dirname } from "node:path";
542
+ import { join as join4, dirname } from "node:path";
390
543
  function getTypeDir(claudeDir, dir) {
391
- if (dir === "commands") return join2(claudeDir, dir, "cbp");
392
- return join2(claudeDir, dir);
544
+ if (dir === "commands") return join4(claudeDir, dir, "cbp");
545
+ return join4(claudeDir, dir);
393
546
  }
394
547
  function getFilePath(claudeDir, typeName, file) {
395
548
  const cfg = typeConfig[typeName];
396
549
  const typeDir = getTypeDir(claudeDir, cfg.dir);
397
550
  if (cfg.subfolder) {
398
- return join2(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
551
+ return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
399
552
  }
400
553
  if (typeName === "command" && file.category) {
401
- return join2(typeDir, file.category, `${file.name}${cfg.ext}`);
554
+ return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
402
555
  }
403
556
  if (typeName === "template") {
404
- return join2(typeDir, file.name);
557
+ return join4(typeDir, file.name);
405
558
  }
406
- return join2(typeDir, `${file.name}${cfg.ext}`);
559
+ return join4(typeDir, `${file.name}${cfg.ext}`);
407
560
  }
408
561
  async function readDirRecursive(dir, base = dir) {
409
562
  const result = /* @__PURE__ */ new Map();
410
563
  try {
411
564
  const entries = await readdir2(dir, { withFileTypes: true });
412
565
  for (const entry of entries) {
413
- const fullPath = join2(dir, entry.name);
566
+ const fullPath = join4(dir, entry.name);
414
567
  if (entry.isDirectory()) {
415
568
  const sub = await readDirRecursive(fullPath, base);
416
569
  for (const [k, v] of sub) result.set(k, v);
417
570
  } else {
418
571
  const relPath = fullPath.slice(base.length + 1);
419
- const fileContent = await readFile2(fullPath, "utf-8");
572
+ const fileContent = await readFile4(fullPath, "utf-8");
420
573
  result.set(relPath, fileContent);
421
574
  }
422
575
  }
@@ -426,7 +579,7 @@ async function readDirRecursive(dir, base = dir) {
426
579
  }
427
580
  async function isGitWorktree(projectPath) {
428
581
  try {
429
- const gitPath = join2(projectPath, ".git");
582
+ const gitPath = join4(projectPath, ".git");
430
583
  const info = await stat(gitPath);
431
584
  return info.isFile();
432
585
  } catch {
@@ -453,7 +606,7 @@ async function executeSyncToLocal(options) {
453
606
  const syncData = syncRes.data;
454
607
  const repoData = repoRes.data;
455
608
  syncData.claude_md = [];
456
- const claudeDir = join2(projectPath, ".claude");
609
+ const claudeDir = join4(projectPath, ".claude");
457
610
  const worktree = await isGitWorktree(projectPath);
458
611
  const byType = {};
459
612
  const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
@@ -489,7 +642,7 @@ async function executeSyncToLocal(options) {
489
642
  remotePathMap.set(relPath, { content: substituted, name: remote.name });
490
643
  }
491
644
  for (const [relPath, { content, name }] of remotePathMap) {
492
- const fullPath = join2(targetDir, relPath);
645
+ const fullPath = join4(targetDir, relPath);
493
646
  const localContent = localFiles.get(relPath);
494
647
  if (localContent === void 0) {
495
648
  const remoteFile = remoteFiles.find((f) => f.name === name);
@@ -501,14 +654,14 @@ async function executeSyncToLocal(options) {
501
654
  });
502
655
  if (!dryRun) {
503
656
  await mkdir(dirname(fullPath), { recursive: true });
504
- await writeFile(fullPath, content, "utf-8");
657
+ await writeFile3(fullPath, content, "utf-8");
505
658
  if (typeName === "hook") await chmod(fullPath, 493);
506
659
  }
507
660
  result.created.push(name);
508
661
  totals.created++;
509
662
  } else if (localContent !== content) {
510
663
  if (!dryRun) {
511
- await writeFile(fullPath, content, "utf-8");
664
+ await writeFile3(fullPath, content, "utf-8");
512
665
  if (typeName === "hook") await chmod(fullPath, 493);
513
666
  }
514
667
  result.updated.push(name);
@@ -520,7 +673,7 @@ async function executeSyncToLocal(options) {
520
673
  }
521
674
  for (const [relPath] of localFiles) {
522
675
  if (!remotePathMap.has(relPath)) {
523
- const fullPath = join2(targetDir, relPath);
676
+ const fullPath = join4(targetDir, relPath);
524
677
  if (!dryRun) {
525
678
  await unlink(fullPath);
526
679
  await removeEmptyParents(fullPath, targetDir);
@@ -535,7 +688,7 @@ async function executeSyncToLocal(options) {
535
688
  {
536
689
  const typeName = "docs_stack";
537
690
  const syncKey = "docs_stack";
538
- const targetDir = join2(projectPath, "docs", "stack");
691
+ const targetDir = join4(projectPath, "docs", "stack");
539
692
  const remoteFiles = syncData[syncKey] ?? [];
540
693
  const result = {
541
694
  created: [],
@@ -549,7 +702,7 @@ async function executeSyncToLocal(options) {
549
702
  const localFiles = await readDirRecursive(targetDir);
550
703
  const remotePathMap = /* @__PURE__ */ new Map();
551
704
  for (const remote of remoteFiles) {
552
- const relPath = remote.category ? join2(remote.category, remote.name) : remote.name;
705
+ const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
553
706
  const substituted = substituteVariables(remote.content, repoData);
554
707
  remotePathMap.set(relPath, {
555
708
  content: substituted,
@@ -557,18 +710,18 @@ async function executeSyncToLocal(options) {
557
710
  });
558
711
  }
559
712
  for (const [relPath, { content, name }] of remotePathMap) {
560
- const fullPath = join2(targetDir, relPath);
713
+ const fullPath = join4(targetDir, relPath);
561
714
  const localContent = localFiles.get(relPath);
562
715
  if (localContent === void 0) {
563
716
  if (!dryRun) {
564
717
  await mkdir(dirname(fullPath), { recursive: true });
565
- await writeFile(fullPath, content, "utf-8");
718
+ await writeFile3(fullPath, content, "utf-8");
566
719
  }
567
720
  result.created.push(name);
568
721
  totals.created++;
569
722
  } else if (localContent !== content) {
570
723
  if (!dryRun) {
571
- await writeFile(fullPath, content, "utf-8");
724
+ await writeFile3(fullPath, content, "utf-8");
572
725
  }
573
726
  result.updated.push(name);
574
727
  totals.updated++;
@@ -579,7 +732,7 @@ async function executeSyncToLocal(options) {
579
732
  }
580
733
  for (const [relPath] of localFiles) {
581
734
  if (!remotePathMap.has(relPath)) {
582
- const fullPath = join2(targetDir, relPath);
735
+ const fullPath = join4(targetDir, relPath);
583
736
  if (!dryRun) {
584
737
  await unlink(fullPath);
585
738
  await removeEmptyParents(fullPath, targetDir);
@@ -599,8 +752,8 @@ async function executeSyncToLocal(options) {
599
752
  globalSettings = { ...globalSettings, ...parsed };
600
753
  }
601
754
  const specialTypes = {
602
- claude_md: () => join2(projectPath, "CLAUDE.md"),
603
- settings: () => join2(projectPath, ".claude", "settings.json")
755
+ claude_md: () => join4(projectPath, "CLAUDE.md"),
756
+ settings: () => join4(projectPath, ".claude", "settings.json")
604
757
  };
605
758
  for (const [typeName, getPath] of Object.entries(specialTypes)) {
606
759
  const remoteFiles = syncData[typeName] ?? [];
@@ -615,7 +768,7 @@ async function executeSyncToLocal(options) {
615
768
  const remoteContent = substituteVariables(remote.content, repoData);
616
769
  let localContent;
617
770
  try {
618
- localContent = await readFile2(targetPath, "utf-8");
771
+ localContent = await readFile4(targetPath, "utf-8");
619
772
  } catch {
620
773
  }
621
774
  if (typeName === "settings") {
@@ -624,7 +777,7 @@ async function executeSyncToLocal(options) {
624
777
  globalSettings,
625
778
  repoSettings
626
779
  );
627
- const hooksDir = join2(projectPath, ".claude", "hooks");
780
+ const hooksDir = join4(projectPath, ".claude", "hooks");
628
781
  const discovered = await discoverHooks(hooksDir);
629
782
  if (localContent === void 0) {
630
783
  const finalSettings = stripPermissionsAllow(combinedTemplate);
@@ -636,7 +789,7 @@ async function executeSyncToLocal(options) {
636
789
  }
637
790
  if (!dryRun) {
638
791
  await mkdir(dirname(targetPath), { recursive: true });
639
- await writeFile(
792
+ await writeFile3(
640
793
  targetPath,
641
794
  JSON.stringify(finalSettings, null, 2) + "\n",
642
795
  "utf-8"
@@ -657,7 +810,7 @@ async function executeSyncToLocal(options) {
657
810
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
658
811
  if (localContent !== mergedContent) {
659
812
  if (!dryRun) {
660
- await writeFile(targetPath, mergedContent, "utf-8");
813
+ await writeFile3(targetPath, mergedContent, "utf-8");
661
814
  }
662
815
  result.updated.push(remote.name);
663
816
  totals.updated++;
@@ -670,13 +823,13 @@ async function executeSyncToLocal(options) {
670
823
  if (localContent === void 0) {
671
824
  if (!dryRun) {
672
825
  await mkdir(dirname(targetPath), { recursive: true });
673
- await writeFile(targetPath, remoteContent, "utf-8");
826
+ await writeFile3(targetPath, remoteContent, "utf-8");
674
827
  }
675
828
  result.created.push(remote.name);
676
829
  totals.created++;
677
830
  } else if (localContent !== remoteContent) {
678
831
  if (!dryRun) {
679
- await writeFile(targetPath, remoteContent, "utf-8");
832
+ await writeFile3(targetPath, remoteContent, "utf-8");
680
833
  }
681
834
  result.updated.push(remote.name);
682
835
  totals.updated++;
@@ -777,15 +930,15 @@ __export(setup_exports, {
777
930
  });
778
931
  import { createInterface } from "node:readline/promises";
779
932
  import { stdin, stdout } from "node:process";
780
- import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
933
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
781
934
  import { homedir } from "node:os";
782
- import { join as join3 } from "node:path";
935
+ import { join as join5 } from "node:path";
783
936
  function getConfigPath(scope) {
784
- return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
937
+ return scope === "user" ? join5(homedir(), ".claude.json") : join5(process.cwd(), ".mcp.json");
785
938
  }
786
939
  async function readConfig(path) {
787
940
  try {
788
- const raw = await readFile3(path, "utf-8");
941
+ const raw = await readFile5(path, "utf-8");
789
942
  const parsed = JSON.parse(raw);
790
943
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
791
944
  return parsed;
@@ -809,7 +962,7 @@ async function writeMcpConfig(scope, apiKey) {
809
962
  config.mcpServers = {};
810
963
  }
811
964
  config.mcpServers.codebyplan = buildMcpEntry(apiKey);
812
- await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
965
+ await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
813
966
  return configPath;
814
967
  }
815
968
  async function verifyMcpConfig(scope, apiKey) {
@@ -913,27 +1066,45 @@ async function runSetup() {
913
1066
  console.log(`
914
1067
  Selected: ${selectedRepo.name}
915
1068
  `);
916
- let worktreeId;
917
1069
  const projectPath = process.cwd();
1070
+ const pathBasedId = await resolveAndCacheWorktreeId(
1071
+ selectedRepo.id,
1072
+ projectPath,
1073
+ { skipWrite: true }
1074
+ );
1075
+ const deviceId = await getOrCreateDeviceId(projectPath);
1076
+ let branch = "main";
918
1077
  try {
919
- const worktreesRes = await apiGet(`/worktrees?repo_id=${selectedRepo.id}`);
920
- const match = worktreesRes.data.find(
921
- (wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/")
922
- );
923
- if (match) worktreeId = match.id;
1078
+ const { execSync: execSync3 } = await import("node:child_process");
1079
+ branch = execSync3("git symbolic-ref --short HEAD", {
1080
+ cwd: projectPath,
1081
+ encoding: "utf-8"
1082
+ }).trim();
924
1083
  } catch {
925
1084
  }
926
- const codebyplanPath = join3(projectPath, ".codebyplan.json");
1085
+ const tupleId = await resolveWorktreeId({
1086
+ repoId: selectedRepo.id,
1087
+ repoPath: projectPath,
1088
+ branch,
1089
+ deviceId
1090
+ });
1091
+ const worktreeId = tupleId ?? pathBasedId;
1092
+ const codebyplanPath = join5(projectPath, ".codebyplan.json");
927
1093
  const codebyplanConfig = {
928
1094
  repo_id: selectedRepo.id
929
1095
  };
930
1096
  if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
931
- await writeFile2(
1097
+ await writeFile4(
932
1098
  codebyplanPath,
933
1099
  JSON.stringify(codebyplanConfig, null, 2) + "\n",
934
1100
  "utf-8"
935
1101
  );
936
1102
  console.log(` Created ${codebyplanPath}`);
1103
+ if (worktreeId) {
1104
+ console.log(
1105
+ ` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
1106
+ );
1107
+ }
937
1108
  console.log("\n Running initial sync...\n");
938
1109
  try {
939
1110
  const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
@@ -968,19 +1139,20 @@ async function runSetup() {
968
1139
  var init_setup = __esm({
969
1140
  "src/cli/setup.ts"() {
970
1141
  "use strict";
971
- init_api();
1142
+ init_resolve_worktree();
1143
+ init_local_config();
972
1144
  }
973
1145
  });
974
1146
 
975
1147
  // src/cli/config.ts
976
- import { readFile as readFile4 } from "node:fs/promises";
977
- import { join as join4, resolve } from "node:path";
1148
+ import { readFile as readFile6 } from "node:fs/promises";
1149
+ import { join as join6, resolve } from "node:path";
978
1150
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
979
1151
  let cursor = resolve(startDir);
980
1152
  for (let depth = 0; depth < maxDepth; depth++) {
981
- const configPath = join4(cursor, ".codebyplan.json");
1153
+ const configPath = join6(cursor, ".codebyplan.json");
982
1154
  try {
983
- const raw = await readFile4(configPath, "utf-8");
1155
+ const raw = await readFile6(configPath, "utf-8");
984
1156
  const parsed = JSON.parse(raw);
985
1157
  return { path: configPath, contents: parsed };
986
1158
  } catch {
@@ -1031,8 +1203,8 @@ var init_config = __esm({
1031
1203
  });
1032
1204
 
1033
1205
  // src/cli/fileMapper.ts
1034
- import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
1035
- import { join as join5, extname } from "node:path";
1206
+ import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
1207
+ import { join as join7, extname } from "node:path";
1036
1208
  function extractScope(content, type) {
1037
1209
  if (type === "hook") {
1038
1210
  const match = content.match(/^#\s*@scope:\s*(\S+)/m);
@@ -1062,29 +1234,29 @@ function compositeKey(type, name, category) {
1062
1234
  }
1063
1235
  async function scanLocalFiles(claudeDir, projectPath) {
1064
1236
  const result = /* @__PURE__ */ new Map();
1065
- await scanCommands(join5(claudeDir, "commands", "cbp"), result);
1237
+ await scanCommands(join7(claudeDir, "commands", "cbp"), result);
1066
1238
  await scanSubfolderType(
1067
- join5(claudeDir, "agents"),
1239
+ join7(claudeDir, "agents"),
1068
1240
  "agent",
1069
1241
  "AGENT.md",
1070
1242
  result
1071
1243
  );
1072
1244
  await scanSubfolderType(
1073
- join5(claudeDir, "skills"),
1245
+ join7(claudeDir, "skills"),
1074
1246
  "skill",
1075
1247
  "SKILL.md",
1076
1248
  result
1077
1249
  );
1078
- await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
1079
- await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
1080
- await scanTemplates(join5(claudeDir, "templates"), result);
1250
+ await scanFlatType(join7(claudeDir, "rules"), "rule", ".md", result);
1251
+ await scanFlatType(join7(claudeDir, "hooks"), "hook", ".sh", result);
1252
+ await scanTemplates(join7(claudeDir, "templates"), result);
1081
1253
  await scanCategorizedType(
1082
- join5(claudeDir, "context"),
1254
+ join7(claudeDir, "context"),
1083
1255
  "context",
1084
1256
  ".md",
1085
1257
  result
1086
1258
  );
1087
- await scanDocsRecursive(join5(claudeDir, "docs"), result);
1259
+ await scanDocsRecursive(join7(claudeDir, "docs"), result);
1088
1260
  await scanSettings(claudeDir, projectPath, result);
1089
1261
  return result;
1090
1262
  }
@@ -1102,12 +1274,12 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
1102
1274
  if (entry.isDirectory()) {
1103
1275
  await scanCommandsRecursive(
1104
1276
  baseDir,
1105
- join5(currentDir, entry.name),
1277
+ join7(currentDir, entry.name),
1106
1278
  result
1107
1279
  );
1108
1280
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1109
1281
  const name = entry.name.slice(0, -3);
1110
- const content = await readFile5(join5(currentDir, entry.name), "utf-8");
1282
+ const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1111
1283
  const relDir = currentDir.slice(baseDir.length + 1);
1112
1284
  const category = relDir || null;
1113
1285
  const scope = extractScope(content, "command");
@@ -1125,9 +1297,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
1125
1297
  }
1126
1298
  for (const entry of entries) {
1127
1299
  if (entry.isDirectory()) {
1128
- const filePath = join5(dir, entry.name, fileName);
1300
+ const filePath = join7(dir, entry.name, fileName);
1129
1301
  try {
1130
- const content = await readFile5(filePath, "utf-8");
1302
+ const content = await readFile7(filePath, "utf-8");
1131
1303
  const scope = extractScope(content, type);
1132
1304
  const key = compositeKey(type, entry.name, null);
1133
1305
  result.set(key, {
@@ -1152,7 +1324,7 @@ async function scanFlatType(dir, type, ext, result) {
1152
1324
  for (const entry of entries) {
1153
1325
  if (entry.isFile() && entry.name.endsWith(ext)) {
1154
1326
  const name = entry.name.slice(0, -ext.length);
1155
- const content = await readFile5(join5(dir, entry.name), "utf-8");
1327
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1156
1328
  const scope = extractScope(content, type);
1157
1329
  const key = compositeKey(type, name, null);
1158
1330
  result.set(key, { type, name, category: null, content, scope });
@@ -1171,7 +1343,7 @@ async function scanCategorizedType(dir, type, ext, result) {
1171
1343
  const category = entry.name;
1172
1344
  let subEntries;
1173
1345
  try {
1174
- subEntries = await readdir3(join5(dir, category), {
1346
+ subEntries = await readdir3(join7(dir, category), {
1175
1347
  withFileTypes: true
1176
1348
  });
1177
1349
  } catch {
@@ -1180,8 +1352,8 @@ async function scanCategorizedType(dir, type, ext, result) {
1180
1352
  for (const sub of subEntries) {
1181
1353
  if (sub.isFile() && sub.name.endsWith(ext)) {
1182
1354
  const name = sub.name.slice(0, -ext.length);
1183
- const content = await readFile5(
1184
- join5(dir, category, sub.name),
1355
+ const content = await readFile7(
1356
+ join7(dir, category, sub.name),
1185
1357
  "utf-8"
1186
1358
  );
1187
1359
  const scope = extractScope(content, type);
@@ -1191,7 +1363,7 @@ async function scanCategorizedType(dir, type, ext, result) {
1191
1363
  }
1192
1364
  } else if (entry.isFile() && entry.name.endsWith(ext)) {
1193
1365
  const name = entry.name.slice(0, -ext.length);
1194
- const content = await readFile5(join5(dir, entry.name), "utf-8");
1366
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1195
1367
  const scope = extractScope(content, type);
1196
1368
  const key = compositeKey(type, name, null);
1197
1369
  result.set(key, { type, name, category: null, content, scope });
@@ -1210,10 +1382,10 @@ async function scanDocsDir(baseDir, currentDir, result) {
1210
1382
  }
1211
1383
  for (const entry of entries) {
1212
1384
  if (entry.isDirectory()) {
1213
- await scanDocsDir(baseDir, join5(currentDir, entry.name), result);
1385
+ await scanDocsDir(baseDir, join7(currentDir, entry.name), result);
1214
1386
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1215
1387
  const name = entry.name.slice(0, -3);
1216
- const content = await readFile5(join5(currentDir, entry.name), "utf-8");
1388
+ const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1217
1389
  const scope = extractScope(content, "docs");
1218
1390
  const relDir = currentDir.slice(baseDir.length + 1);
1219
1391
  const category = relDir || null;
@@ -1231,7 +1403,7 @@ async function scanTemplates(dir, result) {
1231
1403
  }
1232
1404
  for (const entry of entries) {
1233
1405
  if (entry.isFile() && extname(entry.name)) {
1234
- const content = await readFile5(join5(dir, entry.name), "utf-8");
1406
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1235
1407
  const scope = extractScope(content, "template");
1236
1408
  const key = compositeKey("template", entry.name, null);
1237
1409
  result.set(key, {
@@ -1245,10 +1417,10 @@ async function scanTemplates(dir, result) {
1245
1417
  }
1246
1418
  }
1247
1419
  async function scanSettings(claudeDir, projectPath, result) {
1248
- const settingsPath = join5(claudeDir, "settings.json");
1420
+ const settingsPath = join7(claudeDir, "settings.json");
1249
1421
  let raw;
1250
1422
  try {
1251
- raw = await readFile5(settingsPath, "utf-8");
1423
+ raw = await readFile7(settingsPath, "utf-8");
1252
1424
  } catch {
1253
1425
  return;
1254
1426
  }
@@ -1260,7 +1432,7 @@ async function scanSettings(claudeDir, projectPath, result) {
1260
1432
  }
1261
1433
  parsed = stripPermissionsAllow(parsed);
1262
1434
  if (parsed.hooks && typeof parsed.hooks === "object") {
1263
- const hooksDir = projectPath ? join5(projectPath, ".claude", "hooks") : join5(claudeDir, "hooks");
1435
+ const hooksDir = projectPath ? join7(projectPath, ".claude", "hooks") : join7(claudeDir, "hooks");
1264
1436
  const discovered = await discoverHooks(hooksDir);
1265
1437
  if (discovered.size > 0) {
1266
1438
  parsed.hooks = stripDiscoveredHooks(
@@ -1592,8 +1764,8 @@ var init_confirm = __esm({
1592
1764
  });
1593
1765
 
1594
1766
  // src/lib/tech-detect.ts
1595
- import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
1596
- import { join as join6, relative } from "node:path";
1767
+ import { readFile as readFile8, access, readdir as readdir4 } from "node:fs/promises";
1768
+ import { join as join8, relative } from "node:path";
1597
1769
  async function fileExists(filePath) {
1598
1770
  try {
1599
1771
  await access(filePath);
@@ -1606,8 +1778,8 @@ async function discoverMonorepoApps(projectPath) {
1606
1778
  const apps = [];
1607
1779
  const patterns = [];
1608
1780
  try {
1609
- const raw = await readFile6(
1610
- join6(projectPath, "pnpm-workspace.yaml"),
1781
+ const raw = await readFile8(
1782
+ join8(projectPath, "pnpm-workspace.yaml"),
1611
1783
  "utf-8"
1612
1784
  );
1613
1785
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1621,7 +1793,7 @@ async function discoverMonorepoApps(projectPath) {
1621
1793
  }
1622
1794
  if (patterns.length === 0) {
1623
1795
  try {
1624
- const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
1796
+ const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1625
1797
  const pkg = JSON.parse(raw);
1626
1798
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1627
1799
  if (ws) patterns.push(...ws);
@@ -1631,14 +1803,14 @@ async function discoverMonorepoApps(projectPath) {
1631
1803
  for (const pattern of patterns) {
1632
1804
  if (pattern.endsWith("/*")) {
1633
1805
  const dir = pattern.slice(0, -2);
1634
- const absDir = join6(projectPath, dir);
1806
+ const absDir = join8(projectPath, dir);
1635
1807
  try {
1636
1808
  const entries = await readdir4(absDir, { withFileTypes: true });
1637
1809
  for (const entry of entries) {
1638
1810
  if (entry.isDirectory()) {
1639
- const relPath = join6(dir, entry.name);
1640
- const absPath = join6(absDir, entry.name);
1641
- if (await fileExists(join6(absPath, "package.json"))) {
1811
+ const relPath = join8(dir, entry.name);
1812
+ const absPath = join8(absDir, entry.name);
1813
+ if (await fileExists(join8(absPath, "package.json"))) {
1642
1814
  apps.push({ name: entry.name, path: relPath, absPath });
1643
1815
  }
1644
1816
  }
@@ -1657,7 +1829,7 @@ async function hasJsxFile(dir, depth = 0) {
1657
1829
  const name = entry.name;
1658
1830
  if (entry.isDirectory()) {
1659
1831
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1660
- if (await hasJsxFile(join6(dir, name), depth + 1)) return true;
1832
+ if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1661
1833
  } else if (entry.isFile()) {
1662
1834
  if (JSX_TEST_PATTERN.test(name)) continue;
1663
1835
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1676,7 +1848,7 @@ async function hasJsxFile(dir, depth = 0) {
1676
1848
  async function detectCapabilities(dirPath, pkgJson) {
1677
1849
  const caps = /* @__PURE__ */ new Set();
1678
1850
  for (const sub of JSX_SCAN_DIRS) {
1679
- if (await hasJsxFile(join6(dirPath, sub))) {
1851
+ if (await hasJsxFile(join8(dirPath, sub))) {
1680
1852
  caps.add("jsx");
1681
1853
  break;
1682
1854
  }
@@ -1698,7 +1870,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1698
1870
  }
1699
1871
  }
1700
1872
  }
1701
- if (!caps.has("node-server") && await fileExists(join6(dirPath, "src", "main.ts"))) {
1873
+ if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
1702
1874
  caps.add("node-server");
1703
1875
  }
1704
1876
  if (pkgJson && pkgJson.bin) {
@@ -1714,7 +1886,7 @@ async function detectFromDirectory(dirPath) {
1714
1886
  const seen = /* @__PURE__ */ new Map();
1715
1887
  let pkgJson = null;
1716
1888
  try {
1717
- const raw = await readFile6(join6(dirPath, "package.json"), "utf-8");
1889
+ const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1718
1890
  pkgJson = JSON.parse(raw);
1719
1891
  const allDeps = {
1720
1892
  ...pkgJson.dependencies ?? {},
@@ -1746,7 +1918,7 @@ async function detectFromDirectory(dirPath) {
1746
1918
  }
1747
1919
  for (const { file, rule } of CONFIG_FILE_MAP) {
1748
1920
  const key = rule.name.toLowerCase();
1749
- if (!seen.has(key) && await fileExists(join6(dirPath, file))) {
1921
+ if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
1750
1922
  seen.set(key, { name: rule.name, category: rule.category });
1751
1923
  }
1752
1924
  }
@@ -1924,7 +2096,7 @@ function categorizeDependency(depName) {
1924
2096
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1925
2097
  if (depth > 4) return [];
1926
2098
  const results = [];
1927
- const pkgPath = join6(dir, "package.json");
2099
+ const pkgPath = join8(dir, "package.json");
1928
2100
  if (await fileExists(pkgPath)) {
1929
2101
  results.push(pkgPath);
1930
2102
  }
@@ -1933,7 +2105,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1933
2105
  for (const entry of entries) {
1934
2106
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1935
2107
  const subResults = await findPackageJsonFiles(
1936
- join6(dir, entry.name),
2108
+ join8(dir, entry.name),
1937
2109
  projectPath,
1938
2110
  depth + 1
1939
2111
  );
@@ -1948,7 +2120,7 @@ async function scanAllDependencies(projectPath) {
1948
2120
  const dependencies = [];
1949
2121
  for (const pkgPath of packageJsonPaths) {
1950
2122
  try {
1951
- const raw = await readFile6(pkgPath, "utf-8");
2123
+ const raw = await readFile8(pkgPath, "utf-8");
1952
2124
  const pkg = JSON.parse(raw);
1953
2125
  const sourcePath = relative(projectPath, pkgPath);
1954
2126
  const depSections = [
@@ -2197,14 +2369,14 @@ var init_server_detect = __esm({
2197
2369
  });
2198
2370
 
2199
2371
  // src/lib/port-verify.ts
2200
- import { readFile as readFile7 } from "node:fs/promises";
2372
+ import { readFile as readFile9 } from "node:fs/promises";
2201
2373
  async function verifyPorts(projectPath, portAllocations) {
2202
2374
  const mismatches = [];
2203
2375
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2204
2376
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2205
2377
  for (const pkgPath of packageJsonPaths) {
2206
2378
  try {
2207
- const raw = await readFile7(pkgPath, "utf-8");
2379
+ const raw = await readFile9(pkgPath, "utf-8");
2208
2380
  const pkg = JSON.parse(raw);
2209
2381
  const scriptPort = detectPortFromScripts(pkg);
2210
2382
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -2267,7 +2439,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
2267
2439
  }
2268
2440
  let pkg;
2269
2441
  try {
2270
- const raw = await readFile7(`${app.absPath}/package.json`, "utf-8");
2442
+ const raw = await readFile9(`${app.absPath}/package.json`, "utf-8");
2271
2443
  pkg = JSON.parse(raw);
2272
2444
  } catch {
2273
2445
  continue;
@@ -2311,8 +2483,70 @@ var init_port_verify = __esm({
2311
2483
  }
2312
2484
  });
2313
2485
 
2486
+ // src/lib/migrate-local-config.ts
2487
+ import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
2488
+ import { join as join9 } from "node:path";
2489
+ function sharedConfigPath(projectPath) {
2490
+ return join9(projectPath, ".codebyplan.json");
2491
+ }
2492
+ async function needsLocalMigration(projectPath) {
2493
+ try {
2494
+ const raw = await readFile10(sharedConfigPath(projectPath), "utf-8");
2495
+ const parsed = JSON.parse(raw);
2496
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2497
+ return false;
2498
+ }
2499
+ const cfg = parsed;
2500
+ if (typeof cfg.worktree_id !== "string" || cfg.worktree_id === "") {
2501
+ return false;
2502
+ }
2503
+ const local = await readLocalConfig(projectPath);
2504
+ if (local?.device_id) {
2505
+ return false;
2506
+ }
2507
+ return true;
2508
+ } catch {
2509
+ return false;
2510
+ }
2511
+ }
2512
+ async function runLocalMigration(projectPath) {
2513
+ const raw = await readFile10(sharedConfigPath(projectPath), "utf-8");
2514
+ const parsed = JSON.parse(raw);
2515
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2516
+ throw new Error(
2517
+ ".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
2518
+ );
2519
+ }
2520
+ const cfg = parsed;
2521
+ const hadWorktreeId = "worktree_id" in cfg;
2522
+ const localBefore = await readLocalConfig(projectPath);
2523
+ const localWillBeCreated = !localBefore?.device_id;
2524
+ const device_id = await getOrCreateDeviceId(projectPath);
2525
+ const cleaned = { ...cfg };
2526
+ delete cleaned.worktree_id;
2527
+ await writeFile5(
2528
+ sharedConfigPath(projectPath),
2529
+ JSON.stringify(cleaned, null, 2) + "\n",
2530
+ "utf-8"
2531
+ );
2532
+ const files_changed = [".codebyplan.json"];
2533
+ if (localWillBeCreated) files_changed.push(".codebyplan.local.json");
2534
+ return {
2535
+ migrated: true,
2536
+ was_dirty: hadWorktreeId || localWillBeCreated,
2537
+ files_changed,
2538
+ device_id
2539
+ };
2540
+ }
2541
+ var init_migrate_local_config = __esm({
2542
+ "src/lib/migrate-local-config.ts"() {
2543
+ "use strict";
2544
+ init_local_config();
2545
+ }
2546
+ });
2547
+
2314
2548
  // src/lib/eslint-generator.ts
2315
- import { createHash } from "node:crypto";
2549
+ import { createHash as createHash2 } from "node:crypto";
2316
2550
  function importedIdentifiers(importLines) {
2317
2551
  const names = /* @__PURE__ */ new Set();
2318
2552
  for (const line of importLines) {
@@ -2380,7 +2614,7 @@ function collectDependencies(presets) {
2380
2614
  return deps;
2381
2615
  }
2382
2616
  function hashConfig(content) {
2383
- return createHash("sha256").update(content).digest("hex");
2617
+ return createHash2("sha256").update(content).digest("hex");
2384
2618
  }
2385
2619
  function buildRules(presets) {
2386
2620
  const merged = {};
@@ -2700,8 +2934,8 @@ __export(eslint_exports, {
2700
2934
  eslintSync: () => eslintSync,
2701
2935
  runEslint: () => runEslint
2702
2936
  });
2703
- import { readFile as readFile8, writeFile as writeFile3, access as access2, readdir as readdir5 } from "node:fs/promises";
2704
- import { join as join7, relative as relative2 } from "node:path";
2937
+ import { readFile as readFile11, writeFile as writeFile6, access as access2, readdir as readdir5 } from "node:fs/promises";
2938
+ import { join as join10, relative as relative2 } from "node:path";
2705
2939
  async function fileExists2(filePath) {
2706
2940
  try {
2707
2941
  await access2(filePath);
@@ -2712,7 +2946,7 @@ async function fileExists2(filePath) {
2712
2946
  }
2713
2947
  async function autoDetectIgnorePatterns(absPath) {
2714
2948
  const patterns = [];
2715
- if (await fileExists2(join7(absPath, "esbuild.js"))) {
2949
+ if (await fileExists2(join10(absPath, "esbuild.js"))) {
2716
2950
  patterns.push("esbuild.js");
2717
2951
  }
2718
2952
  let entries = [];
@@ -2732,19 +2966,19 @@ async function autoDetectIgnorePatterns(absPath) {
2732
2966
  }
2733
2967
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2734
2968
  const candidate = `vitest.config.${ext}`;
2735
- if (await fileExists2(join7(absPath, candidate))) {
2969
+ if (await fileExists2(join10(absPath, candidate))) {
2736
2970
  patterns.push(candidate);
2737
2971
  break;
2738
2972
  }
2739
2973
  }
2740
2974
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2741
2975
  const candidate = `vite.config.${ext}`;
2742
- if (await fileExists2(join7(absPath, candidate))) {
2976
+ if (await fileExists2(join10(absPath, candidate))) {
2743
2977
  patterns.push(candidate);
2744
2978
  break;
2745
2979
  }
2746
2980
  }
2747
- if (await fileExists2(join7(absPath, "tauri.conf.json"))) {
2981
+ if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
2748
2982
  patterns.push("src-tauri/**");
2749
2983
  patterns.push("**/*.d.ts");
2750
2984
  }
@@ -2752,14 +2986,14 @@ async function autoDetectIgnorePatterns(absPath) {
2752
2986
  }
2753
2987
  function detectPackageManager(projectPath) {
2754
2988
  return (async () => {
2755
- if (await fileExists2(join7(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2756
- if (await fileExists2(join7(projectPath, "yarn.lock"))) return "yarn";
2989
+ if (await fileExists2(join10(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2990
+ if (await fileExists2(join10(projectPath, "yarn.lock"))) return "yarn";
2757
2991
  return "npm";
2758
2992
  })();
2759
2993
  }
2760
2994
  async function getInstalledDeps(pkgJsonPath) {
2761
2995
  try {
2762
- const raw = await readFile8(pkgJsonPath, "utf-8");
2996
+ const raw = await readFile11(pkgJsonPath, "utf-8");
2763
2997
  const pkg = JSON.parse(raw);
2764
2998
  const all = /* @__PURE__ */ new Set();
2765
2999
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2872,7 +3106,7 @@ async function eslintInit(repoId, projectPath) {
2872
3106
  ignorePatterns: detectedIgnores
2873
3107
  });
2874
3108
  const hash = hashConfig(content);
2875
- const configPath = join7(target.absPath, "eslint.config.mjs");
3109
+ const configPath = join10(target.absPath, "eslint.config.mjs");
2876
3110
  configsToWrite.push({
2877
3111
  target,
2878
3112
  presets,
@@ -2894,11 +3128,11 @@ async function eslintInit(repoId, projectPath) {
2894
3128
  return;
2895
3129
  }
2896
3130
  const pm = await detectPackageManager(projectPath);
2897
- const rootPkgJsonPath = join7(projectPath, "package.json");
3131
+ const rootPkgJsonPath = join10(projectPath, "package.json");
2898
3132
  const installed = await getInstalledDeps(rootPkgJsonPath);
2899
3133
  if (isMonorepo) {
2900
3134
  for (const { target } of configsToWrite) {
2901
- const appPkgJson = join7(target.absPath, "package.json");
3135
+ const appPkgJson = join10(target.absPath, "package.json");
2902
3136
  const appDeps = await getInstalledDeps(appPkgJson);
2903
3137
  for (const dep of appDeps) {
2904
3138
  installed.add(dep);
@@ -2924,9 +3158,9 @@ async function eslintInit(repoId, projectPath) {
2924
3158
  Install ${missingPkgs.length} missing packages? [Y/n] `
2925
3159
  );
2926
3160
  if (confirmed) {
2927
- const { execSync } = await import("node:child_process");
3161
+ const { execSync: execSync3 } = await import("node:child_process");
2928
3162
  try {
2929
- execSync(installCmd, { cwd: projectPath, stdio: "inherit" });
3163
+ execSync3(installCmd, { cwd: projectPath, stdio: "inherit" });
2930
3164
  console.log(" Packages installed.\n");
2931
3165
  } catch (err) {
2932
3166
  console.error(
@@ -2950,7 +3184,7 @@ async function eslintInit(repoId, projectPath) {
2950
3184
  } of configsToWrite) {
2951
3185
  if (await fileExists2(configPath)) {
2952
3186
  try {
2953
- const existing = await readFile8(configPath, "utf-8");
3187
+ const existing = await readFile11(configPath, "utf-8");
2954
3188
  const existingHash = hashConfig(existing);
2955
3189
  if (existingHash === hash) {
2956
3190
  console.log(
@@ -2970,7 +3204,7 @@ async function eslintInit(repoId, projectPath) {
2970
3204
  }
2971
3205
  }
2972
3206
  try {
2973
- await writeFile3(configPath, content, "utf-8");
3207
+ await writeFile6(configPath, content, "utf-8");
2974
3208
  } catch (err) {
2975
3209
  console.error(
2976
3210
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3020,8 +3254,8 @@ async function eslintSync(repoId, projectPath) {
3020
3254
  let skippedCount = 0;
3021
3255
  let driftCount = 0;
3022
3256
  for (const config of configs) {
3023
- const absPath = config.source_path === "." ? projectPath : join7(projectPath, config.source_path);
3024
- const configPath = join7(absPath, "eslint.config.mjs");
3257
+ const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3258
+ const configPath = join10(absPath, "eslint.config.mjs");
3025
3259
  const detected = await detectTechStack(absPath);
3026
3260
  const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
3027
3261
  const capabilities = collectCapabilities(detected.flat);
@@ -3035,7 +3269,7 @@ async function eslintSync(repoId, projectPath) {
3035
3269
  if (!presetsChanged) {
3036
3270
  if (await fileExists2(configPath)) {
3037
3271
  try {
3038
- const currentContent = await readFile8(configPath, "utf-8");
3272
+ const currentContent = await readFile11(configPath, "utf-8");
3039
3273
  const currentHash = hashConfig(currentContent);
3040
3274
  if (config.generated_hash && currentHash !== config.generated_hash) {
3041
3275
  console.log(
@@ -3068,7 +3302,7 @@ async function eslintSync(repoId, projectPath) {
3068
3302
  ignorePatterns: detectedIgnores
3069
3303
  });
3070
3304
  try {
3071
- await writeFile3(configPath, content, "utf-8");
3305
+ await writeFile6(configPath, content, "utf-8");
3072
3306
  } catch (err) {
3073
3307
  console.error(
3074
3308
  ` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3104,11 +3338,11 @@ async function checkEslintDrift(repoId, projectPath) {
3104
3338
  const configs = res.data ?? [];
3105
3339
  for (const config of configs) {
3106
3340
  if (!config.generated_hash) continue;
3107
- const absPath = config.source_path === "." ? projectPath : join7(projectPath, config.source_path);
3108
- const configPath = join7(absPath, "eslint.config.mjs");
3341
+ const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3342
+ const configPath = join10(absPath, "eslint.config.mjs");
3109
3343
  if (!await fileExists2(configPath)) continue;
3110
3344
  try {
3111
- const content = await readFile8(configPath, "utf-8");
3345
+ const content = await readFile11(configPath, "utf-8");
3112
3346
  const currentHash = hashConfig(content);
3113
3347
  if (currentHash !== config.generated_hash) {
3114
3348
  return true;
@@ -3159,11 +3393,11 @@ var sync_exports = {};
3159
3393
  __export(sync_exports, {
3160
3394
  runSync: () => runSync
3161
3395
  });
3162
- import { createHash as createHash2 } from "node:crypto";
3163
- import { readFile as readFile9, writeFile as writeFile4, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
3164
- import { join as join8, dirname as dirname2 } from "node:path";
3396
+ import { createHash as createHash3 } from "node:crypto";
3397
+ import { readFile as readFile12, writeFile as writeFile7, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
3398
+ import { join as join11, dirname as dirname2 } from "node:path";
3165
3399
  function contentHash(content) {
3166
- return createHash2("sha256").update(content).digest("hex");
3400
+ return createHash3("sha256").update(content).digest("hex");
3167
3401
  }
3168
3402
  async function runSync() {
3169
3403
  const flags = parseFlags(3);
@@ -3229,7 +3463,7 @@ async function runSync() {
3229
3463
  }
3230
3464
  async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3231
3465
  console.log(" Reading local and remote state...");
3232
- const claudeDir = join8(projectPath, ".claude");
3466
+ const claudeDir = join11(projectPath, ".claude");
3233
3467
  let localFiles = /* @__PURE__ */ new Map();
3234
3468
  try {
3235
3469
  localFiles = await scanLocalFiles(claudeDir, projectPath);
@@ -3464,7 +3698,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3464
3698
  for (const p of toPull) {
3465
3699
  if (p.filePath && p.remoteContent !== null) {
3466
3700
  await mkdir2(dirname2(p.filePath), { recursive: true });
3467
- await writeFile4(p.filePath, p.remoteContent, "utf-8");
3701
+ await writeFile7(p.filePath, p.remoteContent, "utf-8");
3468
3702
  if (p.isHook) await chmod2(p.filePath, 493);
3469
3703
  }
3470
3704
  }
@@ -3618,7 +3852,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3618
3852
  console.log("\n Sync complete.\n");
3619
3853
  }
3620
3854
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
3621
- const settingsPath = join8(claudeDir, "settings.json");
3855
+ const settingsPath = join11(claudeDir, "settings.json");
3622
3856
  const globalSettingsFiles = syncData.global_settings ?? [];
3623
3857
  let globalSettings = {};
3624
3858
  for (const gf of globalSettingsFiles) {
@@ -3638,11 +3872,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3638
3872
  globalSettings,
3639
3873
  repoSettings
3640
3874
  );
3641
- const hooksDir = join8(projectPath, ".claude", "hooks");
3875
+ const hooksDir = join11(projectPath, ".claude", "hooks");
3642
3876
  const discovered = await discoverHooks(hooksDir);
3643
3877
  let localSettings = {};
3644
3878
  try {
3645
- const raw = await readFile9(settingsPath, "utf-8");
3879
+ const raw = await readFile12(settingsPath, "utf-8");
3646
3880
  localSettings = JSON.parse(raw);
3647
3881
  } catch {
3648
3882
  }
@@ -3657,7 +3891,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3657
3891
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
3658
3892
  let currentContent = "";
3659
3893
  try {
3660
- currentContent = await readFile9(settingsPath, "utf-8");
3894
+ currentContent = await readFile12(settingsPath, "utf-8");
3661
3895
  } catch {
3662
3896
  }
3663
3897
  if (currentContent === mergedContent) {
@@ -3669,18 +3903,74 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3669
3903
  return;
3670
3904
  }
3671
3905
  await mkdir2(dirname2(settingsPath), { recursive: true });
3672
- await writeFile4(settingsPath, mergedContent, "utf-8");
3906
+ await writeFile7(settingsPath, mergedContent, "utf-8");
3673
3907
  console.log(" Updated settings.json");
3674
3908
  }
3675
3909
  async function syncConfig(repoId, projectPath, dryRun) {
3676
- const configPath = join8(projectPath, ".codebyplan.json");
3910
+ const configPath = join11(projectPath, ".codebyplan.json");
3677
3911
  let currentConfig = {};
3678
3912
  try {
3679
- const raw = await readFile9(configPath, "utf-8");
3913
+ const raw = await readFile12(configPath, "utf-8");
3680
3914
  currentConfig = JSON.parse(raw);
3681
3915
  } catch {
3682
3916
  currentConfig = { repo_id: repoId };
3683
3917
  }
3918
+ if (dryRun) {
3919
+ try {
3920
+ if (await needsLocalMigration(projectPath)) {
3921
+ console.log(
3922
+ ` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
3923
+ );
3924
+ }
3925
+ } catch {
3926
+ }
3927
+ } else {
3928
+ try {
3929
+ if (await needsLocalMigration(projectPath)) {
3930
+ const result = await runLocalMigration(projectPath);
3931
+ delete currentConfig.worktree_id;
3932
+ console.log(
3933
+ ` Migrated .codebyplan.json -> moved worktree_id to gitignored .codebyplan.local.json (device_id=${result.device_id.slice(0, 8)})`
3934
+ );
3935
+ console.log(
3936
+ ` Suggest /cbp-git-commit to stage the cleaned shared file.`
3937
+ );
3938
+ }
3939
+ } catch (err) {
3940
+ console.warn(
3941
+ ` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
3942
+ );
3943
+ }
3944
+ }
3945
+ let resolvedWorktreeId;
3946
+ try {
3947
+ const deviceId = await getOrCreateDeviceId(projectPath);
3948
+ let branch = "main";
3949
+ try {
3950
+ const { execSync: execSync3 } = await import("node:child_process");
3951
+ branch = execSync3("git symbolic-ref --short HEAD", {
3952
+ cwd: projectPath,
3953
+ encoding: "utf-8"
3954
+ }).trim();
3955
+ } catch {
3956
+ }
3957
+ const tupleId = await resolveWorktreeId({
3958
+ repoId,
3959
+ repoPath: projectPath,
3960
+ branch,
3961
+ deviceId
3962
+ });
3963
+ if (tupleId) {
3964
+ resolvedWorktreeId = tupleId;
3965
+ } else {
3966
+ resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
3967
+ }
3968
+ } catch (err) {
3969
+ const msg = err instanceof Error ? err.message : String(err);
3970
+ console.warn(
3971
+ ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
3972
+ );
3973
+ }
3684
3974
  const repoRes = await apiGet(`/repos/${repoId}`);
3685
3975
  const repo = repoRes.data;
3686
3976
  let portAllocations = [];
@@ -3690,8 +3980,8 @@ async function syncConfig(repoId, projectPath, dryRun) {
3690
3980
  { repo_id: repoId }
3691
3981
  );
3692
3982
  const allAllocations = portsRes.data ?? [];
3693
- const worktreeId2 = currentConfig.worktree_id;
3694
- const filtered = worktreeId2 ? allAllocations.filter((a) => a.worktree_id === worktreeId2) : allAllocations.filter((a) => !a.worktree_id);
3983
+ const filteredByWorktree = resolvedWorktreeId;
3984
+ const filtered = filteredByWorktree ? allAllocations.filter((a) => a.worktree_id === filteredByWorktree) : allAllocations.filter((a) => !a.worktree_id);
3695
3985
  const ALLOWED_FIELDS = [
3696
3986
  "id",
3697
3987
  "repo_id",
@@ -3719,7 +4009,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
3719
4009
  ` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
3720
4010
  );
3721
4011
  }
3722
- const worktreeId = currentConfig.worktree_id;
4012
+ const worktreeId = resolvedWorktreeId;
3723
4013
  const matchingAlloc = portAllocations[0];
3724
4014
  const defaultBranchConfig = {
3725
4015
  protected: ["main", "development"],
@@ -3730,7 +4020,9 @@ async function syncConfig(repoId, projectPath, dryRun) {
3730
4020
  const branchConfig = repo.branch_config ?? defaultBranchConfig;
3731
4021
  const newConfig = {
3732
4022
  repo_id: repoId,
3733
- ...worktreeId ? { worktree_id: worktreeId } : {},
4023
+ // worktree_id is intentionally omitted it is never persisted in
4024
+ // .codebyplan.json (CHK-108). The in-memory worktreeId is used only
4025
+ // for server_port / server_type resolution immediately below.
3734
4026
  server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
3735
4027
  server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
3736
4028
  git_branch: repo.git_branch ?? "development",
@@ -3748,7 +4040,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
3748
4040
  console.log(" Config would be updated (dry-run).");
3749
4041
  return;
3750
4042
  }
3751
- await writeFile4(configPath, newJson + "\n", "utf-8");
4043
+ await writeFile7(configPath, newJson + "\n", "utf-8");
3752
4044
  console.log(" Updated .codebyplan.json");
3753
4045
  }
3754
4046
  async function syncTechStack(repoId, projectPath, dryRun) {
@@ -3900,28 +4192,28 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
3900
4192
  hook: { dir: "hooks", ext: ".sh" },
3901
4193
  template: { dir: "templates", ext: "" },
3902
4194
  context: { dir: "context", ext: ".md" },
3903
- docs_stack: { dir: join8("docs", "stack"), ext: ".md" },
4195
+ docs_stack: { dir: join11("docs", "stack"), ext: ".md" },
3904
4196
  docs: { dir: "docs", ext: ".md" },
3905
4197
  claude_md: { dir: "", ext: "" },
3906
4198
  settings: { dir: "", ext: "" }
3907
4199
  };
3908
- if (remote.type === "claude_md") return join8(projectPath, "CLAUDE.md");
3909
- if (remote.type === "settings") return join8(claudeDir, "settings.json");
4200
+ if (remote.type === "claude_md") return join11(projectPath, "CLAUDE.md");
4201
+ if (remote.type === "settings") return join11(claudeDir, "settings.json");
3910
4202
  const cfg = typeConfig2[remote.type];
3911
- if (!cfg) return join8(claudeDir, remote.name);
3912
- const typeDir = remote.type === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
4203
+ if (!cfg) return join11(claudeDir, remote.name);
4204
+ const typeDir = remote.type === "command" ? join11(claudeDir, cfg.dir, "cbp") : join11(claudeDir, cfg.dir);
3913
4205
  if (cfg.subfolder)
3914
- return join8(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
4206
+ return join11(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
3915
4207
  if (remote.type === "command" && remote.category)
3916
- return join8(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3917
- if (remote.type === "template") return join8(typeDir, remote.name);
4208
+ return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4209
+ if (remote.type === "template") return join11(typeDir, remote.name);
3918
4210
  if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
3919
- return join8(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3920
- return join8(typeDir, `${remote.name}${cfg.ext}`);
4211
+ return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4212
+ return join11(typeDir, `${remote.name}${cfg.ext}`);
3921
4213
  }
3922
4214
  function getSyncVersion() {
3923
4215
  try {
3924
- return "1.4.2";
4216
+ return "1.5.0";
3925
4217
  } catch {
3926
4218
  return "unknown";
3927
4219
  }
@@ -3970,10 +4262,64 @@ var init_sync = __esm({
3970
4262
  init_settings_merge();
3971
4263
  init_hook_registry();
3972
4264
  init_port_verify();
4265
+ init_resolve_worktree();
4266
+ init_local_config();
4267
+ init_migrate_local_config();
3973
4268
  init_eslint();
3974
4269
  }
3975
4270
  });
3976
4271
 
4272
+ // src/cli/resolve-worktree.ts
4273
+ var resolve_worktree_exports = {};
4274
+ __export(resolve_worktree_exports, {
4275
+ runResolveWorktree: () => runResolveWorktree
4276
+ });
4277
+ import { execSync as execSync2 } from "node:child_process";
4278
+ async function runResolveWorktree() {
4279
+ try {
4280
+ const projectPath = process.cwd();
4281
+ const found = await findCodebyplanConfig(projectPath);
4282
+ if (!found?.contents.repo_id) {
4283
+ process.exit(0);
4284
+ }
4285
+ const repoId = found.contents.repo_id;
4286
+ const deviceId = await getOrCreateDeviceId(projectPath);
4287
+ let branch = "";
4288
+ try {
4289
+ branch = execSync2("git symbolic-ref --short HEAD", {
4290
+ cwd: projectPath,
4291
+ encoding: "utf-8"
4292
+ }).trim();
4293
+ } catch {
4294
+ }
4295
+ const worktreeId = await resolveWorktreeId({
4296
+ repoId,
4297
+ repoPath: projectPath,
4298
+ branch,
4299
+ deviceId
4300
+ });
4301
+ if (worktreeId) {
4302
+ process.stdout.write(worktreeId);
4303
+ }
4304
+ process.exit(0);
4305
+ } catch (err) {
4306
+ if (process.env.CODEBYPLAN_DEBUG === "1") {
4307
+ const msg = err instanceof Error ? err.message : String(err);
4308
+ process.stderr.write(`resolve-worktree: ${msg}
4309
+ `);
4310
+ }
4311
+ process.exit(0);
4312
+ }
4313
+ }
4314
+ var init_resolve_worktree2 = __esm({
4315
+ "src/cli/resolve-worktree.ts"() {
4316
+ "use strict";
4317
+ init_config();
4318
+ init_local_config();
4319
+ init_resolve_worktree();
4320
+ }
4321
+ });
4322
+
3977
4323
  // src/index.ts
3978
4324
  init_version();
3979
4325
  import { readFileSync } from "node:fs";
@@ -4038,16 +4384,22 @@ void (async () => {
4038
4384
  }
4039
4385
  process.exit(0);
4040
4386
  }
4387
+ if (arg === "resolve-worktree") {
4388
+ const { runResolveWorktree: runResolveWorktree2 } = await Promise.resolve().then(() => (init_resolve_worktree2(), resolve_worktree_exports));
4389
+ await runResolveWorktree2();
4390
+ process.exit(0);
4391
+ }
4041
4392
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
4042
4393
  console.log(`
4043
4394
  CodeByPlan CLI v${VERSION}
4044
4395
 
4045
4396
  Usage:
4046
- codebyplan setup Interactive setup (API key + project init + first sync)
4047
- codebyplan sync Bidirectional sync (pull + push + config)
4048
- codebyplan eslint ESLint config management (init, sync)
4049
- codebyplan help Show this help message
4050
- codebyplan --version Print version
4397
+ codebyplan setup Interactive setup (API key + project init + first sync)
4398
+ codebyplan sync Bidirectional sync (pull + push + config)
4399
+ codebyplan eslint ESLint config management (init, sync)
4400
+ codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
4401
+ codebyplan help Show this help message
4402
+ codebyplan --version Print version
4051
4403
 
4052
4404
  Sync options:
4053
4405
  --path <dir> Project root directory (default: cwd)