codebyplan 1.4.3 → 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 (2) hide show
  1. package/dist/cli.js +431 -155
  2. 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.3";
17
+ VERSION = "1.5.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -198,6 +198,25 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
198
198
  }
199
199
  return worktreeId;
200
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
+ }
201
220
  var init_resolve_worktree = __esm({
202
221
  "src/lib/resolve-worktree.ts"() {
203
222
  "use strict";
@@ -205,6 +224,78 @@ var init_resolve_worktree = __esm({
205
224
  }
206
225
  });
207
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
+
208
299
  // src/lib/settings-merge.ts
209
300
  function mergeSettings(template, local) {
210
301
  const merged = { ...local };
@@ -270,8 +361,8 @@ var init_settings_merge = __esm({
270
361
  });
271
362
 
272
363
  // src/lib/hook-registry.ts
273
- import { readdir, readFile as readFile2 } from "node:fs/promises";
274
- import { join as join2 } from "node:path";
364
+ import { readdir, readFile as readFile3 } from "node:fs/promises";
365
+ import { join as join3 } from "node:path";
275
366
  function parseHookMeta(content) {
276
367
  const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
277
368
  if (!lineMatch) return null;
@@ -293,7 +384,7 @@ async function discoverHooks(hooksDir) {
293
384
  return discovered;
294
385
  }
295
386
  for (const filename of filenames) {
296
- const content = await readFile2(join2(hooksDir, filename), "utf-8");
387
+ const content = await readFile3(join3(hooksDir, filename), "utf-8");
297
388
  const meta = parseHookMeta(content);
298
389
  if (meta) {
299
390
  discovered.set(filename.replace(/\.sh$/, ""), meta);
@@ -440,45 +531,45 @@ __export(sync_engine_exports, {
440
531
  });
441
532
  import {
442
533
  readdir as readdir2,
443
- readFile as readFile3,
444
- writeFile as writeFile2,
534
+ readFile as readFile4,
535
+ writeFile as writeFile3,
445
536
  unlink,
446
537
  mkdir,
447
538
  rmdir,
448
539
  chmod,
449
540
  stat
450
541
  } from "node:fs/promises";
451
- import { join as join3, dirname } from "node:path";
542
+ import { join as join4, dirname } from "node:path";
452
543
  function getTypeDir(claudeDir, dir) {
453
- if (dir === "commands") return join3(claudeDir, dir, "cbp");
454
- return join3(claudeDir, dir);
544
+ if (dir === "commands") return join4(claudeDir, dir, "cbp");
545
+ return join4(claudeDir, dir);
455
546
  }
456
547
  function getFilePath(claudeDir, typeName, file) {
457
548
  const cfg = typeConfig[typeName];
458
549
  const typeDir = getTypeDir(claudeDir, cfg.dir);
459
550
  if (cfg.subfolder) {
460
- return join3(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
551
+ return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
461
552
  }
462
553
  if (typeName === "command" && file.category) {
463
- return join3(typeDir, file.category, `${file.name}${cfg.ext}`);
554
+ return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
464
555
  }
465
556
  if (typeName === "template") {
466
- return join3(typeDir, file.name);
557
+ return join4(typeDir, file.name);
467
558
  }
468
- return join3(typeDir, `${file.name}${cfg.ext}`);
559
+ return join4(typeDir, `${file.name}${cfg.ext}`);
469
560
  }
470
561
  async function readDirRecursive(dir, base = dir) {
471
562
  const result = /* @__PURE__ */ new Map();
472
563
  try {
473
564
  const entries = await readdir2(dir, { withFileTypes: true });
474
565
  for (const entry of entries) {
475
- const fullPath = join3(dir, entry.name);
566
+ const fullPath = join4(dir, entry.name);
476
567
  if (entry.isDirectory()) {
477
568
  const sub = await readDirRecursive(fullPath, base);
478
569
  for (const [k, v] of sub) result.set(k, v);
479
570
  } else {
480
571
  const relPath = fullPath.slice(base.length + 1);
481
- const fileContent = await readFile3(fullPath, "utf-8");
572
+ const fileContent = await readFile4(fullPath, "utf-8");
482
573
  result.set(relPath, fileContent);
483
574
  }
484
575
  }
@@ -488,7 +579,7 @@ async function readDirRecursive(dir, base = dir) {
488
579
  }
489
580
  async function isGitWorktree(projectPath) {
490
581
  try {
491
- const gitPath = join3(projectPath, ".git");
582
+ const gitPath = join4(projectPath, ".git");
492
583
  const info = await stat(gitPath);
493
584
  return info.isFile();
494
585
  } catch {
@@ -515,7 +606,7 @@ async function executeSyncToLocal(options) {
515
606
  const syncData = syncRes.data;
516
607
  const repoData = repoRes.data;
517
608
  syncData.claude_md = [];
518
- const claudeDir = join3(projectPath, ".claude");
609
+ const claudeDir = join4(projectPath, ".claude");
519
610
  const worktree = await isGitWorktree(projectPath);
520
611
  const byType = {};
521
612
  const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
@@ -551,7 +642,7 @@ async function executeSyncToLocal(options) {
551
642
  remotePathMap.set(relPath, { content: substituted, name: remote.name });
552
643
  }
553
644
  for (const [relPath, { content, name }] of remotePathMap) {
554
- const fullPath = join3(targetDir, relPath);
645
+ const fullPath = join4(targetDir, relPath);
555
646
  const localContent = localFiles.get(relPath);
556
647
  if (localContent === void 0) {
557
648
  const remoteFile = remoteFiles.find((f) => f.name === name);
@@ -563,14 +654,14 @@ async function executeSyncToLocal(options) {
563
654
  });
564
655
  if (!dryRun) {
565
656
  await mkdir(dirname(fullPath), { recursive: true });
566
- await writeFile2(fullPath, content, "utf-8");
657
+ await writeFile3(fullPath, content, "utf-8");
567
658
  if (typeName === "hook") await chmod(fullPath, 493);
568
659
  }
569
660
  result.created.push(name);
570
661
  totals.created++;
571
662
  } else if (localContent !== content) {
572
663
  if (!dryRun) {
573
- await writeFile2(fullPath, content, "utf-8");
664
+ await writeFile3(fullPath, content, "utf-8");
574
665
  if (typeName === "hook") await chmod(fullPath, 493);
575
666
  }
576
667
  result.updated.push(name);
@@ -582,7 +673,7 @@ async function executeSyncToLocal(options) {
582
673
  }
583
674
  for (const [relPath] of localFiles) {
584
675
  if (!remotePathMap.has(relPath)) {
585
- const fullPath = join3(targetDir, relPath);
676
+ const fullPath = join4(targetDir, relPath);
586
677
  if (!dryRun) {
587
678
  await unlink(fullPath);
588
679
  await removeEmptyParents(fullPath, targetDir);
@@ -597,7 +688,7 @@ async function executeSyncToLocal(options) {
597
688
  {
598
689
  const typeName = "docs_stack";
599
690
  const syncKey = "docs_stack";
600
- const targetDir = join3(projectPath, "docs", "stack");
691
+ const targetDir = join4(projectPath, "docs", "stack");
601
692
  const remoteFiles = syncData[syncKey] ?? [];
602
693
  const result = {
603
694
  created: [],
@@ -611,7 +702,7 @@ async function executeSyncToLocal(options) {
611
702
  const localFiles = await readDirRecursive(targetDir);
612
703
  const remotePathMap = /* @__PURE__ */ new Map();
613
704
  for (const remote of remoteFiles) {
614
- const relPath = remote.category ? join3(remote.category, remote.name) : remote.name;
705
+ const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
615
706
  const substituted = substituteVariables(remote.content, repoData);
616
707
  remotePathMap.set(relPath, {
617
708
  content: substituted,
@@ -619,18 +710,18 @@ async function executeSyncToLocal(options) {
619
710
  });
620
711
  }
621
712
  for (const [relPath, { content, name }] of remotePathMap) {
622
- const fullPath = join3(targetDir, relPath);
713
+ const fullPath = join4(targetDir, relPath);
623
714
  const localContent = localFiles.get(relPath);
624
715
  if (localContent === void 0) {
625
716
  if (!dryRun) {
626
717
  await mkdir(dirname(fullPath), { recursive: true });
627
- await writeFile2(fullPath, content, "utf-8");
718
+ await writeFile3(fullPath, content, "utf-8");
628
719
  }
629
720
  result.created.push(name);
630
721
  totals.created++;
631
722
  } else if (localContent !== content) {
632
723
  if (!dryRun) {
633
- await writeFile2(fullPath, content, "utf-8");
724
+ await writeFile3(fullPath, content, "utf-8");
634
725
  }
635
726
  result.updated.push(name);
636
727
  totals.updated++;
@@ -641,7 +732,7 @@ async function executeSyncToLocal(options) {
641
732
  }
642
733
  for (const [relPath] of localFiles) {
643
734
  if (!remotePathMap.has(relPath)) {
644
- const fullPath = join3(targetDir, relPath);
735
+ const fullPath = join4(targetDir, relPath);
645
736
  if (!dryRun) {
646
737
  await unlink(fullPath);
647
738
  await removeEmptyParents(fullPath, targetDir);
@@ -661,8 +752,8 @@ async function executeSyncToLocal(options) {
661
752
  globalSettings = { ...globalSettings, ...parsed };
662
753
  }
663
754
  const specialTypes = {
664
- claude_md: () => join3(projectPath, "CLAUDE.md"),
665
- settings: () => join3(projectPath, ".claude", "settings.json")
755
+ claude_md: () => join4(projectPath, "CLAUDE.md"),
756
+ settings: () => join4(projectPath, ".claude", "settings.json")
666
757
  };
667
758
  for (const [typeName, getPath] of Object.entries(specialTypes)) {
668
759
  const remoteFiles = syncData[typeName] ?? [];
@@ -677,7 +768,7 @@ async function executeSyncToLocal(options) {
677
768
  const remoteContent = substituteVariables(remote.content, repoData);
678
769
  let localContent;
679
770
  try {
680
- localContent = await readFile3(targetPath, "utf-8");
771
+ localContent = await readFile4(targetPath, "utf-8");
681
772
  } catch {
682
773
  }
683
774
  if (typeName === "settings") {
@@ -686,7 +777,7 @@ async function executeSyncToLocal(options) {
686
777
  globalSettings,
687
778
  repoSettings
688
779
  );
689
- const hooksDir = join3(projectPath, ".claude", "hooks");
780
+ const hooksDir = join4(projectPath, ".claude", "hooks");
690
781
  const discovered = await discoverHooks(hooksDir);
691
782
  if (localContent === void 0) {
692
783
  const finalSettings = stripPermissionsAllow(combinedTemplate);
@@ -698,7 +789,7 @@ async function executeSyncToLocal(options) {
698
789
  }
699
790
  if (!dryRun) {
700
791
  await mkdir(dirname(targetPath), { recursive: true });
701
- await writeFile2(
792
+ await writeFile3(
702
793
  targetPath,
703
794
  JSON.stringify(finalSettings, null, 2) + "\n",
704
795
  "utf-8"
@@ -719,7 +810,7 @@ async function executeSyncToLocal(options) {
719
810
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
720
811
  if (localContent !== mergedContent) {
721
812
  if (!dryRun) {
722
- await writeFile2(targetPath, mergedContent, "utf-8");
813
+ await writeFile3(targetPath, mergedContent, "utf-8");
723
814
  }
724
815
  result.updated.push(remote.name);
725
816
  totals.updated++;
@@ -732,13 +823,13 @@ async function executeSyncToLocal(options) {
732
823
  if (localContent === void 0) {
733
824
  if (!dryRun) {
734
825
  await mkdir(dirname(targetPath), { recursive: true });
735
- await writeFile2(targetPath, remoteContent, "utf-8");
826
+ await writeFile3(targetPath, remoteContent, "utf-8");
736
827
  }
737
828
  result.created.push(remote.name);
738
829
  totals.created++;
739
830
  } else if (localContent !== remoteContent) {
740
831
  if (!dryRun) {
741
- await writeFile2(targetPath, remoteContent, "utf-8");
832
+ await writeFile3(targetPath, remoteContent, "utf-8");
742
833
  }
743
834
  result.updated.push(remote.name);
744
835
  totals.updated++;
@@ -839,15 +930,15 @@ __export(setup_exports, {
839
930
  });
840
931
  import { createInterface } from "node:readline/promises";
841
932
  import { stdin, stdout } from "node:process";
842
- import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
933
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
843
934
  import { homedir } from "node:os";
844
- import { join as join4 } from "node:path";
935
+ import { join as join5 } from "node:path";
845
936
  function getConfigPath(scope) {
846
- return scope === "user" ? join4(homedir(), ".claude.json") : join4(process.cwd(), ".mcp.json");
937
+ return scope === "user" ? join5(homedir(), ".claude.json") : join5(process.cwd(), ".mcp.json");
847
938
  }
848
939
  async function readConfig(path) {
849
940
  try {
850
- const raw = await readFile4(path, "utf-8");
941
+ const raw = await readFile5(path, "utf-8");
851
942
  const parsed = JSON.parse(raw);
852
943
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
853
944
  return parsed;
@@ -871,7 +962,7 @@ async function writeMcpConfig(scope, apiKey) {
871
962
  config.mcpServers = {};
872
963
  }
873
964
  config.mcpServers.codebyplan = buildMcpEntry(apiKey);
874
- await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
965
+ await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
875
966
  return configPath;
876
967
  }
877
968
  async function verifyMcpConfig(scope, apiKey) {
@@ -976,17 +1067,34 @@ async function runSetup() {
976
1067
  Selected: ${selectedRepo.name}
977
1068
  `);
978
1069
  const projectPath = process.cwd();
979
- const worktreeId = await resolveAndCacheWorktreeId(
1070
+ const pathBasedId = await resolveAndCacheWorktreeId(
980
1071
  selectedRepo.id,
981
1072
  projectPath,
982
1073
  { skipWrite: true }
983
1074
  );
984
- const codebyplanPath = join4(projectPath, ".codebyplan.json");
1075
+ const deviceId = await getOrCreateDeviceId(projectPath);
1076
+ let branch = "main";
1077
+ try {
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();
1083
+ } catch {
1084
+ }
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");
985
1093
  const codebyplanConfig = {
986
1094
  repo_id: selectedRepo.id
987
1095
  };
988
1096
  if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
989
- await writeFile3(
1097
+ await writeFile4(
990
1098
  codebyplanPath,
991
1099
  JSON.stringify(codebyplanConfig, null, 2) + "\n",
992
1100
  "utf-8"
@@ -1032,18 +1140,19 @@ var init_setup = __esm({
1032
1140
  "src/cli/setup.ts"() {
1033
1141
  "use strict";
1034
1142
  init_resolve_worktree();
1143
+ init_local_config();
1035
1144
  }
1036
1145
  });
1037
1146
 
1038
1147
  // src/cli/config.ts
1039
- import { readFile as readFile5 } from "node:fs/promises";
1040
- import { join as join5, resolve } from "node:path";
1148
+ import { readFile as readFile6 } from "node:fs/promises";
1149
+ import { join as join6, resolve } from "node:path";
1041
1150
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1042
1151
  let cursor = resolve(startDir);
1043
1152
  for (let depth = 0; depth < maxDepth; depth++) {
1044
- const configPath = join5(cursor, ".codebyplan.json");
1153
+ const configPath = join6(cursor, ".codebyplan.json");
1045
1154
  try {
1046
- const raw = await readFile5(configPath, "utf-8");
1155
+ const raw = await readFile6(configPath, "utf-8");
1047
1156
  const parsed = JSON.parse(raw);
1048
1157
  return { path: configPath, contents: parsed };
1049
1158
  } catch {
@@ -1094,8 +1203,8 @@ var init_config = __esm({
1094
1203
  });
1095
1204
 
1096
1205
  // src/cli/fileMapper.ts
1097
- import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
1098
- import { join as join6, 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";
1099
1208
  function extractScope(content, type) {
1100
1209
  if (type === "hook") {
1101
1210
  const match = content.match(/^#\s*@scope:\s*(\S+)/m);
@@ -1125,29 +1234,29 @@ function compositeKey(type, name, category) {
1125
1234
  }
1126
1235
  async function scanLocalFiles(claudeDir, projectPath) {
1127
1236
  const result = /* @__PURE__ */ new Map();
1128
- await scanCommands(join6(claudeDir, "commands", "cbp"), result);
1237
+ await scanCommands(join7(claudeDir, "commands", "cbp"), result);
1129
1238
  await scanSubfolderType(
1130
- join6(claudeDir, "agents"),
1239
+ join7(claudeDir, "agents"),
1131
1240
  "agent",
1132
1241
  "AGENT.md",
1133
1242
  result
1134
1243
  );
1135
1244
  await scanSubfolderType(
1136
- join6(claudeDir, "skills"),
1245
+ join7(claudeDir, "skills"),
1137
1246
  "skill",
1138
1247
  "SKILL.md",
1139
1248
  result
1140
1249
  );
1141
- await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
1142
- await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
1143
- await scanTemplates(join6(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);
1144
1253
  await scanCategorizedType(
1145
- join6(claudeDir, "context"),
1254
+ join7(claudeDir, "context"),
1146
1255
  "context",
1147
1256
  ".md",
1148
1257
  result
1149
1258
  );
1150
- await scanDocsRecursive(join6(claudeDir, "docs"), result);
1259
+ await scanDocsRecursive(join7(claudeDir, "docs"), result);
1151
1260
  await scanSettings(claudeDir, projectPath, result);
1152
1261
  return result;
1153
1262
  }
@@ -1165,12 +1274,12 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
1165
1274
  if (entry.isDirectory()) {
1166
1275
  await scanCommandsRecursive(
1167
1276
  baseDir,
1168
- join6(currentDir, entry.name),
1277
+ join7(currentDir, entry.name),
1169
1278
  result
1170
1279
  );
1171
1280
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1172
1281
  const name = entry.name.slice(0, -3);
1173
- const content = await readFile6(join6(currentDir, entry.name), "utf-8");
1282
+ const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1174
1283
  const relDir = currentDir.slice(baseDir.length + 1);
1175
1284
  const category = relDir || null;
1176
1285
  const scope = extractScope(content, "command");
@@ -1188,9 +1297,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
1188
1297
  }
1189
1298
  for (const entry of entries) {
1190
1299
  if (entry.isDirectory()) {
1191
- const filePath = join6(dir, entry.name, fileName);
1300
+ const filePath = join7(dir, entry.name, fileName);
1192
1301
  try {
1193
- const content = await readFile6(filePath, "utf-8");
1302
+ const content = await readFile7(filePath, "utf-8");
1194
1303
  const scope = extractScope(content, type);
1195
1304
  const key = compositeKey(type, entry.name, null);
1196
1305
  result.set(key, {
@@ -1215,7 +1324,7 @@ async function scanFlatType(dir, type, ext, result) {
1215
1324
  for (const entry of entries) {
1216
1325
  if (entry.isFile() && entry.name.endsWith(ext)) {
1217
1326
  const name = entry.name.slice(0, -ext.length);
1218
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1327
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1219
1328
  const scope = extractScope(content, type);
1220
1329
  const key = compositeKey(type, name, null);
1221
1330
  result.set(key, { type, name, category: null, content, scope });
@@ -1234,7 +1343,7 @@ async function scanCategorizedType(dir, type, ext, result) {
1234
1343
  const category = entry.name;
1235
1344
  let subEntries;
1236
1345
  try {
1237
- subEntries = await readdir3(join6(dir, category), {
1346
+ subEntries = await readdir3(join7(dir, category), {
1238
1347
  withFileTypes: true
1239
1348
  });
1240
1349
  } catch {
@@ -1243,8 +1352,8 @@ async function scanCategorizedType(dir, type, ext, result) {
1243
1352
  for (const sub of subEntries) {
1244
1353
  if (sub.isFile() && sub.name.endsWith(ext)) {
1245
1354
  const name = sub.name.slice(0, -ext.length);
1246
- const content = await readFile6(
1247
- join6(dir, category, sub.name),
1355
+ const content = await readFile7(
1356
+ join7(dir, category, sub.name),
1248
1357
  "utf-8"
1249
1358
  );
1250
1359
  const scope = extractScope(content, type);
@@ -1254,7 +1363,7 @@ async function scanCategorizedType(dir, type, ext, result) {
1254
1363
  }
1255
1364
  } else if (entry.isFile() && entry.name.endsWith(ext)) {
1256
1365
  const name = entry.name.slice(0, -ext.length);
1257
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1366
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1258
1367
  const scope = extractScope(content, type);
1259
1368
  const key = compositeKey(type, name, null);
1260
1369
  result.set(key, { type, name, category: null, content, scope });
@@ -1273,10 +1382,10 @@ async function scanDocsDir(baseDir, currentDir, result) {
1273
1382
  }
1274
1383
  for (const entry of entries) {
1275
1384
  if (entry.isDirectory()) {
1276
- await scanDocsDir(baseDir, join6(currentDir, entry.name), result);
1385
+ await scanDocsDir(baseDir, join7(currentDir, entry.name), result);
1277
1386
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1278
1387
  const name = entry.name.slice(0, -3);
1279
- const content = await readFile6(join6(currentDir, entry.name), "utf-8");
1388
+ const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1280
1389
  const scope = extractScope(content, "docs");
1281
1390
  const relDir = currentDir.slice(baseDir.length + 1);
1282
1391
  const category = relDir || null;
@@ -1294,7 +1403,7 @@ async function scanTemplates(dir, result) {
1294
1403
  }
1295
1404
  for (const entry of entries) {
1296
1405
  if (entry.isFile() && extname(entry.name)) {
1297
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1406
+ const content = await readFile7(join7(dir, entry.name), "utf-8");
1298
1407
  const scope = extractScope(content, "template");
1299
1408
  const key = compositeKey("template", entry.name, null);
1300
1409
  result.set(key, {
@@ -1308,10 +1417,10 @@ async function scanTemplates(dir, result) {
1308
1417
  }
1309
1418
  }
1310
1419
  async function scanSettings(claudeDir, projectPath, result) {
1311
- const settingsPath = join6(claudeDir, "settings.json");
1420
+ const settingsPath = join7(claudeDir, "settings.json");
1312
1421
  let raw;
1313
1422
  try {
1314
- raw = await readFile6(settingsPath, "utf-8");
1423
+ raw = await readFile7(settingsPath, "utf-8");
1315
1424
  } catch {
1316
1425
  return;
1317
1426
  }
@@ -1323,7 +1432,7 @@ async function scanSettings(claudeDir, projectPath, result) {
1323
1432
  }
1324
1433
  parsed = stripPermissionsAllow(parsed);
1325
1434
  if (parsed.hooks && typeof parsed.hooks === "object") {
1326
- const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
1435
+ const hooksDir = projectPath ? join7(projectPath, ".claude", "hooks") : join7(claudeDir, "hooks");
1327
1436
  const discovered = await discoverHooks(hooksDir);
1328
1437
  if (discovered.size > 0) {
1329
1438
  parsed.hooks = stripDiscoveredHooks(
@@ -1655,8 +1764,8 @@ var init_confirm = __esm({
1655
1764
  });
1656
1765
 
1657
1766
  // src/lib/tech-detect.ts
1658
- import { readFile as readFile7, access, readdir as readdir4 } from "node:fs/promises";
1659
- import { join as join7, 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";
1660
1769
  async function fileExists(filePath) {
1661
1770
  try {
1662
1771
  await access(filePath);
@@ -1669,8 +1778,8 @@ async function discoverMonorepoApps(projectPath) {
1669
1778
  const apps = [];
1670
1779
  const patterns = [];
1671
1780
  try {
1672
- const raw = await readFile7(
1673
- join7(projectPath, "pnpm-workspace.yaml"),
1781
+ const raw = await readFile8(
1782
+ join8(projectPath, "pnpm-workspace.yaml"),
1674
1783
  "utf-8"
1675
1784
  );
1676
1785
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1684,7 +1793,7 @@ async function discoverMonorepoApps(projectPath) {
1684
1793
  }
1685
1794
  if (patterns.length === 0) {
1686
1795
  try {
1687
- const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
1796
+ const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1688
1797
  const pkg = JSON.parse(raw);
1689
1798
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1690
1799
  if (ws) patterns.push(...ws);
@@ -1694,14 +1803,14 @@ async function discoverMonorepoApps(projectPath) {
1694
1803
  for (const pattern of patterns) {
1695
1804
  if (pattern.endsWith("/*")) {
1696
1805
  const dir = pattern.slice(0, -2);
1697
- const absDir = join7(projectPath, dir);
1806
+ const absDir = join8(projectPath, dir);
1698
1807
  try {
1699
1808
  const entries = await readdir4(absDir, { withFileTypes: true });
1700
1809
  for (const entry of entries) {
1701
1810
  if (entry.isDirectory()) {
1702
- const relPath = join7(dir, entry.name);
1703
- const absPath = join7(absDir, entry.name);
1704
- if (await fileExists(join7(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"))) {
1705
1814
  apps.push({ name: entry.name, path: relPath, absPath });
1706
1815
  }
1707
1816
  }
@@ -1720,7 +1829,7 @@ async function hasJsxFile(dir, depth = 0) {
1720
1829
  const name = entry.name;
1721
1830
  if (entry.isDirectory()) {
1722
1831
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1723
- if (await hasJsxFile(join7(dir, name), depth + 1)) return true;
1832
+ if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1724
1833
  } else if (entry.isFile()) {
1725
1834
  if (JSX_TEST_PATTERN.test(name)) continue;
1726
1835
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1739,7 +1848,7 @@ async function hasJsxFile(dir, depth = 0) {
1739
1848
  async function detectCapabilities(dirPath, pkgJson) {
1740
1849
  const caps = /* @__PURE__ */ new Set();
1741
1850
  for (const sub of JSX_SCAN_DIRS) {
1742
- if (await hasJsxFile(join7(dirPath, sub))) {
1851
+ if (await hasJsxFile(join8(dirPath, sub))) {
1743
1852
  caps.add("jsx");
1744
1853
  break;
1745
1854
  }
@@ -1761,7 +1870,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1761
1870
  }
1762
1871
  }
1763
1872
  }
1764
- if (!caps.has("node-server") && await fileExists(join7(dirPath, "src", "main.ts"))) {
1873
+ if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
1765
1874
  caps.add("node-server");
1766
1875
  }
1767
1876
  if (pkgJson && pkgJson.bin) {
@@ -1777,7 +1886,7 @@ async function detectFromDirectory(dirPath) {
1777
1886
  const seen = /* @__PURE__ */ new Map();
1778
1887
  let pkgJson = null;
1779
1888
  try {
1780
- const raw = await readFile7(join7(dirPath, "package.json"), "utf-8");
1889
+ const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1781
1890
  pkgJson = JSON.parse(raw);
1782
1891
  const allDeps = {
1783
1892
  ...pkgJson.dependencies ?? {},
@@ -1809,7 +1918,7 @@ async function detectFromDirectory(dirPath) {
1809
1918
  }
1810
1919
  for (const { file, rule } of CONFIG_FILE_MAP) {
1811
1920
  const key = rule.name.toLowerCase();
1812
- if (!seen.has(key) && await fileExists(join7(dirPath, file))) {
1921
+ if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
1813
1922
  seen.set(key, { name: rule.name, category: rule.category });
1814
1923
  }
1815
1924
  }
@@ -1987,7 +2096,7 @@ function categorizeDependency(depName) {
1987
2096
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1988
2097
  if (depth > 4) return [];
1989
2098
  const results = [];
1990
- const pkgPath = join7(dir, "package.json");
2099
+ const pkgPath = join8(dir, "package.json");
1991
2100
  if (await fileExists(pkgPath)) {
1992
2101
  results.push(pkgPath);
1993
2102
  }
@@ -1996,7 +2105,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1996
2105
  for (const entry of entries) {
1997
2106
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1998
2107
  const subResults = await findPackageJsonFiles(
1999
- join7(dir, entry.name),
2108
+ join8(dir, entry.name),
2000
2109
  projectPath,
2001
2110
  depth + 1
2002
2111
  );
@@ -2011,7 +2120,7 @@ async function scanAllDependencies(projectPath) {
2011
2120
  const dependencies = [];
2012
2121
  for (const pkgPath of packageJsonPaths) {
2013
2122
  try {
2014
- const raw = await readFile7(pkgPath, "utf-8");
2123
+ const raw = await readFile8(pkgPath, "utf-8");
2015
2124
  const pkg = JSON.parse(raw);
2016
2125
  const sourcePath = relative(projectPath, pkgPath);
2017
2126
  const depSections = [
@@ -2260,14 +2369,14 @@ var init_server_detect = __esm({
2260
2369
  });
2261
2370
 
2262
2371
  // src/lib/port-verify.ts
2263
- import { readFile as readFile8 } from "node:fs/promises";
2372
+ import { readFile as readFile9 } from "node:fs/promises";
2264
2373
  async function verifyPorts(projectPath, portAllocations) {
2265
2374
  const mismatches = [];
2266
2375
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2267
2376
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2268
2377
  for (const pkgPath of packageJsonPaths) {
2269
2378
  try {
2270
- const raw = await readFile8(pkgPath, "utf-8");
2379
+ const raw = await readFile9(pkgPath, "utf-8");
2271
2380
  const pkg = JSON.parse(raw);
2272
2381
  const scriptPort = detectPortFromScripts(pkg);
2273
2382
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -2330,7 +2439,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
2330
2439
  }
2331
2440
  let pkg;
2332
2441
  try {
2333
- const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
2442
+ const raw = await readFile9(`${app.absPath}/package.json`, "utf-8");
2334
2443
  pkg = JSON.parse(raw);
2335
2444
  } catch {
2336
2445
  continue;
@@ -2374,8 +2483,70 @@ var init_port_verify = __esm({
2374
2483
  }
2375
2484
  });
2376
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
+
2377
2548
  // src/lib/eslint-generator.ts
2378
- import { createHash } from "node:crypto";
2549
+ import { createHash as createHash2 } from "node:crypto";
2379
2550
  function importedIdentifiers(importLines) {
2380
2551
  const names = /* @__PURE__ */ new Set();
2381
2552
  for (const line of importLines) {
@@ -2443,7 +2614,7 @@ function collectDependencies(presets) {
2443
2614
  return deps;
2444
2615
  }
2445
2616
  function hashConfig(content) {
2446
- return createHash("sha256").update(content).digest("hex");
2617
+ return createHash2("sha256").update(content).digest("hex");
2447
2618
  }
2448
2619
  function buildRules(presets) {
2449
2620
  const merged = {};
@@ -2763,8 +2934,8 @@ __export(eslint_exports, {
2763
2934
  eslintSync: () => eslintSync,
2764
2935
  runEslint: () => runEslint
2765
2936
  });
2766
- import { readFile as readFile9, writeFile as writeFile4, access as access2, readdir as readdir5 } from "node:fs/promises";
2767
- import { join as join8, 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";
2768
2939
  async function fileExists2(filePath) {
2769
2940
  try {
2770
2941
  await access2(filePath);
@@ -2775,7 +2946,7 @@ async function fileExists2(filePath) {
2775
2946
  }
2776
2947
  async function autoDetectIgnorePatterns(absPath) {
2777
2948
  const patterns = [];
2778
- if (await fileExists2(join8(absPath, "esbuild.js"))) {
2949
+ if (await fileExists2(join10(absPath, "esbuild.js"))) {
2779
2950
  patterns.push("esbuild.js");
2780
2951
  }
2781
2952
  let entries = [];
@@ -2795,19 +2966,19 @@ async function autoDetectIgnorePatterns(absPath) {
2795
2966
  }
2796
2967
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2797
2968
  const candidate = `vitest.config.${ext}`;
2798
- if (await fileExists2(join8(absPath, candidate))) {
2969
+ if (await fileExists2(join10(absPath, candidate))) {
2799
2970
  patterns.push(candidate);
2800
2971
  break;
2801
2972
  }
2802
2973
  }
2803
2974
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2804
2975
  const candidate = `vite.config.${ext}`;
2805
- if (await fileExists2(join8(absPath, candidate))) {
2976
+ if (await fileExists2(join10(absPath, candidate))) {
2806
2977
  patterns.push(candidate);
2807
2978
  break;
2808
2979
  }
2809
2980
  }
2810
- if (await fileExists2(join8(absPath, "tauri.conf.json"))) {
2981
+ if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
2811
2982
  patterns.push("src-tauri/**");
2812
2983
  patterns.push("**/*.d.ts");
2813
2984
  }
@@ -2815,14 +2986,14 @@ async function autoDetectIgnorePatterns(absPath) {
2815
2986
  }
2816
2987
  function detectPackageManager(projectPath) {
2817
2988
  return (async () => {
2818
- if (await fileExists2(join8(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2819
- if (await fileExists2(join8(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";
2820
2991
  return "npm";
2821
2992
  })();
2822
2993
  }
2823
2994
  async function getInstalledDeps(pkgJsonPath) {
2824
2995
  try {
2825
- const raw = await readFile9(pkgJsonPath, "utf-8");
2996
+ const raw = await readFile11(pkgJsonPath, "utf-8");
2826
2997
  const pkg = JSON.parse(raw);
2827
2998
  const all = /* @__PURE__ */ new Set();
2828
2999
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2935,7 +3106,7 @@ async function eslintInit(repoId, projectPath) {
2935
3106
  ignorePatterns: detectedIgnores
2936
3107
  });
2937
3108
  const hash = hashConfig(content);
2938
- const configPath = join8(target.absPath, "eslint.config.mjs");
3109
+ const configPath = join10(target.absPath, "eslint.config.mjs");
2939
3110
  configsToWrite.push({
2940
3111
  target,
2941
3112
  presets,
@@ -2957,11 +3128,11 @@ async function eslintInit(repoId, projectPath) {
2957
3128
  return;
2958
3129
  }
2959
3130
  const pm = await detectPackageManager(projectPath);
2960
- const rootPkgJsonPath = join8(projectPath, "package.json");
3131
+ const rootPkgJsonPath = join10(projectPath, "package.json");
2961
3132
  const installed = await getInstalledDeps(rootPkgJsonPath);
2962
3133
  if (isMonorepo) {
2963
3134
  for (const { target } of configsToWrite) {
2964
- const appPkgJson = join8(target.absPath, "package.json");
3135
+ const appPkgJson = join10(target.absPath, "package.json");
2965
3136
  const appDeps = await getInstalledDeps(appPkgJson);
2966
3137
  for (const dep of appDeps) {
2967
3138
  installed.add(dep);
@@ -2987,9 +3158,9 @@ async function eslintInit(repoId, projectPath) {
2987
3158
  Install ${missingPkgs.length} missing packages? [Y/n] `
2988
3159
  );
2989
3160
  if (confirmed) {
2990
- const { execSync } = await import("node:child_process");
3161
+ const { execSync: execSync3 } = await import("node:child_process");
2991
3162
  try {
2992
- execSync(installCmd, { cwd: projectPath, stdio: "inherit" });
3163
+ execSync3(installCmd, { cwd: projectPath, stdio: "inherit" });
2993
3164
  console.log(" Packages installed.\n");
2994
3165
  } catch (err) {
2995
3166
  console.error(
@@ -3013,7 +3184,7 @@ async function eslintInit(repoId, projectPath) {
3013
3184
  } of configsToWrite) {
3014
3185
  if (await fileExists2(configPath)) {
3015
3186
  try {
3016
- const existing = await readFile9(configPath, "utf-8");
3187
+ const existing = await readFile11(configPath, "utf-8");
3017
3188
  const existingHash = hashConfig(existing);
3018
3189
  if (existingHash === hash) {
3019
3190
  console.log(
@@ -3033,7 +3204,7 @@ async function eslintInit(repoId, projectPath) {
3033
3204
  }
3034
3205
  }
3035
3206
  try {
3036
- await writeFile4(configPath, content, "utf-8");
3207
+ await writeFile6(configPath, content, "utf-8");
3037
3208
  } catch (err) {
3038
3209
  console.error(
3039
3210
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3083,8 +3254,8 @@ async function eslintSync(repoId, projectPath) {
3083
3254
  let skippedCount = 0;
3084
3255
  let driftCount = 0;
3085
3256
  for (const config of configs) {
3086
- const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
3087
- const configPath = join8(absPath, "eslint.config.mjs");
3257
+ const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3258
+ const configPath = join10(absPath, "eslint.config.mjs");
3088
3259
  const detected = await detectTechStack(absPath);
3089
3260
  const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
3090
3261
  const capabilities = collectCapabilities(detected.flat);
@@ -3098,7 +3269,7 @@ async function eslintSync(repoId, projectPath) {
3098
3269
  if (!presetsChanged) {
3099
3270
  if (await fileExists2(configPath)) {
3100
3271
  try {
3101
- const currentContent = await readFile9(configPath, "utf-8");
3272
+ const currentContent = await readFile11(configPath, "utf-8");
3102
3273
  const currentHash = hashConfig(currentContent);
3103
3274
  if (config.generated_hash && currentHash !== config.generated_hash) {
3104
3275
  console.log(
@@ -3131,7 +3302,7 @@ async function eslintSync(repoId, projectPath) {
3131
3302
  ignorePatterns: detectedIgnores
3132
3303
  });
3133
3304
  try {
3134
- await writeFile4(configPath, content, "utf-8");
3305
+ await writeFile6(configPath, content, "utf-8");
3135
3306
  } catch (err) {
3136
3307
  console.error(
3137
3308
  ` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3167,11 +3338,11 @@ async function checkEslintDrift(repoId, projectPath) {
3167
3338
  const configs = res.data ?? [];
3168
3339
  for (const config of configs) {
3169
3340
  if (!config.generated_hash) continue;
3170
- const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
3171
- const configPath = join8(absPath, "eslint.config.mjs");
3341
+ const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3342
+ const configPath = join10(absPath, "eslint.config.mjs");
3172
3343
  if (!await fileExists2(configPath)) continue;
3173
3344
  try {
3174
- const content = await readFile9(configPath, "utf-8");
3345
+ const content = await readFile11(configPath, "utf-8");
3175
3346
  const currentHash = hashConfig(content);
3176
3347
  if (currentHash !== config.generated_hash) {
3177
3348
  return true;
@@ -3222,11 +3393,11 @@ var sync_exports = {};
3222
3393
  __export(sync_exports, {
3223
3394
  runSync: () => runSync
3224
3395
  });
3225
- import { createHash as createHash2 } from "node:crypto";
3226
- import { readFile as readFile10, writeFile as writeFile5, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
3227
- import { join as join9, 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";
3228
3399
  function contentHash(content) {
3229
- return createHash2("sha256").update(content).digest("hex");
3400
+ return createHash3("sha256").update(content).digest("hex");
3230
3401
  }
3231
3402
  async function runSync() {
3232
3403
  const flags = parseFlags(3);
@@ -3292,7 +3463,7 @@ async function runSync() {
3292
3463
  }
3293
3464
  async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3294
3465
  console.log(" Reading local and remote state...");
3295
- const claudeDir = join9(projectPath, ".claude");
3466
+ const claudeDir = join11(projectPath, ".claude");
3296
3467
  let localFiles = /* @__PURE__ */ new Map();
3297
3468
  try {
3298
3469
  localFiles = await scanLocalFiles(claudeDir, projectPath);
@@ -3527,7 +3698,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3527
3698
  for (const p of toPull) {
3528
3699
  if (p.filePath && p.remoteContent !== null) {
3529
3700
  await mkdir2(dirname2(p.filePath), { recursive: true });
3530
- await writeFile5(p.filePath, p.remoteContent, "utf-8");
3701
+ await writeFile7(p.filePath, p.remoteContent, "utf-8");
3531
3702
  if (p.isHook) await chmod2(p.filePath, 493);
3532
3703
  }
3533
3704
  }
@@ -3681,7 +3852,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3681
3852
  console.log("\n Sync complete.\n");
3682
3853
  }
3683
3854
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
3684
- const settingsPath = join9(claudeDir, "settings.json");
3855
+ const settingsPath = join11(claudeDir, "settings.json");
3685
3856
  const globalSettingsFiles = syncData.global_settings ?? [];
3686
3857
  let globalSettings = {};
3687
3858
  for (const gf of globalSettingsFiles) {
@@ -3701,11 +3872,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3701
3872
  globalSettings,
3702
3873
  repoSettings
3703
3874
  );
3704
- const hooksDir = join9(projectPath, ".claude", "hooks");
3875
+ const hooksDir = join11(projectPath, ".claude", "hooks");
3705
3876
  const discovered = await discoverHooks(hooksDir);
3706
3877
  let localSettings = {};
3707
3878
  try {
3708
- const raw = await readFile10(settingsPath, "utf-8");
3879
+ const raw = await readFile12(settingsPath, "utf-8");
3709
3880
  localSettings = JSON.parse(raw);
3710
3881
  } catch {
3711
3882
  }
@@ -3720,7 +3891,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3720
3891
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
3721
3892
  let currentContent = "";
3722
3893
  try {
3723
- currentContent = await readFile10(settingsPath, "utf-8");
3894
+ currentContent = await readFile12(settingsPath, "utf-8");
3724
3895
  } catch {
3725
3896
  }
3726
3897
  if (currentContent === mergedContent) {
@@ -3732,30 +3903,74 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
3732
3903
  return;
3733
3904
  }
3734
3905
  await mkdir2(dirname2(settingsPath), { recursive: true });
3735
- await writeFile5(settingsPath, mergedContent, "utf-8");
3906
+ await writeFile7(settingsPath, mergedContent, "utf-8");
3736
3907
  console.log(" Updated settings.json");
3737
3908
  }
3738
3909
  async function syncConfig(repoId, projectPath, dryRun) {
3739
- const configPath = join9(projectPath, ".codebyplan.json");
3910
+ const configPath = join11(projectPath, ".codebyplan.json");
3740
3911
  let currentConfig = {};
3741
3912
  try {
3742
- const raw = await readFile10(configPath, "utf-8");
3913
+ const raw = await readFile12(configPath, "utf-8");
3743
3914
  currentConfig = JSON.parse(raw);
3744
3915
  } catch {
3745
3916
  currentConfig = { repo_id: repoId };
3746
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
+ }
3747
3945
  let resolvedWorktreeId;
3748
3946
  try {
3749
- resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath);
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
+ }
3750
3968
  } catch (err) {
3751
3969
  const msg = err instanceof Error ? err.message : String(err);
3752
3970
  console.warn(
3753
3971
  ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
3754
3972
  );
3755
3973
  }
3756
- if (resolvedWorktreeId && currentConfig.worktree_id !== resolvedWorktreeId) {
3757
- currentConfig = { ...currentConfig, worktree_id: resolvedWorktreeId };
3758
- }
3759
3974
  const repoRes = await apiGet(`/repos/${repoId}`);
3760
3975
  const repo = repoRes.data;
3761
3976
  let portAllocations = [];
@@ -3765,8 +3980,8 @@ async function syncConfig(repoId, projectPath, dryRun) {
3765
3980
  { repo_id: repoId }
3766
3981
  );
3767
3982
  const allAllocations = portsRes.data ?? [];
3768
- const worktreeId2 = currentConfig.worktree_id;
3769
- 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);
3770
3985
  const ALLOWED_FIELDS = [
3771
3986
  "id",
3772
3987
  "repo_id",
@@ -3794,7 +4009,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
3794
4009
  ` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
3795
4010
  );
3796
4011
  }
3797
- const worktreeId = currentConfig.worktree_id;
4012
+ const worktreeId = resolvedWorktreeId;
3798
4013
  const matchingAlloc = portAllocations[0];
3799
4014
  const defaultBranchConfig = {
3800
4015
  protected: ["main", "development"],
@@ -3805,7 +4020,9 @@ async function syncConfig(repoId, projectPath, dryRun) {
3805
4020
  const branchConfig = repo.branch_config ?? defaultBranchConfig;
3806
4021
  const newConfig = {
3807
4022
  repo_id: repoId,
3808
- ...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.
3809
4026
  server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
3810
4027
  server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
3811
4028
  git_branch: repo.git_branch ?? "development",
@@ -3823,7 +4040,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
3823
4040
  console.log(" Config would be updated (dry-run).");
3824
4041
  return;
3825
4042
  }
3826
- await writeFile5(configPath, newJson + "\n", "utf-8");
4043
+ await writeFile7(configPath, newJson + "\n", "utf-8");
3827
4044
  console.log(" Updated .codebyplan.json");
3828
4045
  }
3829
4046
  async function syncTechStack(repoId, projectPath, dryRun) {
@@ -3975,28 +4192,28 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
3975
4192
  hook: { dir: "hooks", ext: ".sh" },
3976
4193
  template: { dir: "templates", ext: "" },
3977
4194
  context: { dir: "context", ext: ".md" },
3978
- docs_stack: { dir: join9("docs", "stack"), ext: ".md" },
4195
+ docs_stack: { dir: join11("docs", "stack"), ext: ".md" },
3979
4196
  docs: { dir: "docs", ext: ".md" },
3980
4197
  claude_md: { dir: "", ext: "" },
3981
4198
  settings: { dir: "", ext: "" }
3982
4199
  };
3983
- if (remote.type === "claude_md") return join9(projectPath, "CLAUDE.md");
3984
- if (remote.type === "settings") return join9(claudeDir, "settings.json");
4200
+ if (remote.type === "claude_md") return join11(projectPath, "CLAUDE.md");
4201
+ if (remote.type === "settings") return join11(claudeDir, "settings.json");
3985
4202
  const cfg = typeConfig2[remote.type];
3986
- if (!cfg) return join9(claudeDir, remote.name);
3987
- const typeDir = remote.type === "command" ? join9(claudeDir, cfg.dir, "cbp") : join9(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);
3988
4205
  if (cfg.subfolder)
3989
- return join9(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
4206
+ return join11(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
3990
4207
  if (remote.type === "command" && remote.category)
3991
- return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3992
- if (remote.type === "template") return join9(typeDir, remote.name);
4208
+ return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4209
+ if (remote.type === "template") return join11(typeDir, remote.name);
3993
4210
  if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
3994
- return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3995
- return join9(typeDir, `${remote.name}${cfg.ext}`);
4211
+ return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4212
+ return join11(typeDir, `${remote.name}${cfg.ext}`);
3996
4213
  }
3997
4214
  function getSyncVersion() {
3998
4215
  try {
3999
- return "1.4.3";
4216
+ return "1.5.0";
4000
4217
  } catch {
4001
4218
  return "unknown";
4002
4219
  }
@@ -4046,10 +4263,63 @@ var init_sync = __esm({
4046
4263
  init_hook_registry();
4047
4264
  init_port_verify();
4048
4265
  init_resolve_worktree();
4266
+ init_local_config();
4267
+ init_migrate_local_config();
4049
4268
  init_eslint();
4050
4269
  }
4051
4270
  });
4052
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
+
4053
4323
  // src/index.ts
4054
4324
  init_version();
4055
4325
  import { readFileSync } from "node:fs";
@@ -4114,16 +4384,22 @@ void (async () => {
4114
4384
  }
4115
4385
  process.exit(0);
4116
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
+ }
4117
4392
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
4118
4393
  console.log(`
4119
4394
  CodeByPlan CLI v${VERSION}
4120
4395
 
4121
4396
  Usage:
4122
- codebyplan setup Interactive setup (API key + project init + first sync)
4123
- codebyplan sync Bidirectional sync (pull + push + config)
4124
- codebyplan eslint ESLint config management (init, sync)
4125
- codebyplan help Show this help message
4126
- 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
4127
4403
 
4128
4404
  Sync options:
4129
4405
  --path <dir> Project root directory (default: cwd)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,7 @@
54
54
  "eslint-plugin-prettier": "^5.2.2",
55
55
  "eslint-plugin-security": "^3.0.1",
56
56
  "globals": "^17.0.0",
57
+ "prettier": "^3.8.1",
57
58
  "typescript": "^5",
58
59
  "typescript-eslint": "^8.20.0",
59
60
  "vitest": "^4.0.18"