@codebyplan/cli 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +534 -762
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -37,179 +37,11 @@ var VERSION, PACKAGE_NAME;
37
37
  var init_version = __esm({
38
38
  "src/lib/version.ts"() {
39
39
  "use strict";
40
- VERSION = "2.3.0";
40
+ VERSION = "3.0.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
44
44
 
45
- // src/cli/setup.ts
46
- var setup_exports = {};
47
- __export(setup_exports, {
48
- runSetup: () => runSetup
49
- });
50
- import { createInterface } from "node:readline/promises";
51
- import { stdin, stdout } from "node:process";
52
- import { readFile, writeFile } from "node:fs/promises";
53
- import { homedir } from "node:os";
54
- import { join } from "node:path";
55
- function getConfigPath(scope) {
56
- return scope === "user" ? join(homedir(), ".claude.json") : join(process.cwd(), ".mcp.json");
57
- }
58
- async function readConfig(path) {
59
- try {
60
- const raw = await readFile(path, "utf-8");
61
- const parsed = JSON.parse(raw);
62
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
63
- return parsed;
64
- }
65
- return {};
66
- } catch {
67
- return {};
68
- }
69
- }
70
- function buildMcpEntry(apiKey) {
71
- return {
72
- command: "npx",
73
- args: ["-y", PACKAGE_NAME],
74
- env: { CODEBYPLAN_API_KEY: apiKey }
75
- };
76
- }
77
- async function writeMcpConfig(scope, apiKey) {
78
- const configPath = getConfigPath(scope);
79
- const config2 = await readConfig(configPath);
80
- if (typeof config2.mcpServers !== "object" || config2.mcpServers === null || Array.isArray(config2.mcpServers)) {
81
- config2.mcpServers = {};
82
- }
83
- config2.mcpServers.codebyplan = buildMcpEntry(apiKey);
84
- await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
85
- return configPath;
86
- }
87
- async function verifyMcpConfig(scope, apiKey) {
88
- try {
89
- const config2 = await readConfig(getConfigPath(scope));
90
- const servers = config2.mcpServers;
91
- if (!servers) return false;
92
- const entry = servers.codebyplan;
93
- return entry?.env?.CODEBYPLAN_API_KEY === apiKey;
94
- } catch {
95
- return false;
96
- }
97
- }
98
- async function runSetup() {
99
- const rl = createInterface({ input: stdin, output: stdout });
100
- console.log("\n CodeByPlan MCP Server Setup\n");
101
- console.log(" This will configure Claude Code to use CodeByPlan.\n");
102
- console.log(" 1. Sign up at https://codebyplan.com");
103
- console.log(" 2. Create an API key at https://codebyplan.com/settings/api-keys/\n");
104
- try {
105
- const apiKey = (await rl.question(" Enter your API key: ")).trim();
106
- if (!apiKey) {
107
- console.log("\n No API key provided. Aborting setup.\n");
108
- return;
109
- }
110
- console.log("\n Validating API key...");
111
- const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
112
- const res = await fetch(`${baseUrl}/api/repos`, {
113
- headers: { "x-api-key": apiKey },
114
- signal: AbortSignal.timeout(1e4)
115
- });
116
- if (res.status === 401) {
117
- console.log(" Invalid API key. Please check and try again.\n");
118
- return;
119
- }
120
- if (!res.ok) {
121
- console.log(` Warning: API returned status ${res.status}, but continuing.
122
- `);
123
- } else {
124
- try {
125
- const body = await res.json();
126
- if (Array.isArray(body.data) && body.data.length === 0) {
127
- console.log(" API key is valid but no repositories found.");
128
- console.log(" Create one at https://codebyplan.com after setup.\n");
129
- } else {
130
- console.log(" API key is valid!\n");
131
- }
132
- } catch {
133
- console.log(" API key is valid!\n");
134
- }
135
- }
136
- console.log(" Where should the MCP server be configured?\n");
137
- console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
138
- console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
139
- const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
140
- const scope = scopeInput === "2" ? "project" : "user";
141
- console.log("\n Configuring MCP server...");
142
- const configPath = await writeMcpConfig(scope, apiKey);
143
- const verified = await verifyMcpConfig(scope, apiKey);
144
- if (verified) {
145
- console.log(` Done! Config written to ${configPath}
146
- `);
147
- if (scope === "project") {
148
- console.log(" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n");
149
- }
150
- console.log(" Start a new Claude Code session to begin using it.\n");
151
- } else {
152
- console.log(" Warning: Could not verify the saved configuration.\n");
153
- console.log(" You can configure manually by adding to your Claude config:\n");
154
- console.log(` claude mcp add codebyplan -e CODEBYPLAN_API_KEY=${apiKey} -- npx -y ${PACKAGE_NAME}
155
- `);
156
- }
157
- } finally {
158
- rl.close();
159
- }
160
- }
161
- var init_setup = __esm({
162
- "src/cli/setup.ts"() {
163
- "use strict";
164
- init_version();
165
- }
166
- });
167
-
168
- // src/cli/config.ts
169
- import { readFile as readFile2 } from "node:fs/promises";
170
- import { join as join2 } from "node:path";
171
- function parseFlags(startIndex) {
172
- const flags = {};
173
- const args = process.argv.slice(startIndex);
174
- for (let i = 0; i < args.length; i++) {
175
- const arg2 = args[i];
176
- if (arg2.startsWith("--") && i + 1 < args.length) {
177
- const key = arg2.slice(2);
178
- flags[key] = args[++i];
179
- }
180
- }
181
- return flags;
182
- }
183
- function hasFlag(name, startIndex) {
184
- return process.argv.slice(startIndex).includes(`--${name}`);
185
- }
186
- async function resolveConfig(flags) {
187
- const projectPath = flags["path"] ?? process.cwd();
188
- let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
189
- let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
190
- if (!repoId || !worktreeId) {
191
- try {
192
- const configPath = join2(projectPath, ".codebyplan.json");
193
- const raw = await readFile2(configPath, "utf-8");
194
- const config2 = JSON.parse(raw);
195
- if (!repoId) repoId = config2.repo_id;
196
- if (!worktreeId) worktreeId = config2.worktree_id;
197
- } catch {
198
- }
199
- }
200
- if (!repoId) {
201
- throw new Error(
202
- 'Could not determine repo_id.\n\nProvide it via one of:\n --repo-id <uuid> CLI flag\n CODEBYPLAN_REPO_ID=<uuid> environment variable\n .codebyplan.json { "repo_id": "<uuid>" } in project root'
203
- );
204
- }
205
- return { repoId, worktreeId, projectPath };
206
- }
207
- var init_config = __esm({
208
- "src/cli/config.ts"() {
209
- "use strict";
210
- }
211
- });
212
-
213
45
  // src/lib/api.ts
214
46
  function validateApiKey() {
215
47
  if (!API_KEY) {
@@ -432,8 +264,8 @@ var init_settings_merge = __esm({
432
264
  });
433
265
 
434
266
  // src/lib/hook-registry.ts
435
- import { readdir, readFile as readFile3 } from "node:fs/promises";
436
- import { join as join3 } from "node:path";
267
+ import { readdir, readFile } from "node:fs/promises";
268
+ import { join } from "node:path";
437
269
  function parseHookMeta(content) {
438
270
  const match = content.match(/^#\s*@hook:\s*(\S+)(?:\s+(.+))?$/m);
439
271
  if (!match) return null;
@@ -452,7 +284,7 @@ async function discoverHooks(hooksDir) {
452
284
  return discovered;
453
285
  }
454
286
  for (const filename of filenames) {
455
- const content = await readFile3(join3(hooksDir, filename), "utf-8");
287
+ const content = await readFile(join(hooksDir, filename), "utf-8");
456
288
  const meta = parseHookMeta(content);
457
289
  if (meta) {
458
290
  discovered.set(filename.replace(/\.sh$/, ""), meta);
@@ -557,38 +389,42 @@ var init_variables = __esm({
557
389
  });
558
390
 
559
391
  // src/lib/sync-engine.ts
560
- import { readdir as readdir2, readFile as readFile4, writeFile as writeFile2, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
561
- import { join as join4, dirname } from "node:path";
392
+ var sync_engine_exports = {};
393
+ __export(sync_engine_exports, {
394
+ executeSyncToLocal: () => executeSyncToLocal
395
+ });
396
+ import { readdir as readdir2, readFile as readFile2, writeFile, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
397
+ import { join as join2, dirname } from "node:path";
562
398
  function getTypeDir(claudeDir, dir) {
563
- if (dir === "commands") return join4(claudeDir, dir, "cbp");
564
- return join4(claudeDir, dir);
399
+ if (dir === "commands") return join2(claudeDir, dir, "cbp");
400
+ return join2(claudeDir, dir);
565
401
  }
566
402
  function getFilePath(claudeDir, typeName, file) {
567
403
  const cfg = typeConfig[typeName];
568
404
  const typeDir = getTypeDir(claudeDir, cfg.dir);
569
405
  if (cfg.subfolder) {
570
- return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
406
+ return join2(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
571
407
  }
572
408
  if (typeName === "command" && file.category) {
573
- return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
409
+ return join2(typeDir, file.category, `${file.name}${cfg.ext}`);
574
410
  }
575
411
  if (typeName === "template") {
576
- return join4(typeDir, file.name);
412
+ return join2(typeDir, file.name);
577
413
  }
578
- return join4(typeDir, `${file.name}${cfg.ext}`);
414
+ return join2(typeDir, `${file.name}${cfg.ext}`);
579
415
  }
580
416
  async function readDirRecursive(dir, base = dir) {
581
417
  const result = /* @__PURE__ */ new Map();
582
418
  try {
583
419
  const entries = await readdir2(dir, { withFileTypes: true });
584
420
  for (const entry of entries) {
585
- const fullPath = join4(dir, entry.name);
421
+ const fullPath = join2(dir, entry.name);
586
422
  if (entry.isDirectory()) {
587
423
  const sub = await readDirRecursive(fullPath, base);
588
424
  for (const [k, v] of sub) result.set(k, v);
589
425
  } else {
590
426
  const relPath = fullPath.slice(base.length + 1);
591
- const fileContent = await readFile4(fullPath, "utf-8");
427
+ const fileContent = await readFile2(fullPath, "utf-8");
592
428
  result.set(relPath, fileContent);
593
429
  }
594
430
  }
@@ -598,7 +434,7 @@ async function readDirRecursive(dir, base = dir) {
598
434
  }
599
435
  async function isGitWorktree(projectPath) {
600
436
  try {
601
- const gitPath = join4(projectPath, ".git");
437
+ const gitPath = join2(projectPath, ".git");
602
438
  const info = await stat(gitPath);
603
439
  return info.isFile();
604
440
  } catch {
@@ -625,7 +461,7 @@ async function executeSyncToLocal(options) {
625
461
  const syncData = syncRes.data;
626
462
  const repoData = repoRes.data;
627
463
  syncData.claude_md = [];
628
- const claudeDir = join4(projectPath, ".claude");
464
+ const claudeDir = join2(projectPath, ".claude");
629
465
  const worktree = await isGitWorktree(projectPath);
630
466
  const byType = {};
631
467
  const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
@@ -650,19 +486,19 @@ async function executeSyncToLocal(options) {
650
486
  remotePathMap.set(relPath, { content: substituted, name: remote.name });
651
487
  }
652
488
  for (const [relPath, { content, name }] of remotePathMap) {
653
- const fullPath = join4(targetDir, relPath);
489
+ const fullPath = join2(targetDir, relPath);
654
490
  const localContent = localFiles.get(relPath);
655
491
  if (localContent === void 0) {
656
492
  if (!dryRun) {
657
493
  await mkdir(dirname(fullPath), { recursive: true });
658
- await writeFile2(fullPath, content, "utf-8");
494
+ await writeFile(fullPath, content, "utf-8");
659
495
  if (typeName === "hook") await chmod(fullPath, 493);
660
496
  }
661
497
  result.created.push(name);
662
498
  totals.created++;
663
499
  } else if (localContent !== content) {
664
500
  if (!dryRun) {
665
- await writeFile2(fullPath, content, "utf-8");
501
+ await writeFile(fullPath, content, "utf-8");
666
502
  if (typeName === "hook") await chmod(fullPath, 493);
667
503
  }
668
504
  result.updated.push(name);
@@ -674,7 +510,7 @@ async function executeSyncToLocal(options) {
674
510
  }
675
511
  for (const [relPath] of localFiles) {
676
512
  if (!remotePathMap.has(relPath)) {
677
- const fullPath = join4(targetDir, relPath);
513
+ const fullPath = join2(targetDir, relPath);
678
514
  if (!dryRun) {
679
515
  await unlink(fullPath);
680
516
  await removeEmptyParents(fullPath, targetDir);
@@ -689,7 +525,7 @@ async function executeSyncToLocal(options) {
689
525
  {
690
526
  const typeName = "docs_stack";
691
527
  const syncKey = "docs_stack";
692
- const targetDir = join4(projectPath, "docs", "stack");
528
+ const targetDir = join2(projectPath, "docs", "stack");
693
529
  const remoteFiles = syncData[syncKey] ?? [];
694
530
  const result = { created: [], updated: [], deleted: [], unchanged: [] };
695
531
  if (remoteFiles.length > 0 && !dryRun) {
@@ -698,23 +534,23 @@ async function executeSyncToLocal(options) {
698
534
  const localFiles = await readDirRecursive(targetDir);
699
535
  const remotePathMap = /* @__PURE__ */ new Map();
700
536
  for (const remote of remoteFiles) {
701
- const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
537
+ const relPath = remote.category ? join2(remote.category, remote.name) : remote.name;
702
538
  const substituted = substituteVariables(remote.content, repoData);
703
539
  remotePathMap.set(relPath, { content: substituted, name: `${remote.category ?? ""}/${remote.name}` });
704
540
  }
705
541
  for (const [relPath, { content, name }] of remotePathMap) {
706
- const fullPath = join4(targetDir, relPath);
542
+ const fullPath = join2(targetDir, relPath);
707
543
  const localContent = localFiles.get(relPath);
708
544
  if (localContent === void 0) {
709
545
  if (!dryRun) {
710
546
  await mkdir(dirname(fullPath), { recursive: true });
711
- await writeFile2(fullPath, content, "utf-8");
547
+ await writeFile(fullPath, content, "utf-8");
712
548
  }
713
549
  result.created.push(name);
714
550
  totals.created++;
715
551
  } else if (localContent !== content) {
716
552
  if (!dryRun) {
717
- await writeFile2(fullPath, content, "utf-8");
553
+ await writeFile(fullPath, content, "utf-8");
718
554
  }
719
555
  result.updated.push(name);
720
556
  totals.updated++;
@@ -725,7 +561,7 @@ async function executeSyncToLocal(options) {
725
561
  }
726
562
  for (const [relPath] of localFiles) {
727
563
  if (!remotePathMap.has(relPath)) {
728
- const fullPath = join4(targetDir, relPath);
564
+ const fullPath = join2(targetDir, relPath);
729
565
  if (!dryRun) {
730
566
  await unlink(fullPath);
731
567
  await removeEmptyParents(fullPath, targetDir);
@@ -743,8 +579,8 @@ async function executeSyncToLocal(options) {
743
579
  globalSettings = { ...globalSettings, ...parsed };
744
580
  }
745
581
  const specialTypes = {
746
- claude_md: () => join4(projectPath, "CLAUDE.md"),
747
- settings: () => join4(projectPath, ".claude", "settings.json")
582
+ claude_md: () => join2(projectPath, "CLAUDE.md"),
583
+ settings: () => join2(projectPath, ".claude", "settings.json")
748
584
  };
749
585
  for (const [typeName, getPath] of Object.entries(specialTypes)) {
750
586
  const remoteFiles = syncData[typeName] ?? [];
@@ -754,13 +590,13 @@ async function executeSyncToLocal(options) {
754
590
  const remoteContent = substituteVariables(remote.content, repoData);
755
591
  let localContent;
756
592
  try {
757
- localContent = await readFile4(targetPath, "utf-8");
593
+ localContent = await readFile2(targetPath, "utf-8");
758
594
  } catch {
759
595
  }
760
596
  if (typeName === "settings") {
761
597
  const repoSettings = JSON.parse(remoteContent);
762
598
  const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
763
- const hooksDir = join4(projectPath, ".claude", "hooks");
599
+ const hooksDir = join2(projectPath, ".claude", "hooks");
764
600
  const discovered = await discoverHooks(hooksDir);
765
601
  if (localContent === void 0) {
766
602
  let finalSettings = stripPermissionsAllow(combinedTemplate);
@@ -772,7 +608,7 @@ async function executeSyncToLocal(options) {
772
608
  }
773
609
  if (!dryRun) {
774
610
  await mkdir(dirname(targetPath), { recursive: true });
775
- await writeFile2(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
611
+ await writeFile(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
776
612
  }
777
613
  result.created.push(remote.name);
778
614
  totals.created++;
@@ -789,7 +625,7 @@ async function executeSyncToLocal(options) {
789
625
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
790
626
  if (localContent !== mergedContent) {
791
627
  if (!dryRun) {
792
- await writeFile2(targetPath, mergedContent, "utf-8");
628
+ await writeFile(targetPath, mergedContent, "utf-8");
793
629
  }
794
630
  result.updated.push(remote.name);
795
631
  totals.updated++;
@@ -802,13 +638,13 @@ async function executeSyncToLocal(options) {
802
638
  if (localContent === void 0) {
803
639
  if (!dryRun) {
804
640
  await mkdir(dirname(targetPath), { recursive: true });
805
- await writeFile2(targetPath, remoteContent, "utf-8");
641
+ await writeFile(targetPath, remoteContent, "utf-8");
806
642
  }
807
643
  result.created.push(remote.name);
808
644
  totals.created++;
809
645
  } else if (localContent !== remoteContent) {
810
646
  if (!dryRun) {
811
- await writeFile2(targetPath, remoteContent, "utf-8");
647
+ await writeFile(targetPath, remoteContent, "utf-8");
812
648
  }
813
649
  result.updated.push(remote.name);
814
650
  totals.updated++;
@@ -852,327 +688,244 @@ var init_sync_engine = __esm({
852
688
  }
853
689
  });
854
690
 
855
- // src/cli/confirm.ts
856
- import { createInterface as createInterface2 } from "node:readline/promises";
857
- import { stdin as stdin2, stdout as stdout2 } from "node:process";
858
- async function confirmProceed(message) {
859
- const rl = createInterface2({ input: stdin2, output: stdout2 });
691
+ // src/cli/setup.ts
692
+ var setup_exports = {};
693
+ __export(setup_exports, {
694
+ runSetup: () => runSetup
695
+ });
696
+ import { createInterface } from "node:readline/promises";
697
+ import { stdin, stdout } from "node:process";
698
+ import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
699
+ import { homedir } from "node:os";
700
+ import { join as join3 } from "node:path";
701
+ function getConfigPath(scope) {
702
+ return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
703
+ }
704
+ async function readConfig(path) {
860
705
  try {
861
- const answer = await rl.question(message ?? " Proceed? [Y/n] ");
862
- const a = answer.trim().toLowerCase();
863
- return a !== "n" && a !== "no";
864
- } finally {
865
- rl.close();
706
+ const raw = await readFile3(path, "utf-8");
707
+ const parsed = JSON.parse(raw);
708
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
709
+ return parsed;
710
+ }
711
+ return {};
712
+ } catch {
713
+ return {};
866
714
  }
867
715
  }
868
- var init_confirm = __esm({
869
- "src/cli/confirm.ts"() {
870
- "use strict";
716
+ function buildMcpEntry(apiKey) {
717
+ return {
718
+ command: "npx",
719
+ args: ["-y", PACKAGE_NAME],
720
+ env: { CODEBYPLAN_API_KEY: apiKey }
721
+ };
722
+ }
723
+ async function writeMcpConfig(scope, apiKey) {
724
+ const configPath = getConfigPath(scope);
725
+ const config2 = await readConfig(configPath);
726
+ if (typeof config2.mcpServers !== "object" || config2.mcpServers === null || Array.isArray(config2.mcpServers)) {
727
+ config2.mcpServers = {};
871
728
  }
872
- });
873
-
874
- // src/lib/tech-detect.ts
875
- import { readFile as readFile5, access } from "node:fs/promises";
876
- import { join as join5 } from "node:path";
877
- async function fileExists(filePath) {
729
+ config2.mcpServers.codebyplan = buildMcpEntry(apiKey);
730
+ await writeFile2(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
731
+ return configPath;
732
+ }
733
+ async function verifyMcpConfig(scope, apiKey) {
878
734
  try {
879
- await access(filePath);
880
- return true;
735
+ const config2 = await readConfig(getConfigPath(scope));
736
+ const servers = config2.mcpServers;
737
+ if (!servers) return false;
738
+ const entry = servers.codebyplan;
739
+ return entry?.env?.CODEBYPLAN_API_KEY === apiKey;
881
740
  } catch {
882
741
  return false;
883
742
  }
884
743
  }
885
- async function detectTechStack(projectPath) {
886
- const seen = /* @__PURE__ */ new Map();
744
+ async function runSetup() {
745
+ const rl = createInterface({ input: stdin, output: stdout });
746
+ console.log("\n CodeByPlan Setup\n");
747
+ console.log(" This will configure Claude Code to use CodeByPlan.\n");
748
+ console.log(" 1. Sign up at https://codebyplan.com");
749
+ console.log(" 2. Create an API key at https://codebyplan.com/settings/api-keys/\n");
887
750
  try {
888
- const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
889
- const pkg = JSON.parse(raw);
890
- const allDeps = {
891
- ...pkg.dependencies,
892
- ...pkg.devDependencies
893
- };
894
- for (const depName of Object.keys(allDeps)) {
895
- const rule = PACKAGE_MAP[depName];
896
- if (rule) {
897
- const key = rule.name.toLowerCase();
898
- if (!seen.has(key)) {
899
- seen.set(key, { name: rule.name, category: rule.category });
751
+ const apiKey = (await rl.question(" Enter your API key: ")).trim();
752
+ if (!apiKey) {
753
+ console.log("\n No API key provided. Aborting setup.\n");
754
+ return;
755
+ }
756
+ console.log("\n Validating API key...");
757
+ const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
758
+ const res = await fetch(`${baseUrl}/api/repos`, {
759
+ headers: { "x-api-key": apiKey },
760
+ signal: AbortSignal.timeout(1e4)
761
+ });
762
+ if (res.status === 401) {
763
+ console.log(" Invalid API key. Please check and try again.\n");
764
+ return;
765
+ }
766
+ let repos = [];
767
+ if (res.ok) {
768
+ try {
769
+ const body = await res.json();
770
+ repos = body.data ?? [];
771
+ if (repos.length === 0) {
772
+ console.log(" API key is valid but no repositories found.");
773
+ console.log(" Create one at https://codebyplan.com after setup.\n");
774
+ } else {
775
+ console.log(" API key is valid!\n");
900
776
  }
777
+ } catch {
778
+ console.log(" API key is valid!\n");
901
779
  }
780
+ } else {
781
+ console.log(` Warning: API returned status ${res.status}, but continuing.
782
+ `);
902
783
  }
903
- } catch {
904
- }
905
- for (const { file, rule } of CONFIG_FILE_MAP) {
906
- const key = rule.name.toLowerCase();
907
- if (!seen.has(key) && await fileExists(join5(projectPath, file))) {
908
- seen.set(key, { name: rule.name, category: rule.category });
784
+ console.log(" Where should the MCP server be configured?\n");
785
+ console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
786
+ console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
787
+ const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
788
+ const scope = scopeInput === "2" ? "project" : "user";
789
+ console.log("\n Configuring MCP server...");
790
+ const configPath = await writeMcpConfig(scope, apiKey);
791
+ const verified = await verifyMcpConfig(scope, apiKey);
792
+ if (verified) {
793
+ console.log(` Done! Config written to ${configPath}
794
+ `);
795
+ if (scope === "project") {
796
+ console.log(" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n");
797
+ }
798
+ } else {
799
+ console.log(" Warning: Could not verify the saved configuration.\n");
800
+ console.log(` claude mcp add codebyplan -e CODEBYPLAN_API_KEY=${apiKey} -- npx -y ${PACKAGE_NAME}
801
+ `);
909
802
  }
910
- }
911
- return Array.from(seen.values()).sort((a, b) => {
912
- const catCmp = a.category.localeCompare(b.category);
913
- if (catCmp !== 0) return catCmp;
914
- return a.name.localeCompare(b.name);
915
- });
916
- }
917
- function mergeTechStack(remote, detected) {
918
- const seen = /* @__PURE__ */ new Map();
919
- for (const entry of remote) {
920
- seen.set(entry.name.toLowerCase(), entry);
921
- }
922
- const added = [];
923
- for (const entry of detected) {
924
- const key = entry.name.toLowerCase();
925
- if (!seen.has(key)) {
926
- seen.set(key, entry);
927
- added.push(entry);
803
+ if (repos.length > 0) {
804
+ console.log(" Initialize this project?\n");
805
+ const initAnswer = (await rl.question(" Link to a repository? (Y/n): ")).trim().toLowerCase();
806
+ if (initAnswer === "" || initAnswer === "y" || initAnswer === "yes") {
807
+ process.env.CODEBYPLAN_API_KEY = apiKey;
808
+ console.log("\n Your repositories:\n");
809
+ for (let i = 0; i < repos.length; i++) {
810
+ console.log(` ${i + 1}. ${repos[i].name}`);
811
+ }
812
+ console.log();
813
+ const choice = (await rl.question(" Select a repository (number): ")).trim();
814
+ const index = parseInt(choice, 10) - 1;
815
+ if (isNaN(index) || index < 0 || index >= repos.length) {
816
+ console.log(" Invalid selection. Skipping project init.\n");
817
+ } else {
818
+ const selectedRepo = repos[index];
819
+ console.log(`
820
+ Selected: ${selectedRepo.name}
821
+ `);
822
+ let worktreeId;
823
+ const projectPath = process.cwd();
824
+ try {
825
+ const worktreesRes = await apiGet(`/worktrees?repo_id=${selectedRepo.id}`);
826
+ const match = worktreesRes.data.find((wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/"));
827
+ if (match) worktreeId = match.id;
828
+ } catch {
829
+ }
830
+ const codebyplanPath = join3(projectPath, ".codebyplan.json");
831
+ const codebyplanConfig = { repo_id: selectedRepo.id };
832
+ if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
833
+ await writeFile2(codebyplanPath, JSON.stringify(codebyplanConfig, null, 2) + "\n", "utf-8");
834
+ console.log(` Created ${codebyplanPath}`);
835
+ console.log("\n Running initial sync...\n");
836
+ try {
837
+ const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
838
+ const syncResult = await executeSyncToLocal2({
839
+ repoId: selectedRepo.id,
840
+ projectPath
841
+ });
842
+ const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
843
+ if (totalChanges > 0) {
844
+ console.log(` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
845
+ `);
846
+ } else {
847
+ console.log(" All files already up to date.\n");
848
+ }
849
+ } catch (err) {
850
+ const msg = err instanceof Error ? err.message : String(err);
851
+ console.log(` Sync failed: ${msg}`);
852
+ console.log(" Run 'codebyplan sync' later to sync files.\n");
853
+ }
854
+ }
855
+ }
928
856
  }
857
+ console.log(" Setup complete! Start a new Claude Code session to begin.\n");
858
+ } finally {
859
+ rl.close();
929
860
  }
930
- const merged = Array.from(seen.values()).sort((a, b) => {
931
- const catCmp = a.category.localeCompare(b.category);
932
- if (catCmp !== 0) return catCmp;
933
- return a.name.localeCompare(b.name);
934
- });
935
- return { merged, added };
936
- }
937
- function parseTechStack(raw) {
938
- if (!Array.isArray(raw)) return [];
939
- return raw.filter(
940
- (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
941
- );
942
861
  }
943
- var PACKAGE_MAP, CONFIG_FILE_MAP;
944
- var init_tech_detect = __esm({
945
- "src/lib/tech-detect.ts"() {
862
+ var init_setup = __esm({
863
+ "src/cli/setup.ts"() {
946
864
  "use strict";
947
- PACKAGE_MAP = {
948
- // Frameworks
949
- next: { name: "Next.js", category: "framework" },
950
- nuxt: { name: "Nuxt", category: "framework" },
951
- gatsby: { name: "Gatsby", category: "framework" },
952
- express: { name: "Express", category: "framework" },
953
- fastify: { name: "Fastify", category: "framework" },
954
- hono: { name: "Hono", category: "framework" },
955
- "@remix-run/node": { name: "Remix", category: "framework" },
956
- svelte: { name: "Svelte", category: "framework" },
957
- astro: { name: "Astro", category: "framework" },
958
- "@angular/core": { name: "Angular", category: "framework" },
959
- // Libraries (UI)
960
- react: { name: "React", category: "framework" },
961
- vue: { name: "Vue", category: "framework" },
962
- "solid-js": { name: "Solid", category: "framework" },
963
- preact: { name: "Preact", category: "framework" },
964
- // Languages (detected via devDeps)
965
- typescript: { name: "TypeScript", category: "language" },
966
- // Styling
967
- tailwindcss: { name: "Tailwind CSS", category: "styling" },
968
- sass: { name: "SCSS", category: "styling" },
969
- "styled-components": { name: "styled-components", category: "styling" },
970
- "@emotion/react": { name: "Emotion", category: "styling" },
971
- // Database
972
- prisma: { name: "Prisma", category: "database" },
973
- "@prisma/client": { name: "Prisma", category: "database" },
974
- "drizzle-orm": { name: "Drizzle", category: "database" },
975
- "@supabase/supabase-js": { name: "Supabase", category: "database" },
976
- mongoose: { name: "MongoDB", category: "database" },
977
- typeorm: { name: "TypeORM", category: "database" },
978
- knex: { name: "Knex", category: "database" },
979
- // Testing
980
- jest: { name: "Jest", category: "testing" },
981
- vitest: { name: "Vitest", category: "testing" },
982
- mocha: { name: "Mocha", category: "testing" },
983
- playwright: { name: "Playwright", category: "testing" },
984
- "@playwright/test": { name: "Playwright", category: "testing" },
985
- cypress: { name: "Cypress", category: "testing" },
986
- // Build tools
987
- turbo: { name: "Turborepo", category: "build" },
988
- vite: { name: "Vite", category: "build" },
989
- webpack: { name: "Webpack", category: "build" },
990
- esbuild: { name: "esbuild", category: "build" },
991
- rollup: { name: "Rollup", category: "build" },
992
- // Tools
993
- eslint: { name: "ESLint", category: "tool" },
994
- prettier: { name: "Prettier", category: "tool" },
995
- "@biomejs/biome": { name: "Biome", category: "tool" }
996
- };
997
- CONFIG_FILE_MAP = [
998
- { file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
999
- { file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
1000
- { file: "next.config.mjs", rule: { name: "Next.js", category: "framework" } },
1001
- { file: "next.config.ts", rule: { name: "Next.js", category: "framework" } },
1002
- { file: "tailwind.config.js", rule: { name: "Tailwind CSS", category: "styling" } },
1003
- { file: "tailwind.config.ts", rule: { name: "Tailwind CSS", category: "styling" } },
1004
- { file: "turbo.json", rule: { name: "Turborepo", category: "build" } },
1005
- { file: "docker-compose.yml", rule: { name: "Docker", category: "deployment" } },
1006
- { file: "docker-compose.yaml", rule: { name: "Docker", category: "deployment" } },
1007
- { file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
1008
- { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } }
1009
- ];
865
+ init_version();
866
+ init_api();
1010
867
  }
1011
868
  });
1012
869
 
1013
- // src/cli/pull.ts
1014
- var pull_exports = {};
1015
- __export(pull_exports, {
1016
- executePull: () => executePull,
1017
- runPull: () => runPull
1018
- });
1019
- async function executePull(options) {
1020
- const { repoId, projectPath, dryRun, quiet, force } = options;
1021
- const log = quiet ? () => {
1022
- } : (msg) => console.log(msg);
1023
- const needsConfirm = !force && !quiet && !dryRun;
1024
- const previewResult = await executeSyncToLocal({
1025
- repoId,
1026
- projectPath,
1027
- dryRun: needsConfirm ? true : dryRun
1028
- });
1029
- for (const [typeKey, result] of Object.entries(previewResult.byType)) {
1030
- const displayType = displayTypeMap[typeKey] ?? typeKey;
1031
- for (const name of result.created) {
1032
- log(` + ${displayType}/${name}`);
1033
- }
1034
- for (const name of result.updated) {
1035
- log(` ~ ${displayType}/${name}`);
1036
- }
1037
- for (const name of result.deleted) {
1038
- log(` - ${displayType}/${name}`);
1039
- }
1040
- }
1041
- const totalChanges = previewResult.totals.created + previewResult.totals.updated + previewResult.totals.deleted;
1042
- if (needsConfirm && totalChanges > 0) {
1043
- log("");
1044
- log(` ${previewResult.totals.created} to create, ${previewResult.totals.updated} to update, ${previewResult.totals.deleted} to delete`);
1045
- const confirmed = await confirmProceed();
1046
- if (!confirmed) {
1047
- log(" Cancelled.");
1048
- return { created: 0, updated: 0, deleted: 0, unchanged: previewResult.totals.unchanged };
870
+ // src/cli/config.ts
871
+ import { readFile as readFile4 } from "node:fs/promises";
872
+ import { join as join4 } from "node:path";
873
+ function parseFlags(startIndex) {
874
+ const flags = {};
875
+ const args = process.argv.slice(startIndex);
876
+ for (let i = 0; i < args.length; i++) {
877
+ const arg2 = args[i];
878
+ if (arg2.startsWith("--") && i + 1 < args.length) {
879
+ const key = arg2.slice(2);
880
+ flags[key] = args[++i];
1049
881
  }
1050
- const realResult = await executeSyncToLocal({ repoId, projectPath, dryRun: false });
1051
- return {
1052
- created: realResult.totals.created,
1053
- updated: realResult.totals.updated,
1054
- deleted: realResult.totals.deleted,
1055
- unchanged: realResult.totals.unchanged
1056
- };
1057
882
  }
1058
- return {
1059
- created: previewResult.totals.created,
1060
- updated: previewResult.totals.updated,
1061
- deleted: previewResult.totals.deleted,
1062
- unchanged: previewResult.totals.unchanged
1063
- };
883
+ return flags;
1064
884
  }
1065
- async function runPull() {
1066
- const flags = parseFlags(3);
1067
- const dryRun = hasFlag("dry-run", 3);
1068
- const force = hasFlag("force", 3);
1069
- const statusOnly = hasFlag("status", 3);
1070
- validateApiKey();
1071
- const config2 = await resolveConfig(flags);
1072
- const { repoId, projectPath } = config2;
1073
- if (statusOnly) {
1074
- console.log(`
1075
- CodeByPlan Sync Status
1076
- `);
1077
- await printSyncStatus(repoId);
1078
- return;
1079
- }
1080
- console.log(`
1081
- CodeByPlan Pull`);
1082
- console.log(` Repo: ${repoId}`);
1083
- console.log(` Path: ${projectPath}`);
1084
- if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
1085
- if (force) console.log(` Mode: force (no confirmation prompt)`);
1086
- console.log();
1087
- const repoRes = await apiGet(`/repos/${repoId}`);
1088
- const result = await executePull({ repoId, projectPath, dryRun, force });
1089
- console.log();
1090
- if (result.created + result.updated + result.deleted === 0) {
1091
- console.log(" All files up to date.");
1092
- } else {
1093
- console.log(` ${result.created} created, ${result.updated} updated, ${result.deleted} deleted, ${result.unchanged} unchanged`);
1094
- }
1095
- if (dryRun) {
1096
- console.log(" (dry-run \u2014 no changes were made)");
1097
- }
1098
- printTechStack(repoRes.data.tech_stack);
1099
- await printSyncStatus(repoId);
885
+ function hasFlag(name, startIndex) {
886
+ return process.argv.slice(startIndex).includes(`--${name}`);
1100
887
  }
1101
- function printTechStack(techStackRaw) {
1102
- try {
1103
- const entries = parseTechStack(techStackRaw);
1104
- if (entries.length === 0) return;
1105
- const grouped = /* @__PURE__ */ new Map();
1106
- for (const entry of entries) {
1107
- const list = grouped.get(entry.category) ?? [];
1108
- list.push(entry);
1109
- grouped.set(entry.category, list);
1110
- }
1111
- console.log();
1112
- console.log(" Tech stack:");
1113
- for (const [category, items] of grouped) {
1114
- const label = category.charAt(0).toUpperCase() + category.slice(1);
1115
- const names = items.map((e) => e.name).join(", ");
1116
- console.log(` ${label}: ${names}`);
888
+ async function resolveConfig(flags) {
889
+ const projectPath = flags["path"] ?? process.cwd();
890
+ let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
891
+ let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
892
+ if (!repoId || !worktreeId) {
893
+ try {
894
+ const configPath = join4(projectPath, ".codebyplan.json");
895
+ const raw = await readFile4(configPath, "utf-8");
896
+ const config2 = JSON.parse(raw);
897
+ if (!repoId) repoId = config2.repo_id;
898
+ if (!worktreeId) worktreeId = config2.worktree_id;
899
+ } catch {
1117
900
  }
1118
- } catch {
1119
901
  }
1120
- }
1121
- async function printSyncStatus(currentRepoId) {
1122
- try {
1123
- const statusRes = await apiGet("/sync/status");
1124
- const { repos } = statusRes.data;
1125
- const otherRepos = repos.filter((r) => r.id !== currentRepoId);
1126
- const stale = otherRepos.filter((r) => r.needs_sync);
1127
- if (stale.length > 0) {
1128
- console.log();
1129
- console.log(` Other repos needing sync (${stale.length}):`);
1130
- for (const repo of stale) {
1131
- const syncedAt = repo.claude_sync_at ? new Date(repo.claude_sync_at).toLocaleDateString() : "never";
1132
- console.log(` - ${repo.name} (last synced: ${syncedAt})`);
1133
- }
1134
- } else if (otherRepos.length > 0) {
1135
- console.log();
1136
- console.log(" All repos are in sync.");
1137
- }
1138
- } catch {
902
+ if (!repoId) {
903
+ throw new Error(
904
+ 'Could not determine repo_id.\n\nProvide it via one of:\n --repo-id <uuid> CLI flag\n CODEBYPLAN_REPO_ID=<uuid> environment variable\n .codebyplan.json { "repo_id": "<uuid>" } in project root'
905
+ );
1139
906
  }
1140
- console.log();
907
+ return { repoId, worktreeId, projectPath };
1141
908
  }
1142
- var displayTypeMap;
1143
- var init_pull = __esm({
1144
- "src/cli/pull.ts"() {
909
+ var init_config = __esm({
910
+ "src/cli/config.ts"() {
1145
911
  "use strict";
1146
- init_config();
1147
- init_api();
1148
- init_sync_engine();
1149
- init_confirm();
1150
- init_tech_detect();
1151
- displayTypeMap = {
1152
- commands: "command",
1153
- agents: "agent",
1154
- skills: "skill",
1155
- rules: "rule",
1156
- hooks: "hook",
1157
- templates: "template"
1158
- };
1159
912
  }
1160
913
  });
1161
914
 
1162
915
  // src/cli/fileMapper.ts
1163
- import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
1164
- import { join as join6, extname } from "node:path";
916
+ import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
917
+ import { join as join5, extname } from "node:path";
1165
918
  function compositeKey(type, name, category) {
1166
919
  return category ? `${type}:${category}/${name}` : `${type}:${name}`;
1167
920
  }
1168
921
  async function scanLocalFiles(claudeDir, projectPath) {
1169
922
  const result = /* @__PURE__ */ new Map();
1170
- await scanCommands(join6(claudeDir, "commands", "cbp"), result);
1171
- await scanSubfolderType(join6(claudeDir, "agents"), "agent", "AGENT.md", result);
1172
- await scanSubfolderType(join6(claudeDir, "skills"), "skill", "SKILL.md", result);
1173
- await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
1174
- await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
1175
- await scanTemplates(join6(claudeDir, "templates"), result);
923
+ await scanCommands(join5(claudeDir, "commands", "cbp"), result);
924
+ await scanSubfolderType(join5(claudeDir, "agents"), "agent", "AGENT.md", result);
925
+ await scanSubfolderType(join5(claudeDir, "skills"), "skill", "SKILL.md", result);
926
+ await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
927
+ await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
928
+ await scanTemplates(join5(claudeDir, "templates"), result);
1176
929
  await scanSettings(claudeDir, projectPath, result);
1177
930
  return result;
1178
931
  }
@@ -1188,10 +941,10 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
1188
941
  }
1189
942
  for (const entry of entries) {
1190
943
  if (entry.isDirectory()) {
1191
- await scanCommandsRecursive(baseDir, join6(currentDir, entry.name), result);
944
+ await scanCommandsRecursive(baseDir, join5(currentDir, entry.name), result);
1192
945
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1193
946
  const name = entry.name.slice(0, -3);
1194
- const content = await readFile6(join6(currentDir, entry.name), "utf-8");
947
+ const content = await readFile5(join5(currentDir, entry.name), "utf-8");
1195
948
  const relDir = currentDir.slice(baseDir.length + 1);
1196
949
  const category = relDir || null;
1197
950
  const key = compositeKey("command", name, category);
@@ -1208,9 +961,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
1208
961
  }
1209
962
  for (const entry of entries) {
1210
963
  if (entry.isDirectory()) {
1211
- const filePath = join6(dir, entry.name, fileName);
964
+ const filePath = join5(dir, entry.name, fileName);
1212
965
  try {
1213
- const content = await readFile6(filePath, "utf-8");
966
+ const content = await readFile5(filePath, "utf-8");
1214
967
  const key = compositeKey(type, entry.name, null);
1215
968
  result.set(key, { type, name: entry.name, category: null, content });
1216
969
  } catch {
@@ -1228,7 +981,7 @@ async function scanFlatType(dir, type, ext, result) {
1228
981
  for (const entry of entries) {
1229
982
  if (entry.isFile() && entry.name.endsWith(ext)) {
1230
983
  const name = entry.name.slice(0, -ext.length);
1231
- const content = await readFile6(join6(dir, entry.name), "utf-8");
984
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
1232
985
  const key = compositeKey(type, name, null);
1233
986
  result.set(key, { type, name, category: null, content });
1234
987
  }
@@ -1243,17 +996,17 @@ async function scanTemplates(dir, result) {
1243
996
  }
1244
997
  for (const entry of entries) {
1245
998
  if (entry.isFile() && extname(entry.name)) {
1246
- const content = await readFile6(join6(dir, entry.name), "utf-8");
999
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
1247
1000
  const key = compositeKey("template", entry.name, null);
1248
1001
  result.set(key, { type: "template", name: entry.name, category: null, content });
1249
1002
  }
1250
1003
  }
1251
1004
  }
1252
1005
  async function scanSettings(claudeDir, projectPath, result) {
1253
- const settingsPath = join6(claudeDir, "settings.json");
1006
+ const settingsPath = join5(claudeDir, "settings.json");
1254
1007
  let raw;
1255
1008
  try {
1256
- raw = await readFile6(settingsPath, "utf-8");
1009
+ raw = await readFile5(settingsPath, "utf-8");
1257
1010
  } catch {
1258
1011
  return;
1259
1012
  }
@@ -1265,7 +1018,7 @@ async function scanSettings(claudeDir, projectPath, result) {
1265
1018
  }
1266
1019
  parsed = stripPermissionsAllow(parsed);
1267
1020
  if (parsed.hooks && typeof parsed.hooks === "object") {
1268
- const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
1021
+ const hooksDir = projectPath ? join5(projectPath, ".claude", "hooks") : join5(claudeDir, "hooks");
1269
1022
  const discovered = await discoverHooks(hooksDir);
1270
1023
  if (discovered.size > 0) {
1271
1024
  parsed.hooks = stripDiscoveredHooks(
@@ -1290,12 +1043,12 @@ var init_fileMapper = __esm({
1290
1043
  });
1291
1044
 
1292
1045
  // src/cli/conflict.ts
1293
- import { createInterface as createInterface3 } from "node:readline/promises";
1294
- import { stdin as stdin3, stdout as stdout3 } from "node:process";
1046
+ import { createInterface as createInterface2 } from "node:readline/promises";
1047
+ import { stdin as stdin2, stdout as stdout2 } from "node:process";
1295
1048
  async function resolveConflicts(conflicts) {
1296
1049
  const resolutions = /* @__PURE__ */ new Map();
1297
1050
  if (conflicts.length === 0) return resolutions;
1298
- const rl = createInterface3({ input: stdin3, output: stdout3 });
1051
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1299
1052
  try {
1300
1053
  console.log(`
1301
1054
  ${conflicts.length} conflict(s) found:
@@ -1357,47 +1110,221 @@ var init_conflict = __esm({
1357
1110
  }
1358
1111
  });
1359
1112
 
1360
- // src/cli/push.ts
1361
- var push_exports = {};
1362
- __export(push_exports, {
1363
- runPush: () => runPush
1113
+ // src/cli/confirm.ts
1114
+ import { createInterface as createInterface3 } from "node:readline/promises";
1115
+ import { stdin as stdin3, stdout as stdout3 } from "node:process";
1116
+ async function confirmProceed(message) {
1117
+ const rl = createInterface3({ input: stdin3, output: stdout3 });
1118
+ try {
1119
+ const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1120
+ const a = answer.trim().toLowerCase();
1121
+ return a !== "n" && a !== "no";
1122
+ } finally {
1123
+ rl.close();
1124
+ }
1125
+ }
1126
+ var init_confirm = __esm({
1127
+ "src/cli/confirm.ts"() {
1128
+ "use strict";
1129
+ }
1130
+ });
1131
+
1132
+ // src/lib/tech-detect.ts
1133
+ import { readFile as readFile6, access } from "node:fs/promises";
1134
+ import { join as join6 } from "node:path";
1135
+ async function fileExists(filePath) {
1136
+ try {
1137
+ await access(filePath);
1138
+ return true;
1139
+ } catch {
1140
+ return false;
1141
+ }
1142
+ }
1143
+ async function detectTechStack(projectPath) {
1144
+ const seen = /* @__PURE__ */ new Map();
1145
+ try {
1146
+ const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
1147
+ const pkg = JSON.parse(raw);
1148
+ const allDeps = {
1149
+ ...pkg.dependencies,
1150
+ ...pkg.devDependencies
1151
+ };
1152
+ for (const depName of Object.keys(allDeps)) {
1153
+ const rule = PACKAGE_MAP[depName];
1154
+ if (rule) {
1155
+ const key = rule.name.toLowerCase();
1156
+ if (!seen.has(key)) {
1157
+ seen.set(key, { name: rule.name, category: rule.category });
1158
+ }
1159
+ }
1160
+ }
1161
+ } catch {
1162
+ }
1163
+ for (const { file, rule } of CONFIG_FILE_MAP) {
1164
+ const key = rule.name.toLowerCase();
1165
+ if (!seen.has(key) && await fileExists(join6(projectPath, file))) {
1166
+ seen.set(key, { name: rule.name, category: rule.category });
1167
+ }
1168
+ }
1169
+ return Array.from(seen.values()).sort((a, b) => {
1170
+ const catCmp = a.category.localeCompare(b.category);
1171
+ if (catCmp !== 0) return catCmp;
1172
+ return a.name.localeCompare(b.name);
1173
+ });
1174
+ }
1175
+ function mergeTechStack(remote, detected) {
1176
+ const seen = /* @__PURE__ */ new Map();
1177
+ for (const entry of remote) {
1178
+ seen.set(entry.name.toLowerCase(), entry);
1179
+ }
1180
+ const added = [];
1181
+ for (const entry of detected) {
1182
+ const key = entry.name.toLowerCase();
1183
+ if (!seen.has(key)) {
1184
+ seen.set(key, entry);
1185
+ added.push(entry);
1186
+ }
1187
+ }
1188
+ const merged = Array.from(seen.values()).sort((a, b) => {
1189
+ const catCmp = a.category.localeCompare(b.category);
1190
+ if (catCmp !== 0) return catCmp;
1191
+ return a.name.localeCompare(b.name);
1192
+ });
1193
+ return { merged, added };
1194
+ }
1195
+ function parseTechStack(raw) {
1196
+ if (!Array.isArray(raw)) return [];
1197
+ return raw.filter(
1198
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
1199
+ );
1200
+ }
1201
+ var PACKAGE_MAP, CONFIG_FILE_MAP;
1202
+ var init_tech_detect = __esm({
1203
+ "src/lib/tech-detect.ts"() {
1204
+ "use strict";
1205
+ PACKAGE_MAP = {
1206
+ // Frameworks
1207
+ next: { name: "Next.js", category: "framework" },
1208
+ nuxt: { name: "Nuxt", category: "framework" },
1209
+ gatsby: { name: "Gatsby", category: "framework" },
1210
+ express: { name: "Express", category: "framework" },
1211
+ fastify: { name: "Fastify", category: "framework" },
1212
+ hono: { name: "Hono", category: "framework" },
1213
+ "@remix-run/node": { name: "Remix", category: "framework" },
1214
+ svelte: { name: "Svelte", category: "framework" },
1215
+ astro: { name: "Astro", category: "framework" },
1216
+ "@angular/core": { name: "Angular", category: "framework" },
1217
+ // Libraries (UI)
1218
+ react: { name: "React", category: "framework" },
1219
+ vue: { name: "Vue", category: "framework" },
1220
+ "solid-js": { name: "Solid", category: "framework" },
1221
+ preact: { name: "Preact", category: "framework" },
1222
+ // Languages (detected via devDeps)
1223
+ typescript: { name: "TypeScript", category: "language" },
1224
+ // Styling
1225
+ tailwindcss: { name: "Tailwind CSS", category: "styling" },
1226
+ sass: { name: "SCSS", category: "styling" },
1227
+ "styled-components": { name: "styled-components", category: "styling" },
1228
+ "@emotion/react": { name: "Emotion", category: "styling" },
1229
+ // Database
1230
+ prisma: { name: "Prisma", category: "database" },
1231
+ "@prisma/client": { name: "Prisma", category: "database" },
1232
+ "drizzle-orm": { name: "Drizzle", category: "database" },
1233
+ "@supabase/supabase-js": { name: "Supabase", category: "database" },
1234
+ mongoose: { name: "MongoDB", category: "database" },
1235
+ typeorm: { name: "TypeORM", category: "database" },
1236
+ knex: { name: "Knex", category: "database" },
1237
+ // Testing
1238
+ jest: { name: "Jest", category: "testing" },
1239
+ vitest: { name: "Vitest", category: "testing" },
1240
+ mocha: { name: "Mocha", category: "testing" },
1241
+ playwright: { name: "Playwright", category: "testing" },
1242
+ "@playwright/test": { name: "Playwright", category: "testing" },
1243
+ cypress: { name: "Cypress", category: "testing" },
1244
+ // Build tools
1245
+ turbo: { name: "Turborepo", category: "build" },
1246
+ vite: { name: "Vite", category: "build" },
1247
+ webpack: { name: "Webpack", category: "build" },
1248
+ esbuild: { name: "esbuild", category: "build" },
1249
+ rollup: { name: "Rollup", category: "build" },
1250
+ // Tools
1251
+ eslint: { name: "ESLint", category: "tool" },
1252
+ prettier: { name: "Prettier", category: "tool" },
1253
+ "@biomejs/biome": { name: "Biome", category: "tool" }
1254
+ };
1255
+ CONFIG_FILE_MAP = [
1256
+ { file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
1257
+ { file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
1258
+ { file: "next.config.mjs", rule: { name: "Next.js", category: "framework" } },
1259
+ { file: "next.config.ts", rule: { name: "Next.js", category: "framework" } },
1260
+ { file: "tailwind.config.js", rule: { name: "Tailwind CSS", category: "styling" } },
1261
+ { file: "tailwind.config.ts", rule: { name: "Tailwind CSS", category: "styling" } },
1262
+ { file: "turbo.json", rule: { name: "Turborepo", category: "build" } },
1263
+ { file: "docker-compose.yml", rule: { name: "Docker", category: "deployment" } },
1264
+ { file: "docker-compose.yaml", rule: { name: "Docker", category: "deployment" } },
1265
+ { file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
1266
+ { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } }
1267
+ ];
1268
+ }
1269
+ });
1270
+
1271
+ // src/cli/sync.ts
1272
+ var sync_exports = {};
1273
+ __export(sync_exports, {
1274
+ runSync: () => runSync
1364
1275
  });
1365
- import { stat as stat2 } from "node:fs/promises";
1276
+ import { readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
1366
1277
  import { join as join7 } from "node:path";
1367
- async function runPush() {
1278
+ async function runSync() {
1368
1279
  const flags = parseFlags(3);
1369
1280
  const dryRun = hasFlag("dry-run", 3);
1370
1281
  const force = hasFlag("force", 3);
1371
- const isGlobal = hasFlag("global", 3);
1372
1282
  validateApiKey();
1373
1283
  const config2 = await resolveConfig(flags);
1374
1284
  const { repoId, projectPath } = config2;
1375
1285
  console.log(`
1376
- CodeByPlan Push`);
1286
+ CodeByPlan Sync`);
1377
1287
  console.log(` Repo: ${repoId}`);
1378
1288
  console.log(` Path: ${projectPath}`);
1379
- if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
1380
- if (force) console.log(` Mode: force (no conflict prompts)`);
1289
+ if (dryRun) console.log(` Mode: dry-run`);
1290
+ if (force) console.log(` Mode: force`);
1381
1291
  console.log();
1292
+ console.log(" Phase 1: Pull (DB \u2192 local)...");
1293
+ const pullResult = await executeSyncToLocal({ repoId, projectPath, dryRun });
1294
+ const pullChanges = pullResult.totals.created + pullResult.totals.updated + pullResult.totals.deleted;
1295
+ if (pullChanges > 0) {
1296
+ for (const [typeKey, result] of Object.entries(pullResult.byType)) {
1297
+ for (const name of result.created) console.log(` + ${typeKey}/${name}`);
1298
+ for (const name of result.updated) console.log(` ~ ${typeKey}/${name}`);
1299
+ for (const name of result.deleted) console.log(` - ${typeKey}/${name}`);
1300
+ }
1301
+ console.log(` ${pullResult.totals.created} created, ${pullResult.totals.updated} updated, ${pullResult.totals.deleted} deleted`);
1302
+ } else {
1303
+ console.log(" All files up to date.");
1304
+ }
1305
+ console.log("\n Phase 2: Push (local \u2192 DB)...");
1306
+ await executePush(repoId, projectPath, dryRun, force);
1307
+ console.log("\n Phase 3: Config sync...");
1308
+ await syncConfig(repoId, projectPath, dryRun);
1309
+ console.log("\n Phase 4: Tech stack...");
1310
+ await syncTechStack(repoId, projectPath, dryRun);
1311
+ console.log("\n Sync complete.\n");
1312
+ }
1313
+ async function executePush(repoId, projectPath, dryRun, force) {
1382
1314
  const claudeDir = join7(projectPath, ".claude");
1315
+ let localFiles;
1383
1316
  try {
1384
- await stat2(claudeDir);
1317
+ localFiles = await scanLocalFiles(claudeDir, projectPath);
1385
1318
  } catch {
1386
- console.log(" No .claude/ directory found. Nothing to push.\n");
1319
+ console.log(" No .claude/ directory found. Skipping push.");
1387
1320
  return;
1388
1321
  }
1389
- console.log(" Scanning local files...");
1390
- const localFiles = await scanLocalFiles(claudeDir, projectPath);
1391
- console.log(` Found ${localFiles.size} local files.`);
1392
- console.log(" Fetching remote state...");
1393
1322
  const [syncRes, repoRes] = await Promise.all([
1394
1323
  apiGet("/sync/files", { repo_id: repoId }),
1395
1324
  apiGet(`/repos/${repoId}`)
1396
1325
  ]);
1397
1326
  const repoData = repoRes.data;
1398
1327
  const remoteFiles = flattenSyncData(syncRes.data);
1399
- console.log(` Found ${remoteFiles.size} remote files.
1400
- `);
1401
1328
  const toUpsert = [];
1402
1329
  const toDelete = [];
1403
1330
  const conflicts = [];
@@ -1415,7 +1342,7 @@ async function runPush() {
1415
1342
  } else {
1416
1343
  conflicts.push({
1417
1344
  key,
1418
- displayPath: formatDisplayPath(local),
1345
+ displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1419
1346
  remoteModified: remote.updated_at
1420
1347
  });
1421
1348
  conflictLocalRecords.set(key, reversed);
@@ -1438,69 +1365,101 @@ async function runPush() {
1438
1365
  }
1439
1366
  }
1440
1367
  if (toUpsert.length > 0) {
1441
- console.log(` Files to push: ${toUpsert.length}`);
1442
1368
  for (const f of toUpsert) {
1443
- console.log(` + ${formatDisplayPath(f)}`);
1369
+ console.log(` + ${f.type}/${f.category ? f.category + "/" : ""}${f.name}`);
1444
1370
  }
1445
1371
  }
1446
1372
  if (toDelete.length > 0) {
1447
- console.log(` Files to delete from remote: ${toDelete.length}`);
1448
1373
  for (const d of toDelete) {
1449
1374
  console.log(` - ${d.type}/${d.category ? d.category + "/" : ""}${d.name}`);
1450
1375
  }
1451
1376
  }
1452
1377
  if (toUpsert.length === 0 && toDelete.length === 0) {
1453
- if (!dryRun) {
1454
- const repoUpdate2 = { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() };
1455
- await pushTechStack(projectPath, repoRes.data.tech_stack, repoUpdate2);
1456
- await apiPut(`/repos/${repoId}`, repoUpdate2);
1457
- }
1458
- console.log(" Everything is in sync. Nothing to push.\n");
1378
+ console.log(" Nothing to push.");
1459
1379
  return;
1460
1380
  }
1461
1381
  if (dryRun) {
1462
- console.log("\n (dry-run \u2014 no changes were made)\n");
1382
+ console.log(" (dry-run \u2014 no changes)");
1463
1383
  return;
1464
1384
  }
1465
1385
  if (!force) {
1466
1386
  const confirmed = await confirmProceed();
1467
1387
  if (!confirmed) {
1468
- console.log(" Cancelled.\n");
1388
+ console.log(" Cancelled.");
1469
1389
  return;
1470
1390
  }
1471
1391
  }
1472
- console.log("\n Pushing changes...");
1473
1392
  const result = await apiPost("/sync/files", {
1474
1393
  repo_id: repoId,
1475
1394
  files: toUpsert.map((f) => ({
1476
1395
  type: f.type,
1477
1396
  name: f.name,
1478
1397
  category: f.category,
1479
- content: f.content,
1480
- ...f.type === "settings" ? { scope: isGlobal ? "global" : "repo" } : {}
1398
+ content: f.content
1481
1399
  })),
1482
1400
  delete_keys: toDelete
1483
1401
  });
1484
- const repoUpdate = { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() };
1485
- await pushTechStack(projectPath, repoRes.data.tech_stack, repoUpdate);
1486
- await apiPut(`/repos/${repoId}`, repoUpdate);
1487
- console.log(` Done: ${result.data.upserted} upserted, ${result.data.deleted} deleted
1488
- `);
1402
+ await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1403
+ console.log(` ${result.data.upserted} upserted, ${result.data.deleted} deleted`);
1404
+ }
1405
+ async function syncConfig(repoId, projectPath, dryRun) {
1406
+ const configPath = join7(projectPath, ".codebyplan.json");
1407
+ let currentConfig = {};
1408
+ try {
1409
+ const raw = await readFile7(configPath, "utf-8");
1410
+ currentConfig = JSON.parse(raw);
1411
+ } catch {
1412
+ currentConfig = { repo_id: repoId };
1413
+ }
1414
+ const repoRes = await apiGet(`/repos/${repoId}`);
1415
+ const repo = repoRes.data;
1416
+ let portAllocations = [];
1417
+ try {
1418
+ const portsRes = await apiGet(`/port-allocations`, { repo_id: repoId });
1419
+ portAllocations = portsRes.data ?? [];
1420
+ } catch {
1421
+ }
1422
+ const newConfig = {
1423
+ repo_id: repoId,
1424
+ ...currentConfig.worktree_id ? { worktree_id: currentConfig.worktree_id } : {},
1425
+ server_port: repo.server_port,
1426
+ server_type: repo.server_type,
1427
+ git_branch: repo.git_branch ?? "development",
1428
+ auto_push_enabled: repo.auto_push_enabled,
1429
+ ...portAllocations.length > 0 ? { port_allocations: portAllocations } : {}
1430
+ };
1431
+ const currentJson = JSON.stringify(currentConfig, null, 2);
1432
+ const newJson = JSON.stringify(newConfig, null, 2);
1433
+ if (currentJson === newJson) {
1434
+ console.log(" Config up to date.");
1435
+ return;
1436
+ }
1437
+ if (dryRun) {
1438
+ console.log(" Config would be updated (dry-run).");
1439
+ return;
1440
+ }
1441
+ await writeFile3(configPath, newJson + "\n", "utf-8");
1442
+ console.log(" Updated .codebyplan.json");
1489
1443
  }
1490
- async function pushTechStack(projectPath, remoteTechStack, repoUpdate) {
1444
+ async function syncTechStack(repoId, projectPath, dryRun) {
1491
1445
  try {
1492
1446
  const detected = await detectTechStack(projectPath);
1493
- if (detected.length === 0) return;
1494
- const remote = parseTechStack(remoteTechStack);
1447
+ if (detected.length === 0) {
1448
+ console.log(" No tech stack detected.");
1449
+ return;
1450
+ }
1451
+ const repoRes = await apiGet(`/repos/${repoId}`);
1452
+ const remote = parseTechStack(repoRes.data.tech_stack);
1495
1453
  const { merged, added } = mergeTechStack(remote, detected);
1496
- console.log(` Tech stack: ${detected.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1454
+ console.log(` ${detected.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1497
1455
  for (const entry of added) {
1498
- console.log(` + ${entry.name} (${entry.category})`);
1456
+ console.log(` + ${entry.name} (${entry.category})`);
1499
1457
  }
1500
- if (added.length > 0) {
1501
- repoUpdate.tech_stack = merged;
1458
+ if (added.length > 0 && !dryRun) {
1459
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
1502
1460
  }
1503
1461
  } catch {
1462
+ console.log(" Tech stack detection skipped.");
1504
1463
  }
1505
1464
  }
1506
1465
  function flattenSyncData(data) {
@@ -1529,206 +1488,20 @@ function flattenSyncData(data) {
1529
1488
  }
1530
1489
  return result;
1531
1490
  }
1532
- function formatDisplayPath(f) {
1533
- if (f.category) return `${f.type}/${f.category}/${f.name}`;
1534
- return `${f.type}/${f.name}`;
1535
- }
1536
- var init_push = __esm({
1537
- "src/cli/push.ts"() {
1491
+ var init_sync = __esm({
1492
+ "src/cli/sync.ts"() {
1538
1493
  "use strict";
1539
1494
  init_config();
1540
1495
  init_fileMapper();
1541
1496
  init_conflict();
1542
1497
  init_confirm();
1543
1498
  init_api();
1499
+ init_sync_engine();
1544
1500
  init_variables();
1545
1501
  init_tech_detect();
1546
1502
  }
1547
1503
  });
1548
1504
 
1549
- // src/cli/init.ts
1550
- var init_exports = {};
1551
- __export(init_exports, {
1552
- runInit: () => runInit
1553
- });
1554
- import { createInterface as createInterface4 } from "node:readline/promises";
1555
- import { stdin as stdin4, stdout as stdout4 } from "node:process";
1556
- import { writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1557
- import { join as join8, dirname as dirname2 } from "node:path";
1558
- async function runInit() {
1559
- const flags = parseFlags(3);
1560
- const projectPath = flags["path"] ?? process.cwd();
1561
- console.log("\n CodeByPlan Init\n");
1562
- validateApiKey();
1563
- const rl = createInterface4({ input: stdin4, output: stdout4 });
1564
- try {
1565
- let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
1566
- if (!repoId) {
1567
- console.log(" Fetching your repositories...\n");
1568
- const res = await apiGet("/repos");
1569
- const repos = res.data;
1570
- if (repos.length === 0) {
1571
- console.log(" No repositories found. Create one at https://codebyplan.com first.\n");
1572
- return;
1573
- }
1574
- for (let i = 0; i < repos.length; i++) {
1575
- console.log(` ${i + 1}. ${repos[i].name}`);
1576
- }
1577
- console.log();
1578
- const choice = (await rl.question(" Select a repository (number): ")).trim();
1579
- const index = parseInt(choice, 10) - 1;
1580
- if (isNaN(index) || index < 0 || index >= repos.length) {
1581
- console.log(" Invalid selection. Aborting.\n");
1582
- return;
1583
- }
1584
- repoId = repos[index].id;
1585
- console.log(`
1586
- Selected: ${repos[index].name}
1587
- `);
1588
- }
1589
- let worktreeId;
1590
- try {
1591
- const worktreesRes = await apiGet(`/worktrees?repo_id=${repoId}`);
1592
- const match = worktreesRes.data.find((wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/"));
1593
- if (match) worktreeId = match.id;
1594
- } catch {
1595
- }
1596
- const configPath = join8(projectPath, ".codebyplan.json");
1597
- const configData = { repo_id: repoId };
1598
- if (worktreeId) configData.worktree_id = worktreeId;
1599
- const configContent = JSON.stringify(configData, null, 2) + "\n";
1600
- await writeFile3(configPath, configContent, "utf-8");
1601
- console.log(` Created ${configPath}`);
1602
- const seedAnswer = (await rl.question("\n Seed with CodeByPlan defaults? (Y/n): ")).trim().toLowerCase();
1603
- if (seedAnswer === "" || seedAnswer === "y" || seedAnswer === "yes") {
1604
- let getFilePath3 = function(typeName, file) {
1605
- const cfg = typeConfig2[typeName];
1606
- const typeDir = typeName === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
1607
- if (cfg.subfolder) {
1608
- return join8(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
1609
- }
1610
- if (typeName === "command" && file.category) {
1611
- return join8(typeDir, file.category, `${file.name}${cfg.ext}`);
1612
- }
1613
- if (typeName === "template") {
1614
- return join8(typeDir, file.name);
1615
- }
1616
- return join8(typeDir, `${file.name}${cfg.ext}`);
1617
- };
1618
- var getFilePath2 = getFilePath3;
1619
- console.log("\n Fetching default files...");
1620
- let defaultsData;
1621
- try {
1622
- const defaultsRes = await apiGet("/sync/defaults");
1623
- defaultsData = defaultsRes.data;
1624
- } catch (err) {
1625
- const msg = err instanceof Error ? err.message : String(err);
1626
- console.log(` Could not fetch defaults: ${msg}`);
1627
- console.log(" You can run 'codebyplan pull' later to sync files.\n");
1628
- printNextSteps(projectPath);
1629
- return;
1630
- }
1631
- const claudeDir = join8(projectPath, ".claude");
1632
- let written = 0;
1633
- const typeConfig2 = {
1634
- command: { dir: "commands", ext: ".md" },
1635
- agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
1636
- skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
1637
- rule: { dir: "rules", ext: ".md" },
1638
- hook: { dir: "hooks", ext: ".sh" },
1639
- template: { dir: "templates", ext: "" }
1640
- };
1641
- const syncKeyToType2 = {
1642
- commands: "command",
1643
- agents: "agent",
1644
- skills: "skill",
1645
- rules: "rule",
1646
- hooks: "hook",
1647
- templates: "template"
1648
- };
1649
- for (const [syncKey, typeName] of Object.entries(syncKeyToType2)) {
1650
- const files = defaultsData[syncKey] ?? [];
1651
- for (const file of files) {
1652
- const filePath = getFilePath3(typeName, file);
1653
- await mkdir2(dirname2(filePath), { recursive: true });
1654
- await writeFile3(filePath, file.content, "utf-8");
1655
- if (typeName === "hook") await chmod2(filePath, 493);
1656
- written++;
1657
- }
1658
- }
1659
- const specialFiles = [
1660
- ...defaultsData.claude_md ?? []
1661
- ];
1662
- for (const file of specialFiles) {
1663
- const targetPath = join8(projectPath, "CLAUDE.md");
1664
- await mkdir2(dirname2(targetPath), { recursive: true });
1665
- await writeFile3(targetPath, file.content, "utf-8");
1666
- written++;
1667
- }
1668
- const settingsFiles = defaultsData.settings ?? [];
1669
- for (const file of settingsFiles) {
1670
- const targetPath = join8(claudeDir, "settings.json");
1671
- await mkdir2(dirname2(targetPath), { recursive: true });
1672
- await writeFile3(targetPath, file.content, "utf-8");
1673
- written++;
1674
- }
1675
- console.log(` Wrote ${written} files to .claude/
1676
- `);
1677
- console.log(" Syncing defaults to your repository...");
1678
- try {
1679
- const allFiles = [];
1680
- for (const [syncKey, typeName] of Object.entries(syncKeyToType2)) {
1681
- const files = defaultsData[syncKey] ?? [];
1682
- for (const file of files) {
1683
- allFiles.push({
1684
- type: typeName,
1685
- name: file.name,
1686
- category: file.category ?? null,
1687
- content: file.content
1688
- });
1689
- }
1690
- }
1691
- for (const file of specialFiles) {
1692
- allFiles.push({ type: "claude_md", name: file.name, content: file.content });
1693
- }
1694
- for (const file of settingsFiles) {
1695
- allFiles.push({ type: "settings", name: file.name, content: file.content, scope: file.scope ?? "repo" });
1696
- }
1697
- if (allFiles.length > 0) {
1698
- await apiPost("/sync/files", {
1699
- repo_id: repoId,
1700
- files: allFiles
1701
- });
1702
- console.log(` Synced ${allFiles.length} files to database.
1703
- `);
1704
- }
1705
- } catch (err) {
1706
- const msg = err instanceof Error ? err.message : String(err);
1707
- console.log(` Warning: Could not sync to database: ${msg}`);
1708
- console.log(" Local files were written successfully. Run 'codebyplan push' to sync later.\n");
1709
- }
1710
- } else {
1711
- console.log();
1712
- }
1713
- printNextSteps(projectPath);
1714
- } finally {
1715
- rl.close();
1716
- }
1717
- }
1718
- function printNextSteps(_projectPath) {
1719
- console.log(" Next steps:");
1720
- console.log(" 1. Start a Claude Code session in this project");
1721
- console.log(" 2. Use 'codebyplan pull' to sync latest files from database");
1722
- console.log(" 3. Use 'codebyplan push' to push local changes back\n");
1723
- }
1724
- var init_init = __esm({
1725
- "src/cli/init.ts"() {
1726
- "use strict";
1727
- init_config();
1728
- init_api();
1729
- }
1730
- });
1731
-
1732
1505
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
1733
1506
  var util, objectUtil, ZodParsedType, getParsedType;
1734
1507
  var init_util = __esm({
@@ -23543,11 +23316,11 @@ async function createPR(options) {
23543
23316
  }
23544
23317
  } catch {
23545
23318
  }
23546
- const { stdout: stdout5 } = await exec(
23319
+ const { stdout: stdout4 } = await exec(
23547
23320
  `gh pr create --head "${head}" --base "${base}" --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`,
23548
23321
  { cwd: repoPath }
23549
23322
  );
23550
- const prUrl = stdout5.trim();
23323
+ const prUrl = stdout4.trim();
23551
23324
  const prNumber = parseInt(prUrl.split("/").pop() ?? "0", 10);
23552
23325
  return { pr_url: prUrl, pr_number: prNumber || null };
23553
23326
  } catch (err) {
@@ -23558,11 +23331,11 @@ async function createPR(options) {
23558
23331
  async function mergePR(options) {
23559
23332
  const { repoPath, prNumber, mergeMethod } = options;
23560
23333
  try {
23561
- const { stdout: stdout5 } = await exec(
23334
+ const { stdout: stdout4 } = await exec(
23562
23335
  `gh pr merge ${prNumber} --${mergeMethod} --delete-branch`,
23563
23336
  { cwd: repoPath }
23564
23337
  );
23565
- return { merged: true, message: stdout5.trim() || `PR #${prNumber} merged via ${mergeMethod}` };
23338
+ return { merged: true, message: stdout4.trim() || `PR #${prNumber} merged via ${mergeMethod}` };
23566
23339
  } catch (err) {
23567
23340
  const errorMessage = err instanceof Error ? err.message : String(err);
23568
23341
  return { merged: false, message: "Merge failed", error: errorMessage };
@@ -23570,11 +23343,11 @@ async function mergePR(options) {
23570
23343
  }
23571
23344
  async function getPRStatus(repoPath, prNumber) {
23572
23345
  try {
23573
- const { stdout: stdout5 } = await exec(
23346
+ const { stdout: stdout4 } = await exec(
23574
23347
  `gh pr view ${prNumber} --json state,mergeable,title,url,number`,
23575
23348
  { cwd: repoPath }
23576
23349
  );
23577
- const pr = JSON.parse(stdout5.trim());
23350
+ const pr = JSON.parse(stdout4.trim());
23578
23351
  return {
23579
23352
  state: pr.state,
23580
23353
  mergeable: pr.mergeable,
@@ -24419,7 +24192,7 @@ function registerFileGenTools(server) {
24419
24192
  }
24420
24193
  const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
24421
24194
  await exec2(addCmd, { cwd: repo.path });
24422
- const { stdout: stdout5, stderr } = await exec2(
24195
+ const { stdout: stdout4, stderr } = await exec2(
24423
24196
  `git commit -m "${message.replace(/"/g, '\\"')}"`,
24424
24197
  { cwd: repo.path }
24425
24198
  );
@@ -24430,7 +24203,7 @@ function registerFileGenTools(server) {
24430
24203
  text: JSON.stringify({
24431
24204
  status: "committed",
24432
24205
  branch: branch.trim(),
24433
- output: stdout5.trim(),
24206
+ output: stdout4.trim(),
24434
24207
  warnings: stderr.trim() || void 0
24435
24208
  }, null, 2)
24436
24209
  }]
@@ -24555,19 +24328,30 @@ if (arg === "setup") {
24555
24328
  await runSetup2();
24556
24329
  process.exit(0);
24557
24330
  }
24558
- if (arg === "pull") {
24559
- const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
24560
- await runPull2();
24561
- process.exit(0);
24562
- }
24563
- if (arg === "push") {
24564
- const { runPush: runPush2 } = await Promise.resolve().then(() => (init_push(), push_exports));
24565
- await runPush2();
24331
+ if (arg === "sync") {
24332
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
24333
+ await runSync2();
24566
24334
  process.exit(0);
24567
24335
  }
24568
- if (arg === "init") {
24569
- const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
24570
- await runInit2();
24336
+ if (arg === "help" || arg === "--help" || arg === "-h") {
24337
+ console.log(`
24338
+ CodeByPlan CLI v${VERSION}
24339
+
24340
+ Usage:
24341
+ codebyplan Start MCP server (for Claude Code)
24342
+ codebyplan setup Interactive setup (API key + project init + first sync)
24343
+ codebyplan sync Bidirectional sync (pull + push + config)
24344
+ codebyplan help Show this help message
24345
+ codebyplan --version Print version
24346
+
24347
+ Sync options:
24348
+ --path <dir> Project root directory (default: cwd)
24349
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
24350
+ --dry-run Preview changes without writing
24351
+ --force Skip confirmation and conflict prompts
24352
+
24353
+ Learn more: https://codebyplan.com
24354
+ `);
24571
24355
  process.exit(0);
24572
24356
  }
24573
24357
  if (arg === void 0) {
@@ -24578,18 +24362,6 @@ if (arg === void 0) {
24578
24362
  });
24579
24363
  } else {
24580
24364
  console.error(`Unknown command: ${arg}`);
24581
- console.error("\nUsage:");
24582
- console.error(" codebyplan Start MCP server");
24583
- console.error(" codebyplan setup Interactive setup");
24584
- console.error(" codebyplan init Initialize project with defaults");
24585
- console.error(" codebyplan pull Pull .claude/ files from database");
24586
- console.error(" codebyplan push Push .claude/ files to database");
24587
- console.error(" codebyplan --version Print version");
24588
- console.error("\nOptions (pull/push):");
24589
- console.error(" --path <dir> Project root directory (default: cwd)");
24590
- console.error(" --repo-id <uuid> Repository ID (or set CODEBYPLAN_REPO_ID or .codebyplan.json)");
24591
- console.error(" --dry-run Preview changes without writing");
24592
- console.error(" --force Skip confirmation and conflict prompts");
24593
- console.error(" --status Show cross-repo sync status (pull only)");
24365
+ console.error("Run 'codebyplan help' for usage.");
24594
24366
  process.exit(1);
24595
24367
  }