codebyplan 1.5.0 → 1.5.1

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 +48 -5
  2. package/dist/cli.js +542 -2387
  3. package/package.json +1 -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.5.0";
17
+ VERSION = "1.5.1";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -117,9 +117,6 @@ async function apiPost(path, body) {
117
117
  async function apiPut(path, body) {
118
118
  return request("PUT", path, { body });
119
119
  }
120
- async function apiDelete(path, params) {
121
- await request("DELETE", path, { params });
122
- }
123
120
  var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
124
121
  var init_api = __esm({
125
122
  "src/lib/api.ts"() {
@@ -296,633 +293,6 @@ var init_local_config = __esm({
296
293
  }
297
294
  });
298
295
 
299
- // src/lib/settings-merge.ts
300
- function mergeSettings(template, local) {
301
- const merged = { ...local };
302
- for (const key of TEMPLATE_MANAGED_KEYS) {
303
- if (key in template) {
304
- merged[key] = template[key];
305
- }
306
- }
307
- if (template.permissions && typeof template.permissions === "object") {
308
- const templatePerms = template.permissions;
309
- const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
310
- const mergedPerms = { ...localPerms };
311
- for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
312
- if (key in templatePerms) {
313
- mergedPerms[key] = templatePerms[key];
314
- }
315
- }
316
- merged.permissions = mergedPerms;
317
- }
318
- return merged;
319
- }
320
- function mergeGlobalAndRepoSettings(global, repo) {
321
- const merged = { ...global, ...repo };
322
- const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
323
- const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
324
- if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
325
- const mergedPerms = { ...globalPerms, ...repoPerms };
326
- for (const key of ARRAY_PERMISSION_KEYS) {
327
- const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
328
- const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
329
- if (globalArr.length > 0 || repoArr.length > 0) {
330
- mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
331
- }
332
- }
333
- merged.permissions = mergedPerms;
334
- }
335
- return merged;
336
- }
337
- function stripPermissionsAllow(settings) {
338
- if (!settings.permissions || typeof settings.permissions !== "object") {
339
- return settings;
340
- }
341
- const perms = { ...settings.permissions };
342
- delete perms.allow;
343
- if (Object.keys(perms).length === 0) {
344
- const { permissions: _, ...rest } = settings;
345
- return rest;
346
- }
347
- return { ...settings, permissions: perms };
348
- }
349
- var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
350
- var init_settings_merge = __esm({
351
- "src/lib/settings-merge.ts"() {
352
- "use strict";
353
- TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
354
- TEMPLATE_MANAGED_PERMISSION_KEYS = [
355
- "deny",
356
- "ask",
357
- "additionalDirectories"
358
- ];
359
- ARRAY_PERMISSION_KEYS = ["deny", "ask"];
360
- }
361
- });
362
-
363
- // src/lib/hook-registry.ts
364
- import { readdir, readFile as readFile3 } from "node:fs/promises";
365
- import { join as join3 } from "node:path";
366
- function parseHookMeta(content) {
367
- const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
368
- if (!lineMatch) return null;
369
- const parts = lineMatch[1].trim().split(/\s+/);
370
- const event = parts[0];
371
- if (!event) return null;
372
- return {
373
- event,
374
- matcher: parts.slice(1).join(" ")
375
- };
376
- }
377
- async function discoverHooks(hooksDir) {
378
- const discovered = /* @__PURE__ */ new Map();
379
- let filenames;
380
- try {
381
- const entries = await readdir(hooksDir);
382
- filenames = entries.filter((e) => e.endsWith(".sh"));
383
- } catch {
384
- return discovered;
385
- }
386
- for (const filename of filenames) {
387
- const content = await readFile3(join3(hooksDir, filename), "utf-8");
388
- const meta = parseHookMeta(content);
389
- if (meta) {
390
- discovered.set(filename.replace(/\.sh$/, ""), meta);
391
- }
392
- }
393
- return discovered;
394
- }
395
- function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
396
- if (discovered.size === 0) return existing;
397
- const merged = {};
398
- for (const [event, matchers] of Object.entries(existing)) {
399
- merged[event] = matchers.map((m) => ({
400
- matcher: m.matcher,
401
- hooks: [...m.hooks]
402
- }));
403
- }
404
- for (const [filename, meta] of discovered) {
405
- const command = `bash ${hooksRelPath}/${filename}.sh`;
406
- if (!merged[meta.event]) {
407
- merged[meta.event] = [];
408
- }
409
- const eventEntries = merged[meta.event];
410
- const alreadyRegistered = eventEntries.some(
411
- (m) => m.hooks.some((h) => h.command === command)
412
- );
413
- if (alreadyRegistered) continue;
414
- const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
415
- if (matcherEntry) {
416
- matcherEntry.hooks.push({ type: "command", command });
417
- } else {
418
- eventEntries.push({
419
- matcher: meta.matcher,
420
- hooks: [{ type: "command", command }]
421
- });
422
- }
423
- }
424
- return merged;
425
- }
426
- function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
427
- const prefix = `bash ${hooksRelPath}/`;
428
- const stripped = {};
429
- for (const [event, matchers] of Object.entries(config)) {
430
- const filteredMatchers = [];
431
- for (const matcher of matchers) {
432
- const filteredHooks = matcher.hooks.filter(
433
- (h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
434
- );
435
- if (filteredHooks.length > 0) {
436
- filteredMatchers.push({
437
- matcher: matcher.matcher,
438
- hooks: filteredHooks
439
- });
440
- }
441
- }
442
- if (filteredMatchers.length > 0) {
443
- stripped[event] = filteredMatchers;
444
- }
445
- }
446
- return stripped;
447
- }
448
- var init_hook_registry = __esm({
449
- "src/lib/hook-registry.ts"() {
450
- "use strict";
451
- }
452
- });
453
-
454
- // src/lib/variables.ts
455
- function splitFrontmatter(content) {
456
- const fmMatch = content.match(/^(---\s*\n[\s\S]*?\n---\n?)([\s\S]*)$/);
457
- if (fmMatch) {
458
- return { frontmatter: fmMatch[1], body: fmMatch[2] };
459
- }
460
- if (content.startsWith("#!/") || content.startsWith("# @")) {
461
- const lines = content.split("\n");
462
- let headerEnd = 0;
463
- for (let i = 0; i < lines.length; i++) {
464
- if (lines[i].startsWith("#") || lines[i].startsWith("#!/") || lines[i].trim() === "") {
465
- headerEnd = i + 1;
466
- } else {
467
- break;
468
- }
469
- }
470
- return {
471
- frontmatter: lines.slice(0, headerEnd).join("\n") + "\n",
472
- body: lines.slice(headerEnd).join("\n")
473
- };
474
- }
475
- return { frontmatter: "", body: content };
476
- }
477
- function substituteVariables(content, repoData) {
478
- if (!content.includes("{{")) return content;
479
- const { frontmatter, body } = splitFrontmatter(content);
480
- let result = body;
481
- for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
482
- const placeholder = `{{${name}}}`;
483
- if (result.includes(placeholder)) {
484
- result = result.replaceAll(placeholder, resolver(repoData));
485
- }
486
- }
487
- return frontmatter + result;
488
- }
489
- function escapeRegex(str) {
490
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
491
- }
492
- function reverseSubstituteVariables(content, repoData) {
493
- const { frontmatter, body } = splitFrontmatter(content);
494
- const entries = [];
495
- for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
496
- const value = resolver(repoData);
497
- if (value.length === 0) continue;
498
- entries.push([value, `{{${name}}}`]);
499
- }
500
- entries.sort((a, b) => b[0].length - a[0].length);
501
- let result = body;
502
- for (const [value, placeholder] of entries) {
503
- if (value.length < 8) {
504
- const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
505
- result = result.replace(pattern, placeholder);
506
- } else {
507
- result = result.replaceAll(value, placeholder);
508
- }
509
- }
510
- return frontmatter + result;
511
- }
512
- var TEMPLATE_VARIABLES;
513
- var init_variables = __esm({
514
- "src/lib/variables.ts"() {
515
- "use strict";
516
- TEMPLATE_VARIABLES = {
517
- REPO_ID: (repo) => repo.id,
518
- REPO_NAME: (repo) => repo.name,
519
- REPO_PATH: (repo) => repo.path ?? "",
520
- GIT_BRANCH: (repo) => repo.git_branch ?? "development",
521
- SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
522
- SERVER_TYPE: (repo) => repo.server_type ?? "none"
523
- };
524
- }
525
- });
526
-
527
- // src/lib/sync-engine.ts
528
- var sync_engine_exports = {};
529
- __export(sync_engine_exports, {
530
- executeSyncToLocal: () => executeSyncToLocal
531
- });
532
- import {
533
- readdir as readdir2,
534
- readFile as readFile4,
535
- writeFile as writeFile3,
536
- unlink,
537
- mkdir,
538
- rmdir,
539
- chmod,
540
- stat
541
- } from "node:fs/promises";
542
- import { join as join4, dirname } from "node:path";
543
- function getTypeDir(claudeDir, dir) {
544
- if (dir === "commands") return join4(claudeDir, dir, "cbp");
545
- return join4(claudeDir, dir);
546
- }
547
- function getFilePath(claudeDir, typeName, file) {
548
- const cfg = typeConfig[typeName];
549
- const typeDir = getTypeDir(claudeDir, cfg.dir);
550
- if (cfg.subfolder) {
551
- return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
552
- }
553
- if (typeName === "command" && file.category) {
554
- return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
555
- }
556
- if (typeName === "template") {
557
- return join4(typeDir, file.name);
558
- }
559
- return join4(typeDir, `${file.name}${cfg.ext}`);
560
- }
561
- async function readDirRecursive(dir, base = dir) {
562
- const result = /* @__PURE__ */ new Map();
563
- try {
564
- const entries = await readdir2(dir, { withFileTypes: true });
565
- for (const entry of entries) {
566
- const fullPath = join4(dir, entry.name);
567
- if (entry.isDirectory()) {
568
- const sub = await readDirRecursive(fullPath, base);
569
- for (const [k, v] of sub) result.set(k, v);
570
- } else {
571
- const relPath = fullPath.slice(base.length + 1);
572
- const fileContent = await readFile4(fullPath, "utf-8");
573
- result.set(relPath, fileContent);
574
- }
575
- }
576
- } catch {
577
- }
578
- return result;
579
- }
580
- async function isGitWorktree(projectPath) {
581
- try {
582
- const gitPath = join4(projectPath, ".git");
583
- const info = await stat(gitPath);
584
- return info.isFile();
585
- } catch {
586
- return false;
587
- }
588
- }
589
- async function removeEmptyParents(filePath, stopAt) {
590
- let dir = dirname(filePath);
591
- while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
592
- try {
593
- await rmdir(dir);
594
- dir = dirname(dir);
595
- } catch {
596
- break;
597
- }
598
- }
599
- }
600
- async function executeSyncToLocal(options) {
601
- const { repoId, projectPath, dryRun = false } = options;
602
- const [syncRes, repoRes] = await Promise.all([
603
- apiGet("/sync/defaults"),
604
- apiGet(`/repos/${repoId}`)
605
- ]);
606
- const syncData = syncRes.data;
607
- const repoData = repoRes.data;
608
- syncData.claude_md = [];
609
- const claudeDir = join4(projectPath, ".claude");
610
- const worktree = await isGitWorktree(projectPath);
611
- const byType = {};
612
- const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
613
- const dbOnlyFiles = [];
614
- for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
615
- if (worktree && typeName === "command") {
616
- byType["commands"] = {
617
- created: [],
618
- updated: [],
619
- deleted: [],
620
- unchanged: []
621
- };
622
- continue;
623
- }
624
- const cfg = typeConfig[typeName];
625
- const targetDir = getTypeDir(claudeDir, cfg.dir);
626
- const remoteFiles = syncData[syncKey] ?? [];
627
- const result = {
628
- created: [],
629
- updated: [],
630
- deleted: [],
631
- unchanged: []
632
- };
633
- if (!dryRun) {
634
- await mkdir(targetDir, { recursive: true });
635
- }
636
- const localFiles = await readDirRecursive(targetDir);
637
- const remotePathMap = /* @__PURE__ */ new Map();
638
- for (const remote of remoteFiles) {
639
- const fullPath = getFilePath(claudeDir, typeName, remote);
640
- const relPath = fullPath.slice(targetDir.length + 1);
641
- const substituted = substituteVariables(remote.content, repoData);
642
- remotePathMap.set(relPath, { content: substituted, name: remote.name });
643
- }
644
- for (const [relPath, { content, name }] of remotePathMap) {
645
- const fullPath = join4(targetDir, relPath);
646
- const localContent = localFiles.get(relPath);
647
- if (localContent === void 0) {
648
- const remoteFile = remoteFiles.find((f) => f.name === name);
649
- dbOnlyFiles.push({
650
- type: typeName,
651
- name,
652
- category: remoteFile?.category ?? null,
653
- localPath: fullPath
654
- });
655
- if (!dryRun) {
656
- await mkdir(dirname(fullPath), { recursive: true });
657
- await writeFile3(fullPath, content, "utf-8");
658
- if (typeName === "hook") await chmod(fullPath, 493);
659
- }
660
- result.created.push(name);
661
- totals.created++;
662
- } else if (localContent !== content) {
663
- if (!dryRun) {
664
- await writeFile3(fullPath, content, "utf-8");
665
- if (typeName === "hook") await chmod(fullPath, 493);
666
- }
667
- result.updated.push(name);
668
- totals.updated++;
669
- } else {
670
- result.unchanged.push(name);
671
- totals.unchanged++;
672
- }
673
- }
674
- for (const [relPath] of localFiles) {
675
- if (!remotePathMap.has(relPath)) {
676
- const fullPath = join4(targetDir, relPath);
677
- if (!dryRun) {
678
- await unlink(fullPath);
679
- await removeEmptyParents(fullPath, targetDir);
680
- }
681
- const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
682
- result.deleted.push(pathName);
683
- totals.deleted++;
684
- }
685
- }
686
- byType[`${typeName}s`] = result;
687
- }
688
- {
689
- const typeName = "docs_stack";
690
- const syncKey = "docs_stack";
691
- const targetDir = join4(projectPath, "docs", "stack");
692
- const remoteFiles = syncData[syncKey] ?? [];
693
- const result = {
694
- created: [],
695
- updated: [],
696
- deleted: [],
697
- unchanged: []
698
- };
699
- if (remoteFiles.length > 0 && !dryRun) {
700
- await mkdir(targetDir, { recursive: true });
701
- }
702
- const localFiles = await readDirRecursive(targetDir);
703
- const remotePathMap = /* @__PURE__ */ new Map();
704
- for (const remote of remoteFiles) {
705
- const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
706
- const substituted = substituteVariables(remote.content, repoData);
707
- remotePathMap.set(relPath, {
708
- content: substituted,
709
- name: remote.category ? `${remote.category}/${remote.name}` : remote.name
710
- });
711
- }
712
- for (const [relPath, { content, name }] of remotePathMap) {
713
- const fullPath = join4(targetDir, relPath);
714
- const localContent = localFiles.get(relPath);
715
- if (localContent === void 0) {
716
- if (!dryRun) {
717
- await mkdir(dirname(fullPath), { recursive: true });
718
- await writeFile3(fullPath, content, "utf-8");
719
- }
720
- result.created.push(name);
721
- totals.created++;
722
- } else if (localContent !== content) {
723
- if (!dryRun) {
724
- await writeFile3(fullPath, content, "utf-8");
725
- }
726
- result.updated.push(name);
727
- totals.updated++;
728
- } else {
729
- result.unchanged.push(name);
730
- totals.unchanged++;
731
- }
732
- }
733
- for (const [relPath] of localFiles) {
734
- if (!remotePathMap.has(relPath)) {
735
- const fullPath = join4(targetDir, relPath);
736
- if (!dryRun) {
737
- await unlink(fullPath);
738
- await removeEmptyParents(fullPath, targetDir);
739
- }
740
- result.deleted.push(relPath);
741
- totals.deleted++;
742
- }
743
- }
744
- byType[typeName] = result;
745
- }
746
- const globalSettingsFiles = syncData.global_settings ?? [];
747
- let globalSettings = {};
748
- for (const gf of globalSettingsFiles) {
749
- const parsed = JSON.parse(
750
- substituteVariables(gf.content, repoData)
751
- );
752
- globalSettings = { ...globalSettings, ...parsed };
753
- }
754
- const specialTypes = {
755
- claude_md: () => join4(projectPath, "CLAUDE.md"),
756
- settings: () => join4(projectPath, ".claude", "settings.json")
757
- };
758
- for (const [typeName, getPath] of Object.entries(specialTypes)) {
759
- const remoteFiles = syncData[typeName] ?? [];
760
- const result = {
761
- created: [],
762
- updated: [],
763
- deleted: [],
764
- unchanged: []
765
- };
766
- for (const remote of remoteFiles) {
767
- const targetPath = getPath(remote.name);
768
- const remoteContent = substituteVariables(remote.content, repoData);
769
- let localContent;
770
- try {
771
- localContent = await readFile4(targetPath, "utf-8");
772
- } catch {
773
- }
774
- if (typeName === "settings") {
775
- const repoSettings = JSON.parse(remoteContent);
776
- const combinedTemplate = mergeGlobalAndRepoSettings(
777
- globalSettings,
778
- repoSettings
779
- );
780
- const hooksDir = join4(projectPath, ".claude", "hooks");
781
- const discovered = await discoverHooks(hooksDir);
782
- if (localContent === void 0) {
783
- const finalSettings = stripPermissionsAllow(combinedTemplate);
784
- if (discovered.size > 0) {
785
- finalSettings.hooks = mergeDiscoveredHooks(
786
- finalSettings.hooks ?? {},
787
- discovered
788
- );
789
- }
790
- if (!dryRun) {
791
- await mkdir(dirname(targetPath), { recursive: true });
792
- await writeFile3(
793
- targetPath,
794
- JSON.stringify(finalSettings, null, 2) + "\n",
795
- "utf-8"
796
- );
797
- }
798
- result.created.push(remote.name);
799
- totals.created++;
800
- } else {
801
- const localSettings = JSON.parse(localContent);
802
- let merged = mergeSettings(combinedTemplate, localSettings);
803
- merged = stripPermissionsAllow(merged);
804
- if (discovered.size > 0) {
805
- merged.hooks = mergeDiscoveredHooks(
806
- merged.hooks ?? {},
807
- discovered
808
- );
809
- }
810
- const mergedContent = JSON.stringify(merged, null, 2) + "\n";
811
- if (localContent !== mergedContent) {
812
- if (!dryRun) {
813
- await writeFile3(targetPath, mergedContent, "utf-8");
814
- }
815
- result.updated.push(remote.name);
816
- totals.updated++;
817
- } else {
818
- result.unchanged.push(remote.name);
819
- totals.unchanged++;
820
- }
821
- }
822
- } else {
823
- if (localContent === void 0) {
824
- if (!dryRun) {
825
- await mkdir(dirname(targetPath), { recursive: true });
826
- await writeFile3(targetPath, remoteContent, "utf-8");
827
- }
828
- result.created.push(remote.name);
829
- totals.created++;
830
- } else if (localContent !== remoteContent) {
831
- if (!dryRun) {
832
- await writeFile3(targetPath, remoteContent, "utf-8");
833
- }
834
- result.updated.push(remote.name);
835
- totals.updated++;
836
- } else {
837
- result.unchanged.push(remote.name);
838
- totals.unchanged++;
839
- }
840
- }
841
- }
842
- byType[typeName] = result;
843
- }
844
- if (!dryRun) {
845
- await apiPost("/sync/state", {
846
- repo_id: repoId,
847
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
848
- was_skipped: false,
849
- files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
850
- files_pushed: 0,
851
- files_pulled: totals.created + totals.updated,
852
- files_deleted: totals.deleted,
853
- files_skipped: 0
854
- });
855
- const fileRepoUpdates = [];
856
- const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
857
- for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
858
- const remoteFiles = syncData[syncKey] ?? [];
859
- for (const file of remoteFiles) {
860
- fileRepoUpdates.push({
861
- claude_file_id: file.id ?? void 0,
862
- file_type: typeName,
863
- file_name: file.name,
864
- file_category: file.category ?? null,
865
- file_scope: file.scope ?? "shared",
866
- last_synced_at: syncTimestamp,
867
- sync_status: "synced"
868
- });
869
- }
870
- }
871
- for (const typeName of ["claude_md", "settings"]) {
872
- const remoteFiles = syncData[typeName] ?? [];
873
- for (const file of remoteFiles) {
874
- fileRepoUpdates.push({
875
- claude_file_id: file.id ?? void 0,
876
- file_type: typeName,
877
- file_name: file.name,
878
- file_category: file.category ?? null,
879
- file_scope: file.scope ?? `local:${repoId}`,
880
- last_synced_at: syncTimestamp,
881
- sync_status: "synced"
882
- });
883
- }
884
- }
885
- if (fileRepoUpdates.length > 0) {
886
- try {
887
- await apiPost("/sync/file-repos", {
888
- repo_id: repoId,
889
- file_repos: fileRepoUpdates
890
- });
891
- } catch {
892
- }
893
- }
894
- }
895
- return { byType, totals, dbOnlyFiles };
896
- }
897
- var typeConfig, syncKeyToType;
898
- var init_sync_engine = __esm({
899
- "src/lib/sync-engine.ts"() {
900
- "use strict";
901
- init_api();
902
- init_settings_merge();
903
- init_hook_registry();
904
- init_variables();
905
- typeConfig = {
906
- command: { dir: "commands", ext: ".md" },
907
- agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
908
- skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
909
- rule: { dir: "rules", ext: ".md" },
910
- hook: { dir: "hooks", ext: ".sh" },
911
- template: { dir: "templates", ext: "" },
912
- context: { dir: "context", ext: ".md" }
913
- };
914
- syncKeyToType = {
915
- commands: "command",
916
- agents: "agent",
917
- skills: "skill",
918
- rules: "rule",
919
- hooks: "hook",
920
- templates: "template",
921
- contexts: "context"
922
- };
923
- }
924
- });
925
-
926
296
  // src/cli/setup.ts
927
297
  var setup_exports = {};
928
298
  __export(setup_exports, {
@@ -930,15 +300,15 @@ __export(setup_exports, {
930
300
  });
931
301
  import { createInterface } from "node:readline/promises";
932
302
  import { stdin, stdout } from "node:process";
933
- import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
303
+ import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
934
304
  import { homedir } from "node:os";
935
- import { join as join5 } from "node:path";
305
+ import { join as join3 } from "node:path";
936
306
  function getConfigPath(scope) {
937
- return scope === "user" ? join5(homedir(), ".claude.json") : join5(process.cwd(), ".mcp.json");
307
+ return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
938
308
  }
939
309
  async function readConfig(path) {
940
310
  try {
941
- const raw = await readFile5(path, "utf-8");
311
+ const raw = await readFile3(path, "utf-8");
942
312
  const parsed = JSON.parse(raw);
943
313
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
944
314
  return parsed;
@@ -962,7 +332,7 @@ async function writeMcpConfig(scope, apiKey) {
962
332
  config.mcpServers = {};
963
333
  }
964
334
  config.mcpServers.codebyplan = buildMcpEntry(apiKey);
965
- await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
335
+ await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
966
336
  return configPath;
967
337
  }
968
338
  async function verifyMcpConfig(scope, apiKey) {
@@ -1089,12 +459,12 @@ async function runSetup() {
1089
459
  deviceId
1090
460
  });
1091
461
  const worktreeId = tupleId ?? pathBasedId;
1092
- const codebyplanPath = join5(projectPath, ".codebyplan.json");
462
+ const codebyplanPath = join3(projectPath, ".codebyplan.json");
1093
463
  const codebyplanConfig = {
1094
464
  repo_id: selectedRepo.id
1095
465
  };
1096
466
  if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
1097
- await writeFile4(
467
+ await writeFile3(
1098
468
  codebyplanPath,
1099
469
  JSON.stringify(codebyplanConfig, null, 2) + "\n",
1100
470
  "utf-8"
@@ -1105,27 +475,6 @@ async function runSetup() {
1105
475
  ` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
1106
476
  );
1107
477
  }
1108
- console.log("\n Running initial sync...\n");
1109
- try {
1110
- const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
1111
- const syncResult = await executeSyncToLocal2({
1112
- repoId: selectedRepo.id,
1113
- projectPath
1114
- });
1115
- const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
1116
- if (totalChanges > 0) {
1117
- console.log(
1118
- ` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
1119
- `
1120
- );
1121
- } else {
1122
- console.log(" All files already up to date.\n");
1123
- }
1124
- } catch (err) {
1125
- const msg = err instanceof Error ? err.message : String(err);
1126
- console.log(` Sync failed: ${msg}`);
1127
- console.log(" Run 'codebyplan sync' later to sync files.\n");
1128
- }
1129
478
  }
1130
479
  }
1131
480
  }
@@ -1144,15 +493,15 @@ var init_setup = __esm({
1144
493
  }
1145
494
  });
1146
495
 
1147
- // src/cli/config.ts
1148
- import { readFile as readFile6 } from "node:fs/promises";
1149
- import { join as join6, resolve } from "node:path";
496
+ // src/cli/flags.ts
497
+ import { readFile as readFile4 } from "node:fs/promises";
498
+ import { join as join4, resolve } from "node:path";
1150
499
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1151
500
  let cursor = resolve(startDir);
1152
501
  for (let depth = 0; depth < maxDepth; depth++) {
1153
- const configPath = join6(cursor, ".codebyplan.json");
502
+ const configPath = join4(cursor, ".codebyplan.json");
1154
503
  try {
1155
- const raw = await readFile6(configPath, "utf-8");
504
+ const raw = await readFile4(configPath, "utf-8");
1156
505
  const parsed = JSON.parse(raw);
1157
506
  return { path: configPath, contents: parsed };
1158
507
  } catch {
@@ -1196,590 +545,72 @@ async function resolveConfig(flags) {
1196
545
  }
1197
546
  return { repoId, worktreeId, projectPath };
1198
547
  }
1199
- var init_config = __esm({
1200
- "src/cli/config.ts"() {
548
+ var init_flags = __esm({
549
+ "src/cli/flags.ts"() {
1201
550
  "use strict";
1202
551
  }
1203
552
  });
1204
553
 
1205
- // src/cli/fileMapper.ts
1206
- import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
1207
- import { join as join7, extname } from "node:path";
1208
- function extractScope(content, type) {
1209
- if (type === "hook") {
1210
- const match = content.match(/^#\s*@scope:\s*(\S+)/m);
1211
- if (match) {
1212
- const raw = match[1];
1213
- return raw === "shared" ? "shared" : `local:${raw}`;
1214
- }
1215
- return "shared";
1216
- }
1217
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
1218
- if (fmMatch) {
1219
- const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
1220
- if (scopeLine) {
1221
- const raw = scopeLine[1];
1222
- return raw === "shared" ? "shared" : `local:${raw}`;
1223
- }
1224
- if (/^scope\b/m.test(fmMatch[1])) {
1225
- console.error(
1226
- ` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
1227
- );
1228
- }
1229
- }
1230
- return "shared";
1231
- }
1232
- function compositeKey(type, name, category) {
1233
- return category ? `${type}:${category}/${name}` : `${type}:${name}`;
1234
- }
1235
- async function scanLocalFiles(claudeDir, projectPath) {
1236
- const result = /* @__PURE__ */ new Map();
1237
- await scanCommands(join7(claudeDir, "commands", "cbp"), result);
1238
- await scanSubfolderType(
1239
- join7(claudeDir, "agents"),
1240
- "agent",
1241
- "AGENT.md",
1242
- result
1243
- );
1244
- await scanSubfolderType(
1245
- join7(claudeDir, "skills"),
1246
- "skill",
1247
- "SKILL.md",
1248
- result
1249
- );
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);
1253
- await scanCategorizedType(
1254
- join7(claudeDir, "context"),
1255
- "context",
1256
- ".md",
1257
- result
1258
- );
1259
- await scanDocsRecursive(join7(claudeDir, "docs"), result);
1260
- await scanSettings(claudeDir, projectPath, result);
1261
- return result;
1262
- }
1263
- async function scanCommands(dir, result) {
1264
- await scanCommandsRecursive(dir, dir, result);
554
+ // src/cli/confirm.ts
555
+ var confirm_exports = {};
556
+ __export(confirm_exports, {
557
+ SyncCancelledError: () => SyncCancelledError,
558
+ confirmProceed: () => confirmProceed
559
+ });
560
+ import { createInterface as createInterface2 } from "node:readline/promises";
561
+ import { stdin as stdin2, stdout as stdout2 } from "node:process";
562
+ function isAbortError(err) {
563
+ return err instanceof Error && err.code === "ABORT_ERR";
1265
564
  }
1266
- async function scanCommandsRecursive(baseDir, currentDir, result) {
1267
- let entries;
565
+ async function confirmProceed(message) {
566
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1268
567
  try {
1269
- entries = await readdir3(currentDir, { withFileTypes: true });
1270
- } catch {
1271
- return;
1272
- }
1273
- for (const entry of entries) {
1274
- if (entry.isDirectory()) {
1275
- await scanCommandsRecursive(
1276
- baseDir,
1277
- join7(currentDir, entry.name),
1278
- result
568
+ while (true) {
569
+ const answer = await rl.question(message ?? " Proceed? [Y/n] ");
570
+ const a = answer.trim().toLowerCase();
571
+ if (a === "" || a === "y" || a === "yes") return true;
572
+ if (a === "n" || a === "no") return false;
573
+ console.log(
574
+ ` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
1279
575
  );
1280
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1281
- const name = entry.name.slice(0, -3);
1282
- const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1283
- const relDir = currentDir.slice(baseDir.length + 1);
1284
- const category = relDir || null;
1285
- const scope = extractScope(content, "command");
1286
- const key = compositeKey("command", name, category);
1287
- result.set(key, { type: "command", name, category, content, scope });
1288
576
  }
577
+ } catch (err) {
578
+ if (isAbortError(err)) throw new SyncCancelledError();
579
+ throw err;
580
+ } finally {
581
+ rl.close();
1289
582
  }
1290
583
  }
1291
- async function scanSubfolderType(dir, type, fileName, result) {
1292
- let entries;
1293
- try {
1294
- entries = await readdir3(dir, { withFileTypes: true });
1295
- } catch {
1296
- return;
1297
- }
1298
- for (const entry of entries) {
1299
- if (entry.isDirectory()) {
1300
- const filePath = join7(dir, entry.name, fileName);
1301
- try {
1302
- const content = await readFile7(filePath, "utf-8");
1303
- const scope = extractScope(content, type);
1304
- const key = compositeKey(type, entry.name, null);
1305
- result.set(key, {
1306
- type,
1307
- name: entry.name,
1308
- category: null,
1309
- content,
1310
- scope
1311
- });
1312
- } catch {
584
+ var SyncCancelledError;
585
+ var init_confirm = __esm({
586
+ "src/cli/confirm.ts"() {
587
+ "use strict";
588
+ SyncCancelledError = class extends Error {
589
+ constructor() {
590
+ super("Sync cancelled");
591
+ this.name = "SyncCancelledError";
1313
592
  }
1314
- }
593
+ };
1315
594
  }
1316
- }
1317
- async function scanFlatType(dir, type, ext, result) {
1318
- let entries;
595
+ });
596
+
597
+ // src/lib/tech-detect.ts
598
+ import { readFile as readFile5, access, readdir } from "node:fs/promises";
599
+ import { join as join5, relative } from "node:path";
600
+ async function fileExists(filePath) {
1319
601
  try {
1320
- entries = await readdir3(dir, { withFileTypes: true });
602
+ await access(filePath);
603
+ return true;
1321
604
  } catch {
1322
- return;
1323
- }
1324
- for (const entry of entries) {
1325
- if (entry.isFile() && entry.name.endsWith(ext)) {
1326
- const name = entry.name.slice(0, -ext.length);
1327
- const content = await readFile7(join7(dir, entry.name), "utf-8");
1328
- const scope = extractScope(content, type);
1329
- const key = compositeKey(type, name, null);
1330
- result.set(key, { type, name, category: null, content, scope });
1331
- }
1332
- }
1333
- }
1334
- async function scanCategorizedType(dir, type, ext, result) {
1335
- let entries;
1336
- try {
1337
- entries = await readdir3(dir, { withFileTypes: true });
1338
- } catch {
1339
- return;
1340
- }
1341
- for (const entry of entries) {
1342
- if (entry.isDirectory()) {
1343
- const category = entry.name;
1344
- let subEntries;
1345
- try {
1346
- subEntries = await readdir3(join7(dir, category), {
1347
- withFileTypes: true
1348
- });
1349
- } catch {
1350
- continue;
1351
- }
1352
- for (const sub of subEntries) {
1353
- if (sub.isFile() && sub.name.endsWith(ext)) {
1354
- const name = sub.name.slice(0, -ext.length);
1355
- const content = await readFile7(
1356
- join7(dir, category, sub.name),
1357
- "utf-8"
1358
- );
1359
- const scope = extractScope(content, type);
1360
- const key = compositeKey(type, name, category);
1361
- result.set(key, { type, name, category, content, scope });
1362
- }
1363
- }
1364
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
1365
- const name = entry.name.slice(0, -ext.length);
1366
- const content = await readFile7(join7(dir, entry.name), "utf-8");
1367
- const scope = extractScope(content, type);
1368
- const key = compositeKey(type, name, null);
1369
- result.set(key, { type, name, category: null, content, scope });
1370
- }
1371
- }
1372
- }
1373
- async function scanDocsRecursive(docsDir, result) {
1374
- await scanDocsDir(docsDir, docsDir, result);
1375
- }
1376
- async function scanDocsDir(baseDir, currentDir, result) {
1377
- let entries;
1378
- try {
1379
- entries = await readdir3(currentDir, { withFileTypes: true });
1380
- } catch {
1381
- return;
1382
- }
1383
- for (const entry of entries) {
1384
- if (entry.isDirectory()) {
1385
- await scanDocsDir(baseDir, join7(currentDir, entry.name), result);
1386
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1387
- const name = entry.name.slice(0, -3);
1388
- const content = await readFile7(join7(currentDir, entry.name), "utf-8");
1389
- const scope = extractScope(content, "docs");
1390
- const relDir = currentDir.slice(baseDir.length + 1);
1391
- const category = relDir || null;
1392
- const key = compositeKey("docs", name, category);
1393
- result.set(key, { type: "docs", name, category, content, scope });
1394
- }
1395
- }
1396
- }
1397
- async function scanTemplates(dir, result) {
1398
- let entries;
1399
- try {
1400
- entries = await readdir3(dir, { withFileTypes: true });
1401
- } catch {
1402
- return;
1403
- }
1404
- for (const entry of entries) {
1405
- if (entry.isFile() && extname(entry.name)) {
1406
- const content = await readFile7(join7(dir, entry.name), "utf-8");
1407
- const scope = extractScope(content, "template");
1408
- const key = compositeKey("template", entry.name, null);
1409
- result.set(key, {
1410
- type: "template",
1411
- name: entry.name,
1412
- category: null,
1413
- content,
1414
- scope
1415
- });
1416
- }
1417
- }
1418
- }
1419
- async function scanSettings(claudeDir, projectPath, result) {
1420
- const settingsPath = join7(claudeDir, "settings.json");
1421
- let raw;
1422
- try {
1423
- raw = await readFile7(settingsPath, "utf-8");
1424
- } catch {
1425
- return;
1426
- }
1427
- let parsed;
1428
- try {
1429
- parsed = JSON.parse(raw);
1430
- } catch {
1431
- return;
1432
- }
1433
- parsed = stripPermissionsAllow(parsed);
1434
- if (parsed.hooks && typeof parsed.hooks === "object") {
1435
- const hooksDir = projectPath ? join7(projectPath, ".claude", "hooks") : join7(claudeDir, "hooks");
1436
- const discovered = await discoverHooks(hooksDir);
1437
- if (discovered.size > 0) {
1438
- parsed.hooks = stripDiscoveredHooks(
1439
- parsed.hooks,
1440
- ".claude/hooks"
1441
- );
1442
- if (Object.keys(parsed.hooks).length === 0) {
1443
- delete parsed.hooks;
1444
- }
1445
- }
1446
- }
1447
- const content = JSON.stringify(parsed, null, 2) + "\n";
1448
- const key = compositeKey("settings", "settings", null);
1449
- result.set(key, {
1450
- type: "settings",
1451
- name: "settings",
1452
- category: null,
1453
- content,
1454
- scope: "shared"
1455
- });
1456
- }
1457
- var init_fileMapper = __esm({
1458
- "src/cli/fileMapper.ts"() {
1459
- "use strict";
1460
- init_settings_merge();
1461
- init_hook_registry();
1462
- }
1463
- });
1464
-
1465
- // src/cli/confirm.ts
1466
- var confirm_exports = {};
1467
- __export(confirm_exports, {
1468
- SyncCancelledError: () => SyncCancelledError,
1469
- confirmEach: () => confirmEach,
1470
- confirmProceed: () => confirmProceed,
1471
- promptChoice: () => promptChoice,
1472
- promptReviewMode: () => promptReviewMode,
1473
- reviewFilesOneByOne: () => reviewFilesOneByOne,
1474
- reviewFolder: () => reviewFolder
1475
- });
1476
- import { createInterface as createInterface2 } from "node:readline/promises";
1477
- import { stdin as stdin2, stdout as stdout2 } from "node:process";
1478
- function isAbortError(err) {
1479
- return err instanceof Error && err.code === "ABORT_ERR";
1480
- }
1481
- async function confirmProceed(message) {
1482
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1483
- try {
1484
- while (true) {
1485
- const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1486
- const a = answer.trim().toLowerCase();
1487
- if (a === "" || a === "y" || a === "yes") return true;
1488
- if (a === "n" || a === "no") return false;
1489
- console.log(
1490
- ` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
1491
- );
1492
- }
1493
- } catch (err) {
1494
- if (isAbortError(err)) throw new SyncCancelledError();
1495
- throw err;
1496
- } finally {
1497
- rl.close();
1498
- }
1499
- }
1500
- async function promptChoice(message, options) {
1501
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1502
- try {
1503
- const answer = await rl.question(message);
1504
- const a = answer.trim().toLowerCase();
1505
- return options.includes(a) ? a : options[0];
1506
- } catch (err) {
1507
- if (isAbortError(err)) throw new SyncCancelledError();
1508
- throw err;
1509
- } finally {
1510
- rl.close();
1511
- }
1512
- }
1513
- async function confirmEach(items, label) {
1514
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1515
- const accepted = [];
1516
- try {
1517
- for (const item of items) {
1518
- const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
1519
- const a = answer.trim().toLowerCase();
1520
- if (a === "a") {
1521
- accepted.push(item, ...items.slice(items.indexOf(item) + 1));
1522
- break;
1523
- }
1524
- if (a === "y" || a === "yes" || a === "") {
1525
- accepted.push(item);
1526
- }
1527
- }
1528
- } catch (err) {
1529
- if (isAbortError(err)) throw new SyncCancelledError();
1530
- throw err;
1531
- } finally {
1532
- rl.close();
1533
- }
1534
- return accepted;
1535
- }
1536
- function parseReviewAction(input) {
1537
- const a = input.trim().toLowerCase();
1538
- switch (a) {
1539
- case "d":
1540
- case "delete":
1541
- return { action: "delete", all: false, special: null };
1542
- case "p":
1543
- case "pull":
1544
- return { action: "pull", all: false, special: null };
1545
- case "s":
1546
- case "push":
1547
- return { action: "push", all: false, special: null };
1548
- case "k":
1549
- case "skip":
1550
- return { action: "skip", all: false, special: null };
1551
- case "da":
1552
- return { action: "delete", all: true, special: null };
1553
- case "pa":
1554
- return { action: "pull", all: true, special: null };
1555
- case "sa":
1556
- return { action: "push", all: true, special: null };
1557
- case "ka":
1558
- return { action: "skip", all: true, special: null };
1559
- case "v":
1560
- case "view":
1561
- return { action: null, all: false, special: "view" };
1562
- case "r":
1563
- case "recommended":
1564
- return { action: null, all: false, special: "recommended" };
1565
- case "":
1566
- return { action: null, all: false, special: "recommended" };
1567
- // Enter = recommended
1568
- default:
1569
- return { action: null, all: false, special: null };
1570
- }
1571
- }
1572
- function formatActionPrompt(recommended, includeView, includeRecommended) {
1573
- const actions = [
1574
- `[d]elete${recommended === "delete" ? "\u2605" : ""}`,
1575
- `[p]ull${recommended === "pull" ? "\u2605" : ""}`,
1576
- `pu[s]h${recommended === "push" ? "\u2605" : ""}`,
1577
- `s[k]ip${recommended === "skip" ? "\u2605" : ""}`
1578
- ];
1579
- if (includeView) actions.push("[v]iew");
1580
- if (includeRecommended) actions.push("[r]ecommended");
1581
- return actions.join(" ");
1582
- }
1583
- function showDiff(local, remote, displayPath) {
1584
- console.log(`
1585
- --- ${displayPath} (diff) ---`);
1586
- if (local === null && remote !== null) {
1587
- console.log(" (no local file \u2014 remote content below)");
1588
- for (const line of remote.split("\n").slice(0, 30)) {
1589
- console.log(` + ${line}`);
1590
- }
1591
- if (remote.split("\n").length > 30) console.log(" ... (truncated)");
1592
- } else if (local !== null && remote === null) {
1593
- console.log(" (no remote file \u2014 local content below)");
1594
- for (const line of local.split("\n").slice(0, 30)) {
1595
- console.log(` - ${line}`);
1596
- }
1597
- if (local.split("\n").length > 30) console.log(" ... (truncated)");
1598
- } else if (local !== null && remote !== null) {
1599
- const localLines = local.split("\n");
1600
- const remoteLines = remote.split("\n");
1601
- let shown = 0;
1602
- const maxLines = 40;
1603
- for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
1604
- const l = localLines[i];
1605
- const r = remoteLines[i];
1606
- if (l === r) {
1607
- console.log(` ${l ?? ""}`);
1608
- } else {
1609
- if (l !== void 0) console.log(` - ${l}`);
1610
- if (r !== void 0) console.log(` + ${r}`);
1611
- }
1612
- shown++;
1613
- }
1614
- if (Math.max(localLines.length, remoteLines.length) > maxLines) {
1615
- console.log(" ... (truncated)");
1616
- }
1617
- }
1618
- console.log();
1619
- }
1620
- async function promptReviewMode() {
1621
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1622
- try {
1623
- while (true) {
1624
- const answer = await rl.question(
1625
- " Review [o]ne-by-one or [f]older-by-folder? "
1626
- );
1627
- const a = answer.trim().toLowerCase();
1628
- if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
1629
- return "file";
1630
- if (a === "f" || a === "folder") return "folder";
1631
- console.log(
1632
- ` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
1633
- );
1634
- }
1635
- } catch (err) {
1636
- if (isAbortError(err)) throw new SyncCancelledError();
1637
- throw err;
1638
- } finally {
1639
- rl.close();
1640
- }
1641
- }
1642
- async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
1643
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1644
- const results = [];
1645
- try {
1646
- let applyAll = null;
1647
- for (const item of items) {
1648
- if (applyAll) {
1649
- results.push(applyAll);
1650
- continue;
1651
- }
1652
- const planned = plannedAction(item);
1653
- const rec = recommendedAction ? recommendedAction(item) : planned;
1654
- const hasContent = content != null;
1655
- const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
1656
- while (true) {
1657
- const answer = await rl.question(prompt);
1658
- const result = parseReviewAction(answer);
1659
- if (result.special === "view") {
1660
- if (content) {
1661
- showDiff(content.local(item), content.remote(item), label(item));
1662
- } else {
1663
- console.log(" No content available for diff.");
1664
- }
1665
- continue;
1666
- }
1667
- if (result.special === "recommended") {
1668
- results.push(rec);
1669
- break;
1670
- }
1671
- if (result.action === null) {
1672
- console.log(
1673
- ` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
1674
- );
1675
- continue;
1676
- }
1677
- results.push(result.action);
1678
- if (result.all) applyAll = result.action;
1679
- break;
1680
- }
1681
- }
1682
- } catch (err) {
1683
- if (isAbortError(err)) throw new SyncCancelledError();
1684
- throw err;
1685
- } finally {
1686
- rl.close();
1687
- }
1688
- return results;
1689
- }
1690
- async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
1691
- console.log(`
1692
- ${folderName} (${items.length} files):`);
1693
- for (const item of items) {
1694
- const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
1695
- const actionLabel = plannedAction(item);
1696
- const star = actionLabel === rec ? "\u2605" : "";
1697
- console.log(` ${label(item)} (${actionLabel}${star})`);
1698
- }
1699
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1700
- try {
1701
- while (true) {
1702
- const promptStr = ` Action for all: ${formatActionPrompt(
1703
- recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1704
- false,
1705
- true
1706
- )} [o]ne-by-one: `;
1707
- const answer = await rl.question(promptStr);
1708
- const a = answer.trim().toLowerCase();
1709
- if (a === "o" || a === "one-by-one") {
1710
- rl.close();
1711
- return reviewFilesOneByOne(
1712
- items,
1713
- label,
1714
- plannedAction,
1715
- recommendedAction,
1716
- content
1717
- );
1718
- }
1719
- if (a === "r" || a === "recommended") {
1720
- return items.map(
1721
- (item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
1722
- );
1723
- }
1724
- if (a === "v" || a === "view") {
1725
- if (content) {
1726
- for (const item of items) {
1727
- showDiff(content.local(item), content.remote(item), label(item));
1728
- }
1729
- } else {
1730
- console.log(" No content available for diff.");
1731
- }
1732
- continue;
1733
- }
1734
- const result = parseReviewAction(a);
1735
- if (result.action !== null) {
1736
- return items.map(() => result.action);
1737
- }
1738
- console.log(
1739
- ` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
1740
- recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1741
- false,
1742
- true
1743
- )} [o]ne-by-one`
1744
- );
1745
- }
1746
- } catch (err) {
1747
- if (isAbortError(err)) throw new SyncCancelledError();
1748
- throw err;
1749
- } finally {
1750
- rl.close();
1751
- }
1752
- }
1753
- var SyncCancelledError;
1754
- var init_confirm = __esm({
1755
- "src/cli/confirm.ts"() {
1756
- "use strict";
1757
- SyncCancelledError = class extends Error {
1758
- constructor() {
1759
- super("Sync cancelled");
1760
- this.name = "SyncCancelledError";
1761
- }
1762
- };
1763
- }
1764
- });
1765
-
1766
- // src/lib/tech-detect.ts
1767
- import { readFile as readFile8, access, readdir as readdir4 } from "node:fs/promises";
1768
- import { join as join8, relative } from "node:path";
1769
- async function fileExists(filePath) {
1770
- try {
1771
- await access(filePath);
1772
- return true;
1773
- } catch {
1774
- return false;
605
+ return false;
1775
606
  }
1776
607
  }
1777
608
  async function discoverMonorepoApps(projectPath) {
1778
609
  const apps = [];
1779
610
  const patterns = [];
1780
611
  try {
1781
- const raw = await readFile8(
1782
- join8(projectPath, "pnpm-workspace.yaml"),
612
+ const raw = await readFile5(
613
+ join5(projectPath, "pnpm-workspace.yaml"),
1783
614
  "utf-8"
1784
615
  );
1785
616
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1793,7 +624,7 @@ async function discoverMonorepoApps(projectPath) {
1793
624
  }
1794
625
  if (patterns.length === 0) {
1795
626
  try {
1796
- const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
627
+ const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
1797
628
  const pkg = JSON.parse(raw);
1798
629
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1799
630
  if (ws) patterns.push(...ws);
@@ -1803,14 +634,14 @@ async function discoverMonorepoApps(projectPath) {
1803
634
  for (const pattern of patterns) {
1804
635
  if (pattern.endsWith("/*")) {
1805
636
  const dir = pattern.slice(0, -2);
1806
- const absDir = join8(projectPath, dir);
637
+ const absDir = join5(projectPath, dir);
1807
638
  try {
1808
- const entries = await readdir4(absDir, { withFileTypes: true });
639
+ const entries = await readdir(absDir, { withFileTypes: true });
1809
640
  for (const entry of entries) {
1810
641
  if (entry.isDirectory()) {
1811
- const relPath = join8(dir, entry.name);
1812
- const absPath = join8(absDir, entry.name);
1813
- if (await fileExists(join8(absPath, "package.json"))) {
642
+ const relPath = join5(dir, entry.name);
643
+ const absPath = join5(absDir, entry.name);
644
+ if (await fileExists(join5(absPath, "package.json"))) {
1814
645
  apps.push({ name: entry.name, path: relPath, absPath });
1815
646
  }
1816
647
  }
@@ -1824,12 +655,12 @@ async function discoverMonorepoApps(projectPath) {
1824
655
  async function hasJsxFile(dir, depth = 0) {
1825
656
  if (depth > 6) return false;
1826
657
  try {
1827
- const entries = await readdir4(dir, { withFileTypes: true });
658
+ const entries = await readdir(dir, { withFileTypes: true });
1828
659
  for (const entry of entries) {
1829
660
  const name = entry.name;
1830
661
  if (entry.isDirectory()) {
1831
662
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1832
- if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
663
+ if (await hasJsxFile(join5(dir, name), depth + 1)) return true;
1833
664
  } else if (entry.isFile()) {
1834
665
  if (JSX_TEST_PATTERN.test(name)) continue;
1835
666
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1848,7 +679,7 @@ async function hasJsxFile(dir, depth = 0) {
1848
679
  async function detectCapabilities(dirPath, pkgJson) {
1849
680
  const caps = /* @__PURE__ */ new Set();
1850
681
  for (const sub of JSX_SCAN_DIRS) {
1851
- if (await hasJsxFile(join8(dirPath, sub))) {
682
+ if (await hasJsxFile(join5(dirPath, sub))) {
1852
683
  caps.add("jsx");
1853
684
  break;
1854
685
  }
@@ -1870,7 +701,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1870
701
  }
1871
702
  }
1872
703
  }
1873
- if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
704
+ if (!caps.has("node-server") && await fileExists(join5(dirPath, "src", "main.ts"))) {
1874
705
  caps.add("node-server");
1875
706
  }
1876
707
  if (pkgJson && pkgJson.bin) {
@@ -1886,7 +717,7 @@ async function detectFromDirectory(dirPath) {
1886
717
  const seen = /* @__PURE__ */ new Map();
1887
718
  let pkgJson = null;
1888
719
  try {
1889
- const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
720
+ const raw = await readFile5(join5(dirPath, "package.json"), "utf-8");
1890
721
  pkgJson = JSON.parse(raw);
1891
722
  const allDeps = {
1892
723
  ...pkgJson.dependencies ?? {},
@@ -1918,7 +749,7 @@ async function detectFromDirectory(dirPath) {
1918
749
  }
1919
750
  for (const { file, rule } of CONFIG_FILE_MAP) {
1920
751
  const key = rule.name.toLowerCase();
1921
- if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
752
+ if (!seen.has(key) && await fileExists(join5(dirPath, file))) {
1922
753
  seen.set(key, { name: rule.name, category: rule.category });
1923
754
  }
1924
755
  }
@@ -2096,16 +927,16 @@ function categorizeDependency(depName) {
2096
927
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2097
928
  if (depth > 4) return [];
2098
929
  const results = [];
2099
- const pkgPath = join8(dir, "package.json");
930
+ const pkgPath = join5(dir, "package.json");
2100
931
  if (await fileExists(pkgPath)) {
2101
932
  results.push(pkgPath);
2102
933
  }
2103
934
  try {
2104
- const entries = await readdir4(dir, { withFileTypes: true });
935
+ const entries = await readdir(dir, { withFileTypes: true });
2105
936
  for (const entry of entries) {
2106
937
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
2107
938
  const subResults = await findPackageJsonFiles(
2108
- join8(dir, entry.name),
939
+ join5(dir, entry.name),
2109
940
  projectPath,
2110
941
  depth + 1
2111
942
  );
@@ -2120,7 +951,7 @@ async function scanAllDependencies(projectPath) {
2120
951
  const dependencies = [];
2121
952
  for (const pkgPath of packageJsonPaths) {
2122
953
  try {
2123
- const raw = await readFile8(pkgPath, "utf-8");
954
+ const raw = await readFile5(pkgPath, "utf-8");
2124
955
  const pkg = JSON.parse(raw);
2125
956
  const sourcePath = relative(projectPath, pkgPath);
2126
957
  const depSections = [
@@ -2334,238 +1165,27 @@ var init_tech_detect = __esm({
2334
1165
  }
2335
1166
  });
2336
1167
 
2337
- // src/lib/server-detect.ts
2338
- function detectFramework(pkg) {
2339
- const deps = pkg.dependencies ?? {};
2340
- const devDeps = pkg.devDependencies ?? {};
2341
- const hasDep = (name) => name in deps || name in devDeps;
2342
- if (hasDep("next")) return "nextjs";
2343
- if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
2344
- if (hasDep("expo")) return "expo";
2345
- if (hasDep("vite")) return "vite";
2346
- if (hasDep("express")) return "express";
2347
- if (hasDep("@nestjs/core")) return "nestjs";
2348
- return "custom";
2349
- }
2350
- function detectPortFromScripts(pkg) {
2351
- const scripts = pkg.scripts;
2352
- if (!scripts?.dev) return null;
2353
- const parts = scripts.dev.split(/\s+/);
2354
- for (let i = 0; i < parts.length - 1; i++) {
2355
- if (parts[i] === "--port" || parts[i] === "-p") {
2356
- const next = parts[i + 1];
2357
- if (next) {
2358
- const port = parseInt(next, 10);
2359
- if (!isNaN(port)) return port;
1168
+ // src/lib/eslint-generator.ts
1169
+ import { createHash as createHash2 } from "node:crypto";
1170
+ function importedIdentifiers(importLines) {
1171
+ const names = /* @__PURE__ */ new Set();
1172
+ for (const line of importLines) {
1173
+ let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
1174
+ if (m) names.add(m[1]);
1175
+ m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
1176
+ if (m) names.add(m[1]);
1177
+ m = line.match(/^import\s*\{([^}]*)\}\s*from/);
1178
+ if (m) {
1179
+ for (const entry of m[1].split(",")) {
1180
+ const parts = entry.trim().split(/\s+as\s+/);
1181
+ const n = (parts[1] ?? parts[0]).trim();
1182
+ if (n) names.add(n);
2360
1183
  }
2361
1184
  }
1185
+ m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
1186
+ if (m) names.add(m[1]);
2362
1187
  }
2363
- return null;
2364
- }
2365
- var init_server_detect = __esm({
2366
- "src/lib/server-detect.ts"() {
2367
- "use strict";
2368
- }
2369
- });
2370
-
2371
- // src/lib/port-verify.ts
2372
- import { readFile as readFile9 } from "node:fs/promises";
2373
- async function verifyPorts(projectPath, portAllocations) {
2374
- const mismatches = [];
2375
- const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2376
- const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2377
- for (const pkgPath of packageJsonPaths) {
2378
- try {
2379
- const raw = await readFile9(pkgPath, "utf-8");
2380
- const pkg = JSON.parse(raw);
2381
- const scriptPort = detectPortFromScripts(pkg);
2382
- if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
2383
- const relativePath = pkgPath.replace(projectPath + "/", "");
2384
- const matchingAlloc = portAllocations.find(
2385
- (a) => a.label === getAppLabel(relativePath)
2386
- );
2387
- mismatches.push({
2388
- packageJsonPath: relativePath,
2389
- scriptPort,
2390
- allocation: matchingAlloc ?? null,
2391
- reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
2392
- });
2393
- }
2394
- } catch {
2395
- }
2396
- }
2397
- return mismatches;
2398
- }
2399
- function isDevServerScript(pkg) {
2400
- const scripts = pkg.scripts;
2401
- const raw = scripts?.dev;
2402
- if (!raw || typeof raw !== "string") return false;
2403
- const script = raw.trim().toLowerCase();
2404
- if (!script) return false;
2405
- for (const pattern of DEV_SERVER_BIN_PATTERNS) {
2406
- if (pattern.test(script)) return true;
2407
- }
2408
- const tokens = script.split(/\s+/);
2409
- for (const token of tokens) {
2410
- if (token === "--port" || token === "-p") return true;
2411
- if (token.startsWith("--port=")) return true;
2412
- }
2413
- return false;
2414
- }
2415
- function labelMatchesAppName(label, appName) {
2416
- if (!label || !appName) return false;
2417
- const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
2418
- const labelTokens = normalize(label).split(" ").filter(Boolean);
2419
- const appToken = normalize(appName);
2420
- if (!appToken) return false;
2421
- const appTokens = appToken.split(" ").filter(Boolean);
2422
- if (appTokens.length === 1) {
2423
- return labelTokens.includes(appTokens[0]);
2424
- }
2425
- for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
2426
- if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
2427
- }
2428
- return false;
2429
- }
2430
- async function findUnallocatedApps(projectPath, portAllocations) {
2431
- const apps = await discoverMonorepoApps(projectPath);
2432
- if (apps.length === 0) {
2433
- return [];
2434
- }
2435
- const unallocated = [];
2436
- for (const app of apps) {
2437
- if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
2438
- continue;
2439
- }
2440
- let pkg;
2441
- try {
2442
- const raw = await readFile9(`${app.absPath}/package.json`, "utf-8");
2443
- pkg = JSON.parse(raw);
2444
- } catch {
2445
- continue;
2446
- }
2447
- if (!isDevServerScript(pkg)) continue;
2448
- const framework = detectFramework(pkg);
2449
- const detectedPort = detectPortFromScripts(pkg);
2450
- const command = `pnpm --filter ${app.name} dev`;
2451
- unallocated.push({
2452
- name: app.name,
2453
- path: app.path,
2454
- framework,
2455
- detectedPort,
2456
- command
2457
- });
2458
- }
2459
- return unallocated;
2460
- }
2461
- function getAppLabel(relativePath) {
2462
- const parts = relativePath.split("/");
2463
- if (parts.length >= 3 && parts[0] === "apps") {
2464
- return parts[1];
2465
- }
2466
- return "root";
2467
- }
2468
- var DEV_SERVER_BIN_PATTERNS;
2469
- var init_port_verify = __esm({
2470
- "src/lib/port-verify.ts"() {
2471
- "use strict";
2472
- init_tech_detect();
2473
- init_server_detect();
2474
- DEV_SERVER_BIN_PATTERNS = [
2475
- /\bnext\s+dev\b/,
2476
- /\bnest\s+start\b/,
2477
- /\bvite\s+(?:dev|serve)\b/,
2478
- /\bvite\s+preview\b/,
2479
- /\bnuxt\s+dev\b/,
2480
- /\b(?:svelte-kit|sveltekit)\s+dev\b/,
2481
- /\bexpo\s+start\b/
2482
- ];
2483
- }
2484
- });
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
-
2548
- // src/lib/eslint-generator.ts
2549
- import { createHash as createHash2 } from "node:crypto";
2550
- function importedIdentifiers(importLines) {
2551
- const names = /* @__PURE__ */ new Set();
2552
- for (const line of importLines) {
2553
- let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
2554
- if (m) names.add(m[1]);
2555
- m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
2556
- if (m) names.add(m[1]);
2557
- m = line.match(/^import\s*\{([^}]*)\}\s*from/);
2558
- if (m) {
2559
- for (const entry of m[1].split(",")) {
2560
- const parts = entry.trim().split(/\s+as\s+/);
2561
- const n = (parts[1] ?? parts[0]).trim();
2562
- if (n) names.add(n);
2563
- }
2564
- }
2565
- m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
2566
- if (m) names.add(m[1]);
2567
- }
2568
- return names;
1188
+ return names;
2569
1189
  }
2570
1190
  function parseFragment(fragment) {
2571
1191
  if (!fragment) return { imports: [], configComments: [] };
@@ -2727,8 +1347,7 @@ function generateEslintConfig(input) {
2727
1347
  sections.push(
2728
1348
  "/**",
2729
1349
  " * ESLint flat config \u2014 generated by CodeByPlan CLI.",
2730
- " * Edit rule overrides via the web UI, then run `codebyplan eslint sync`.",
2731
- " * Manual edits will be detected as drift.",
1350
+ " * Edit rule overrides via the web UI, then run `codebyplan eslint init` to regenerate.",
2732
1351
  " */",
2733
1352
  ""
2734
1353
  );
@@ -2929,13 +1548,11 @@ var init_eslint_generator = __esm({
2929
1548
  var eslint_exports = {};
2930
1549
  __export(eslint_exports, {
2931
1550
  autoDetectIgnorePatterns: () => autoDetectIgnorePatterns,
2932
- checkEslintDrift: () => checkEslintDrift,
2933
1551
  eslintInit: () => eslintInit,
2934
- eslintSync: () => eslintSync,
2935
1552
  runEslint: () => runEslint
2936
1553
  });
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";
1554
+ import { readFile as readFile6, writeFile as writeFile4, access as access2, readdir as readdir2 } from "node:fs/promises";
1555
+ import { join as join6, relative as relative2 } from "node:path";
2939
1556
  async function fileExists2(filePath) {
2940
1557
  try {
2941
1558
  await access2(filePath);
@@ -2946,12 +1563,12 @@ async function fileExists2(filePath) {
2946
1563
  }
2947
1564
  async function autoDetectIgnorePatterns(absPath) {
2948
1565
  const patterns = [];
2949
- if (await fileExists2(join10(absPath, "esbuild.js"))) {
1566
+ if (await fileExists2(join6(absPath, "esbuild.js"))) {
2950
1567
  patterns.push("esbuild.js");
2951
1568
  }
2952
1569
  let entries = [];
2953
1570
  try {
2954
- entries = await readdir5(absPath);
1571
+ entries = await readdir2(absPath);
2955
1572
  } catch (err) {
2956
1573
  console.error(
2957
1574
  ` autoDetectIgnorePatterns: failed to read ${absPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2966,19 +1583,19 @@ async function autoDetectIgnorePatterns(absPath) {
2966
1583
  }
2967
1584
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2968
1585
  const candidate = `vitest.config.${ext}`;
2969
- if (await fileExists2(join10(absPath, candidate))) {
1586
+ if (await fileExists2(join6(absPath, candidate))) {
2970
1587
  patterns.push(candidate);
2971
1588
  break;
2972
1589
  }
2973
1590
  }
2974
1591
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2975
1592
  const candidate = `vite.config.${ext}`;
2976
- if (await fileExists2(join10(absPath, candidate))) {
1593
+ if (await fileExists2(join6(absPath, candidate))) {
2977
1594
  patterns.push(candidate);
2978
1595
  break;
2979
1596
  }
2980
1597
  }
2981
- if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
1598
+ if (await fileExists2(join6(absPath, "tauri.conf.json"))) {
2982
1599
  patterns.push("src-tauri/**");
2983
1600
  patterns.push("**/*.d.ts");
2984
1601
  }
@@ -2986,14 +1603,14 @@ async function autoDetectIgnorePatterns(absPath) {
2986
1603
  }
2987
1604
  function detectPackageManager(projectPath) {
2988
1605
  return (async () => {
2989
- if (await fileExists2(join10(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2990
- if (await fileExists2(join10(projectPath, "yarn.lock"))) return "yarn";
1606
+ if (await fileExists2(join6(projectPath, "pnpm-lock.yaml"))) return "pnpm";
1607
+ if (await fileExists2(join6(projectPath, "yarn.lock"))) return "yarn";
2991
1608
  return "npm";
2992
1609
  })();
2993
1610
  }
2994
1611
  async function getInstalledDeps(pkgJsonPath) {
2995
1612
  try {
2996
- const raw = await readFile11(pkgJsonPath, "utf-8");
1613
+ const raw = await readFile6(pkgJsonPath, "utf-8");
2997
1614
  const pkg = JSON.parse(raw);
2998
1615
  const all = /* @__PURE__ */ new Set();
2999
1616
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -3106,7 +1723,7 @@ async function eslintInit(repoId, projectPath) {
3106
1723
  ignorePatterns: detectedIgnores
3107
1724
  });
3108
1725
  const hash = hashConfig(content);
3109
- const configPath = join10(target.absPath, "eslint.config.mjs");
1726
+ const configPath = join6(target.absPath, "eslint.config.mjs");
3110
1727
  configsToWrite.push({
3111
1728
  target,
3112
1729
  presets,
@@ -3128,11 +1745,11 @@ async function eslintInit(repoId, projectPath) {
3128
1745
  return;
3129
1746
  }
3130
1747
  const pm = await detectPackageManager(projectPath);
3131
- const rootPkgJsonPath = join10(projectPath, "package.json");
1748
+ const rootPkgJsonPath = join6(projectPath, "package.json");
3132
1749
  const installed = await getInstalledDeps(rootPkgJsonPath);
3133
1750
  if (isMonorepo) {
3134
1751
  for (const { target } of configsToWrite) {
3135
- const appPkgJson = join10(target.absPath, "package.json");
1752
+ const appPkgJson = join6(target.absPath, "package.json");
3136
1753
  const appDeps = await getInstalledDeps(appPkgJson);
3137
1754
  for (const dep of appDeps) {
3138
1755
  installed.add(dep);
@@ -3184,7 +1801,7 @@ async function eslintInit(repoId, projectPath) {
3184
1801
  } of configsToWrite) {
3185
1802
  if (await fileExists2(configPath)) {
3186
1803
  try {
3187
- const existing = await readFile11(configPath, "utf-8");
1804
+ const existing = await readFile6(configPath, "utf-8");
3188
1805
  const existingHash = hashConfig(existing);
3189
1806
  if (existingHash === hash) {
3190
1807
  console.log(
@@ -3204,7 +1821,7 @@ async function eslintInit(repoId, projectPath) {
3204
1821
  }
3205
1822
  }
3206
1823
  try {
3207
- await writeFile6(configPath, content, "utf-8");
1824
+ await writeFile4(configPath, content, "utf-8");
3208
1825
  } catch (err) {
3209
1826
  console.error(
3210
1827
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3227,721 +1844,117 @@ async function eslintInit(repoId, projectPath) {
3227
1844
  }
3228
1845
  console.log("\n ESLint init complete.\n");
3229
1846
  }
3230
- async function eslintSync(repoId, projectPath) {
3231
- console.log("\n ESLint Sync");
3232
- console.log(` Repo: ${repoId}`);
3233
- console.log(` Path: ${projectPath}
1847
+ async function runEslint() {
1848
+ const subcommand = process.argv[3];
1849
+ const flags = parseFlags(4);
1850
+ validateApiKey();
1851
+ const config = await resolveConfig(flags);
1852
+ const { repoId, projectPath } = config;
1853
+ switch (subcommand) {
1854
+ case "init":
1855
+ await eslintInit(repoId, projectPath);
1856
+ break;
1857
+ default:
1858
+ console.log(`
1859
+ Usage:
1860
+ codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
3234
1861
  `);
3235
- let configs;
3236
- try {
3237
- const res = await apiGet(
3238
- `/repos/${repoId}/eslint-config`
3239
- );
3240
- configs = res.data ?? [];
3241
- } catch {
3242
- console.log(
3243
- " No existing ESLint config found. Run `codebyplan eslint init` first.\n"
3244
- );
3245
- return;
1862
+ break;
3246
1863
  }
3247
- if (configs.length === 0) {
3248
- console.log(
3249
- " No ESLint configs registered. Run `codebyplan eslint init` first.\n"
3250
- );
3251
- return;
1864
+ }
1865
+ var init_eslint = __esm({
1866
+ "src/cli/eslint.ts"() {
1867
+ "use strict";
1868
+ init_flags();
1869
+ init_confirm();
1870
+ init_api();
1871
+ init_tech_detect();
1872
+ init_eslint_generator();
3252
1873
  }
3253
- let updatedCount = 0;
3254
- let skippedCount = 0;
3255
- let driftCount = 0;
3256
- for (const config of configs) {
3257
- const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3258
- const configPath = join10(absPath, "eslint.config.mjs");
3259
- const detected = await detectTechStack(absPath);
3260
- const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
3261
- const capabilities = collectCapabilities(detected.flat);
3262
- const currentPresets = await resolvePresetsForTechStack(
3263
- techNames,
3264
- capabilities
3265
- );
3266
- const currentPresetIds = currentPresets.map((p) => p.id).sort();
3267
- const savedPresetIds = [...config.active_preset_ids ?? []].sort();
3268
- const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
3269
- if (!presetsChanged) {
3270
- if (await fileExists2(configPath)) {
3271
- try {
3272
- const currentContent = await readFile11(configPath, "utf-8");
3273
- const currentHash = hashConfig(currentContent);
3274
- if (config.generated_hash && currentHash !== config.generated_hash) {
3275
- console.log(
3276
- ` ${config.source_path}: drift detected (manually edited). Not overwriting.`
3277
- );
3278
- driftCount++;
3279
- continue;
3280
- }
3281
- skippedCount++;
3282
- continue;
3283
- } catch {
3284
- console.warn(
3285
- ` ${config.source_path}: config file unreadable, regenerating...`
3286
- );
3287
- }
3288
- } else {
3289
- console.log(
3290
- ` ${config.source_path}: config file missing, regenerating...`
3291
- );
3292
- }
3293
- }
3294
- if (presetsChanged) {
3295
- console.log(` ${config.source_path}: presets changed, regenerating...`);
1874
+ });
1875
+
1876
+ // src/cli/resolve-worktree.ts
1877
+ var resolve_worktree_exports = {};
1878
+ __export(resolve_worktree_exports, {
1879
+ runResolveWorktree: () => runResolveWorktree
1880
+ });
1881
+ import { execSync as execSync2 } from "node:child_process";
1882
+ async function runResolveWorktree() {
1883
+ try {
1884
+ const projectPath = process.cwd();
1885
+ const found = await findCodebyplanConfig(projectPath);
1886
+ if (!found?.contents.repo_id) {
1887
+ process.exit(0);
3296
1888
  }
3297
- const userOverrides = config.rule_overrides;
3298
- const detectedIgnores = await autoDetectIgnorePatterns(absPath);
3299
- const content = generateEslintConfig({
3300
- presets: currentPresets,
3301
- ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0,
3302
- ignorePatterns: detectedIgnores
3303
- });
3304
- try {
3305
- await writeFile6(configPath, content, "utf-8");
3306
- } catch (err) {
3307
- console.error(
3308
- ` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
3309
- );
3310
- continue;
3311
- }
3312
- const newHash = hashConfig(content);
1889
+ const repoId = found.contents.repo_id;
1890
+ const deviceId = await getOrCreateDeviceId(projectPath);
1891
+ let branch = "";
3313
1892
  try {
3314
- await apiPut(`/repos/${repoId}/eslint-config`, {
3315
- source_path: config.source_path,
3316
- preset_ids: currentPresetIds,
3317
- rule_overrides: userOverrides ?? {},
3318
- generated_hash: newHash
3319
- });
3320
- } catch (err) {
3321
- console.error(
3322
- ` Warning: Failed to update server: ${err instanceof Error ? err.message : String(err)}`
3323
- );
1893
+ branch = execSync2("git symbolic-ref --short HEAD", {
1894
+ cwd: projectPath,
1895
+ encoding: "utf-8"
1896
+ }).trim();
1897
+ } catch {
3324
1898
  }
3325
- updatedCount++;
3326
- }
3327
- console.log(
3328
- `
3329
- Sync: ${updatedCount} updated, ${skippedCount} unchanged, ${driftCount} drift detected.
3330
- `
3331
- );
3332
- }
3333
- async function checkEslintDrift(repoId, projectPath) {
3334
- try {
3335
- const res = await apiGet(
3336
- `/repos/${repoId}/eslint-config`
3337
- );
3338
- const configs = res.data ?? [];
3339
- for (const config of configs) {
3340
- if (!config.generated_hash) continue;
3341
- const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
3342
- const configPath = join10(absPath, "eslint.config.mjs");
3343
- if (!await fileExists2(configPath)) continue;
3344
- try {
3345
- const content = await readFile11(configPath, "utf-8");
3346
- const currentHash = hashConfig(content);
3347
- if (currentHash !== config.generated_hash) {
3348
- return true;
3349
- }
3350
- } catch {
3351
- }
1899
+ const worktreeId = await resolveWorktreeId({
1900
+ repoId,
1901
+ repoPath: projectPath,
1902
+ branch,
1903
+ deviceId
1904
+ });
1905
+ if (worktreeId) {
1906
+ process.stdout.write(worktreeId);
3352
1907
  }
3353
- return false;
3354
- } catch {
3355
- return false;
3356
- }
3357
- }
3358
- async function runEslint() {
3359
- const subcommand = process.argv[3];
3360
- const flags = parseFlags(4);
3361
- validateApiKey();
3362
- const config = await resolveConfig(flags);
3363
- const { repoId, projectPath } = config;
3364
- switch (subcommand) {
3365
- case "init":
3366
- await eslintInit(repoId, projectPath);
3367
- break;
3368
- case "sync":
3369
- await eslintSync(repoId, projectPath);
3370
- break;
3371
- default:
3372
- console.log(`
3373
- Usage:
3374
- codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
3375
- codebyplan eslint sync Regenerate if presets changed, detect drift
1908
+ process.exit(0);
1909
+ } catch (err) {
1910
+ if (process.env.CODEBYPLAN_DEBUG === "1") {
1911
+ const msg = err instanceof Error ? err.message : String(err);
1912
+ process.stderr.write(`resolve-worktree: ${msg}
3376
1913
  `);
3377
- break;
1914
+ }
1915
+ process.exit(0);
3378
1916
  }
3379
1917
  }
3380
- var init_eslint = __esm({
3381
- "src/cli/eslint.ts"() {
1918
+ var init_resolve_worktree2 = __esm({
1919
+ "src/cli/resolve-worktree.ts"() {
3382
1920
  "use strict";
3383
- init_config();
3384
- init_confirm();
3385
- init_api();
3386
- init_tech_detect();
3387
- init_eslint_generator();
1921
+ init_flags();
1922
+ init_local_config();
1923
+ init_resolve_worktree();
3388
1924
  }
3389
1925
  });
3390
1926
 
3391
- // src/cli/sync.ts
3392
- var sync_exports = {};
3393
- __export(sync_exports, {
3394
- runSync: () => runSync
1927
+ // src/cli/config.ts
1928
+ var config_exports = {};
1929
+ __export(config_exports, {
1930
+ runConfig: () => runConfig
3395
1931
  });
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";
3399
- function contentHash(content) {
3400
- return createHash3("sha256").update(content).digest("hex");
3401
- }
3402
- async function runSync() {
1932
+ import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
1933
+ import { join as join7 } from "node:path";
1934
+ async function runConfig() {
3403
1935
  const flags = parseFlags(3);
3404
1936
  const dryRun = hasFlag("dry-run", 3);
3405
- const force = hasFlag("force", 3);
3406
- const fix = hasFlag("fix", 3);
3407
1937
  validateApiKey();
3408
1938
  const config = await resolveConfig(flags);
3409
1939
  const { repoId, projectPath } = config;
3410
1940
  console.log(`
3411
- CodeByPlan Sync`);
1941
+ CodeByPlan Config`);
3412
1942
  console.log(` Repo: ${repoId}`);
3413
1943
  console.log(` Path: ${projectPath}`);
3414
1944
  if (dryRun) console.log(` Mode: dry-run`);
3415
- if (force) console.log(` Mode: force`);
3416
1945
  console.log();
3417
- if (!dryRun) {
3418
- console.log(" Acquiring sync lock...");
3419
- try {
3420
- await apiPost("/sync/lock", {
3421
- repo_id: repoId,
3422
- locked_by: `cli-sync`,
3423
- reason: "Bidirectional sync",
3424
- ttl_minutes: 10
3425
- });
3426
- console.log(" Lock acquired.\n");
3427
- } catch (lockErr) {
3428
- const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
3429
- if (lockStatus.data.locked && lockStatus.data.lock) {
3430
- const lock = lockStatus.data.lock;
3431
- console.log(
3432
- ` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`
3433
- );
3434
- console.log(` Expires: ${lock.expires_at}`);
3435
- console.log(` Use --force to override, or wait for lock to expire.
3436
- `);
3437
- if (!force) return;
3438
- await apiPost("/sync/lock", {
3439
- repo_id: repoId,
3440
- locked_by: `cli-sync`,
3441
- reason: "Bidirectional sync (forced)",
3442
- ttl_minutes: 10
3443
- });
3444
- console.log(" Lock acquired (forced).\n");
3445
- } else {
3446
- throw lockErr;
3447
- }
3448
- }
3449
- }
3450
- try {
3451
- await runSyncInner(repoId, projectPath, dryRun, force, fix);
3452
- } finally {
3453
- if (!dryRun) {
3454
- try {
3455
- await apiDelete("/sync/lock", { repo_id: repoId });
3456
- } catch (err) {
3457
- console.error(
3458
- ` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
3459
- );
3460
- }
3461
- }
3462
- }
1946
+ await syncConfigToFile(repoId, projectPath, dryRun);
1947
+ console.log("\n Config complete.\n");
3463
1948
  }
3464
- async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3465
- console.log(" Reading local and remote state...");
3466
- const claudeDir = join11(projectPath, ".claude");
3467
- let localFiles = /* @__PURE__ */ new Map();
3468
- try {
3469
- localFiles = await scanLocalFiles(claudeDir, projectPath);
3470
- } catch (err) {
3471
- console.warn(
3472
- ` Local file scan incomplete: ${err instanceof Error ? err.message : String(err)}`
3473
- );
3474
- }
3475
- const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
3476
- [
3477
- apiGet("/sync/defaults"),
3478
- apiGet("/sync/files", { repo_id: repoId }),
3479
- apiGet(`/repos/${repoId}`),
3480
- apiGet("/sync/state", {
3481
- repo_id: repoId
3482
- }),
3483
- apiGet("/sync/file-repos", {
3484
- repo_id: repoId
3485
- })
3486
- ]
3487
- );
3488
- const syncStartTime = Date.now();
3489
- const repoData = repoRes.data;
3490
- const remoteDefaults = flattenSyncData(defaultsRes.data);
3491
- const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
3492
- const fileRepoHashes = /* @__PURE__ */ new Map();
3493
- const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
3494
- for (const entry of fileReposRes.data ?? []) {
3495
- const baseKey = compositeKey(
3496
- entry.file_type,
3497
- entry.file_name,
3498
- entry.file_category
3499
- );
3500
- const scopedKey = `${baseKey}:${entry.file_scope}`;
3501
- fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
3502
- if (!fileRepoHashes.has(baseKey)) {
3503
- fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
3504
- }
3505
- if (entry.claude_file_id) {
3506
- fileRepoByClaudeFileId.set(
3507
- entry.claude_file_id,
3508
- entry.last_synced_content_hash
3509
- );
3510
- }
3511
- }
3512
- const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
3513
- console.log(
3514
- ` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
3515
- `
3516
- );
3517
- const plan = [];
3518
- const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
3519
- for (const key of allKeys) {
3520
- const local = localFiles.get(key);
3521
- const remote = remoteFiles.get(key);
3522
- if (local && !remote) {
3523
- plan.push({
3524
- key,
3525
- displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
3526
- action: "push",
3527
- recommended: "push",
3528
- localContent: local.content,
3529
- remoteContent: null,
3530
- pushContent: reverseSubstituteVariables(local.content, repoData),
3531
- filePath: getLocalFilePath(claudeDir, projectPath, {
3532
- type: local.type,
3533
- name: local.name,
3534
- category: local.category
3535
- }),
3536
- type: local.type,
3537
- name: local.name,
3538
- category: local.category,
3539
- scope: local.scope,
3540
- isHook: local.type === "hook",
3541
- claudeFileId: null
3542
- });
3543
- } else if (!local && remote) {
3544
- const remoteScope = remote.scope ?? "shared";
3545
- if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
3546
- continue;
3547
- }
3548
- const resolvedContent = substituteVariables(remote.content, repoData);
3549
- const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
3550
- const recommended = hadSyncedThisFile ? "delete" : "pull";
3551
- plan.push({
3552
- key,
3553
- displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
3554
- action: recommended,
3555
- recommended,
3556
- localContent: null,
3557
- remoteContent: resolvedContent,
3558
- pushContent: null,
3559
- filePath: getLocalFilePath(claudeDir, projectPath, remote),
3560
- type: remote.type,
3561
- name: remote.name,
3562
- category: remote.category ?? null,
3563
- scope: remote.scope ?? "shared",
3564
- isHook: remote.type === "hook",
3565
- claudeFileId: remote.id ?? null
3566
- });
3567
- } else if (local && remote) {
3568
- const remoteScope = remote.scope ?? "shared";
3569
- if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
3570
- continue;
3571
- }
3572
- const resolvedRemote = substituteVariables(remote.content, repoData);
3573
- if (local.content === resolvedRemote) {
3574
- continue;
3575
- }
3576
- const localHash = contentHash(local.content);
3577
- const scopedKey = `${key}:${local.scope}`;
3578
- const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
3579
- const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
3580
- let action;
3581
- if (force) {
3582
- action = "pull";
3583
- } else if (!localChanged) {
3584
- action = "pull";
3585
- } else if (lastSyncedHash === null) {
3586
- action = "conflict";
3587
- } else {
3588
- const remoteResolvedHash = contentHash(resolvedRemote);
3589
- const remoteChanged = remoteResolvedHash !== lastSyncedHash;
3590
- if (remoteChanged) {
3591
- action = "conflict";
3592
- } else {
3593
- action = "push";
3594
- }
3595
- }
3596
- plan.push({
3597
- key,
3598
- displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
3599
- action,
3600
- recommended: action === "conflict" ? "pull" : action,
3601
- localContent: local.content,
3602
- remoteContent: resolvedRemote,
3603
- pushContent: reverseSubstituteVariables(local.content, repoData),
3604
- filePath: getLocalFilePath(claudeDir, projectPath, remote),
3605
- type: local.type,
3606
- name: local.name,
3607
- category: local.category,
3608
- scope: local.scope,
3609
- isHook: local.type === "hook",
3610
- claudeFileId: remote.id ?? null
3611
- });
3612
- }
3613
- }
3614
- const pulls = plan.filter((p) => p.action === "pull");
3615
- const pushes = plan.filter((p) => p.action === "push");
3616
- const conflicts = plan.filter((p) => p.action === "conflict");
3617
- const contentPulls = pulls.filter((p) => p.localContent !== null);
3618
- const dbOnlyPull = plan.filter(
3619
- (p) => p.localContent === null && p.action === "pull"
3620
- );
3621
- const dbOnlyDelete = plan.filter(
3622
- (p) => p.localContent === null && p.action === "delete"
3623
- );
3624
- if (contentPulls.length > 0) {
3625
- console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
3626
- for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
3627
- }
3628
- if (pushes.length > 0) {
3629
- console.log(` Push (local \u2192 DB): ${pushes.length}`);
3630
- for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
3631
- }
3632
- if (dbOnlyPull.length > 0) {
3633
- console.log(`
3634
- DB-only (new, will pull): ${dbOnlyPull.length}`);
3635
- for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
3636
- }
3637
- if (dbOnlyDelete.length > 0) {
3638
- console.log(
3639
- `
3640
- DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
3641
- );
3642
- for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
3643
- }
3644
- if (conflicts.length > 0) {
3645
- console.log(`
3646
- Conflicts (both sides changed): ${conflicts.length}`);
3647
- for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
3648
- }
3649
- if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
3650
- console.log(" All .claude/ files in sync.");
3651
- }
3652
- if (plan.length > 0 && !dryRun) {
3653
- if (!force) {
3654
- const agreed = await confirmProceed(`
3655
- Agree with sync? [Y/n] `);
3656
- if (!agreed) {
3657
- const mode = await promptReviewMode();
3658
- const contentProvider = {
3659
- local: (p) => p.localContent,
3660
- remote: (p) => p.remoteContent
3661
- };
3662
- if (mode === "file") {
3663
- const actions = await reviewFilesOneByOne(
3664
- plan,
3665
- (p) => p.displayPath,
3666
- (p) => p.action,
3667
- (p) => p.recommended,
3668
- contentProvider
3669
- );
3670
- for (let i = 0; i < plan.length; i++) {
3671
- plan[i].action = actions[i];
3672
- }
3673
- } else {
3674
- const groups = groupByType(plan);
3675
- for (const [typeName, items] of groups) {
3676
- const actions = await reviewFolder(
3677
- typeName,
3678
- items,
3679
- (p) => p.displayPath,
3680
- (p) => p.action,
3681
- (p) => p.recommended,
3682
- contentProvider
3683
- );
3684
- for (let i = 0; i < items.length; i++) {
3685
- items[i].action = actions[i];
3686
- }
3687
- }
3688
- }
3689
- }
3690
- }
3691
- const toPull = plan.filter((p) => p.action === "pull");
3692
- const toPush = plan.filter((p) => p.action === "push");
3693
- const toDelete = plan.filter((p) => p.action === "delete");
3694
- const skipped = plan.filter((p) => p.action === "skip");
3695
- if (toPull.length + toPush.length + toDelete.length === 0) {
3696
- console.log("\n All items skipped \u2014 no changes applied.");
3697
- } else {
3698
- for (const p of toPull) {
3699
- if (p.filePath && p.remoteContent !== null) {
3700
- await mkdir2(dirname2(p.filePath), { recursive: true });
3701
- await writeFile7(p.filePath, p.remoteContent, "utf-8");
3702
- if (p.isHook) await chmod2(p.filePath, 493);
3703
- }
3704
- }
3705
- const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
3706
- type: p.type,
3707
- name: p.name,
3708
- category: p.category,
3709
- content: p.pushContent,
3710
- scope: p.scope
3711
- }));
3712
- if (toUpsert.length > 0) {
3713
- await apiPost("/sync/files", {
3714
- repo_id: repoId,
3715
- files: toUpsert,
3716
- changed_by_repo_id: repoId
3717
- });
3718
- }
3719
- if (toDelete.length > 0) {
3720
- const deleteKeys = toDelete.map((p) => ({
3721
- type: p.type,
3722
- name: p.name,
3723
- category: p.category
3724
- }));
3725
- await apiPost("/sync/files", {
3726
- repo_id: repoId,
3727
- delete_keys: deleteKeys
3728
- });
3729
- for (const p of toDelete) {
3730
- if (p.filePath) {
3731
- try {
3732
- await unlink2(p.filePath);
3733
- } catch (err) {
3734
- if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
3735
- console.error(
3736
- ` Warning: failed to delete ${p.filePath}: ${err.message}`
3737
- );
3738
- }
3739
- }
3740
- }
3741
- }
3742
- }
3743
- const syncDurationMs = Date.now() - syncStartTime;
3744
- await apiPost("/sync/state", {
3745
- repo_id: repoId,
3746
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
3747
- was_skipped: skipped.length > 0,
3748
- files_synced_count: toPull.length + toPush.length + toDelete.length,
3749
- files_pushed: toPush.length,
3750
- files_pulled: toPull.length,
3751
- files_deleted: toDelete.length,
3752
- files_skipped: skipped.length,
3753
- sync_duration_ms: syncDurationMs,
3754
- sync_version: getSyncVersion()
3755
- });
3756
- const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
3757
- const fileRepoUpdates = [];
3758
- for (const p of toPull) {
3759
- if (p.remoteContent !== null) {
3760
- fileRepoUpdates.push({
3761
- claude_file_id: p.claudeFileId ?? void 0,
3762
- file_type: p.type,
3763
- file_name: p.name,
3764
- file_category: p.category,
3765
- file_scope: p.scope,
3766
- last_synced_at: syncTimestamp,
3767
- last_synced_content_hash: contentHash(p.remoteContent),
3768
- sync_status: "synced"
3769
- });
3770
- }
3771
- }
3772
- for (const p of toPush) {
3773
- if (p.localContent !== null) {
3774
- fileRepoUpdates.push({
3775
- claude_file_id: p.claudeFileId ?? void 0,
3776
- file_type: p.type,
3777
- file_name: p.name,
3778
- file_category: p.category,
3779
- file_scope: p.scope,
3780
- last_synced_at: syncTimestamp,
3781
- last_synced_content_hash: contentHash(p.localContent),
3782
- sync_status: "synced"
3783
- });
3784
- }
3785
- }
3786
- if (fileRepoUpdates.length > 0) {
3787
- try {
3788
- await apiPost("/sync/file-repos", {
3789
- repo_id: repoId,
3790
- file_repos: fileRepoUpdates
3791
- });
3792
- } catch (err) {
3793
- console.warn(
3794
- ` Warning: failed to update file-repo tracking for ${fileRepoUpdates.length} files: ${err instanceof Error ? err.message : String(err)}`
3795
- );
3796
- }
3797
- }
3798
- console.log(
3799
- `
3800
- Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
3801
- );
3802
- }
3803
- const unresolvedConflicts = plan.filter(
3804
- (p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
3805
- );
3806
- if (unresolvedConflicts.length > 0) {
3807
- let stored = 0;
3808
- for (const p of unresolvedConflicts) {
3809
- try {
3810
- await apiPost("/sync/conflicts", {
3811
- repo_id: repoId,
3812
- claude_file_id: p.claudeFileId ?? void 0,
3813
- file_type: p.type,
3814
- file_name: p.name,
3815
- file_category: p.category,
3816
- file_scope: p.scope,
3817
- conflict_type: "both_modified",
3818
- local_content: p.localContent,
3819
- remote_content: p.remoteContent
3820
- });
3821
- stored++;
3822
- } catch (err) {
3823
- console.error(`Failed to store conflict for ${p.displayPath}:`, err);
3824
- }
3825
- }
3826
- if (stored > 0) {
3827
- console.log(
3828
- `
3829
- ${stored} conflict(s) stored in DB for later resolution.`
3830
- );
3831
- }
3832
- }
3833
- } else if (dryRun) {
3834
- console.log("\n (dry-run \u2014 no changes)");
3835
- }
3836
- console.log("\n Settings sync...");
3837
- await syncSettings(
3838
- claudeDir,
3839
- projectPath,
3840
- defaultsRes.data,
3841
- repoData,
3842
- dryRun
3843
- );
3844
- console.log(" Config sync...");
3845
- await syncConfig(repoId, projectPath, dryRun);
3846
- console.log(" Tech stack...");
3847
- await syncTechStack(repoId, projectPath, dryRun);
3848
- console.log(" ESLint config...");
3849
- await syncEslintDriftCheck(repoId, projectPath);
3850
- console.log(" Port verification...");
3851
- await syncPortVerification(repoId, projectPath, dryRun, fix);
3852
- console.log("\n Sync complete.\n");
3853
- }
3854
- async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
3855
- const settingsPath = join11(claudeDir, "settings.json");
3856
- const globalSettingsFiles = syncData.global_settings ?? [];
3857
- let globalSettings = {};
3858
- for (const gf of globalSettingsFiles) {
3859
- const parsed = JSON.parse(
3860
- substituteVariables(gf.content, repoData)
3861
- );
3862
- globalSettings = { ...globalSettings, ...parsed };
3863
- }
3864
- const repoSettingsFiles = syncData.settings ?? [];
3865
- let repoSettings = {};
3866
- for (const rf of repoSettingsFiles) {
3867
- repoSettings = JSON.parse(
3868
- substituteVariables(rf.content, repoData)
3869
- );
3870
- }
3871
- const combinedTemplate = mergeGlobalAndRepoSettings(
3872
- globalSettings,
3873
- repoSettings
3874
- );
3875
- const hooksDir = join11(projectPath, ".claude", "hooks");
3876
- const discovered = await discoverHooks(hooksDir);
3877
- let localSettings = {};
3878
- try {
3879
- const raw = await readFile12(settingsPath, "utf-8");
3880
- localSettings = JSON.parse(raw);
3881
- } catch {
3882
- }
3883
- let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
3884
- merged = stripPermissionsAllow(merged);
3885
- if (discovered.size > 0) {
3886
- merged.hooks = mergeDiscoveredHooks(
3887
- merged.hooks ?? {},
3888
- discovered
3889
- );
3890
- }
3891
- const mergedContent = JSON.stringify(merged, null, 2) + "\n";
3892
- let currentContent = "";
3893
- try {
3894
- currentContent = await readFile12(settingsPath, "utf-8");
3895
- } catch {
3896
- }
3897
- if (currentContent === mergedContent) {
3898
- console.log(" Settings up to date.");
3899
- return;
3900
- }
3901
- if (dryRun) {
3902
- console.log(" Settings would be updated (dry-run).");
3903
- return;
3904
- }
3905
- await mkdir2(dirname2(settingsPath), { recursive: true });
3906
- await writeFile7(settingsPath, mergedContent, "utf-8");
3907
- console.log(" Updated settings.json");
3908
- }
3909
- async function syncConfig(repoId, projectPath, dryRun) {
3910
- const configPath = join11(projectPath, ".codebyplan.json");
1949
+ async function syncConfigToFile(repoId, projectPath, dryRun) {
1950
+ const configPath = join7(projectPath, ".codebyplan.json");
3911
1951
  let currentConfig = {};
3912
1952
  try {
3913
- const raw = await readFile12(configPath, "utf-8");
1953
+ const raw = await readFile7(configPath, "utf-8");
3914
1954
  currentConfig = JSON.parse(raw);
3915
1955
  } catch {
3916
1956
  currentConfig = { repo_id: repoId };
3917
1957
  }
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
1958
  let resolvedWorktreeId;
3946
1959
  try {
3947
1960
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4033,68 +2046,194 @@ async function syncConfig(repoId, projectPath, dryRun) {
4033
2046
  const currentJson = JSON.stringify(currentConfig, null, 2);
4034
2047
  const newJson = JSON.stringify(newConfig, null, 2);
4035
2048
  if (currentJson === newJson) {
4036
- console.log(" Config up to date.");
2049
+ console.log(" Config up to date.");
4037
2050
  return;
4038
2051
  }
4039
2052
  if (dryRun) {
4040
- console.log(" Config would be updated (dry-run).");
2053
+ console.log(" Config would be updated (dry-run).");
4041
2054
  return;
4042
2055
  }
4043
- await writeFile7(configPath, newJson + "\n", "utf-8");
4044
- console.log(" Updated .codebyplan.json");
2056
+ await writeFile5(configPath, newJson + "\n", "utf-8");
2057
+ console.log(" Updated .codebyplan.json");
4045
2058
  }
4046
- async function syncTechStack(repoId, projectPath, dryRun) {
4047
- try {
4048
- const { dependencies } = await scanAllDependencies(projectPath);
4049
- if (dependencies.length === 0) {
4050
- console.log(" No dependencies found.");
4051
- return;
2059
+ var init_config = __esm({
2060
+ "src/cli/config.ts"() {
2061
+ "use strict";
2062
+ init_flags();
2063
+ init_api();
2064
+ init_resolve_worktree();
2065
+ init_local_config();
2066
+ }
2067
+ });
2068
+
2069
+ // src/lib/server-detect.ts
2070
+ function detectFramework(pkg) {
2071
+ const deps = pkg.dependencies ?? {};
2072
+ const devDeps = pkg.devDependencies ?? {};
2073
+ const hasDep = (name) => name in deps || name in devDeps;
2074
+ if (hasDep("next")) return "nextjs";
2075
+ if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
2076
+ if (hasDep("expo")) return "expo";
2077
+ if (hasDep("vite")) return "vite";
2078
+ if (hasDep("express")) return "express";
2079
+ if (hasDep("@nestjs/core")) return "nestjs";
2080
+ return "custom";
2081
+ }
2082
+ function detectPortFromScripts(pkg) {
2083
+ const scripts = pkg.scripts;
2084
+ if (!scripts?.dev) return null;
2085
+ const parts = scripts.dev.split(/\s+/);
2086
+ for (let i = 0; i < parts.length - 1; i++) {
2087
+ if (parts[i] === "--port" || parts[i] === "-p") {
2088
+ const next = parts[i + 1];
2089
+ if (next) {
2090
+ const port = parseInt(next, 10);
2091
+ if (!isNaN(port)) return port;
2092
+ }
4052
2093
  }
4053
- const sourcePaths = new Set(dependencies.map((d) => d.source_path));
4054
- console.log(
4055
- ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
4056
- );
4057
- if (!dryRun) {
4058
- const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
4059
- if (result.data.stale_removed > 0) {
4060
- console.log(
4061
- ` ${result.data.stale_removed} stale dependencies removed`
2094
+ }
2095
+ return null;
2096
+ }
2097
+ var init_server_detect = __esm({
2098
+ "src/lib/server-detect.ts"() {
2099
+ "use strict";
2100
+ }
2101
+ });
2102
+
2103
+ // src/lib/port-verify.ts
2104
+ import { readFile as readFile8 } from "node:fs/promises";
2105
+ async function verifyPorts(projectPath, portAllocations) {
2106
+ const mismatches = [];
2107
+ const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2108
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2109
+ for (const pkgPath of packageJsonPaths) {
2110
+ try {
2111
+ const raw = await readFile8(pkgPath, "utf-8");
2112
+ const pkg = JSON.parse(raw);
2113
+ const scriptPort = detectPortFromScripts(pkg);
2114
+ if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
2115
+ const relativePath = pkgPath.replace(projectPath + "/", "");
2116
+ const matchingAlloc = portAllocations.find(
2117
+ (a) => a.label === getAppLabel(relativePath)
4062
2118
  );
2119
+ mismatches.push({
2120
+ packageJsonPath: relativePath,
2121
+ scriptPort,
2122
+ allocation: matchingAlloc ?? null,
2123
+ reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
2124
+ });
4063
2125
  }
2126
+ } catch {
2127
+ }
2128
+ }
2129
+ return mismatches;
2130
+ }
2131
+ function isDevServerScript(pkg) {
2132
+ const scripts = pkg.scripts;
2133
+ const raw = scripts?.dev;
2134
+ if (!raw || typeof raw !== "string") return false;
2135
+ const script = raw.trim().toLowerCase();
2136
+ if (!script) return false;
2137
+ for (const pattern of DEV_SERVER_BIN_PATTERNS) {
2138
+ if (pattern.test(script)) return true;
2139
+ }
2140
+ const tokens = script.split(/\s+/);
2141
+ for (const token of tokens) {
2142
+ if (token === "--port" || token === "-p") return true;
2143
+ if (token.startsWith("--port=")) return true;
2144
+ }
2145
+ return false;
2146
+ }
2147
+ function labelMatchesAppName(label, appName) {
2148
+ if (!label || !appName) return false;
2149
+ const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
2150
+ const labelTokens = normalize(label).split(" ").filter(Boolean);
2151
+ const appToken = normalize(appName);
2152
+ if (!appToken) return false;
2153
+ const appTokens = appToken.split(" ").filter(Boolean);
2154
+ if (appTokens.length === 1) {
2155
+ return labelTokens.includes(appTokens[0]);
2156
+ }
2157
+ for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
2158
+ if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
2159
+ }
2160
+ return false;
2161
+ }
2162
+ async function findUnallocatedApps(projectPath, portAllocations) {
2163
+ const apps = await discoverMonorepoApps(projectPath);
2164
+ if (apps.length === 0) {
2165
+ return [];
2166
+ }
2167
+ const unallocated = [];
2168
+ for (const app of apps) {
2169
+ if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
2170
+ continue;
4064
2171
  }
4065
- const detected = await detectTechStack(projectPath);
4066
- if (detected.flat.length > 0) {
4067
- const repoRes = await apiGet(`/repos/${repoId}`);
4068
- const remote = parseTechStackResult(repoRes.data.tech_stack);
4069
- const { merged, added } = mergeTechStack(remote, detected);
4070
- if (added.length > 0) {
4071
- console.log(` ${added.length} new tech entries`);
4072
- if (!dryRun) {
4073
- await apiPut(`/repos/${repoId}`, { tech_stack: merged });
4074
- }
4075
- }
2172
+ let pkg;
2173
+ try {
2174
+ const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
2175
+ pkg = JSON.parse(raw);
2176
+ } catch {
2177
+ continue;
4076
2178
  }
4077
- } catch (err) {
4078
- console.warn(
4079
- ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
4080
- );
2179
+ if (!isDevServerScript(pkg)) continue;
2180
+ const framework = detectFramework(pkg);
2181
+ const detectedPort = detectPortFromScripts(pkg);
2182
+ const command = `pnpm --filter ${app.name} dev`;
2183
+ unallocated.push({
2184
+ name: app.name,
2185
+ path: app.path,
2186
+ framework,
2187
+ detectedPort,
2188
+ command
2189
+ });
4081
2190
  }
2191
+ return unallocated;
4082
2192
  }
4083
- async function syncEslintDriftCheck(repoId, projectPath) {
4084
- try {
4085
- const hasDrift = await checkEslintDrift(repoId, projectPath);
4086
- if (hasDrift) {
4087
- console.log(
4088
- " ESLint config drift detected. Run `codebyplan eslint sync` to update."
4089
- );
4090
- } else {
4091
- console.log(" ESLint configs up to date.");
4092
- }
4093
- } catch (error) {
4094
- console.warn(" ESLint drift check skipped:", error);
2193
+ function getAppLabel(relativePath) {
2194
+ const parts = relativePath.split("/");
2195
+ if (parts.length >= 3 && parts[0] === "apps") {
2196
+ return parts[1];
4095
2197
  }
2198
+ return "root";
4096
2199
  }
4097
- async function syncPortVerification(repoId, projectPath, dryRun, fix) {
2200
+ var DEV_SERVER_BIN_PATTERNS;
2201
+ var init_port_verify = __esm({
2202
+ "src/lib/port-verify.ts"() {
2203
+ "use strict";
2204
+ init_tech_detect();
2205
+ init_server_detect();
2206
+ DEV_SERVER_BIN_PATTERNS = [
2207
+ /\bnext\s+dev\b/,
2208
+ /\bnest\s+start\b/,
2209
+ /\bvite\s+(?:dev|serve)\b/,
2210
+ /\bvite\s+preview\b/,
2211
+ /\bnuxt\s+dev\b/,
2212
+ /\b(?:svelte-kit|sveltekit)\s+dev\b/,
2213
+ /\bexpo\s+start\b/
2214
+ ];
2215
+ }
2216
+ });
2217
+
2218
+ // src/cli/ports.ts
2219
+ var ports_exports = {};
2220
+ __export(ports_exports, {
2221
+ runPorts: () => runPorts
2222
+ });
2223
+ async function runPorts() {
2224
+ const flags = parseFlags(3);
2225
+ const dryRun = hasFlag("dry-run", 3);
2226
+ const fix = hasFlag("fix", 3);
2227
+ validateApiKey();
2228
+ const config = await resolveConfig(flags);
2229
+ const { repoId, projectPath } = config;
2230
+ console.log(`
2231
+ CodeByPlan Ports`);
2232
+ console.log(` Repo: ${repoId}`);
2233
+ console.log(` Path: ${projectPath}`);
2234
+ if (dryRun) console.log(` Mode: dry-run`);
2235
+ if (fix) console.log(` Mode: fix`);
2236
+ console.log();
4098
2237
  try {
4099
2238
  const portsRes = await apiGet(
4100
2239
  `/port-allocations`,
@@ -4102,22 +2241,23 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
4102
2241
  );
4103
2242
  const allocations = portsRes.data ?? [];
4104
2243
  if (allocations.length === 0) {
4105
- console.log(" No port allocations found \u2014 skipping verification.");
2244
+ console.log(" No port allocations found \u2014 skipping verification.");
2245
+ console.log("\n Ports complete.\n");
4106
2246
  return;
4107
2247
  }
4108
2248
  const mismatches = await verifyPorts(projectPath, allocations);
4109
2249
  if (mismatches.length > 0) {
4110
- console.log(` Port mismatches: ${mismatches.length}`);
2250
+ console.log(` Port mismatches: ${mismatches.length}`);
4111
2251
  for (const m of mismatches) {
4112
- console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
2252
+ console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
4113
2253
  }
4114
2254
  }
4115
2255
  const unallocated = await findUnallocatedApps(projectPath, allocations);
4116
2256
  if (unallocated.length > 0) {
4117
- console.log(` Unallocated apps: ${unallocated.length}`);
2257
+ console.log(` Unallocated apps: ${unallocated.length}`);
4118
2258
  for (const app of unallocated) {
4119
2259
  console.log(
4120
- ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
2260
+ ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
4121
2261
  );
4122
2262
  }
4123
2263
  if (fix && !dryRun) {
@@ -4135,11 +2275,11 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
4135
2275
  command: app.command,
4136
2276
  working_dir: app.path
4137
2277
  });
4138
- console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
2278
+ console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
4139
2279
  } catch (err) {
4140
2280
  const msg = err instanceof Error ? err.message : String(err);
4141
2281
  console.log(
4142
- ` Failed to create allocation for ${app.name}: ${msg}`
2282
+ ` Failed to create allocation for ${app.name}: ${msg}`
4143
2283
  );
4144
2284
  }
4145
2285
  if (app.detectedPort && app.detectedPort >= nextPort) {
@@ -4147,176 +2287,180 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
4147
2287
  }
4148
2288
  }
4149
2289
  } else if (fix && dryRun) {
4150
- console.log(" (dry-run \u2014 would create allocations with --fix)");
2290
+ console.log(" (dry-run \u2014 would create allocations with --fix)");
4151
2291
  } else {
4152
- console.log(" Run with --fix to auto-create allocations.");
2292
+ console.log(" Run with --fix to auto-create allocations.");
4153
2293
  }
4154
2294
  }
4155
2295
  if (mismatches.length === 0 && unallocated.length === 0) {
4156
- console.log(" Ports verified.");
2296
+ console.log(" Ports verified.");
4157
2297
  }
4158
2298
  } catch (err) {
4159
2299
  console.warn(
4160
- ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
2300
+ ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
4161
2301
  );
4162
2302
  }
2303
+ console.log("\n Ports complete.\n");
4163
2304
  }
4164
- function groupByType(items) {
4165
- const groups = /* @__PURE__ */ new Map();
4166
- const typeLabels = {
4167
- command: "Commands",
4168
- agent: "Agents",
4169
- skill: "Skills",
4170
- rule: "Rules",
4171
- hook: "Hooks",
4172
- template: "Templates",
4173
- settings: "Settings",
4174
- context: "Context",
4175
- docs_stack: "Stack Docs",
4176
- docs: "Docs"
4177
- };
4178
- for (const item of items) {
4179
- const label = typeLabels[item.type] ?? item.type;
4180
- const group = groups.get(label) ?? [];
4181
- group.push(item);
4182
- groups.set(label, group);
2305
+ var init_ports = __esm({
2306
+ "src/cli/ports.ts"() {
2307
+ "use strict";
2308
+ init_flags();
2309
+ init_api();
2310
+ init_port_verify();
4183
2311
  }
4184
- return groups;
4185
- }
4186
- function getLocalFilePath(claudeDir, projectPath, remote) {
4187
- const typeConfig2 = {
4188
- command: { dir: "commands", ext: ".md" },
4189
- agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
4190
- skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
4191
- rule: { dir: "rules", ext: ".md" },
4192
- hook: { dir: "hooks", ext: ".sh" },
4193
- template: { dir: "templates", ext: "" },
4194
- context: { dir: "context", ext: ".md" },
4195
- docs_stack: { dir: join11("docs", "stack"), ext: ".md" },
4196
- docs: { dir: "docs", ext: ".md" },
4197
- claude_md: { dir: "", ext: "" },
4198
- settings: { dir: "", ext: "" }
4199
- };
4200
- if (remote.type === "claude_md") return join11(projectPath, "CLAUDE.md");
4201
- if (remote.type === "settings") return join11(claudeDir, "settings.json");
4202
- const cfg = typeConfig2[remote.type];
4203
- if (!cfg) return join11(claudeDir, remote.name);
4204
- const typeDir = remote.type === "command" ? join11(claudeDir, cfg.dir, "cbp") : join11(claudeDir, cfg.dir);
4205
- if (cfg.subfolder)
4206
- return join11(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
4207
- if (remote.type === "command" && remote.category)
4208
- return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4209
- if (remote.type === "template") return join11(typeDir, remote.name);
4210
- if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
4211
- return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
4212
- return join11(typeDir, `${remote.name}${cfg.ext}`);
2312
+ });
2313
+
2314
+ // src/lib/migrate-local-config.ts
2315
+ import { readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
2316
+ import { join as join8 } from "node:path";
2317
+ function sharedConfigPath(projectPath) {
2318
+ return join8(projectPath, ".codebyplan.json");
4213
2319
  }
4214
- function getSyncVersion() {
2320
+ async function needsLocalMigration(projectPath) {
4215
2321
  try {
4216
- return "1.5.0";
2322
+ const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
2323
+ const parsed = JSON.parse(raw);
2324
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2325
+ return false;
2326
+ }
2327
+ const cfg = parsed;
2328
+ if (typeof cfg.worktree_id !== "string" || cfg.worktree_id === "") {
2329
+ return false;
2330
+ }
2331
+ const local = await readLocalConfig(projectPath);
2332
+ if (local?.device_id) {
2333
+ return false;
2334
+ }
2335
+ return true;
4217
2336
  } catch {
4218
- return "unknown";
2337
+ return false;
4219
2338
  }
4220
2339
  }
4221
- function flattenSyncData(data) {
4222
- const result = /* @__PURE__ */ new Map();
4223
- const typeMap = {
4224
- commands: "command",
4225
- agents: "agent",
4226
- skills: "skill",
4227
- rules: "rule",
4228
- hooks: "hook",
4229
- templates: "template",
4230
- settings: "settings",
4231
- contexts: "context",
4232
- docs_stack: "docs_stack",
4233
- docs: "docs"
4234
- };
4235
- for (const [syncKey, typeName] of Object.entries(typeMap)) {
4236
- const files = data[syncKey] ?? [];
4237
- for (const file of files) {
4238
- const key = compositeKey(typeName, file.name, file.category ?? null);
4239
- result.set(key, {
4240
- id: file.id,
4241
- type: typeName,
4242
- name: file.name,
4243
- content: file.content,
4244
- category: file.category,
4245
- updated_at: file.updated_at,
4246
- content_hash: file.content_hash,
4247
- scope: file.scope
4248
- });
4249
- }
2340
+ async function runLocalMigration(projectPath) {
2341
+ const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
2342
+ const parsed = JSON.parse(raw);
2343
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2344
+ throw new Error(
2345
+ ".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
2346
+ );
4250
2347
  }
4251
- return result;
2348
+ const cfg = parsed;
2349
+ const hadWorktreeId = "worktree_id" in cfg;
2350
+ const localBefore = await readLocalConfig(projectPath);
2351
+ const localWillBeCreated = !localBefore?.device_id;
2352
+ const device_id = await getOrCreateDeviceId(projectPath);
2353
+ const cleaned = { ...cfg };
2354
+ delete cleaned.worktree_id;
2355
+ await writeFile6(
2356
+ sharedConfigPath(projectPath),
2357
+ JSON.stringify(cleaned, null, 2) + "\n",
2358
+ "utf-8"
2359
+ );
2360
+ const files_changed = [".codebyplan.json"];
2361
+ if (localWillBeCreated) files_changed.push(".codebyplan.local.json");
2362
+ return {
2363
+ migrated: true,
2364
+ was_dirty: hadWorktreeId || localWillBeCreated,
2365
+ files_changed,
2366
+ device_id
2367
+ };
4252
2368
  }
4253
- var init_sync = __esm({
4254
- "src/cli/sync.ts"() {
2369
+ var init_migrate_local_config = __esm({
2370
+ "src/lib/migrate-local-config.ts"() {
4255
2371
  "use strict";
4256
- init_config();
4257
- init_fileMapper();
4258
- init_confirm();
4259
- init_api();
4260
- init_variables();
4261
- init_tech_detect();
4262
- init_settings_merge();
4263
- init_hook_registry();
4264
- init_port_verify();
4265
- init_resolve_worktree();
4266
2372
  init_local_config();
4267
- init_migrate_local_config();
4268
- init_eslint();
4269
2373
  }
4270
2374
  });
4271
2375
 
4272
- // src/cli/resolve-worktree.ts
4273
- var resolve_worktree_exports = {};
4274
- __export(resolve_worktree_exports, {
4275
- runResolveWorktree: () => runResolveWorktree
2376
+ // src/cli/tech-stack.ts
2377
+ var tech_stack_exports = {};
2378
+ __export(tech_stack_exports, {
2379
+ runTechStack: () => runTechStack
4276
2380
  });
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 = "";
2381
+ async function runTechStack() {
2382
+ const flags = parseFlags(3);
2383
+ const dryRun = hasFlag("dry-run", 3);
2384
+ validateApiKey();
2385
+ const config = await resolveConfig(flags);
2386
+ const { repoId, projectPath } = config;
2387
+ console.log(`
2388
+ CodeByPlan Tech Stack`);
2389
+ console.log(` Repo: ${repoId}`);
2390
+ console.log(` Path: ${projectPath}`);
2391
+ if (dryRun) console.log(` Mode: dry-run`);
2392
+ console.log();
2393
+ if (dryRun) {
4288
2394
  try {
4289
- branch = execSync2("git symbolic-ref --short HEAD", {
4290
- cwd: projectPath,
4291
- encoding: "utf-8"
4292
- }).trim();
2395
+ if (await needsLocalMigration(projectPath)) {
2396
+ console.log(
2397
+ ` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
2398
+ );
2399
+ }
4293
2400
  } catch {
4294
2401
  }
4295
- const worktreeId = await resolveWorktreeId({
4296
- repoId,
4297
- repoPath: projectPath,
4298
- branch,
4299
- deviceId
4300
- });
4301
- if (worktreeId) {
4302
- process.stdout.write(worktreeId);
2402
+ } else {
2403
+ try {
2404
+ if (await needsLocalMigration(projectPath)) {
2405
+ const result = await runLocalMigration(projectPath);
2406
+ console.log(
2407
+ ` Migrated .codebyplan.json -> moved worktree_id to gitignored .codebyplan.local.json (device_id=${result.device_id.slice(0, 8)})`
2408
+ );
2409
+ console.log(
2410
+ ` Suggest /cbp-git-commit to stage the cleaned shared file.`
2411
+ );
2412
+ }
2413
+ } catch (err) {
2414
+ console.warn(
2415
+ ` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
2416
+ );
4303
2417
  }
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
- `);
2418
+ }
2419
+ try {
2420
+ const { dependencies } = await scanAllDependencies(projectPath);
2421
+ if (dependencies.length === 0) {
2422
+ console.log(" No dependencies found.");
2423
+ console.log("\n Tech stack complete.\n");
2424
+ return;
4310
2425
  }
4311
- process.exit(0);
2426
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
2427
+ console.log(
2428
+ ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
2429
+ );
2430
+ if (!dryRun) {
2431
+ const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
2432
+ if (result.data.stale_removed > 0) {
2433
+ console.log(
2434
+ ` ${result.data.stale_removed} stale dependencies removed`
2435
+ );
2436
+ }
2437
+ }
2438
+ const detected = await detectTechStack(projectPath);
2439
+ if (detected.flat.length > 0) {
2440
+ const repoRes = await apiGet(`/repos/${repoId}`);
2441
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
2442
+ const { merged, added } = mergeTechStack(remote, detected);
2443
+ if (added.length > 0) {
2444
+ console.log(` ${added.length} new tech entries`);
2445
+ if (!dryRun) {
2446
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
2447
+ }
2448
+ }
2449
+ }
2450
+ } catch (err) {
2451
+ console.warn(
2452
+ ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
2453
+ );
4312
2454
  }
2455
+ console.log("\n Tech stack complete.\n");
4313
2456
  }
4314
- var init_resolve_worktree2 = __esm({
4315
- "src/cli/resolve-worktree.ts"() {
2457
+ var init_tech_stack = __esm({
2458
+ "src/cli/tech-stack.ts"() {
4316
2459
  "use strict";
4317
- init_config();
4318
- init_local_config();
4319
- init_resolve_worktree();
2460
+ init_flags();
2461
+ init_api();
2462
+ init_tech_detect();
2463
+ init_migrate_local_config();
4320
2464
  }
4321
2465
  });
4322
2466
 
@@ -4356,20 +2500,6 @@ void (async () => {
4356
2500
  await runSetup2();
4357
2501
  process.exit(0);
4358
2502
  }
4359
- if (arg === "sync") {
4360
- const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
4361
- const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
4362
- try {
4363
- await runSync2();
4364
- } catch (err) {
4365
- if (err instanceof SyncCancelledError2) {
4366
- console.log("\n Sync cancelled.\n");
4367
- process.exit(0);
4368
- }
4369
- throw err;
4370
- }
4371
- process.exit(0);
4372
- }
4373
2503
  if (arg === "eslint") {
4374
2504
  const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
4375
2505
  const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
@@ -4389,28 +2519,53 @@ void (async () => {
4389
2519
  await runResolveWorktree2();
4390
2520
  process.exit(0);
4391
2521
  }
2522
+ if (arg === "config") {
2523
+ const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2524
+ await runConfig2();
2525
+ process.exit(0);
2526
+ }
2527
+ if (arg === "ports") {
2528
+ const { runPorts: runPorts2 } = await Promise.resolve().then(() => (init_ports(), ports_exports));
2529
+ await runPorts2();
2530
+ process.exit(0);
2531
+ }
2532
+ if (arg === "tech-stack") {
2533
+ const { runTechStack: runTechStack2 } = await Promise.resolve().then(() => (init_tech_stack(), tech_stack_exports));
2534
+ await runTechStack2();
2535
+ process.exit(0);
2536
+ }
4392
2537
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
4393
2538
  console.log(`
4394
2539
  CodeByPlan CLI v${VERSION}
4395
2540
 
4396
2541
  Usage:
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)
2542
+ codebyplan setup Interactive setup (API key + project init)
2543
+ codebyplan config Sync repo config from DB to .codebyplan.json
2544
+ codebyplan ports Verify port allocations against local package.json scripts
2545
+ codebyplan tech-stack Detect and sync tech stack dependencies
2546
+ codebyplan eslint ESLint config management (init)
4400
2547
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
4401
2548
  codebyplan help Show this help message
4402
2549
  codebyplan --version Print version
4403
2550
 
4404
- Sync options:
2551
+ Config options:
2552
+ --path <dir> Project root directory (default: cwd)
2553
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
2554
+ --dry-run Preview changes without writing
2555
+
2556
+ Ports options:
4405
2557
  --path <dir> Project root directory (default: cwd)
4406
2558
  --repo-id <uuid> Repository ID (or set via .codebyplan.json)
4407
2559
  --dry-run Preview changes without writing
4408
- --force Skip confirmation and conflict prompts
4409
2560
  --fix Auto-create missing port allocations
4410
2561
 
2562
+ Tech stack options:
2563
+ --path <dir> Project root directory (default: cwd)
2564
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
2565
+ --dry-run Preview changes without writing
2566
+
4411
2567
  ESLint commands:
4412
2568
  codebyplan eslint init Detect tech stack, resolve presets, generate configs
4413
- codebyplan eslint sync Regenerate if presets changed, detect drift
4414
2569
 
4415
2570
  MCP Server:
4416
2571
  Claude Code connects to CodeByPlan via remote MCP: