codebyplan 0.0.1 → 1.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 (4) hide show
  1. package/README.md +51 -11
  2. package/dist/cli.js +2649 -0
  3. package/package.json +29 -7
  4. package/index.js +0 -17
package/dist/cli.js ADDED
@@ -0,0 +1,2649 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/lib/version.ts
13
+ var VERSION, PACKAGE_NAME;
14
+ var init_version = __esm({
15
+ "src/lib/version.ts"() {
16
+ "use strict";
17
+ VERSION = "1.0.0";
18
+ PACKAGE_NAME = "codebyplan";
19
+ }
20
+ });
21
+
22
+ // src/lib/api.ts
23
+ function validateApiKey() {
24
+ if (!API_KEY) {
25
+ throw new Error(
26
+ `Missing CODEBYPLAN_API_KEY environment variable.
27
+
28
+ Quick setup:
29
+ npx ${PACKAGE_NAME} setup
30
+
31
+ Or manually:
32
+ 1. Get your API key at https://codebyplan.com/settings/api-keys/
33
+ 2. claude mcp add codebyplan -e CODEBYPLAN_API_KEY=<key> -- npx -y ${PACKAGE_NAME}`
34
+ );
35
+ }
36
+ }
37
+ function buildUrl(path, params) {
38
+ const url = new URL(`${BASE_URL}/api${path}`);
39
+ if (params) {
40
+ for (const [key, value] of Object.entries(params)) {
41
+ if (value !== void 0) {
42
+ url.searchParams.set(key, value);
43
+ }
44
+ }
45
+ }
46
+ return url.toString();
47
+ }
48
+ function isRetryable(err) {
49
+ if (err instanceof TypeError) return true;
50
+ if (err instanceof Error && err.name === "AbortError") return false;
51
+ if (err instanceof ApiError) return err.status >= 500;
52
+ return false;
53
+ }
54
+ function delay(ms) {
55
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
56
+ }
57
+ async function request(method, path, options) {
58
+ const url = buildUrl(path, options?.params);
59
+ let lastError;
60
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
61
+ if (attempt > 0) {
62
+ const jitter = Math.random() * 0.5 + 0.75;
63
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt - 1) * jitter;
64
+ await delay(backoff);
65
+ }
66
+ try {
67
+ const res = await fetch(url, {
68
+ method,
69
+ headers: {
70
+ "x-api-key": API_KEY,
71
+ ...options?.body !== void 0 ? { "Content-Type": "application/json" } : {}
72
+ },
73
+ body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
74
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
75
+ });
76
+ if (!res.ok) {
77
+ let message = `API ${method} ${path} failed with status ${res.status}`;
78
+ let code;
79
+ try {
80
+ const body = await res.json();
81
+ if (body.error && typeof body.error === "object") {
82
+ const err = body.error;
83
+ if (err.message) message = err.message;
84
+ if (err.code) code = err.code;
85
+ } else if (typeof body.error === "string") {
86
+ message = body.error;
87
+ }
88
+ if (body.code && typeof body.code === "string") code = body.code;
89
+ } catch {
90
+ }
91
+ const apiError = new ApiError(message, res.status, code);
92
+ if (isRetryable(apiError) && attempt < MAX_RETRIES) {
93
+ lastError = apiError;
94
+ continue;
95
+ }
96
+ throw apiError;
97
+ }
98
+ if (res.status === 204) {
99
+ return void 0;
100
+ }
101
+ return res.json();
102
+ } catch (err) {
103
+ lastError = err;
104
+ if (!isRetryable(err) || attempt >= MAX_RETRIES) {
105
+ throw err;
106
+ }
107
+ }
108
+ }
109
+ throw lastError;
110
+ }
111
+ async function apiGet(path, params) {
112
+ return request("GET", path, { params });
113
+ }
114
+ async function apiPost(path, body) {
115
+ return request("POST", path, { body });
116
+ }
117
+ async function apiPut(path, body) {
118
+ return request("PUT", path, { body });
119
+ }
120
+ async function apiDelete(path, params) {
121
+ await request("DELETE", path, { params });
122
+ }
123
+ var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
124
+ var init_api = __esm({
125
+ "src/lib/api.ts"() {
126
+ "use strict";
127
+ init_version();
128
+ API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
129
+ BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(/\/$/, "");
130
+ REQUEST_TIMEOUT_MS = 3e4;
131
+ MAX_RETRIES = 3;
132
+ BASE_DELAY_MS = 1e3;
133
+ ApiError = class extends Error {
134
+ status;
135
+ code;
136
+ constructor(message, status, code) {
137
+ super(message);
138
+ this.name = "ApiError";
139
+ this.status = status;
140
+ this.code = code;
141
+ }
142
+ };
143
+ }
144
+ });
145
+
146
+ // src/lib/settings-merge.ts
147
+ function mergeSettings(template, local) {
148
+ const merged = { ...local };
149
+ for (const key of TEMPLATE_MANAGED_KEYS) {
150
+ if (key in template) {
151
+ merged[key] = template[key];
152
+ }
153
+ }
154
+ if (template.permissions && typeof template.permissions === "object") {
155
+ const templatePerms = template.permissions;
156
+ const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
157
+ const mergedPerms = { ...localPerms };
158
+ for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
159
+ if (key in templatePerms) {
160
+ mergedPerms[key] = templatePerms[key];
161
+ }
162
+ }
163
+ merged.permissions = mergedPerms;
164
+ }
165
+ return merged;
166
+ }
167
+ function mergeGlobalAndRepoSettings(global, repo) {
168
+ const merged = { ...global, ...repo };
169
+ const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
170
+ const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
171
+ if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
172
+ const mergedPerms = { ...globalPerms, ...repoPerms };
173
+ for (const key of ARRAY_PERMISSION_KEYS) {
174
+ const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
175
+ const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
176
+ if (globalArr.length > 0 || repoArr.length > 0) {
177
+ mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
178
+ }
179
+ }
180
+ merged.permissions = mergedPerms;
181
+ }
182
+ return merged;
183
+ }
184
+ function stripPermissionsAllow(settings) {
185
+ if (!settings.permissions || typeof settings.permissions !== "object") {
186
+ return settings;
187
+ }
188
+ const perms = { ...settings.permissions };
189
+ delete perms.allow;
190
+ if (Object.keys(perms).length === 0) {
191
+ const { permissions: _, ...rest } = settings;
192
+ return rest;
193
+ }
194
+ return { ...settings, permissions: perms };
195
+ }
196
+ var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
197
+ var init_settings_merge = __esm({
198
+ "src/lib/settings-merge.ts"() {
199
+ "use strict";
200
+ TEMPLATE_MANAGED_KEYS = [
201
+ "attribution",
202
+ "hooks",
203
+ "statusLine"
204
+ ];
205
+ TEMPLATE_MANAGED_PERMISSION_KEYS = [
206
+ "deny",
207
+ "ask",
208
+ "additionalDirectories"
209
+ ];
210
+ ARRAY_PERMISSION_KEYS = ["deny", "ask"];
211
+ }
212
+ });
213
+
214
+ // src/lib/hook-registry.ts
215
+ import { readdir, readFile } from "node:fs/promises";
216
+ import { join } from "node:path";
217
+ function parseHookMeta(content) {
218
+ const match = content.match(/^#\s*@hook:\s*(\S+)(?:\s+(.+))?$/m);
219
+ if (!match) return null;
220
+ return {
221
+ event: match[1],
222
+ matcher: match[2]?.trim() ?? ""
223
+ };
224
+ }
225
+ async function discoverHooks(hooksDir) {
226
+ const discovered = /* @__PURE__ */ new Map();
227
+ let filenames;
228
+ try {
229
+ const entries = await readdir(hooksDir);
230
+ filenames = entries.filter((e) => e.endsWith(".sh"));
231
+ } catch {
232
+ return discovered;
233
+ }
234
+ for (const filename of filenames) {
235
+ const content = await readFile(join(hooksDir, filename), "utf-8");
236
+ const meta = parseHookMeta(content);
237
+ if (meta) {
238
+ discovered.set(filename.replace(/\.sh$/, ""), meta);
239
+ }
240
+ }
241
+ return discovered;
242
+ }
243
+ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
244
+ if (discovered.size === 0) return existing;
245
+ const merged = {};
246
+ for (const [event, matchers] of Object.entries(existing)) {
247
+ merged[event] = matchers.map((m) => ({
248
+ matcher: m.matcher,
249
+ hooks: [...m.hooks]
250
+ }));
251
+ }
252
+ for (const [filename, meta] of discovered) {
253
+ const command = `bash ${hooksRelPath}/${filename}.sh`;
254
+ if (!merged[meta.event]) {
255
+ merged[meta.event] = [];
256
+ }
257
+ const eventEntries = merged[meta.event];
258
+ const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
259
+ if (matcherEntry) {
260
+ const exists = matcherEntry.hooks.some((h) => h.command === command);
261
+ if (!exists) {
262
+ matcherEntry.hooks.push({ type: "command", command });
263
+ }
264
+ } else {
265
+ eventEntries.push({
266
+ matcher: meta.matcher,
267
+ hooks: [{ type: "command", command }]
268
+ });
269
+ }
270
+ }
271
+ return merged;
272
+ }
273
+ function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
274
+ const prefix = `bash ${hooksRelPath}/`;
275
+ const stripped = {};
276
+ for (const [event, matchers] of Object.entries(config)) {
277
+ const filteredMatchers = [];
278
+ for (const matcher of matchers) {
279
+ const filteredHooks = matcher.hooks.filter(
280
+ (h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
281
+ );
282
+ if (filteredHooks.length > 0) {
283
+ filteredMatchers.push({ matcher: matcher.matcher, hooks: filteredHooks });
284
+ }
285
+ }
286
+ if (filteredMatchers.length > 0) {
287
+ stripped[event] = filteredMatchers;
288
+ }
289
+ }
290
+ return stripped;
291
+ }
292
+ var init_hook_registry = __esm({
293
+ "src/lib/hook-registry.ts"() {
294
+ "use strict";
295
+ }
296
+ });
297
+
298
+ // src/lib/variables.ts
299
+ function substituteVariables(content, repoData) {
300
+ if (!content.includes("{{")) return content;
301
+ let result = content;
302
+ for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
303
+ const placeholder = `{{${name}}}`;
304
+ if (result.includes(placeholder)) {
305
+ result = result.replaceAll(placeholder, resolver(repoData));
306
+ }
307
+ }
308
+ return result;
309
+ }
310
+ function reverseSubstituteVariables(content, repoData) {
311
+ const entries = [];
312
+ for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
313
+ const value = resolver(repoData);
314
+ if (value.length < 3) continue;
315
+ entries.push([value, `{{${name}}}`]);
316
+ }
317
+ entries.sort((a, b) => b[0].length - a[0].length);
318
+ let result = content;
319
+ for (const [value, placeholder] of entries) {
320
+ result = result.replaceAll(value, placeholder);
321
+ }
322
+ return result;
323
+ }
324
+ var TEMPLATE_VARIABLES;
325
+ var init_variables = __esm({
326
+ "src/lib/variables.ts"() {
327
+ "use strict";
328
+ TEMPLATE_VARIABLES = {
329
+ REPO_ID: (repo) => repo.id,
330
+ REPO_NAME: (repo) => repo.name,
331
+ REPO_PATH: (repo) => repo.path ?? "",
332
+ GIT_BRANCH: (repo) => repo.git_branch ?? "development",
333
+ SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
334
+ SERVER_TYPE: (repo) => repo.server_type ?? "none"
335
+ };
336
+ }
337
+ });
338
+
339
+ // src/lib/sync-engine.ts
340
+ var sync_engine_exports = {};
341
+ __export(sync_engine_exports, {
342
+ executeSyncToLocal: () => executeSyncToLocal
343
+ });
344
+ import { readdir as readdir2, readFile as readFile2, writeFile, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
345
+ import { join as join2, dirname } from "node:path";
346
+ function getTypeDir(claudeDir, dir) {
347
+ if (dir === "commands") return join2(claudeDir, dir, "cbp");
348
+ return join2(claudeDir, dir);
349
+ }
350
+ function getFilePath(claudeDir, typeName, file) {
351
+ const cfg = typeConfig[typeName];
352
+ const typeDir = getTypeDir(claudeDir, cfg.dir);
353
+ if (cfg.subfolder) {
354
+ return join2(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
355
+ }
356
+ if (typeName === "command" && file.category) {
357
+ return join2(typeDir, file.category, `${file.name}${cfg.ext}`);
358
+ }
359
+ if (typeName === "template") {
360
+ return join2(typeDir, file.name);
361
+ }
362
+ return join2(typeDir, `${file.name}${cfg.ext}`);
363
+ }
364
+ async function readDirRecursive(dir, base = dir) {
365
+ const result = /* @__PURE__ */ new Map();
366
+ try {
367
+ const entries = await readdir2(dir, { withFileTypes: true });
368
+ for (const entry of entries) {
369
+ const fullPath = join2(dir, entry.name);
370
+ if (entry.isDirectory()) {
371
+ const sub = await readDirRecursive(fullPath, base);
372
+ for (const [k, v] of sub) result.set(k, v);
373
+ } else {
374
+ const relPath = fullPath.slice(base.length + 1);
375
+ const fileContent = await readFile2(fullPath, "utf-8");
376
+ result.set(relPath, fileContent);
377
+ }
378
+ }
379
+ } catch {
380
+ }
381
+ return result;
382
+ }
383
+ async function isGitWorktree(projectPath) {
384
+ try {
385
+ const gitPath = join2(projectPath, ".git");
386
+ const info = await stat(gitPath);
387
+ return info.isFile();
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+ async function removeEmptyParents(filePath, stopAt) {
393
+ let dir = dirname(filePath);
394
+ while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
395
+ try {
396
+ await rmdir(dir);
397
+ dir = dirname(dir);
398
+ } catch {
399
+ break;
400
+ }
401
+ }
402
+ }
403
+ async function executeSyncToLocal(options) {
404
+ const { repoId, projectPath, dryRun = false } = options;
405
+ const [syncRes, repoRes] = await Promise.all([
406
+ apiGet("/sync/defaults"),
407
+ apiGet(`/repos/${repoId}`)
408
+ ]);
409
+ const syncData = syncRes.data;
410
+ const repoData = repoRes.data;
411
+ syncData.claude_md = [];
412
+ const claudeDir = join2(projectPath, ".claude");
413
+ const worktree = await isGitWorktree(projectPath);
414
+ const byType = {};
415
+ const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
416
+ const dbOnlyFiles = [];
417
+ for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
418
+ if (worktree && typeName === "command") {
419
+ byType["commands"] = { created: [], updated: [], deleted: [], unchanged: [] };
420
+ continue;
421
+ }
422
+ const cfg = typeConfig[typeName];
423
+ const targetDir = getTypeDir(claudeDir, cfg.dir);
424
+ const remoteFiles = syncData[syncKey] ?? [];
425
+ const result = { created: [], updated: [], deleted: [], unchanged: [] };
426
+ if (!dryRun) {
427
+ await mkdir(targetDir, { recursive: true });
428
+ }
429
+ const localFiles = await readDirRecursive(targetDir);
430
+ const remotePathMap = /* @__PURE__ */ new Map();
431
+ for (const remote of remoteFiles) {
432
+ const fullPath = getFilePath(claudeDir, typeName, remote);
433
+ const relPath = fullPath.slice(targetDir.length + 1);
434
+ const substituted = substituteVariables(remote.content, repoData);
435
+ remotePathMap.set(relPath, { content: substituted, name: remote.name });
436
+ }
437
+ for (const [relPath, { content, name }] of remotePathMap) {
438
+ const fullPath = join2(targetDir, relPath);
439
+ const localContent = localFiles.get(relPath);
440
+ if (localContent === void 0) {
441
+ const remoteFile = remoteFiles.find((f) => f.name === name);
442
+ dbOnlyFiles.push({
443
+ type: typeName,
444
+ name,
445
+ category: remoteFile?.category ?? null,
446
+ localPath: fullPath
447
+ });
448
+ if (!dryRun) {
449
+ await mkdir(dirname(fullPath), { recursive: true });
450
+ await writeFile(fullPath, content, "utf-8");
451
+ if (typeName === "hook") await chmod(fullPath, 493);
452
+ }
453
+ result.created.push(name);
454
+ totals.created++;
455
+ } else if (localContent !== content) {
456
+ if (!dryRun) {
457
+ await writeFile(fullPath, content, "utf-8");
458
+ if (typeName === "hook") await chmod(fullPath, 493);
459
+ }
460
+ result.updated.push(name);
461
+ totals.updated++;
462
+ } else {
463
+ result.unchanged.push(name);
464
+ totals.unchanged++;
465
+ }
466
+ }
467
+ for (const [relPath] of localFiles) {
468
+ if (!remotePathMap.has(relPath)) {
469
+ const fullPath = join2(targetDir, relPath);
470
+ if (!dryRun) {
471
+ await unlink(fullPath);
472
+ await removeEmptyParents(fullPath, targetDir);
473
+ }
474
+ const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
475
+ result.deleted.push(pathName);
476
+ totals.deleted++;
477
+ }
478
+ }
479
+ byType[`${typeName}s`] = result;
480
+ }
481
+ {
482
+ const typeName = "docs_stack";
483
+ const syncKey = "docs_stack";
484
+ const targetDir = join2(projectPath, "docs", "stack");
485
+ const remoteFiles = syncData[syncKey] ?? [];
486
+ const result = { created: [], updated: [], deleted: [], unchanged: [] };
487
+ if (remoteFiles.length > 0 && !dryRun) {
488
+ await mkdir(targetDir, { recursive: true });
489
+ }
490
+ const localFiles = await readDirRecursive(targetDir);
491
+ const remotePathMap = /* @__PURE__ */ new Map();
492
+ for (const remote of remoteFiles) {
493
+ const relPath = remote.category ? join2(remote.category, remote.name) : remote.name;
494
+ const substituted = substituteVariables(remote.content, repoData);
495
+ remotePathMap.set(relPath, { content: substituted, name: `${remote.category ?? ""}/${remote.name}` });
496
+ }
497
+ for (const [relPath, { content, name }] of remotePathMap) {
498
+ const fullPath = join2(targetDir, relPath);
499
+ const localContent = localFiles.get(relPath);
500
+ if (localContent === void 0) {
501
+ if (!dryRun) {
502
+ await mkdir(dirname(fullPath), { recursive: true });
503
+ await writeFile(fullPath, content, "utf-8");
504
+ }
505
+ result.created.push(name);
506
+ totals.created++;
507
+ } else if (localContent !== content) {
508
+ if (!dryRun) {
509
+ await writeFile(fullPath, content, "utf-8");
510
+ }
511
+ result.updated.push(name);
512
+ totals.updated++;
513
+ } else {
514
+ result.unchanged.push(name);
515
+ totals.unchanged++;
516
+ }
517
+ }
518
+ for (const [relPath] of localFiles) {
519
+ if (!remotePathMap.has(relPath)) {
520
+ const fullPath = join2(targetDir, relPath);
521
+ if (!dryRun) {
522
+ await unlink(fullPath);
523
+ await removeEmptyParents(fullPath, targetDir);
524
+ }
525
+ result.deleted.push(relPath);
526
+ totals.deleted++;
527
+ }
528
+ }
529
+ byType[typeName] = result;
530
+ }
531
+ const globalSettingsFiles = syncData.global_settings ?? [];
532
+ let globalSettings = {};
533
+ for (const gf of globalSettingsFiles) {
534
+ const parsed = JSON.parse(substituteVariables(gf.content, repoData));
535
+ globalSettings = { ...globalSettings, ...parsed };
536
+ }
537
+ const specialTypes = {
538
+ claude_md: () => join2(projectPath, "CLAUDE.md"),
539
+ settings: () => join2(projectPath, ".claude", "settings.json")
540
+ };
541
+ for (const [typeName, getPath] of Object.entries(specialTypes)) {
542
+ const remoteFiles = syncData[typeName] ?? [];
543
+ const result = { created: [], updated: [], deleted: [], unchanged: [] };
544
+ for (const remote of remoteFiles) {
545
+ const targetPath = getPath(remote.name);
546
+ const remoteContent = substituteVariables(remote.content, repoData);
547
+ let localContent;
548
+ try {
549
+ localContent = await readFile2(targetPath, "utf-8");
550
+ } catch {
551
+ }
552
+ if (typeName === "settings") {
553
+ const repoSettings = JSON.parse(remoteContent);
554
+ const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
555
+ const hooksDir = join2(projectPath, ".claude", "hooks");
556
+ const discovered = await discoverHooks(hooksDir);
557
+ if (localContent === void 0) {
558
+ let finalSettings = stripPermissionsAllow(combinedTemplate);
559
+ if (discovered.size > 0) {
560
+ finalSettings.hooks = mergeDiscoveredHooks(
561
+ finalSettings.hooks ?? {},
562
+ discovered
563
+ );
564
+ }
565
+ if (!dryRun) {
566
+ await mkdir(dirname(targetPath), { recursive: true });
567
+ await writeFile(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
568
+ }
569
+ result.created.push(remote.name);
570
+ totals.created++;
571
+ } else {
572
+ const localSettings = JSON.parse(localContent);
573
+ let merged = mergeSettings(combinedTemplate, localSettings);
574
+ merged = stripPermissionsAllow(merged);
575
+ if (discovered.size > 0) {
576
+ merged.hooks = mergeDiscoveredHooks(
577
+ merged.hooks ?? {},
578
+ discovered
579
+ );
580
+ }
581
+ const mergedContent = JSON.stringify(merged, null, 2) + "\n";
582
+ if (localContent !== mergedContent) {
583
+ if (!dryRun) {
584
+ await writeFile(targetPath, mergedContent, "utf-8");
585
+ }
586
+ result.updated.push(remote.name);
587
+ totals.updated++;
588
+ } else {
589
+ result.unchanged.push(remote.name);
590
+ totals.unchanged++;
591
+ }
592
+ }
593
+ } else {
594
+ if (localContent === void 0) {
595
+ if (!dryRun) {
596
+ await mkdir(dirname(targetPath), { recursive: true });
597
+ await writeFile(targetPath, remoteContent, "utf-8");
598
+ }
599
+ result.created.push(remote.name);
600
+ totals.created++;
601
+ } else if (localContent !== remoteContent) {
602
+ if (!dryRun) {
603
+ await writeFile(targetPath, remoteContent, "utf-8");
604
+ }
605
+ result.updated.push(remote.name);
606
+ totals.updated++;
607
+ } else {
608
+ result.unchanged.push(remote.name);
609
+ totals.unchanged++;
610
+ }
611
+ }
612
+ }
613
+ byType[typeName] = result;
614
+ }
615
+ if (!dryRun) {
616
+ await apiPost("/sync/state", {
617
+ repo_id: repoId,
618
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
619
+ was_skipped: false,
620
+ files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
621
+ files_pushed: 0,
622
+ files_pulled: totals.created + totals.updated,
623
+ files_deleted: totals.deleted,
624
+ files_skipped: 0
625
+ });
626
+ const fileRepoUpdates = [];
627
+ const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
628
+ for (const [syncKey] of Object.entries(syncKeyToType)) {
629
+ const remoteFiles = syncData[syncKey] ?? [];
630
+ for (const file of remoteFiles) {
631
+ if (file.id) {
632
+ fileRepoUpdates.push({
633
+ claude_file_id: file.id,
634
+ last_synced_at: syncTimestamp,
635
+ sync_status: "synced"
636
+ });
637
+ }
638
+ }
639
+ }
640
+ for (const typeName of ["claude_md", "settings"]) {
641
+ const remoteFiles = syncData[typeName] ?? [];
642
+ for (const file of remoteFiles) {
643
+ if (file.id) {
644
+ fileRepoUpdates.push({
645
+ claude_file_id: file.id,
646
+ last_synced_at: syncTimestamp,
647
+ sync_status: "synced"
648
+ });
649
+ }
650
+ }
651
+ }
652
+ if (fileRepoUpdates.length > 0) {
653
+ try {
654
+ await apiPost("/sync/file-repos", {
655
+ repo_id: repoId,
656
+ file_repos: fileRepoUpdates
657
+ });
658
+ } catch {
659
+ }
660
+ }
661
+ }
662
+ return { byType, totals, dbOnlyFiles };
663
+ }
664
+ var typeConfig, syncKeyToType;
665
+ var init_sync_engine = __esm({
666
+ "src/lib/sync-engine.ts"() {
667
+ "use strict";
668
+ init_api();
669
+ init_settings_merge();
670
+ init_hook_registry();
671
+ init_variables();
672
+ typeConfig = {
673
+ command: { dir: "commands", ext: ".md" },
674
+ agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
675
+ skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
676
+ rule: { dir: "rules", ext: ".md" },
677
+ hook: { dir: "hooks", ext: ".sh" },
678
+ template: { dir: "templates", ext: "" },
679
+ context: { dir: "context", ext: ".md" }
680
+ };
681
+ syncKeyToType = {
682
+ commands: "command",
683
+ agents: "agent",
684
+ skills: "skill",
685
+ rules: "rule",
686
+ hooks: "hook",
687
+ templates: "template",
688
+ contexts: "context"
689
+ };
690
+ }
691
+ });
692
+
693
+ // src/cli/setup.ts
694
+ var setup_exports = {};
695
+ __export(setup_exports, {
696
+ runSetup: () => runSetup
697
+ });
698
+ import { createInterface } from "node:readline/promises";
699
+ import { stdin, stdout } from "node:process";
700
+ import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
701
+ import { homedir } from "node:os";
702
+ import { join as join3 } from "node:path";
703
+ function getConfigPath(scope) {
704
+ return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
705
+ }
706
+ async function readConfig(path) {
707
+ try {
708
+ const raw = await readFile3(path, "utf-8");
709
+ const parsed = JSON.parse(raw);
710
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
711
+ return parsed;
712
+ }
713
+ return {};
714
+ } catch {
715
+ return {};
716
+ }
717
+ }
718
+ function buildMcpEntry(apiKey) {
719
+ const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
720
+ return {
721
+ url: `${baseUrl}/mcp`,
722
+ headers: { "x-api-key": apiKey }
723
+ };
724
+ }
725
+ async function writeMcpConfig(scope, apiKey) {
726
+ const configPath = getConfigPath(scope);
727
+ const config = await readConfig(configPath);
728
+ if (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers)) {
729
+ config.mcpServers = {};
730
+ }
731
+ config.mcpServers.codebyplan = buildMcpEntry(apiKey);
732
+ await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
733
+ return configPath;
734
+ }
735
+ async function verifyMcpConfig(scope, apiKey) {
736
+ try {
737
+ const config = await readConfig(getConfigPath(scope));
738
+ const servers = config.mcpServers;
739
+ if (!servers) return false;
740
+ const entry = servers.codebyplan;
741
+ return entry?.url !== void 0 && entry?.headers?.["x-api-key"] === apiKey;
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ async function runSetup() {
747
+ const rl = createInterface({ input: stdin, output: stdout });
748
+ console.log("\n CodeByPlan Setup\n");
749
+ console.log(" This will configure Claude Code to use CodeByPlan.\n");
750
+ console.log(" 1. Sign up at https://codebyplan.com");
751
+ console.log(" 2. Create an API key at https://codebyplan.com/settings/api-keys/\n");
752
+ try {
753
+ const apiKey = (await rl.question(" Enter your API key: ")).trim();
754
+ if (!apiKey) {
755
+ console.log("\n No API key provided. Aborting setup.\n");
756
+ return;
757
+ }
758
+ console.log("\n Validating API key...");
759
+ const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
760
+ const res = await fetch(`${baseUrl}/api/repos`, {
761
+ headers: { "x-api-key": apiKey },
762
+ signal: AbortSignal.timeout(1e4)
763
+ });
764
+ if (res.status === 401) {
765
+ console.log(" Invalid API key. Please check and try again.\n");
766
+ return;
767
+ }
768
+ let repos = [];
769
+ if (res.ok) {
770
+ try {
771
+ const body = await res.json();
772
+ repos = body.data ?? [];
773
+ if (repos.length === 0) {
774
+ console.log(" API key is valid but no repositories found.");
775
+ console.log(" Create one at https://codebyplan.com after setup.\n");
776
+ } else {
777
+ console.log(" API key is valid!\n");
778
+ }
779
+ } catch {
780
+ console.log(" API key is valid!\n");
781
+ }
782
+ } else {
783
+ console.log(` Warning: API returned status ${res.status}, but continuing.
784
+ `);
785
+ }
786
+ console.log(" Where should the MCP server be configured?\n");
787
+ console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
788
+ console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
789
+ const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
790
+ const scope = scopeInput === "2" ? "project" : "user";
791
+ console.log("\n Configuring MCP server...");
792
+ const configPath = await writeMcpConfig(scope, apiKey);
793
+ const verified = await verifyMcpConfig(scope, apiKey);
794
+ if (verified) {
795
+ console.log(` Done! Config written to ${configPath}
796
+ `);
797
+ if (scope === "project") {
798
+ console.log(" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n");
799
+ }
800
+ } else {
801
+ console.log(" Warning: Could not verify the saved configuration.\n");
802
+ console.log(` Manually add to ~/.claude.json under mcpServers.codebyplan:
803
+ `);
804
+ console.log(` { "url": "https://codebyplan.com/mcp", "headers": { "x-api-key": "${apiKey}" } }
805
+ `);
806
+ }
807
+ if (repos.length > 0) {
808
+ console.log(" Initialize this project?\n");
809
+ const initAnswer = (await rl.question(" Link to a repository? (Y/n): ")).trim().toLowerCase();
810
+ if (initAnswer === "" || initAnswer === "y" || initAnswer === "yes") {
811
+ process.env.CODEBYPLAN_API_KEY = apiKey;
812
+ console.log("\n Your repositories:\n");
813
+ for (let i = 0; i < repos.length; i++) {
814
+ console.log(` ${i + 1}. ${repos[i].name}`);
815
+ }
816
+ console.log();
817
+ const choice = (await rl.question(" Select a repository (number): ")).trim();
818
+ const index = parseInt(choice, 10) - 1;
819
+ if (isNaN(index) || index < 0 || index >= repos.length) {
820
+ console.log(" Invalid selection. Skipping project init.\n");
821
+ } else {
822
+ const selectedRepo = repos[index];
823
+ console.log(`
824
+ Selected: ${selectedRepo.name}
825
+ `);
826
+ let worktreeId;
827
+ const projectPath = process.cwd();
828
+ try {
829
+ const worktreesRes = await apiGet(`/worktrees?repo_id=${selectedRepo.id}`);
830
+ const match = worktreesRes.data.find((wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/"));
831
+ if (match) worktreeId = match.id;
832
+ } catch {
833
+ }
834
+ const codebyplanPath = join3(projectPath, ".codebyplan.json");
835
+ const codebyplanConfig = { repo_id: selectedRepo.id };
836
+ if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
837
+ await writeFile2(codebyplanPath, JSON.stringify(codebyplanConfig, null, 2) + "\n", "utf-8");
838
+ console.log(` Created ${codebyplanPath}`);
839
+ console.log("\n Running initial sync...\n");
840
+ try {
841
+ const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
842
+ const syncResult = await executeSyncToLocal2({
843
+ repoId: selectedRepo.id,
844
+ projectPath
845
+ });
846
+ const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
847
+ if (totalChanges > 0) {
848
+ console.log(` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
849
+ `);
850
+ } else {
851
+ console.log(" All files already up to date.\n");
852
+ }
853
+ } catch (err) {
854
+ const msg = err instanceof Error ? err.message : String(err);
855
+ console.log(` Sync failed: ${msg}`);
856
+ console.log(" Run 'codebyplan sync' later to sync files.\n");
857
+ }
858
+ }
859
+ }
860
+ }
861
+ console.log(" Setup complete! Start a new Claude Code session to begin.\n");
862
+ } finally {
863
+ rl.close();
864
+ }
865
+ }
866
+ var init_setup = __esm({
867
+ "src/cli/setup.ts"() {
868
+ "use strict";
869
+ init_api();
870
+ }
871
+ });
872
+
873
+ // src/cli/config.ts
874
+ import { readFile as readFile4 } from "node:fs/promises";
875
+ import { join as join4 } from "node:path";
876
+ function parseFlags(startIndex) {
877
+ const flags = {};
878
+ const args = process.argv.slice(startIndex);
879
+ for (let i = 0; i < args.length; i++) {
880
+ const arg2 = args[i];
881
+ if (arg2.startsWith("--") && i + 1 < args.length) {
882
+ const key = arg2.slice(2);
883
+ flags[key] = args[++i];
884
+ }
885
+ }
886
+ return flags;
887
+ }
888
+ function hasFlag(name, startIndex) {
889
+ return process.argv.slice(startIndex).includes(`--${name}`);
890
+ }
891
+ async function resolveConfig(flags) {
892
+ const projectPath = flags["path"] ?? process.cwd();
893
+ let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
894
+ let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
895
+ if (!repoId || !worktreeId) {
896
+ try {
897
+ const configPath = join4(projectPath, ".codebyplan.json");
898
+ const raw = await readFile4(configPath, "utf-8");
899
+ const config = JSON.parse(raw);
900
+ if (!repoId) repoId = config.repo_id;
901
+ if (!worktreeId) worktreeId = config.worktree_id;
902
+ } catch {
903
+ }
904
+ }
905
+ if (!repoId) {
906
+ throw new Error(
907
+ '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'
908
+ );
909
+ }
910
+ return { repoId, worktreeId, projectPath };
911
+ }
912
+ var init_config = __esm({
913
+ "src/cli/config.ts"() {
914
+ "use strict";
915
+ }
916
+ });
917
+
918
+ // src/cli/fileMapper.ts
919
+ import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
920
+ import { join as join5, extname } from "node:path";
921
+ function compositeKey(type, name, category) {
922
+ return category ? `${type}:${category}/${name}` : `${type}:${name}`;
923
+ }
924
+ async function scanLocalFiles(claudeDir, projectPath) {
925
+ const result = /* @__PURE__ */ new Map();
926
+ await scanCommands(join5(claudeDir, "commands", "cbp"), result);
927
+ await scanSubfolderType(join5(claudeDir, "agents"), "agent", "AGENT.md", result);
928
+ await scanSubfolderType(join5(claudeDir, "skills"), "skill", "SKILL.md", result);
929
+ await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
930
+ await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
931
+ await scanTemplates(join5(claudeDir, "templates"), result);
932
+ await scanSettings(claudeDir, projectPath, result);
933
+ return result;
934
+ }
935
+ async function scanCommands(dir, result) {
936
+ await scanCommandsRecursive(dir, dir, result);
937
+ }
938
+ async function scanCommandsRecursive(baseDir, currentDir, result) {
939
+ let entries;
940
+ try {
941
+ entries = await readdir3(currentDir, { withFileTypes: true });
942
+ } catch {
943
+ return;
944
+ }
945
+ for (const entry of entries) {
946
+ if (entry.isDirectory()) {
947
+ await scanCommandsRecursive(baseDir, join5(currentDir, entry.name), result);
948
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
949
+ const name = entry.name.slice(0, -3);
950
+ const content = await readFile5(join5(currentDir, entry.name), "utf-8");
951
+ const relDir = currentDir.slice(baseDir.length + 1);
952
+ const category = relDir || null;
953
+ const key = compositeKey("command", name, category);
954
+ result.set(key, { type: "command", name, category, content });
955
+ }
956
+ }
957
+ }
958
+ async function scanSubfolderType(dir, type, fileName, result) {
959
+ let entries;
960
+ try {
961
+ entries = await readdir3(dir, { withFileTypes: true });
962
+ } catch {
963
+ return;
964
+ }
965
+ for (const entry of entries) {
966
+ if (entry.isDirectory()) {
967
+ const filePath = join5(dir, entry.name, fileName);
968
+ try {
969
+ const content = await readFile5(filePath, "utf-8");
970
+ const key = compositeKey(type, entry.name, null);
971
+ result.set(key, { type, name: entry.name, category: null, content });
972
+ } catch {
973
+ }
974
+ }
975
+ }
976
+ }
977
+ async function scanFlatType(dir, type, ext, result) {
978
+ let entries;
979
+ try {
980
+ entries = await readdir3(dir, { withFileTypes: true });
981
+ } catch {
982
+ return;
983
+ }
984
+ for (const entry of entries) {
985
+ if (entry.isFile() && entry.name.endsWith(ext)) {
986
+ const name = entry.name.slice(0, -ext.length);
987
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
988
+ const key = compositeKey(type, name, null);
989
+ result.set(key, { type, name, category: null, content });
990
+ }
991
+ }
992
+ }
993
+ async function scanTemplates(dir, result) {
994
+ let entries;
995
+ try {
996
+ entries = await readdir3(dir, { withFileTypes: true });
997
+ } catch {
998
+ return;
999
+ }
1000
+ for (const entry of entries) {
1001
+ if (entry.isFile() && extname(entry.name)) {
1002
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
1003
+ const key = compositeKey("template", entry.name, null);
1004
+ result.set(key, { type: "template", name: entry.name, category: null, content });
1005
+ }
1006
+ }
1007
+ }
1008
+ async function scanSettings(claudeDir, projectPath, result) {
1009
+ const settingsPath = join5(claudeDir, "settings.json");
1010
+ let raw;
1011
+ try {
1012
+ raw = await readFile5(settingsPath, "utf-8");
1013
+ } catch {
1014
+ return;
1015
+ }
1016
+ let parsed;
1017
+ try {
1018
+ parsed = JSON.parse(raw);
1019
+ } catch {
1020
+ return;
1021
+ }
1022
+ parsed = stripPermissionsAllow(parsed);
1023
+ if (parsed.hooks && typeof parsed.hooks === "object") {
1024
+ const hooksDir = projectPath ? join5(projectPath, ".claude", "hooks") : join5(claudeDir, "hooks");
1025
+ const discovered = await discoverHooks(hooksDir);
1026
+ if (discovered.size > 0) {
1027
+ parsed.hooks = stripDiscoveredHooks(
1028
+ parsed.hooks,
1029
+ ".claude/hooks"
1030
+ );
1031
+ if (Object.keys(parsed.hooks).length === 0) {
1032
+ delete parsed.hooks;
1033
+ }
1034
+ }
1035
+ }
1036
+ const content = JSON.stringify(parsed, null, 2) + "\n";
1037
+ const key = compositeKey("settings", "settings", null);
1038
+ result.set(key, { type: "settings", name: "settings", category: null, content });
1039
+ }
1040
+ var init_fileMapper = __esm({
1041
+ "src/cli/fileMapper.ts"() {
1042
+ "use strict";
1043
+ init_settings_merge();
1044
+ init_hook_registry();
1045
+ }
1046
+ });
1047
+
1048
+ // src/cli/confirm.ts
1049
+ var confirm_exports = {};
1050
+ __export(confirm_exports, {
1051
+ SyncCancelledError: () => SyncCancelledError,
1052
+ confirmEach: () => confirmEach,
1053
+ confirmProceed: () => confirmProceed,
1054
+ promptChoice: () => promptChoice,
1055
+ promptReviewMode: () => promptReviewMode,
1056
+ reviewFilesOneByOne: () => reviewFilesOneByOne,
1057
+ reviewFolder: () => reviewFolder
1058
+ });
1059
+ import { createInterface as createInterface2 } from "node:readline/promises";
1060
+ import { stdin as stdin2, stdout as stdout2 } from "node:process";
1061
+ function isAbortError(err) {
1062
+ return err instanceof Error && err.code === "ABORT_ERR";
1063
+ }
1064
+ async function confirmProceed(message) {
1065
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1066
+ try {
1067
+ while (true) {
1068
+ const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1069
+ const a = answer.trim().toLowerCase();
1070
+ if (a === "" || a === "y" || a === "yes") return true;
1071
+ if (a === "n" || a === "no") return false;
1072
+ console.log(` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`);
1073
+ }
1074
+ } catch (err) {
1075
+ if (isAbortError(err)) throw new SyncCancelledError();
1076
+ throw err;
1077
+ } finally {
1078
+ rl.close();
1079
+ }
1080
+ }
1081
+ async function promptChoice(message, options) {
1082
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1083
+ try {
1084
+ const answer = await rl.question(message);
1085
+ const a = answer.trim().toLowerCase();
1086
+ return options.includes(a) ? a : options[0];
1087
+ } catch (err) {
1088
+ if (isAbortError(err)) throw new SyncCancelledError();
1089
+ throw err;
1090
+ } finally {
1091
+ rl.close();
1092
+ }
1093
+ }
1094
+ async function confirmEach(items, label) {
1095
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1096
+ const accepted = [];
1097
+ try {
1098
+ for (const item of items) {
1099
+ const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
1100
+ const a = answer.trim().toLowerCase();
1101
+ if (a === "a") {
1102
+ accepted.push(item, ...items.slice(items.indexOf(item) + 1));
1103
+ break;
1104
+ }
1105
+ if (a === "y" || a === "yes" || a === "") {
1106
+ accepted.push(item);
1107
+ }
1108
+ }
1109
+ } catch (err) {
1110
+ if (isAbortError(err)) throw new SyncCancelledError();
1111
+ throw err;
1112
+ } finally {
1113
+ rl.close();
1114
+ }
1115
+ return accepted;
1116
+ }
1117
+ function parseReviewAction(input) {
1118
+ const a = input.trim().toLowerCase();
1119
+ switch (a) {
1120
+ case "d":
1121
+ case "delete":
1122
+ return { action: "delete", all: false, special: null };
1123
+ case "p":
1124
+ case "pull":
1125
+ return { action: "pull", all: false, special: null };
1126
+ case "s":
1127
+ case "push":
1128
+ return { action: "push", all: false, special: null };
1129
+ case "k":
1130
+ case "skip":
1131
+ return { action: "skip", all: false, special: null };
1132
+ case "da":
1133
+ return { action: "delete", all: true, special: null };
1134
+ case "pa":
1135
+ return { action: "pull", all: true, special: null };
1136
+ case "sa":
1137
+ return { action: "push", all: true, special: null };
1138
+ case "ka":
1139
+ return { action: "skip", all: true, special: null };
1140
+ case "v":
1141
+ case "view":
1142
+ return { action: null, all: false, special: "view" };
1143
+ case "r":
1144
+ case "recommended":
1145
+ return { action: null, all: false, special: "recommended" };
1146
+ case "":
1147
+ return { action: null, all: false, special: "recommended" };
1148
+ // Enter = recommended
1149
+ default:
1150
+ return { action: null, all: false, special: null };
1151
+ }
1152
+ }
1153
+ function formatActionPrompt(recommended, includeView, includeRecommended) {
1154
+ const actions = [
1155
+ `[d]elete${recommended === "delete" ? "\u2605" : ""}`,
1156
+ `[p]ull${recommended === "pull" ? "\u2605" : ""}`,
1157
+ `pu[s]h${recommended === "push" ? "\u2605" : ""}`,
1158
+ `s[k]ip${recommended === "skip" ? "\u2605" : ""}`
1159
+ ];
1160
+ if (includeView) actions.push("[v]iew");
1161
+ if (includeRecommended) actions.push("[r]ecommended");
1162
+ return actions.join(" ");
1163
+ }
1164
+ function showDiff(local, remote, displayPath) {
1165
+ console.log(`
1166
+ --- ${displayPath} (diff) ---`);
1167
+ if (local === null && remote !== null) {
1168
+ console.log(" (no local file \u2014 remote content below)");
1169
+ for (const line of remote.split("\n").slice(0, 30)) {
1170
+ console.log(` + ${line}`);
1171
+ }
1172
+ if (remote.split("\n").length > 30) console.log(" ... (truncated)");
1173
+ } else if (local !== null && remote === null) {
1174
+ console.log(" (no remote file \u2014 local content below)");
1175
+ for (const line of local.split("\n").slice(0, 30)) {
1176
+ console.log(` - ${line}`);
1177
+ }
1178
+ if (local.split("\n").length > 30) console.log(" ... (truncated)");
1179
+ } else if (local !== null && remote !== null) {
1180
+ const localLines = local.split("\n");
1181
+ const remoteLines = remote.split("\n");
1182
+ let shown = 0;
1183
+ const maxLines = 40;
1184
+ for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
1185
+ const l = localLines[i];
1186
+ const r = remoteLines[i];
1187
+ if (l === r) {
1188
+ console.log(` ${l ?? ""}`);
1189
+ } else {
1190
+ if (l !== void 0) console.log(` - ${l}`);
1191
+ if (r !== void 0) console.log(` + ${r}`);
1192
+ }
1193
+ shown++;
1194
+ }
1195
+ if (Math.max(localLines.length, remoteLines.length) > maxLines) {
1196
+ console.log(" ... (truncated)");
1197
+ }
1198
+ }
1199
+ console.log();
1200
+ }
1201
+ async function promptReviewMode() {
1202
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1203
+ try {
1204
+ while (true) {
1205
+ const answer = await rl.question(" Review [o]ne-by-one or [f]older-by-folder? ");
1206
+ const a = answer.trim().toLowerCase();
1207
+ if (a === "o" || a === "one-by-one" || a === "one" || a === "file") return "file";
1208
+ if (a === "f" || a === "folder") return "folder";
1209
+ console.log(` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`);
1210
+ }
1211
+ } catch (err) {
1212
+ if (isAbortError(err)) throw new SyncCancelledError();
1213
+ throw err;
1214
+ } finally {
1215
+ rl.close();
1216
+ }
1217
+ }
1218
+ async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
1219
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1220
+ const results = [];
1221
+ try {
1222
+ let applyAll = null;
1223
+ for (const item of items) {
1224
+ if (applyAll) {
1225
+ results.push(applyAll);
1226
+ continue;
1227
+ }
1228
+ const planned = plannedAction(item);
1229
+ const rec = recommendedAction ? recommendedAction(item) : planned;
1230
+ const hasContent = content != null;
1231
+ const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
1232
+ while (true) {
1233
+ const answer = await rl.question(prompt);
1234
+ const result = parseReviewAction(answer);
1235
+ if (result.special === "view") {
1236
+ if (content) {
1237
+ showDiff(content.local(item), content.remote(item), label(item));
1238
+ } else {
1239
+ console.log(" No content available for diff.");
1240
+ }
1241
+ continue;
1242
+ }
1243
+ if (result.special === "recommended") {
1244
+ results.push(rec);
1245
+ break;
1246
+ }
1247
+ if (result.action === null) {
1248
+ console.log(` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`);
1249
+ continue;
1250
+ }
1251
+ results.push(result.action);
1252
+ if (result.all) applyAll = result.action;
1253
+ break;
1254
+ }
1255
+ }
1256
+ } catch (err) {
1257
+ if (isAbortError(err)) throw new SyncCancelledError();
1258
+ throw err;
1259
+ } finally {
1260
+ rl.close();
1261
+ }
1262
+ return results;
1263
+ }
1264
+ async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
1265
+ console.log(`
1266
+ ${folderName} (${items.length} files):`);
1267
+ for (const item of items) {
1268
+ const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
1269
+ const actionLabel = plannedAction(item);
1270
+ const star = actionLabel === rec ? "\u2605" : "";
1271
+ console.log(` ${label(item)} (${actionLabel}${star})`);
1272
+ }
1273
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1274
+ try {
1275
+ while (true) {
1276
+ const promptStr = ` Action for all: ${formatActionPrompt(
1277
+ recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1278
+ false,
1279
+ true
1280
+ )} [o]ne-by-one: `;
1281
+ const answer = await rl.question(promptStr);
1282
+ const a = answer.trim().toLowerCase();
1283
+ if (a === "o" || a === "one-by-one") {
1284
+ rl.close();
1285
+ return reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content);
1286
+ }
1287
+ if (a === "r" || a === "recommended") {
1288
+ return items.map(
1289
+ (item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
1290
+ );
1291
+ }
1292
+ if (a === "v" || a === "view") {
1293
+ if (content) {
1294
+ for (const item of items) {
1295
+ showDiff(content.local(item), content.remote(item), label(item));
1296
+ }
1297
+ } else {
1298
+ console.log(" No content available for diff.");
1299
+ }
1300
+ continue;
1301
+ }
1302
+ const result = parseReviewAction(a);
1303
+ if (result.action !== null) {
1304
+ return items.map(() => result.action);
1305
+ }
1306
+ console.log(` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
1307
+ recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1308
+ false,
1309
+ true
1310
+ )} [o]ne-by-one`);
1311
+ }
1312
+ } catch (err) {
1313
+ if (isAbortError(err)) throw new SyncCancelledError();
1314
+ throw err;
1315
+ } finally {
1316
+ rl.close();
1317
+ }
1318
+ }
1319
+ var SyncCancelledError;
1320
+ var init_confirm = __esm({
1321
+ "src/cli/confirm.ts"() {
1322
+ "use strict";
1323
+ SyncCancelledError = class extends Error {
1324
+ constructor() {
1325
+ super("Sync cancelled");
1326
+ this.name = "SyncCancelledError";
1327
+ }
1328
+ };
1329
+ }
1330
+ });
1331
+
1332
+ // src/lib/tech-detect.ts
1333
+ import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
1334
+ import { join as join6, relative } from "node:path";
1335
+ async function fileExists(filePath) {
1336
+ try {
1337
+ await access(filePath);
1338
+ return true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1343
+ async function discoverMonorepoApps(projectPath) {
1344
+ const apps = [];
1345
+ const patterns = [];
1346
+ try {
1347
+ const raw = await readFile6(
1348
+ join6(projectPath, "pnpm-workspace.yaml"),
1349
+ "utf-8"
1350
+ );
1351
+ const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
1352
+ if (matches) {
1353
+ for (const m of matches) {
1354
+ const pattern = m.replace(/^\s*-\s*['"]?/, "").replace(/['"]?\s*$/, "").trim();
1355
+ if (pattern) patterns.push(pattern);
1356
+ }
1357
+ }
1358
+ } catch {
1359
+ }
1360
+ if (patterns.length === 0) {
1361
+ try {
1362
+ const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
1363
+ const pkg = JSON.parse(raw);
1364
+ const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1365
+ if (ws) patterns.push(...ws);
1366
+ } catch {
1367
+ }
1368
+ }
1369
+ for (const pattern of patterns) {
1370
+ if (pattern.endsWith("/*")) {
1371
+ const dir = pattern.slice(0, -2);
1372
+ const absDir = join6(projectPath, dir);
1373
+ try {
1374
+ const entries = await readdir4(absDir, { withFileTypes: true });
1375
+ for (const entry of entries) {
1376
+ if (entry.isDirectory()) {
1377
+ const relPath = join6(dir, entry.name);
1378
+ const absPath = join6(absDir, entry.name);
1379
+ if (await fileExists(join6(absPath, "package.json"))) {
1380
+ apps.push({ name: entry.name, path: relPath, absPath });
1381
+ }
1382
+ }
1383
+ }
1384
+ } catch {
1385
+ }
1386
+ }
1387
+ }
1388
+ return apps;
1389
+ }
1390
+ async function detectFromDirectory(dirPath) {
1391
+ const seen = /* @__PURE__ */ new Map();
1392
+ try {
1393
+ const raw = await readFile6(join6(dirPath, "package.json"), "utf-8");
1394
+ const pkg = JSON.parse(raw);
1395
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1396
+ for (const depName of Object.keys(allDeps)) {
1397
+ const rule = PACKAGE_MAP[depName];
1398
+ if (rule) {
1399
+ const key = rule.name.toLowerCase();
1400
+ if (!seen.has(key)) {
1401
+ seen.set(key, { name: rule.name, category: rule.category });
1402
+ }
1403
+ continue;
1404
+ }
1405
+ for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
1406
+ if (depName.startsWith(prefix)) {
1407
+ const key = prefixRule.name.toLowerCase();
1408
+ if (!seen.has(key)) {
1409
+ seen.set(key, {
1410
+ name: prefixRule.name,
1411
+ category: prefixRule.category
1412
+ });
1413
+ }
1414
+ break;
1415
+ }
1416
+ }
1417
+ }
1418
+ } catch {
1419
+ }
1420
+ for (const { file, rule } of CONFIG_FILE_MAP) {
1421
+ const key = rule.name.toLowerCase();
1422
+ if (!seen.has(key) && await fileExists(join6(dirPath, file))) {
1423
+ seen.set(key, { name: rule.name, category: rule.category });
1424
+ }
1425
+ }
1426
+ return Array.from(seen.values()).sort((a, b) => {
1427
+ const catCmp = a.category.localeCompare(b.category);
1428
+ if (catCmp !== 0) return catCmp;
1429
+ return a.name.localeCompare(b.name);
1430
+ });
1431
+ }
1432
+ async function detectTechStack(projectPath) {
1433
+ const repo = await detectFromDirectory(projectPath);
1434
+ const discoveredApps = await discoverMonorepoApps(projectPath);
1435
+ const apps = [];
1436
+ for (const app of discoveredApps) {
1437
+ const stack = await detectFromDirectory(app.absPath);
1438
+ if (stack.length > 0) {
1439
+ apps.push({ name: app.name, path: app.path, stack });
1440
+ }
1441
+ }
1442
+ const flatMap = /* @__PURE__ */ new Map();
1443
+ for (const entry of repo) {
1444
+ flatMap.set(entry.name.toLowerCase(), entry);
1445
+ }
1446
+ for (const app of apps) {
1447
+ for (const entry of app.stack) {
1448
+ const key = entry.name.toLowerCase();
1449
+ if (!flatMap.has(key)) {
1450
+ flatMap.set(key, entry);
1451
+ }
1452
+ }
1453
+ }
1454
+ const flat = Array.from(flatMap.values()).sort((a, b) => {
1455
+ const catCmp = a.category.localeCompare(b.category);
1456
+ if (catCmp !== 0) return catCmp;
1457
+ return a.name.localeCompare(b.name);
1458
+ });
1459
+ return { repo, apps, flat };
1460
+ }
1461
+ function mergeTechStack(remote, detected) {
1462
+ const remoteResult = Array.isArray(remote) ? { repo: remote, apps: [], flat: remote } : remote;
1463
+ const seen = /* @__PURE__ */ new Map();
1464
+ for (const entry of remoteResult.flat) {
1465
+ seen.set(entry.name.toLowerCase(), entry);
1466
+ }
1467
+ const added = [];
1468
+ for (const entry of detected.flat) {
1469
+ const key = entry.name.toLowerCase();
1470
+ if (!seen.has(key)) {
1471
+ seen.set(key, entry);
1472
+ added.push(entry);
1473
+ }
1474
+ }
1475
+ const flat = Array.from(seen.values()).sort((a, b) => {
1476
+ const catCmp = a.category.localeCompare(b.category);
1477
+ if (catCmp !== 0) return catCmp;
1478
+ return a.name.localeCompare(b.name);
1479
+ });
1480
+ const merged = {
1481
+ repo: detected.repo,
1482
+ apps: detected.apps,
1483
+ flat
1484
+ };
1485
+ return { merged, added };
1486
+ }
1487
+ function parseTechStack(raw) {
1488
+ if (Array.isArray(raw)) {
1489
+ return raw.filter(
1490
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
1491
+ );
1492
+ }
1493
+ if (typeof raw === "object" && raw !== null && "flat" in raw) {
1494
+ return parseTechStack(raw.flat);
1495
+ }
1496
+ return [];
1497
+ }
1498
+ function parseAppTechStacks(raw) {
1499
+ if (!Array.isArray(raw)) return [];
1500
+ return raw.filter(
1501
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.path === "string" && Array.isArray(item.stack)
1502
+ ).map((item) => ({
1503
+ name: item.name,
1504
+ path: item.path,
1505
+ stack: parseTechStack(item.stack)
1506
+ }));
1507
+ }
1508
+ function parseTechStackResult(raw) {
1509
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && "flat" in raw) {
1510
+ const obj = raw;
1511
+ return {
1512
+ repo: parseTechStack(obj.repo),
1513
+ apps: parseAppTechStacks(obj.apps),
1514
+ flat: parseTechStack(obj.flat)
1515
+ };
1516
+ }
1517
+ const flat = parseTechStack(raw);
1518
+ return { repo: flat, apps: [], flat };
1519
+ }
1520
+ function categorizeDependency(depName) {
1521
+ const rule = PACKAGE_MAP[depName];
1522
+ if (rule) return rule.category;
1523
+ for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
1524
+ if (depName.startsWith(prefix)) return prefixRule.category;
1525
+ }
1526
+ return "other";
1527
+ }
1528
+ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1529
+ if (depth > 4) return [];
1530
+ const results = [];
1531
+ const pkgPath = join6(dir, "package.json");
1532
+ if (await fileExists(pkgPath)) {
1533
+ results.push(pkgPath);
1534
+ }
1535
+ try {
1536
+ const entries = await readdir4(dir, { withFileTypes: true });
1537
+ for (const entry of entries) {
1538
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1539
+ const subResults = await findPackageJsonFiles(
1540
+ join6(dir, entry.name),
1541
+ projectPath,
1542
+ depth + 1
1543
+ );
1544
+ results.push(...subResults);
1545
+ }
1546
+ } catch {
1547
+ }
1548
+ return results;
1549
+ }
1550
+ async function scanAllDependencies(projectPath) {
1551
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
1552
+ const dependencies = [];
1553
+ for (const pkgPath of packageJsonPaths) {
1554
+ try {
1555
+ const raw = await readFile6(pkgPath, "utf-8");
1556
+ const pkg = JSON.parse(raw);
1557
+ const sourcePath = relative(projectPath, pkgPath);
1558
+ const depSections = [
1559
+ { deps: pkg.dependencies, depType: "production", isDev: false },
1560
+ { deps: pkg.devDependencies, depType: "dev", isDev: true },
1561
+ { deps: pkg.peerDependencies, depType: "peer", isDev: false },
1562
+ { deps: pkg.optionalDependencies, depType: "optional", isDev: false }
1563
+ ];
1564
+ for (const { deps, depType, isDev } of depSections) {
1565
+ if (!deps) continue;
1566
+ for (const [name, version] of Object.entries(deps)) {
1567
+ dependencies.push({
1568
+ name,
1569
+ version,
1570
+ category: categorizeDependency(name),
1571
+ source_path: sourcePath,
1572
+ is_dev: isDev,
1573
+ dep_type: depType
1574
+ });
1575
+ }
1576
+ }
1577
+ } catch {
1578
+ }
1579
+ }
1580
+ return { dependencies };
1581
+ }
1582
+ var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP, SKIP_DIRS;
1583
+ var init_tech_detect = __esm({
1584
+ "src/lib/tech-detect.ts"() {
1585
+ "use strict";
1586
+ PACKAGE_MAP = {
1587
+ // Frameworks
1588
+ next: { name: "Next.js", category: "framework" },
1589
+ nuxt: { name: "Nuxt", category: "framework" },
1590
+ gatsby: { name: "Gatsby", category: "framework" },
1591
+ express: { name: "Express", category: "framework" },
1592
+ fastify: { name: "Fastify", category: "framework" },
1593
+ hono: { name: "Hono", category: "framework" },
1594
+ "@remix-run/node": { name: "Remix", category: "framework" },
1595
+ svelte: { name: "Svelte", category: "framework" },
1596
+ astro: { name: "Astro", category: "framework" },
1597
+ "@angular/core": { name: "Angular", category: "framework" },
1598
+ // Libraries (UI)
1599
+ react: { name: "React", category: "framework" },
1600
+ vue: { name: "Vue", category: "framework" },
1601
+ "solid-js": { name: "Solid", category: "framework" },
1602
+ preact: { name: "Preact", category: "framework" },
1603
+ // Languages (detected via devDeps)
1604
+ typescript: { name: "TypeScript", category: "language" },
1605
+ // Styling
1606
+ tailwindcss: { name: "Tailwind CSS", category: "styling" },
1607
+ sass: { name: "SCSS", category: "styling" },
1608
+ "styled-components": { name: "styled-components", category: "styling" },
1609
+ "@emotion/react": { name: "Emotion", category: "styling" },
1610
+ // Database
1611
+ prisma: { name: "Prisma", category: "database" },
1612
+ "@prisma/client": { name: "Prisma", category: "database" },
1613
+ "drizzle-orm": { name: "Drizzle", category: "database" },
1614
+ "@supabase/supabase-js": { name: "Supabase", category: "database" },
1615
+ mongoose: { name: "MongoDB", category: "database" },
1616
+ typeorm: { name: "TypeORM", category: "database" },
1617
+ knex: { name: "Knex", category: "database" },
1618
+ // Testing
1619
+ jest: { name: "Jest", category: "testing" },
1620
+ vitest: { name: "Vitest", category: "testing" },
1621
+ mocha: { name: "Mocha", category: "testing" },
1622
+ playwright: { name: "Playwright", category: "testing" },
1623
+ "@playwright/test": { name: "Playwright", category: "testing" },
1624
+ cypress: { name: "Cypress", category: "testing" },
1625
+ supertest: { name: "Supertest", category: "testing" },
1626
+ // Build tools
1627
+ turbo: { name: "Turborepo", category: "build" },
1628
+ vite: { name: "Vite", category: "build" },
1629
+ webpack: { name: "Webpack", category: "build" },
1630
+ esbuild: { name: "esbuild", category: "build" },
1631
+ rollup: { name: "Rollup", category: "build" },
1632
+ nx: { name: "Nx", category: "build" },
1633
+ lerna: { name: "Lerna", category: "build" },
1634
+ tsup: { name: "tsup", category: "build" },
1635
+ "@swc/core": { name: "SWC", category: "build" },
1636
+ parcel: { name: "Parcel", category: "build" },
1637
+ // Tools
1638
+ eslint: { name: "ESLint", category: "tool" },
1639
+ prettier: { name: "Prettier", category: "tool" },
1640
+ "@biomejs/biome": { name: "Biome", category: "tool" },
1641
+ storybook: { name: "Storybook", category: "tool" },
1642
+ // Component libs
1643
+ "@mui/material": { name: "MUI", category: "component-lib" },
1644
+ "@chakra-ui/react": { name: "Chakra UI", category: "component-lib" },
1645
+ "@mantine/core": { name: "Mantine", category: "component-lib" },
1646
+ // GraphQL
1647
+ graphql: { name: "GraphQL", category: "graphql" },
1648
+ "@apollo/client": { name: "Apollo Client", category: "graphql" },
1649
+ urql: { name: "urql", category: "graphql" },
1650
+ "graphql-request": { name: "graphql-request", category: "graphql" },
1651
+ // Documentation
1652
+ typedoc: { name: "TypeDoc", category: "documentation" },
1653
+ "@docusaurus/core": { name: "Docusaurus", category: "documentation" },
1654
+ vitepress: { name: "VitePress", category: "documentation" },
1655
+ // Code quality
1656
+ husky: { name: "Husky", category: "quality" },
1657
+ "lint-staged": { name: "lint-staged", category: "quality" },
1658
+ commitlint: { name: "commitlint", category: "quality" },
1659
+ "@commitlint/cli": { name: "commitlint", category: "quality" },
1660
+ // Mobile
1661
+ "react-native": { name: "React Native", category: "mobile" },
1662
+ expo: { name: "Expo", category: "mobile" }
1663
+ };
1664
+ PACKAGE_PREFIX_MAP = [
1665
+ {
1666
+ prefix: "@radix-ui/",
1667
+ rule: { name: "Radix UI", category: "component-lib" }
1668
+ },
1669
+ { prefix: "@storybook/", rule: { name: "Storybook", category: "tool" } },
1670
+ {
1671
+ prefix: "@testing-library/",
1672
+ rule: { name: "Testing Library", category: "testing" }
1673
+ }
1674
+ ];
1675
+ CONFIG_FILE_MAP = [
1676
+ { file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
1677
+ { file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
1678
+ { file: "next.config.mjs", rule: { name: "Next.js", category: "framework" } },
1679
+ { file: "next.config.ts", rule: { name: "Next.js", category: "framework" } },
1680
+ {
1681
+ file: "tailwind.config.js",
1682
+ rule: { name: "Tailwind CSS", category: "styling" }
1683
+ },
1684
+ {
1685
+ file: "tailwind.config.ts",
1686
+ rule: { name: "Tailwind CSS", category: "styling" }
1687
+ },
1688
+ { file: "turbo.json", rule: { name: "Turborepo", category: "build" } },
1689
+ {
1690
+ file: "docker-compose.yml",
1691
+ rule: { name: "Docker", category: "deployment" }
1692
+ },
1693
+ {
1694
+ file: "docker-compose.yaml",
1695
+ rule: { name: "Docker", category: "deployment" }
1696
+ },
1697
+ { file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
1698
+ { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } },
1699
+ { file: ".storybook/main.js", rule: { name: "Storybook", category: "tool" } },
1700
+ { file: ".storybook/main.ts", rule: { name: "Storybook", category: "tool" } },
1701
+ {
1702
+ file: ".storybook/main.mjs",
1703
+ rule: { name: "Storybook", category: "tool" }
1704
+ },
1705
+ {
1706
+ file: "components.json",
1707
+ rule: { name: "shadcn/ui", category: "component-lib" }
1708
+ },
1709
+ { file: "nx.json", rule: { name: "Nx", category: "build" } },
1710
+ { file: "lerna.json", rule: { name: "Lerna", category: "build" } }
1711
+ ];
1712
+ SKIP_DIRS = /* @__PURE__ */ new Set([
1713
+ "node_modules",
1714
+ ".next",
1715
+ "dist",
1716
+ ".turbo",
1717
+ ".git",
1718
+ "coverage",
1719
+ "build",
1720
+ "out",
1721
+ ".vercel",
1722
+ ".expo"
1723
+ ]);
1724
+ }
1725
+ });
1726
+
1727
+ // src/lib/server-detect.ts
1728
+ function detectFramework(pkg) {
1729
+ const deps = pkg.dependencies ?? {};
1730
+ const devDeps = pkg.devDependencies ?? {};
1731
+ const hasDep = (name) => name in deps || name in devDeps;
1732
+ if (hasDep("next")) return "nextjs";
1733
+ if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
1734
+ if (hasDep("expo")) return "expo";
1735
+ if (hasDep("vite")) return "vite";
1736
+ if (hasDep("express")) return "express";
1737
+ if (hasDep("@nestjs/core")) return "nestjs";
1738
+ return "custom";
1739
+ }
1740
+ function detectPortFromScripts(pkg) {
1741
+ const scripts = pkg.scripts;
1742
+ if (!scripts?.dev) return null;
1743
+ const parts = scripts.dev.split(/\s+/);
1744
+ for (let i = 0; i < parts.length - 1; i++) {
1745
+ if (parts[i] === "--port" || parts[i] === "-p") {
1746
+ const next = parts[i + 1];
1747
+ if (next) {
1748
+ const port = parseInt(next, 10);
1749
+ if (!isNaN(port)) return port;
1750
+ }
1751
+ }
1752
+ }
1753
+ return null;
1754
+ }
1755
+ var init_server_detect = __esm({
1756
+ "src/lib/server-detect.ts"() {
1757
+ "use strict";
1758
+ }
1759
+ });
1760
+
1761
+ // src/lib/port-verify.ts
1762
+ import { readFile as readFile7 } from "node:fs/promises";
1763
+ async function verifyPorts(projectPath, portAllocations) {
1764
+ const mismatches = [];
1765
+ const allocatedPorts = new Set(portAllocations.map((a) => a.port));
1766
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
1767
+ for (const pkgPath of packageJsonPaths) {
1768
+ try {
1769
+ const raw = await readFile7(pkgPath, "utf-8");
1770
+ const pkg = JSON.parse(raw);
1771
+ const scriptPort = detectPortFromScripts(pkg);
1772
+ if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
1773
+ const relativePath = pkgPath.replace(projectPath + "/", "");
1774
+ const matchingAlloc = portAllocations.find(
1775
+ (a) => a.label === getAppLabel(relativePath)
1776
+ );
1777
+ mismatches.push({
1778
+ packageJsonPath: relativePath,
1779
+ scriptPort,
1780
+ allocation: matchingAlloc ?? null,
1781
+ reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
1782
+ });
1783
+ }
1784
+ } catch {
1785
+ }
1786
+ }
1787
+ return mismatches;
1788
+ }
1789
+ async function findUnallocatedApps(projectPath, portAllocations) {
1790
+ const apps = await discoverMonorepoApps(projectPath);
1791
+ if (apps.length === 0) {
1792
+ return [];
1793
+ }
1794
+ const allocatedLabels = new Set(portAllocations.map((a) => a.label));
1795
+ const unallocated = [];
1796
+ for (const app of apps) {
1797
+ if (allocatedLabels.has(app.name)) continue;
1798
+ try {
1799
+ const raw = await readFile7(`${app.absPath}/package.json`, "utf-8");
1800
+ const pkg = JSON.parse(raw);
1801
+ const framework = detectFramework(pkg);
1802
+ const detectedPort = detectPortFromScripts(pkg);
1803
+ const command = `pnpm --filter ${app.name} dev`;
1804
+ unallocated.push({
1805
+ name: app.name,
1806
+ path: app.path,
1807
+ framework,
1808
+ detectedPort,
1809
+ command
1810
+ });
1811
+ } catch {
1812
+ }
1813
+ }
1814
+ return unallocated;
1815
+ }
1816
+ function getAppLabel(relativePath) {
1817
+ const parts = relativePath.split("/");
1818
+ if (parts.length >= 3 && parts[0] === "apps") {
1819
+ return parts[1];
1820
+ }
1821
+ return "root";
1822
+ }
1823
+ var init_port_verify = __esm({
1824
+ "src/lib/port-verify.ts"() {
1825
+ "use strict";
1826
+ init_tech_detect();
1827
+ init_server_detect();
1828
+ }
1829
+ });
1830
+
1831
+ // src/cli/sync.ts
1832
+ var sync_exports = {};
1833
+ __export(sync_exports, {
1834
+ runSync: () => runSync
1835
+ });
1836
+ import { createHash } from "node:crypto";
1837
+ import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
1838
+ import { join as join7, dirname as dirname2 } from "node:path";
1839
+ function contentHash(content) {
1840
+ return createHash("sha256").update(content).digest("hex");
1841
+ }
1842
+ async function runSync() {
1843
+ const flags = parseFlags(3);
1844
+ const dryRun = hasFlag("dry-run", 3);
1845
+ const force = hasFlag("force", 3);
1846
+ const fix = hasFlag("fix", 3);
1847
+ validateApiKey();
1848
+ const config = await resolveConfig(flags);
1849
+ const { repoId, projectPath } = config;
1850
+ console.log(`
1851
+ CodeByPlan Sync`);
1852
+ console.log(` Repo: ${repoId}`);
1853
+ console.log(` Path: ${projectPath}`);
1854
+ if (dryRun) console.log(` Mode: dry-run`);
1855
+ if (force) console.log(` Mode: force`);
1856
+ console.log();
1857
+ if (!dryRun) {
1858
+ console.log(" Acquiring sync lock...");
1859
+ try {
1860
+ await apiPost("/sync/lock", {
1861
+ repo_id: repoId,
1862
+ locked_by: `cli-sync`,
1863
+ reason: "Bidirectional sync",
1864
+ ttl_minutes: 10
1865
+ });
1866
+ console.log(" Lock acquired.\n");
1867
+ } catch (lockErr) {
1868
+ const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
1869
+ if (lockStatus.data.locked && lockStatus.data.lock) {
1870
+ const lock = lockStatus.data.lock;
1871
+ console.log(
1872
+ ` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`
1873
+ );
1874
+ console.log(` Expires: ${lock.expires_at}`);
1875
+ console.log(` Use --force to override, or wait for lock to expire.
1876
+ `);
1877
+ if (!force) return;
1878
+ await apiPost("/sync/lock", {
1879
+ repo_id: repoId,
1880
+ locked_by: `cli-sync`,
1881
+ reason: "Bidirectional sync (forced)",
1882
+ ttl_minutes: 10
1883
+ });
1884
+ console.log(" Lock acquired (forced).\n");
1885
+ } else {
1886
+ throw lockErr;
1887
+ }
1888
+ }
1889
+ }
1890
+ try {
1891
+ await runSyncInner(repoId, projectPath, dryRun, force, fix);
1892
+ } finally {
1893
+ if (!dryRun) {
1894
+ try {
1895
+ await apiDelete("/sync/lock", { repo_id: repoId });
1896
+ } catch {
1897
+ }
1898
+ }
1899
+ }
1900
+ }
1901
+ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
1902
+ console.log(" Reading local and remote state...");
1903
+ const claudeDir = join7(projectPath, ".claude");
1904
+ let localFiles = /* @__PURE__ */ new Map();
1905
+ try {
1906
+ localFiles = await scanLocalFiles(claudeDir, projectPath);
1907
+ } catch {
1908
+ }
1909
+ const [defaultsRes, repoSyncRes, repoRes, syncStateRes, fileReposRes] = await Promise.all([
1910
+ apiGet("/sync/defaults"),
1911
+ apiGet("/sync/files", { repo_id: repoId }),
1912
+ apiGet(`/repos/${repoId}`),
1913
+ apiGet("/sync/state", {
1914
+ repo_id: repoId
1915
+ }),
1916
+ apiGet("/sync/file-repos", {
1917
+ repo_id: repoId
1918
+ })
1919
+ ]);
1920
+ const syncStartTime = Date.now();
1921
+ const repoData = repoRes.data;
1922
+ const remoteDefaults = flattenSyncData(defaultsRes.data);
1923
+ const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
1924
+ const syncState = syncStateRes.data;
1925
+ const fileRepoHashes = /* @__PURE__ */ new Map();
1926
+ const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
1927
+ for (const entry of fileReposRes.data ?? []) {
1928
+ if (entry.claude_files) {
1929
+ const key = compositeKey(
1930
+ entry.claude_files.type,
1931
+ entry.claude_files.name,
1932
+ entry.claude_files.category
1933
+ );
1934
+ fileRepoHashes.set(key, entry.last_synced_content_hash);
1935
+ }
1936
+ fileRepoByClaudeFileId.set(
1937
+ entry.claude_file_id,
1938
+ entry.last_synced_content_hash
1939
+ );
1940
+ }
1941
+ const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
1942
+ console.log(
1943
+ ` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
1944
+ `
1945
+ );
1946
+ const plan = [];
1947
+ const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
1948
+ for (const key of allKeys) {
1949
+ const local = localFiles.get(key);
1950
+ const remote = remoteFiles.get(key);
1951
+ if (local && !remote) {
1952
+ plan.push({
1953
+ key,
1954
+ displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1955
+ action: "push",
1956
+ recommended: "push",
1957
+ localContent: local.content,
1958
+ remoteContent: null,
1959
+ pushContent: reverseSubstituteVariables(local.content, repoData),
1960
+ filePath: getLocalFilePath(claudeDir, projectPath, {
1961
+ type: local.type,
1962
+ name: local.name,
1963
+ category: local.category
1964
+ }),
1965
+ type: local.type,
1966
+ name: local.name,
1967
+ category: local.category,
1968
+ isHook: local.type === "hook",
1969
+ claudeFileId: null
1970
+ });
1971
+ } else if (!local && remote) {
1972
+ const resolvedContent = substituteVariables(remote.content, repoData);
1973
+ const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
1974
+ const recommended = hadSyncedThisFile ? "delete" : "pull";
1975
+ plan.push({
1976
+ key,
1977
+ displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
1978
+ action: recommended,
1979
+ recommended,
1980
+ localContent: null,
1981
+ remoteContent: resolvedContent,
1982
+ pushContent: null,
1983
+ filePath: getLocalFilePath(claudeDir, projectPath, remote),
1984
+ type: remote.type,
1985
+ name: remote.name,
1986
+ category: remote.category ?? null,
1987
+ isHook: remote.type === "hook",
1988
+ claudeFileId: remote.id ?? null
1989
+ });
1990
+ } else if (local && remote) {
1991
+ const resolvedRemote = substituteVariables(remote.content, repoData);
1992
+ if (local.content === resolvedRemote) {
1993
+ continue;
1994
+ }
1995
+ const localHash = contentHash(local.content);
1996
+ const lastSyncedHash = fileRepoHashes.get(key) ?? null;
1997
+ const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
1998
+ let action;
1999
+ if (force) {
2000
+ action = "pull";
2001
+ } else if (!localChanged) {
2002
+ action = "pull";
2003
+ } else if (lastSyncedHash === null) {
2004
+ action = "conflict";
2005
+ } else {
2006
+ const remoteDbHash = remote.content_hash ?? null;
2007
+ const remoteChanged = remoteDbHash ? remoteDbHash !== lastSyncedHash : true;
2008
+ if (remoteChanged) {
2009
+ action = "conflict";
2010
+ } else {
2011
+ action = "push";
2012
+ }
2013
+ }
2014
+ plan.push({
2015
+ key,
2016
+ displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
2017
+ action,
2018
+ recommended: action === "conflict" ? "pull" : action,
2019
+ localContent: local.content,
2020
+ remoteContent: resolvedRemote,
2021
+ pushContent: reverseSubstituteVariables(local.content, repoData),
2022
+ filePath: getLocalFilePath(claudeDir, projectPath, remote),
2023
+ type: local.type,
2024
+ name: local.name,
2025
+ category: local.category,
2026
+ isHook: local.type === "hook",
2027
+ claudeFileId: remote.id ?? null
2028
+ });
2029
+ }
2030
+ }
2031
+ const pulls = plan.filter((p) => p.action === "pull");
2032
+ const pushes = plan.filter((p) => p.action === "push");
2033
+ const conflicts = plan.filter((p) => p.action === "conflict");
2034
+ const contentPulls = pulls.filter((p) => p.localContent !== null);
2035
+ const dbOnlyPull = plan.filter(
2036
+ (p) => p.localContent === null && p.action === "pull"
2037
+ );
2038
+ const dbOnlyDelete = plan.filter(
2039
+ (p) => p.localContent === null && p.action === "delete"
2040
+ );
2041
+ if (contentPulls.length > 0) {
2042
+ console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
2043
+ for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
2044
+ }
2045
+ if (pushes.length > 0) {
2046
+ console.log(` Push (local \u2192 DB): ${pushes.length}`);
2047
+ for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
2048
+ }
2049
+ if (dbOnlyPull.length > 0) {
2050
+ console.log(`
2051
+ DB-only (new, will pull): ${dbOnlyPull.length}`);
2052
+ for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
2053
+ }
2054
+ if (dbOnlyDelete.length > 0) {
2055
+ console.log(
2056
+ `
2057
+ DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
2058
+ );
2059
+ for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
2060
+ }
2061
+ if (conflicts.length > 0) {
2062
+ console.log(`
2063
+ Conflicts (both sides changed): ${conflicts.length}`);
2064
+ for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
2065
+ }
2066
+ if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
2067
+ console.log(" All .claude/ files in sync.");
2068
+ }
2069
+ if (plan.length > 0 && !dryRun) {
2070
+ if (!force) {
2071
+ const agreed = await confirmProceed(`
2072
+ Agree with sync? [Y/n] `);
2073
+ if (!agreed) {
2074
+ const mode = await promptReviewMode();
2075
+ const contentProvider = {
2076
+ local: (p) => p.localContent,
2077
+ remote: (p) => p.remoteContent
2078
+ };
2079
+ if (mode === "file") {
2080
+ const actions = await reviewFilesOneByOne(
2081
+ plan,
2082
+ (p) => p.displayPath,
2083
+ (p) => p.action,
2084
+ (p) => p.recommended,
2085
+ contentProvider
2086
+ );
2087
+ for (let i = 0; i < plan.length; i++) {
2088
+ plan[i].action = actions[i];
2089
+ }
2090
+ } else {
2091
+ const groups = groupByType(plan);
2092
+ for (const [typeName, items] of groups) {
2093
+ const actions = await reviewFolder(
2094
+ typeName,
2095
+ items,
2096
+ (p) => p.displayPath,
2097
+ (p) => p.action,
2098
+ (p) => p.recommended,
2099
+ contentProvider
2100
+ );
2101
+ for (let i = 0; i < items.length; i++) {
2102
+ items[i].action = actions[i];
2103
+ }
2104
+ }
2105
+ }
2106
+ }
2107
+ }
2108
+ const toPull = plan.filter((p) => p.action === "pull");
2109
+ const toPush = plan.filter((p) => p.action === "push");
2110
+ const toDelete = plan.filter((p) => p.action === "delete");
2111
+ const skipped = plan.filter((p) => p.action === "skip");
2112
+ if (toPull.length + toPush.length + toDelete.length === 0) {
2113
+ console.log("\n All items skipped \u2014 no changes applied.");
2114
+ } else {
2115
+ for (const p of toPull) {
2116
+ if (p.filePath && p.remoteContent !== null) {
2117
+ await mkdir2(dirname2(p.filePath), { recursive: true });
2118
+ await writeFile3(p.filePath, p.remoteContent, "utf-8");
2119
+ if (p.isHook) await chmod2(p.filePath, 493);
2120
+ }
2121
+ }
2122
+ const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
2123
+ type: p.type,
2124
+ name: p.name,
2125
+ category: p.category,
2126
+ content: p.pushContent
2127
+ }));
2128
+ if (toUpsert.length > 0) {
2129
+ await apiPost("/sync/files", {
2130
+ repo_id: repoId,
2131
+ files: toUpsert,
2132
+ changed_by_repo_id: repoId
2133
+ });
2134
+ }
2135
+ if (toDelete.length > 0) {
2136
+ const deleteKeys = toDelete.map((p) => ({
2137
+ type: p.type,
2138
+ name: p.name,
2139
+ category: p.category
2140
+ }));
2141
+ await apiPost("/sync/files", {
2142
+ repo_id: repoId,
2143
+ delete_keys: deleteKeys
2144
+ });
2145
+ for (const p of toDelete) {
2146
+ if (p.filePath) {
2147
+ try {
2148
+ await unlink2(p.filePath);
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ }
2153
+ }
2154
+ const unresolvedConflicts = plan.filter(
2155
+ (p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
2156
+ );
2157
+ if (unresolvedConflicts.length > 0) {
2158
+ let stored = 0;
2159
+ for (const p of unresolvedConflicts) {
2160
+ if (p.claudeFileId) {
2161
+ try {
2162
+ await apiPost("/sync/conflicts", {
2163
+ repo_id: repoId,
2164
+ claude_file_id: p.claudeFileId,
2165
+ conflict_type: "both_modified",
2166
+ local_content: p.localContent,
2167
+ remote_content: p.remoteContent
2168
+ });
2169
+ stored++;
2170
+ } catch {
2171
+ }
2172
+ }
2173
+ }
2174
+ if (stored > 0) {
2175
+ console.log(
2176
+ `
2177
+ ${stored} conflict(s) stored in DB for later resolution.`
2178
+ );
2179
+ }
2180
+ }
2181
+ const syncDurationMs = Date.now() - syncStartTime;
2182
+ await apiPost("/sync/state", {
2183
+ repo_id: repoId,
2184
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
2185
+ was_skipped: skipped.length > 0,
2186
+ files_synced_count: toPull.length + toPush.length + toDelete.length,
2187
+ files_pushed: toPush.length,
2188
+ files_pulled: toPull.length,
2189
+ files_deleted: toDelete.length,
2190
+ files_skipped: skipped.length,
2191
+ sync_duration_ms: syncDurationMs,
2192
+ sync_version: getSyncVersion()
2193
+ });
2194
+ const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
2195
+ const fileRepoUpdates = [];
2196
+ for (const p of toPull) {
2197
+ if (p.claudeFileId && p.remoteContent !== null) {
2198
+ fileRepoUpdates.push({
2199
+ claude_file_id: p.claudeFileId,
2200
+ last_synced_at: syncTimestamp,
2201
+ last_synced_content_hash: contentHash(p.remoteContent),
2202
+ sync_status: "synced"
2203
+ });
2204
+ }
2205
+ }
2206
+ for (const p of toPush) {
2207
+ if (p.claudeFileId && p.localContent !== null) {
2208
+ fileRepoUpdates.push({
2209
+ claude_file_id: p.claudeFileId,
2210
+ last_synced_at: syncTimestamp,
2211
+ last_synced_content_hash: contentHash(p.localContent),
2212
+ sync_status: "synced"
2213
+ });
2214
+ }
2215
+ }
2216
+ if (fileRepoUpdates.length > 0) {
2217
+ try {
2218
+ await apiPost("/sync/file-repos", {
2219
+ repo_id: repoId,
2220
+ file_repos: fileRepoUpdates
2221
+ });
2222
+ } catch {
2223
+ }
2224
+ }
2225
+ console.log(
2226
+ `
2227
+ Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
2228
+ );
2229
+ }
2230
+ } else if (dryRun) {
2231
+ console.log("\n (dry-run \u2014 no changes)");
2232
+ }
2233
+ console.log("\n Settings sync...");
2234
+ await syncSettings(
2235
+ claudeDir,
2236
+ projectPath,
2237
+ defaultsRes.data,
2238
+ repoData,
2239
+ dryRun
2240
+ );
2241
+ console.log(" Config sync...");
2242
+ await syncConfig(repoId, projectPath, dryRun);
2243
+ console.log(" Tech stack...");
2244
+ await syncTechStack(repoId, projectPath, dryRun);
2245
+ console.log(" Port verification...");
2246
+ await syncPortVerification(repoId, projectPath, dryRun, fix);
2247
+ console.log("\n Sync complete.\n");
2248
+ }
2249
+ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
2250
+ const settingsPath = join7(claudeDir, "settings.json");
2251
+ const globalSettingsFiles = syncData.global_settings ?? [];
2252
+ let globalSettings = {};
2253
+ for (const gf of globalSettingsFiles) {
2254
+ const parsed = JSON.parse(
2255
+ substituteVariables(gf.content, repoData)
2256
+ );
2257
+ globalSettings = { ...globalSettings, ...parsed };
2258
+ }
2259
+ const repoSettingsFiles = syncData.settings ?? [];
2260
+ let repoSettings = {};
2261
+ for (const rf of repoSettingsFiles) {
2262
+ repoSettings = JSON.parse(
2263
+ substituteVariables(rf.content, repoData)
2264
+ );
2265
+ }
2266
+ const combinedTemplate = mergeGlobalAndRepoSettings(
2267
+ globalSettings,
2268
+ repoSettings
2269
+ );
2270
+ const hooksDir = join7(projectPath, ".claude", "hooks");
2271
+ const discovered = await discoverHooks(hooksDir);
2272
+ let localSettings = {};
2273
+ try {
2274
+ const raw = await readFile8(settingsPath, "utf-8");
2275
+ localSettings = JSON.parse(raw);
2276
+ } catch {
2277
+ }
2278
+ let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
2279
+ merged = stripPermissionsAllow(merged);
2280
+ if (discovered.size > 0) {
2281
+ merged.hooks = mergeDiscoveredHooks(
2282
+ merged.hooks ?? {},
2283
+ discovered
2284
+ );
2285
+ }
2286
+ const mergedContent = JSON.stringify(merged, null, 2) + "\n";
2287
+ let currentContent = "";
2288
+ try {
2289
+ currentContent = await readFile8(settingsPath, "utf-8");
2290
+ } catch {
2291
+ }
2292
+ if (currentContent === mergedContent) {
2293
+ console.log(" Settings up to date.");
2294
+ return;
2295
+ }
2296
+ if (dryRun) {
2297
+ console.log(" Settings would be updated (dry-run).");
2298
+ return;
2299
+ }
2300
+ await mkdir2(dirname2(settingsPath), { recursive: true });
2301
+ await writeFile3(settingsPath, mergedContent, "utf-8");
2302
+ console.log(" Updated settings.json");
2303
+ }
2304
+ async function syncConfig(repoId, projectPath, dryRun) {
2305
+ const configPath = join7(projectPath, ".codebyplan.json");
2306
+ let currentConfig = {};
2307
+ try {
2308
+ const raw = await readFile8(configPath, "utf-8");
2309
+ currentConfig = JSON.parse(raw);
2310
+ } catch {
2311
+ currentConfig = { repo_id: repoId };
2312
+ }
2313
+ const repoRes = await apiGet(`/repos/${repoId}`);
2314
+ const repo = repoRes.data;
2315
+ let portAllocations = [];
2316
+ try {
2317
+ const portsRes = await apiGet(
2318
+ `/port-allocations`,
2319
+ { repo_id: repoId }
2320
+ );
2321
+ const allAllocations = portsRes.data ?? [];
2322
+ const worktreeId2 = currentConfig.worktree_id;
2323
+ const filtered = worktreeId2 ? allAllocations.filter((a) => a.worktree_id === worktreeId2) : allAllocations.filter((a) => !a.worktree_id);
2324
+ const ALLOWED_FIELDS = [
2325
+ "id",
2326
+ "repo_id",
2327
+ "port",
2328
+ "label",
2329
+ "server_type",
2330
+ "auto_start",
2331
+ "command",
2332
+ "working_dir",
2333
+ "env_vars",
2334
+ "external_refs",
2335
+ "worktree_id",
2336
+ "created_at",
2337
+ "updated_at"
2338
+ ];
2339
+ portAllocations = filtered.map((a) => {
2340
+ const clean = {};
2341
+ for (const key of ALLOWED_FIELDS) {
2342
+ if (key in a) clean[key] = a[key];
2343
+ }
2344
+ return clean;
2345
+ });
2346
+ } catch {
2347
+ }
2348
+ const worktreeId = currentConfig.worktree_id;
2349
+ const matchingAlloc = portAllocations[0];
2350
+ const defaultBranchConfig = {
2351
+ protected: ["main", "development"],
2352
+ integration: "development",
2353
+ production: "main",
2354
+ staging: null
2355
+ };
2356
+ const branchConfig = repo.branch_config ?? defaultBranchConfig;
2357
+ const newConfig = {
2358
+ repo_id: repoId,
2359
+ ...worktreeId ? { worktree_id: worktreeId } : {},
2360
+ server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
2361
+ server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
2362
+ git_branch: repo.git_branch ?? "development",
2363
+ auto_push_enabled: repo.auto_push_enabled,
2364
+ branch_config: branchConfig,
2365
+ ...portAllocations.length > 0 ? { port_allocations: portAllocations } : {}
2366
+ };
2367
+ const currentJson = JSON.stringify(currentConfig, null, 2);
2368
+ const newJson = JSON.stringify(newConfig, null, 2);
2369
+ if (currentJson === newJson) {
2370
+ console.log(" Config up to date.");
2371
+ return;
2372
+ }
2373
+ if (dryRun) {
2374
+ console.log(" Config would be updated (dry-run).");
2375
+ return;
2376
+ }
2377
+ await writeFile3(configPath, newJson + "\n", "utf-8");
2378
+ console.log(" Updated .codebyplan.json");
2379
+ }
2380
+ async function syncTechStack(repoId, projectPath, dryRun) {
2381
+ try {
2382
+ const { dependencies } = await scanAllDependencies(projectPath);
2383
+ if (dependencies.length === 0) {
2384
+ console.log(" No dependencies found.");
2385
+ return;
2386
+ }
2387
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
2388
+ console.log(
2389
+ ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
2390
+ );
2391
+ if (!dryRun) {
2392
+ const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
2393
+ if (result.data.stale_removed > 0) {
2394
+ console.log(
2395
+ ` ${result.data.stale_removed} stale dependencies removed`
2396
+ );
2397
+ }
2398
+ }
2399
+ const detected = await detectTechStack(projectPath);
2400
+ if (detected.flat.length > 0) {
2401
+ const repoRes = await apiGet(`/repos/${repoId}`);
2402
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
2403
+ const { merged, added } = mergeTechStack(remote, detected);
2404
+ if (added.length > 0) {
2405
+ console.log(` ${added.length} new tech entries`);
2406
+ if (!dryRun) {
2407
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
2408
+ }
2409
+ }
2410
+ }
2411
+ } catch {
2412
+ console.log(" Tech stack detection skipped.");
2413
+ }
2414
+ }
2415
+ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
2416
+ try {
2417
+ const portsRes = await apiGet(
2418
+ `/port-allocations`,
2419
+ { repo_id: repoId }
2420
+ );
2421
+ const allocations = portsRes.data ?? [];
2422
+ if (allocations.length === 0) {
2423
+ console.log(" No port allocations found \u2014 skipping verification.");
2424
+ return;
2425
+ }
2426
+ const mismatches = await verifyPorts(projectPath, allocations);
2427
+ if (mismatches.length > 0) {
2428
+ console.log(` Port mismatches: ${mismatches.length}`);
2429
+ for (const m of mismatches) {
2430
+ console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
2431
+ }
2432
+ }
2433
+ const unallocated = await findUnallocatedApps(projectPath, allocations);
2434
+ if (unallocated.length > 0) {
2435
+ console.log(` Unallocated apps: ${unallocated.length}`);
2436
+ for (const app of unallocated) {
2437
+ console.log(
2438
+ ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
2439
+ );
2440
+ }
2441
+ if (fix && !dryRun) {
2442
+ const maxPort = Math.max(...allocations.map((a) => a.port), 2999);
2443
+ let nextPort = maxPort + 1;
2444
+ for (const app of unallocated) {
2445
+ const port = app.detectedPort ?? nextPort++;
2446
+ try {
2447
+ await apiPost("/port-allocations", {
2448
+ repo_id: repoId,
2449
+ port,
2450
+ label: app.name,
2451
+ server_type: app.framework,
2452
+ auto_start: "manual",
2453
+ command: app.command,
2454
+ working_dir: app.path
2455
+ });
2456
+ console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
2457
+ } catch (err) {
2458
+ const msg = err instanceof Error ? err.message : String(err);
2459
+ console.log(
2460
+ ` Failed to create allocation for ${app.name}: ${msg}`
2461
+ );
2462
+ }
2463
+ if (app.detectedPort && app.detectedPort >= nextPort) {
2464
+ nextPort = app.detectedPort + 1;
2465
+ }
2466
+ }
2467
+ } else if (fix && dryRun) {
2468
+ console.log(" (dry-run \u2014 would create allocations with --fix)");
2469
+ } else {
2470
+ console.log(" Run with --fix to auto-create allocations.");
2471
+ }
2472
+ }
2473
+ if (mismatches.length === 0 && unallocated.length === 0) {
2474
+ console.log(" Ports verified.");
2475
+ }
2476
+ } catch {
2477
+ console.log(" Port verification skipped.");
2478
+ }
2479
+ }
2480
+ function groupByType(items) {
2481
+ const groups = /* @__PURE__ */ new Map();
2482
+ const typeLabels = {
2483
+ command: "Commands",
2484
+ agent: "Agents",
2485
+ skill: "Skills",
2486
+ rule: "Rules",
2487
+ hook: "Hooks",
2488
+ template: "Templates",
2489
+ settings: "Settings"
2490
+ };
2491
+ for (const item of items) {
2492
+ const label = typeLabels[item.type] ?? item.type;
2493
+ const group = groups.get(label) ?? [];
2494
+ group.push(item);
2495
+ groups.set(label, group);
2496
+ }
2497
+ return groups;
2498
+ }
2499
+ function getLocalFilePath(claudeDir, projectPath, remote) {
2500
+ const typeConfig2 = {
2501
+ command: { dir: "commands", ext: ".md" },
2502
+ agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
2503
+ skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
2504
+ rule: { dir: "rules", ext: ".md" },
2505
+ hook: { dir: "hooks", ext: ".sh" },
2506
+ template: { dir: "templates", ext: "" },
2507
+ claude_md: { dir: "", ext: "" },
2508
+ settings: { dir: "", ext: "" }
2509
+ };
2510
+ if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
2511
+ if (remote.type === "settings") return join7(claudeDir, "settings.json");
2512
+ const cfg = typeConfig2[remote.type];
2513
+ if (!cfg) return join7(claudeDir, remote.name);
2514
+ const typeDir = remote.type === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
2515
+ if (cfg.subfolder)
2516
+ return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
2517
+ if (remote.type === "command" && remote.category)
2518
+ return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
2519
+ if (remote.type === "template") return join7(typeDir, remote.name);
2520
+ return join7(typeDir, `${remote.name}${cfg.ext}`);
2521
+ }
2522
+ function getSyncVersion() {
2523
+ try {
2524
+ return "1.0.0";
2525
+ } catch {
2526
+ return "unknown";
2527
+ }
2528
+ }
2529
+ function flattenSyncData(data) {
2530
+ const result = /* @__PURE__ */ new Map();
2531
+ const typeMap = {
2532
+ commands: "command",
2533
+ agents: "agent",
2534
+ skills: "skill",
2535
+ rules: "rule",
2536
+ hooks: "hook",
2537
+ templates: "template",
2538
+ settings: "settings"
2539
+ };
2540
+ for (const [syncKey, typeName] of Object.entries(typeMap)) {
2541
+ const files = data[syncKey] ?? [];
2542
+ for (const file of files) {
2543
+ const key = compositeKey(typeName, file.name, file.category ?? null);
2544
+ result.set(key, {
2545
+ id: file.id,
2546
+ type: typeName,
2547
+ name: file.name,
2548
+ content: file.content,
2549
+ category: file.category,
2550
+ updated_at: file.updated_at,
2551
+ content_hash: file.content_hash
2552
+ });
2553
+ }
2554
+ }
2555
+ return result;
2556
+ }
2557
+ var init_sync = __esm({
2558
+ "src/cli/sync.ts"() {
2559
+ "use strict";
2560
+ init_config();
2561
+ init_fileMapper();
2562
+ init_confirm();
2563
+ init_api();
2564
+ init_variables();
2565
+ init_tech_detect();
2566
+ init_settings_merge();
2567
+ init_hook_registry();
2568
+ init_port_verify();
2569
+ }
2570
+ });
2571
+
2572
+ // src/index.ts
2573
+ init_version();
2574
+ import { readFileSync } from "node:fs";
2575
+ import { resolve } from "node:path";
2576
+ if (!process.env.CODEBYPLAN_API_KEY) {
2577
+ try {
2578
+ const envPath = resolve(process.cwd(), ".env.local");
2579
+ const content = readFileSync(envPath, "utf-8");
2580
+ for (const line of content.split("\n")) {
2581
+ const trimmed = line.trim();
2582
+ if (!trimmed || trimmed.startsWith("#")) continue;
2583
+ const eq = trimmed.indexOf("=");
2584
+ if (eq === -1) continue;
2585
+ const key = trimmed.slice(0, eq).trim();
2586
+ const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
2587
+ if (!process.env[key]) process.env[key] = val;
2588
+ }
2589
+ } catch {
2590
+ }
2591
+ }
2592
+ if (process.env.CODEBYPLAN_API_KEY?.startsWith("CODEBYPLAN_API_KEY=")) {
2593
+ process.env.CODEBYPLAN_API_KEY = process.env.CODEBYPLAN_API_KEY.slice(
2594
+ "CODEBYPLAN_API_KEY=".length
2595
+ );
2596
+ }
2597
+ var arg = process.argv[2];
2598
+ if (arg === "--version" || arg === "-v") {
2599
+ console.log(VERSION);
2600
+ process.exit(0);
2601
+ }
2602
+ if (arg === "setup") {
2603
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
2604
+ await runSetup2();
2605
+ process.exit(0);
2606
+ }
2607
+ if (arg === "sync") {
2608
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
2609
+ const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
2610
+ try {
2611
+ await runSync2();
2612
+ } catch (err) {
2613
+ if (err instanceof SyncCancelledError2) {
2614
+ console.log("\n Sync cancelled.\n");
2615
+ process.exit(0);
2616
+ }
2617
+ throw err;
2618
+ }
2619
+ process.exit(0);
2620
+ }
2621
+ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2622
+ console.log(`
2623
+ CodeByPlan CLI v${VERSION}
2624
+
2625
+ Usage:
2626
+ codebyplan setup Interactive setup (API key + project init + first sync)
2627
+ codebyplan sync Bidirectional sync (pull + push + config)
2628
+ codebyplan help Show this help message
2629
+ codebyplan --version Print version
2630
+
2631
+ Sync options:
2632
+ --path <dir> Project root directory (default: cwd)
2633
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
2634
+ --dry-run Preview changes without writing
2635
+ --force Skip confirmation and conflict prompts
2636
+ --fix Auto-create missing port allocations
2637
+
2638
+ MCP Server:
2639
+ Claude Code connects to CodeByPlan via remote MCP:
2640
+ URL: https://codebyplan.com/mcp
2641
+ Auth: x-api-key header (configured during setup)
2642
+
2643
+ Learn more: https://codebyplan.com
2644
+ `);
2645
+ process.exit(0);
2646
+ }
2647
+ console.error(`Unknown command: ${arg}`);
2648
+ console.error("Run 'codebyplan help' for usage.");
2649
+ process.exit(1);