codebyplan 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +48 -5
  2. package/dist/cli.js +671 -2240
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.4.3";
17
+ VERSION = "1.5.1";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -117,9 +117,6 @@ async function apiPost(path, body) {
117
117
  async function apiPut(path, body) {
118
118
  return request("PUT", path, { body });
119
119
  }
120
- async function apiDelete(path, params) {
121
- await request("DELETE", path, { params });
122
- }
123
120
  var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
124
121
  var init_api = __esm({
125
122
  "src/lib/api.ts"() {
@@ -198,6 +195,25 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
198
195
  }
199
196
  return worktreeId;
200
197
  }
198
+ async function resolveWorktreeId({
199
+ repoId,
200
+ repoPath,
201
+ branch,
202
+ deviceId
203
+ }) {
204
+ try {
205
+ const res = await apiPost(
206
+ "/worktrees/resolve",
207
+ { repo_id: repoId, device_id: deviceId, repo_path: repoPath, branch }
208
+ );
209
+ return res.worktree_id ?? null;
210
+ } catch (err) {
211
+ console.error(
212
+ `Tuple worktree resolve failed: ${err instanceof Error ? err.message : String(err)}`
213
+ );
214
+ return null;
215
+ }
216
+ }
201
217
  var init_resolve_worktree = __esm({
202
218
  "src/lib/resolve-worktree.ts"() {
203
219
  "use strict";
@@ -205,630 +221,75 @@ var init_resolve_worktree = __esm({
205
221
  }
206
222
  });
207
223
 
208
- // src/lib/settings-merge.ts
209
- function mergeSettings(template, local) {
210
- const merged = { ...local };
211
- for (const key of TEMPLATE_MANAGED_KEYS) {
212
- if (key in template) {
213
- merged[key] = template[key];
214
- }
215
- }
216
- if (template.permissions && typeof template.permissions === "object") {
217
- const templatePerms = template.permissions;
218
- const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
219
- const mergedPerms = { ...localPerms };
220
- for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
221
- if (key in templatePerms) {
222
- mergedPerms[key] = templatePerms[key];
223
- }
224
- }
225
- merged.permissions = mergedPerms;
226
- }
227
- return merged;
228
- }
229
- function mergeGlobalAndRepoSettings(global, repo) {
230
- const merged = { ...global, ...repo };
231
- const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
232
- const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
233
- if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
234
- const mergedPerms = { ...globalPerms, ...repoPerms };
235
- for (const key of ARRAY_PERMISSION_KEYS) {
236
- const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
237
- const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
238
- if (globalArr.length > 0 || repoArr.length > 0) {
239
- mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
240
- }
241
- }
242
- merged.permissions = mergedPerms;
243
- }
244
- return merged;
245
- }
246
- function stripPermissionsAllow(settings) {
247
- if (!settings.permissions || typeof settings.permissions !== "object") {
248
- return settings;
249
- }
250
- const perms = { ...settings.permissions };
251
- delete perms.allow;
252
- if (Object.keys(perms).length === 0) {
253
- const { permissions: _, ...rest } = settings;
254
- return rest;
255
- }
256
- return { ...settings, permissions: perms };
257
- }
258
- var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
259
- var init_settings_merge = __esm({
260
- "src/lib/settings-merge.ts"() {
261
- "use strict";
262
- TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
263
- TEMPLATE_MANAGED_PERMISSION_KEYS = [
264
- "deny",
265
- "ask",
266
- "additionalDirectories"
267
- ];
268
- ARRAY_PERMISSION_KEYS = ["deny", "ask"];
269
- }
270
- });
271
-
272
- // src/lib/hook-registry.ts
273
- import { readdir, readFile as readFile2 } from "node:fs/promises";
224
+ // src/lib/local-config.ts
225
+ import { execSync } from "node:child_process";
226
+ import { createHash } from "node:crypto";
227
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
228
+ import { hostname } from "node:os";
274
229
  import { join as join2 } from "node:path";
275
- function parseHookMeta(content) {
276
- const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
277
- if (!lineMatch) return null;
278
- const parts = lineMatch[1].trim().split(/\s+/);
279
- const event = parts[0];
280
- if (!event) return null;
281
- return {
282
- event,
283
- matcher: parts.slice(1).join(" ")
284
- };
230
+ function localConfigPath(projectPath) {
231
+ return join2(projectPath, ".codebyplan.local.json");
285
232
  }
286
- async function discoverHooks(hooksDir) {
287
- const discovered = /* @__PURE__ */ new Map();
288
- let filenames;
233
+ async function readLocalConfig(projectPath) {
289
234
  try {
290
- const entries = await readdir(hooksDir);
291
- filenames = entries.filter((e) => e.endsWith(".sh"));
292
- } catch {
293
- return discovered;
294
- }
295
- for (const filename of filenames) {
296
- const content = await readFile2(join2(hooksDir, filename), "utf-8");
297
- const meta = parseHookMeta(content);
298
- if (meta) {
299
- discovered.set(filename.replace(/\.sh$/, ""), meta);
300
- }
301
- }
302
- return discovered;
303
- }
304
- function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
305
- if (discovered.size === 0) return existing;
306
- const merged = {};
307
- for (const [event, matchers] of Object.entries(existing)) {
308
- merged[event] = matchers.map((m) => ({
309
- matcher: m.matcher,
310
- hooks: [...m.hooks]
311
- }));
312
- }
313
- for (const [filename, meta] of discovered) {
314
- const command = `bash ${hooksRelPath}/${filename}.sh`;
315
- if (!merged[meta.event]) {
316
- merged[meta.event] = [];
235
+ const raw = await readFile2(localConfigPath(projectPath), "utf-8");
236
+ const parsed = JSON.parse(raw);
237
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
238
+ return parsed;
317
239
  }
318
- const eventEntries = merged[meta.event];
319
- const alreadyRegistered = eventEntries.some(
320
- (m) => m.hooks.some((h) => h.command === command)
240
+ console.error("Failed to read local config: invalid shape");
241
+ return null;
242
+ } catch (err) {
243
+ console.error(
244
+ `Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
321
245
  );
322
- if (alreadyRegistered) continue;
323
- const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
324
- if (matcherEntry) {
325
- matcherEntry.hooks.push({ type: "command", command });
326
- } else {
327
- eventEntries.push({
328
- matcher: meta.matcher,
329
- hooks: [{ type: "command", command }]
330
- });
331
- }
332
- }
333
- return merged;
334
- }
335
- function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
336
- const prefix = `bash ${hooksRelPath}/`;
337
- const stripped = {};
338
- for (const [event, matchers] of Object.entries(config)) {
339
- const filteredMatchers = [];
340
- for (const matcher of matchers) {
341
- const filteredHooks = matcher.hooks.filter(
342
- (h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
343
- );
344
- if (filteredHooks.length > 0) {
345
- filteredMatchers.push({
346
- matcher: matcher.matcher,
347
- hooks: filteredHooks
348
- });
349
- }
350
- }
351
- if (filteredMatchers.length > 0) {
352
- stripped[event] = filteredMatchers;
353
- }
354
- }
355
- return stripped;
356
- }
357
- var init_hook_registry = __esm({
358
- "src/lib/hook-registry.ts"() {
359
- "use strict";
360
- }
361
- });
362
-
363
- // src/lib/variables.ts
364
- function splitFrontmatter(content) {
365
- const fmMatch = content.match(/^(---\s*\n[\s\S]*?\n---\n?)([\s\S]*)$/);
366
- if (fmMatch) {
367
- return { frontmatter: fmMatch[1], body: fmMatch[2] };
368
- }
369
- if (content.startsWith("#!/") || content.startsWith("# @")) {
370
- const lines = content.split("\n");
371
- let headerEnd = 0;
372
- for (let i = 0; i < lines.length; i++) {
373
- if (lines[i].startsWith("#") || lines[i].startsWith("#!/") || lines[i].trim() === "") {
374
- headerEnd = i + 1;
375
- } else {
376
- break;
377
- }
378
- }
379
- return {
380
- frontmatter: lines.slice(0, headerEnd).join("\n") + "\n",
381
- body: lines.slice(headerEnd).join("\n")
382
- };
246
+ return null;
383
247
  }
384
- return { frontmatter: "", body: content };
385
248
  }
386
- function substituteVariables(content, repoData) {
387
- if (!content.includes("{{")) return content;
388
- const { frontmatter, body } = splitFrontmatter(content);
389
- let result = body;
390
- for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
391
- const placeholder = `{{${name}}}`;
392
- if (result.includes(placeholder)) {
393
- result = result.replaceAll(placeholder, resolver(repoData));
394
- }
395
- }
396
- return frontmatter + result;
397
- }
398
- function escapeRegex(str) {
399
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
400
- }
401
- function reverseSubstituteVariables(content, repoData) {
402
- const { frontmatter, body } = splitFrontmatter(content);
403
- const entries = [];
404
- for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
405
- const value = resolver(repoData);
406
- if (value.length === 0) continue;
407
- entries.push([value, `{{${name}}}`]);
408
- }
409
- entries.sort((a, b) => b[0].length - a[0].length);
410
- let result = body;
411
- for (const [value, placeholder] of entries) {
412
- if (value.length < 8) {
413
- const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
414
- result = result.replace(pattern, placeholder);
415
- } else {
416
- result = result.replaceAll(value, placeholder);
417
- }
418
- }
419
- return frontmatter + result;
420
- }
421
- var TEMPLATE_VARIABLES;
422
- var init_variables = __esm({
423
- "src/lib/variables.ts"() {
424
- "use strict";
425
- TEMPLATE_VARIABLES = {
426
- REPO_ID: (repo) => repo.id,
427
- REPO_NAME: (repo) => repo.name,
428
- REPO_PATH: (repo) => repo.path ?? "",
429
- GIT_BRANCH: (repo) => repo.git_branch ?? "development",
430
- SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
431
- SERVER_TYPE: (repo) => repo.server_type ?? "none"
432
- };
433
- }
434
- });
435
-
436
- // src/lib/sync-engine.ts
437
- var sync_engine_exports = {};
438
- __export(sync_engine_exports, {
439
- executeSyncToLocal: () => executeSyncToLocal
440
- });
441
- import {
442
- readdir as readdir2,
443
- readFile as readFile3,
444
- writeFile as writeFile2,
445
- unlink,
446
- mkdir,
447
- rmdir,
448
- chmod,
449
- stat
450
- } from "node:fs/promises";
451
- import { join as join3, dirname } from "node:path";
452
- function getTypeDir(claudeDir, dir) {
453
- if (dir === "commands") return join3(claudeDir, dir, "cbp");
454
- return join3(claudeDir, dir);
455
- }
456
- function getFilePath(claudeDir, typeName, file) {
457
- const cfg = typeConfig[typeName];
458
- const typeDir = getTypeDir(claudeDir, cfg.dir);
459
- if (cfg.subfolder) {
460
- return join3(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
461
- }
462
- if (typeName === "command" && file.category) {
463
- return join3(typeDir, file.category, `${file.name}${cfg.ext}`);
464
- }
465
- if (typeName === "template") {
466
- return join3(typeDir, file.name);
467
- }
468
- return join3(typeDir, `${file.name}${cfg.ext}`);
469
- }
470
- async function readDirRecursive(dir, base = dir) {
471
- const result = /* @__PURE__ */ new Map();
249
+ async function writeLocalConfig(projectPath, config) {
250
+ const content = { device_id: config.device_id };
472
251
  try {
473
- const entries = await readdir2(dir, { withFileTypes: true });
474
- for (const entry of entries) {
475
- const fullPath = join3(dir, entry.name);
476
- if (entry.isDirectory()) {
477
- const sub = await readDirRecursive(fullPath, base);
478
- for (const [k, v] of sub) result.set(k, v);
479
- } else {
480
- const relPath = fullPath.slice(base.length + 1);
481
- const fileContent = await readFile3(fullPath, "utf-8");
482
- result.set(relPath, fileContent);
483
- }
484
- }
485
- } catch {
252
+ await writeFile2(
253
+ localConfigPath(projectPath),
254
+ JSON.stringify(content, null, 2) + "\n",
255
+ "utf-8"
256
+ );
257
+ } catch (err) {
258
+ console.error(
259
+ `Failed to write local config: ${err instanceof Error ? err.message : String(err)}`
260
+ );
261
+ throw err;
486
262
  }
487
- return result;
488
263
  }
489
- async function isGitWorktree(projectPath) {
264
+ async function resolveMachineSeed() {
490
265
  try {
491
- const gitPath = join3(projectPath, ".git");
492
- const info = await stat(gitPath);
493
- return info.isFile();
266
+ const raw = await readFile2("/etc/machine-id", "utf-8");
267
+ const trimmed = raw.trim();
268
+ if (trimmed) return trimmed;
494
269
  } catch {
495
- return false;
496
270
  }
497
- }
498
- async function removeEmptyParents(filePath, stopAt) {
499
- let dir = dirname(filePath);
500
- while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
271
+ if (process.platform === "darwin") {
501
272
  try {
502
- await rmdir(dir);
503
- dir = dirname(dir);
273
+ const out = execSync("sysctl -n kern.uuid", { encoding: "utf-8" }).trim();
274
+ if (out) return out;
504
275
  } catch {
505
- break;
506
276
  }
507
277
  }
278
+ return hostname();
508
279
  }
509
- async function executeSyncToLocal(options) {
510
- const { repoId, projectPath, dryRun = false } = options;
511
- const [syncRes, repoRes] = await Promise.all([
512
- apiGet("/sync/defaults"),
513
- apiGet(`/repos/${repoId}`)
514
- ]);
515
- const syncData = syncRes.data;
516
- const repoData = repoRes.data;
517
- syncData.claude_md = [];
518
- const claudeDir = join3(projectPath, ".claude");
519
- const worktree = await isGitWorktree(projectPath);
520
- const byType = {};
521
- const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
522
- const dbOnlyFiles = [];
523
- for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
524
- if (worktree && typeName === "command") {
525
- byType["commands"] = {
526
- created: [],
527
- updated: [],
528
- deleted: [],
529
- unchanged: []
530
- };
531
- continue;
532
- }
533
- const cfg = typeConfig[typeName];
534
- const targetDir = getTypeDir(claudeDir, cfg.dir);
535
- const remoteFiles = syncData[syncKey] ?? [];
536
- const result = {
537
- created: [],
538
- updated: [],
539
- deleted: [],
540
- unchanged: []
541
- };
542
- if (!dryRun) {
543
- await mkdir(targetDir, { recursive: true });
544
- }
545
- const localFiles = await readDirRecursive(targetDir);
546
- const remotePathMap = /* @__PURE__ */ new Map();
547
- for (const remote of remoteFiles) {
548
- const fullPath = getFilePath(claudeDir, typeName, remote);
549
- const relPath = fullPath.slice(targetDir.length + 1);
550
- const substituted = substituteVariables(remote.content, repoData);
551
- remotePathMap.set(relPath, { content: substituted, name: remote.name });
552
- }
553
- for (const [relPath, { content, name }] of remotePathMap) {
554
- const fullPath = join3(targetDir, relPath);
555
- const localContent = localFiles.get(relPath);
556
- if (localContent === void 0) {
557
- const remoteFile = remoteFiles.find((f) => f.name === name);
558
- dbOnlyFiles.push({
559
- type: typeName,
560
- name,
561
- category: remoteFile?.category ?? null,
562
- localPath: fullPath
563
- });
564
- if (!dryRun) {
565
- await mkdir(dirname(fullPath), { recursive: true });
566
- await writeFile2(fullPath, content, "utf-8");
567
- if (typeName === "hook") await chmod(fullPath, 493);
568
- }
569
- result.created.push(name);
570
- totals.created++;
571
- } else if (localContent !== content) {
572
- if (!dryRun) {
573
- await writeFile2(fullPath, content, "utf-8");
574
- if (typeName === "hook") await chmod(fullPath, 493);
575
- }
576
- result.updated.push(name);
577
- totals.updated++;
578
- } else {
579
- result.unchanged.push(name);
580
- totals.unchanged++;
581
- }
582
- }
583
- for (const [relPath] of localFiles) {
584
- if (!remotePathMap.has(relPath)) {
585
- const fullPath = join3(targetDir, relPath);
586
- if (!dryRun) {
587
- await unlink(fullPath);
588
- await removeEmptyParents(fullPath, targetDir);
589
- }
590
- const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
591
- result.deleted.push(pathName);
592
- totals.deleted++;
593
- }
594
- }
595
- byType[`${typeName}s`] = result;
596
- }
597
- {
598
- const typeName = "docs_stack";
599
- const syncKey = "docs_stack";
600
- const targetDir = join3(projectPath, "docs", "stack");
601
- const remoteFiles = syncData[syncKey] ?? [];
602
- const result = {
603
- created: [],
604
- updated: [],
605
- deleted: [],
606
- unchanged: []
607
- };
608
- if (remoteFiles.length > 0 && !dryRun) {
609
- await mkdir(targetDir, { recursive: true });
610
- }
611
- const localFiles = await readDirRecursive(targetDir);
612
- const remotePathMap = /* @__PURE__ */ new Map();
613
- for (const remote of remoteFiles) {
614
- const relPath = remote.category ? join3(remote.category, remote.name) : remote.name;
615
- const substituted = substituteVariables(remote.content, repoData);
616
- remotePathMap.set(relPath, {
617
- content: substituted,
618
- name: remote.category ? `${remote.category}/${remote.name}` : remote.name
619
- });
620
- }
621
- for (const [relPath, { content, name }] of remotePathMap) {
622
- const fullPath = join3(targetDir, relPath);
623
- const localContent = localFiles.get(relPath);
624
- if (localContent === void 0) {
625
- if (!dryRun) {
626
- await mkdir(dirname(fullPath), { recursive: true });
627
- await writeFile2(fullPath, content, "utf-8");
628
- }
629
- result.created.push(name);
630
- totals.created++;
631
- } else if (localContent !== content) {
632
- if (!dryRun) {
633
- await writeFile2(fullPath, content, "utf-8");
634
- }
635
- result.updated.push(name);
636
- totals.updated++;
637
- } else {
638
- result.unchanged.push(name);
639
- totals.unchanged++;
640
- }
641
- }
642
- for (const [relPath] of localFiles) {
643
- if (!remotePathMap.has(relPath)) {
644
- const fullPath = join3(targetDir, relPath);
645
- if (!dryRun) {
646
- await unlink(fullPath);
647
- await removeEmptyParents(fullPath, targetDir);
648
- }
649
- result.deleted.push(relPath);
650
- totals.deleted++;
651
- }
652
- }
653
- byType[typeName] = result;
654
- }
655
- const globalSettingsFiles = syncData.global_settings ?? [];
656
- let globalSettings = {};
657
- for (const gf of globalSettingsFiles) {
658
- const parsed = JSON.parse(
659
- substituteVariables(gf.content, repoData)
660
- );
661
- globalSettings = { ...globalSettings, ...parsed };
662
- }
663
- const specialTypes = {
664
- claude_md: () => join3(projectPath, "CLAUDE.md"),
665
- settings: () => join3(projectPath, ".claude", "settings.json")
666
- };
667
- for (const [typeName, getPath] of Object.entries(specialTypes)) {
668
- const remoteFiles = syncData[typeName] ?? [];
669
- const result = {
670
- created: [],
671
- updated: [],
672
- deleted: [],
673
- unchanged: []
674
- };
675
- for (const remote of remoteFiles) {
676
- const targetPath = getPath(remote.name);
677
- const remoteContent = substituteVariables(remote.content, repoData);
678
- let localContent;
679
- try {
680
- localContent = await readFile3(targetPath, "utf-8");
681
- } catch {
682
- }
683
- if (typeName === "settings") {
684
- const repoSettings = JSON.parse(remoteContent);
685
- const combinedTemplate = mergeGlobalAndRepoSettings(
686
- globalSettings,
687
- repoSettings
688
- );
689
- const hooksDir = join3(projectPath, ".claude", "hooks");
690
- const discovered = await discoverHooks(hooksDir);
691
- if (localContent === void 0) {
692
- const finalSettings = stripPermissionsAllow(combinedTemplate);
693
- if (discovered.size > 0) {
694
- finalSettings.hooks = mergeDiscoveredHooks(
695
- finalSettings.hooks ?? {},
696
- discovered
697
- );
698
- }
699
- if (!dryRun) {
700
- await mkdir(dirname(targetPath), { recursive: true });
701
- await writeFile2(
702
- targetPath,
703
- JSON.stringify(finalSettings, null, 2) + "\n",
704
- "utf-8"
705
- );
706
- }
707
- result.created.push(remote.name);
708
- totals.created++;
709
- } else {
710
- const localSettings = JSON.parse(localContent);
711
- let merged = mergeSettings(combinedTemplate, localSettings);
712
- merged = stripPermissionsAllow(merged);
713
- if (discovered.size > 0) {
714
- merged.hooks = mergeDiscoveredHooks(
715
- merged.hooks ?? {},
716
- discovered
717
- );
718
- }
719
- const mergedContent = JSON.stringify(merged, null, 2) + "\n";
720
- if (localContent !== mergedContent) {
721
- if (!dryRun) {
722
- await writeFile2(targetPath, mergedContent, "utf-8");
723
- }
724
- result.updated.push(remote.name);
725
- totals.updated++;
726
- } else {
727
- result.unchanged.push(remote.name);
728
- totals.unchanged++;
729
- }
730
- }
731
- } else {
732
- if (localContent === void 0) {
733
- if (!dryRun) {
734
- await mkdir(dirname(targetPath), { recursive: true });
735
- await writeFile2(targetPath, remoteContent, "utf-8");
736
- }
737
- result.created.push(remote.name);
738
- totals.created++;
739
- } else if (localContent !== remoteContent) {
740
- if (!dryRun) {
741
- await writeFile2(targetPath, remoteContent, "utf-8");
742
- }
743
- result.updated.push(remote.name);
744
- totals.updated++;
745
- } else {
746
- result.unchanged.push(remote.name);
747
- totals.unchanged++;
748
- }
749
- }
750
- }
751
- byType[typeName] = result;
752
- }
753
- if (!dryRun) {
754
- await apiPost("/sync/state", {
755
- repo_id: repoId,
756
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
757
- was_skipped: false,
758
- files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
759
- files_pushed: 0,
760
- files_pulled: totals.created + totals.updated,
761
- files_deleted: totals.deleted,
762
- files_skipped: 0
763
- });
764
- const fileRepoUpdates = [];
765
- const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
766
- for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
767
- const remoteFiles = syncData[syncKey] ?? [];
768
- for (const file of remoteFiles) {
769
- fileRepoUpdates.push({
770
- claude_file_id: file.id ?? void 0,
771
- file_type: typeName,
772
- file_name: file.name,
773
- file_category: file.category ?? null,
774
- file_scope: file.scope ?? "shared",
775
- last_synced_at: syncTimestamp,
776
- sync_status: "synced"
777
- });
778
- }
779
- }
780
- for (const typeName of ["claude_md", "settings"]) {
781
- const remoteFiles = syncData[typeName] ?? [];
782
- for (const file of remoteFiles) {
783
- fileRepoUpdates.push({
784
- claude_file_id: file.id ?? void 0,
785
- file_type: typeName,
786
- file_name: file.name,
787
- file_category: file.category ?? null,
788
- file_scope: file.scope ?? `local:${repoId}`,
789
- last_synced_at: syncTimestamp,
790
- sync_status: "synced"
791
- });
792
- }
793
- }
794
- if (fileRepoUpdates.length > 0) {
795
- try {
796
- await apiPost("/sync/file-repos", {
797
- repo_id: repoId,
798
- file_repos: fileRepoUpdates
799
- });
800
- } catch {
801
- }
802
- }
280
+ async function getOrCreateDeviceId(projectPath) {
281
+ const existing = await readLocalConfig(projectPath);
282
+ if (existing?.device_id) {
283
+ return existing.device_id;
803
284
  }
804
- return { byType, totals, dbOnlyFiles };
285
+ const seed = await resolveMachineSeed();
286
+ const deviceId = createHash("sha256").update(seed).digest("hex").slice(0, 16);
287
+ await writeLocalConfig(projectPath, { device_id: deviceId });
288
+ return deviceId;
805
289
  }
806
- var typeConfig, syncKeyToType;
807
- var init_sync_engine = __esm({
808
- "src/lib/sync-engine.ts"() {
290
+ var init_local_config = __esm({
291
+ "src/lib/local-config.ts"() {
809
292
  "use strict";
810
- init_api();
811
- init_settings_merge();
812
- init_hook_registry();
813
- init_variables();
814
- typeConfig = {
815
- command: { dir: "commands", ext: ".md" },
816
- agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
817
- skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
818
- rule: { dir: "rules", ext: ".md" },
819
- hook: { dir: "hooks", ext: ".sh" },
820
- template: { dir: "templates", ext: "" },
821
- context: { dir: "context", ext: ".md" }
822
- };
823
- syncKeyToType = {
824
- commands: "command",
825
- agents: "agent",
826
- skills: "skill",
827
- rules: "rule",
828
- hooks: "hook",
829
- templates: "template",
830
- contexts: "context"
831
- };
832
293
  }
833
294
  });
834
295
 
@@ -839,15 +300,15 @@ __export(setup_exports, {
839
300
  });
840
301
  import { createInterface } from "node:readline/promises";
841
302
  import { stdin, stdout } from "node:process";
842
- import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
303
+ import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
843
304
  import { homedir } from "node:os";
844
- import { join as join4 } from "node:path";
305
+ import { join as join3 } from "node:path";
845
306
  function getConfigPath(scope) {
846
- return scope === "user" ? join4(homedir(), ".claude.json") : join4(process.cwd(), ".mcp.json");
307
+ return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
847
308
  }
848
309
  async function readConfig(path) {
849
310
  try {
850
- const raw = await readFile4(path, "utf-8");
311
+ const raw = await readFile3(path, "utf-8");
851
312
  const parsed = JSON.parse(raw);
852
313
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
853
314
  return parsed;
@@ -976,12 +437,29 @@ async function runSetup() {
976
437
  Selected: ${selectedRepo.name}
977
438
  `);
978
439
  const projectPath = process.cwd();
979
- const worktreeId = await resolveAndCacheWorktreeId(
440
+ const pathBasedId = await resolveAndCacheWorktreeId(
980
441
  selectedRepo.id,
981
442
  projectPath,
982
443
  { skipWrite: true }
983
444
  );
984
- const codebyplanPath = join4(projectPath, ".codebyplan.json");
445
+ const deviceId = await getOrCreateDeviceId(projectPath);
446
+ let branch = "main";
447
+ try {
448
+ const { execSync: execSync3 } = await import("node:child_process");
449
+ branch = execSync3("git symbolic-ref --short HEAD", {
450
+ cwd: projectPath,
451
+ encoding: "utf-8"
452
+ }).trim();
453
+ } catch {
454
+ }
455
+ const tupleId = await resolveWorktreeId({
456
+ repoId: selectedRepo.id,
457
+ repoPath: projectPath,
458
+ branch,
459
+ deviceId
460
+ });
461
+ const worktreeId = tupleId ?? pathBasedId;
462
+ const codebyplanPath = join3(projectPath, ".codebyplan.json");
985
463
  const codebyplanConfig = {
986
464
  repo_id: selectedRepo.id
987
465
  };
@@ -997,27 +475,6 @@ async function runSetup() {
997
475
  ` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
998
476
  );
999
477
  }
1000
- console.log("\n Running initial sync...\n");
1001
- try {
1002
- const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
1003
- const syncResult = await executeSyncToLocal2({
1004
- repoId: selectedRepo.id,
1005
- projectPath
1006
- });
1007
- const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
1008
- if (totalChanges > 0) {
1009
- console.log(
1010
- ` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
1011
- `
1012
- );
1013
- } else {
1014
- console.log(" All files already up to date.\n");
1015
- }
1016
- } catch (err) {
1017
- const msg = err instanceof Error ? err.message : String(err);
1018
- console.log(` Sync failed: ${msg}`);
1019
- console.log(" Run 'codebyplan sync' later to sync files.\n");
1020
- }
1021
478
  }
1022
479
  }
1023
480
  }
@@ -1032,18 +489,19 @@ var init_setup = __esm({
1032
489
  "src/cli/setup.ts"() {
1033
490
  "use strict";
1034
491
  init_resolve_worktree();
492
+ init_local_config();
1035
493
  }
1036
494
  });
1037
495
 
1038
- // src/cli/config.ts
1039
- import { readFile as readFile5 } from "node:fs/promises";
1040
- import { join as join5, resolve } from "node:path";
496
+ // src/cli/flags.ts
497
+ import { readFile as readFile4 } from "node:fs/promises";
498
+ import { join as join4, resolve } from "node:path";
1041
499
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1042
500
  let cursor = resolve(startDir);
1043
501
  for (let depth = 0; depth < maxDepth; depth++) {
1044
- const configPath = join5(cursor, ".codebyplan.json");
502
+ const configPath = join4(cursor, ".codebyplan.json");
1045
503
  try {
1046
- const raw = await readFile5(configPath, "utf-8");
504
+ const raw = await readFile4(configPath, "utf-8");
1047
505
  const parsed = JSON.parse(raw);
1048
506
  return { path: configPath, contents: parsed };
1049
507
  } catch {
@@ -1087,590 +545,72 @@ async function resolveConfig(flags) {
1087
545
  }
1088
546
  return { repoId, worktreeId, projectPath };
1089
547
  }
1090
- var init_config = __esm({
1091
- "src/cli/config.ts"() {
548
+ var init_flags = __esm({
549
+ "src/cli/flags.ts"() {
1092
550
  "use strict";
1093
551
  }
1094
552
  });
1095
553
 
1096
- // src/cli/fileMapper.ts
1097
- import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
1098
- import { join as join6, extname } from "node:path";
1099
- function extractScope(content, type) {
1100
- if (type === "hook") {
1101
- const match = content.match(/^#\s*@scope:\s*(\S+)/m);
1102
- if (match) {
1103
- const raw = match[1];
1104
- return raw === "shared" ? "shared" : `local:${raw}`;
1105
- }
1106
- return "shared";
1107
- }
1108
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
1109
- if (fmMatch) {
1110
- const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
1111
- if (scopeLine) {
1112
- const raw = scopeLine[1];
1113
- return raw === "shared" ? "shared" : `local:${raw}`;
1114
- }
1115
- if (/^scope\b/m.test(fmMatch[1])) {
1116
- console.error(
1117
- ` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
1118
- );
1119
- }
1120
- }
1121
- return "shared";
1122
- }
1123
- function compositeKey(type, name, category) {
1124
- return category ? `${type}:${category}/${name}` : `${type}:${name}`;
1125
- }
1126
- async function scanLocalFiles(claudeDir, projectPath) {
1127
- const result = /* @__PURE__ */ new Map();
1128
- await scanCommands(join6(claudeDir, "commands", "cbp"), result);
1129
- await scanSubfolderType(
1130
- join6(claudeDir, "agents"),
1131
- "agent",
1132
- "AGENT.md",
1133
- result
1134
- );
1135
- await scanSubfolderType(
1136
- join6(claudeDir, "skills"),
1137
- "skill",
1138
- "SKILL.md",
1139
- result
1140
- );
1141
- await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
1142
- await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
1143
- await scanTemplates(join6(claudeDir, "templates"), result);
1144
- await scanCategorizedType(
1145
- join6(claudeDir, "context"),
1146
- "context",
1147
- ".md",
1148
- result
1149
- );
1150
- await scanDocsRecursive(join6(claudeDir, "docs"), result);
1151
- await scanSettings(claudeDir, projectPath, result);
1152
- return result;
1153
- }
1154
- async function scanCommands(dir, result) {
1155
- await scanCommandsRecursive(dir, dir, result);
554
+ // src/cli/confirm.ts
555
+ var confirm_exports = {};
556
+ __export(confirm_exports, {
557
+ SyncCancelledError: () => SyncCancelledError,
558
+ confirmProceed: () => confirmProceed
559
+ });
560
+ import { createInterface as createInterface2 } from "node:readline/promises";
561
+ import { stdin as stdin2, stdout as stdout2 } from "node:process";
562
+ function isAbortError(err) {
563
+ return err instanceof Error && err.code === "ABORT_ERR";
1156
564
  }
1157
- async function scanCommandsRecursive(baseDir, currentDir, result) {
1158
- let entries;
565
+ async function confirmProceed(message) {
566
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1159
567
  try {
1160
- entries = await readdir3(currentDir, { withFileTypes: true });
1161
- } catch {
1162
- return;
1163
- }
1164
- for (const entry of entries) {
1165
- if (entry.isDirectory()) {
1166
- await scanCommandsRecursive(
1167
- baseDir,
1168
- join6(currentDir, entry.name),
1169
- result
568
+ while (true) {
569
+ const answer = await rl.question(message ?? " Proceed? [Y/n] ");
570
+ const a = answer.trim().toLowerCase();
571
+ if (a === "" || a === "y" || a === "yes") return true;
572
+ if (a === "n" || a === "no") return false;
573
+ console.log(
574
+ ` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
1170
575
  );
1171
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1172
- const name = entry.name.slice(0, -3);
1173
- const content = await readFile6(join6(currentDir, entry.name), "utf-8");
1174
- const relDir = currentDir.slice(baseDir.length + 1);
1175
- const category = relDir || null;
1176
- const scope = extractScope(content, "command");
1177
- const key = compositeKey("command", name, category);
1178
- result.set(key, { type: "command", name, category, content, scope });
1179
576
  }
577
+ } catch (err) {
578
+ if (isAbortError(err)) throw new SyncCancelledError();
579
+ throw err;
580
+ } finally {
581
+ rl.close();
1180
582
  }
1181
583
  }
1182
- async function scanSubfolderType(dir, type, fileName, result) {
1183
- let entries;
1184
- try {
1185
- entries = await readdir3(dir, { withFileTypes: true });
1186
- } catch {
1187
- return;
1188
- }
1189
- for (const entry of entries) {
1190
- if (entry.isDirectory()) {
1191
- const filePath = join6(dir, entry.name, fileName);
1192
- try {
1193
- const content = await readFile6(filePath, "utf-8");
1194
- const scope = extractScope(content, type);
1195
- const key = compositeKey(type, entry.name, null);
1196
- result.set(key, {
1197
- type,
1198
- name: entry.name,
1199
- category: null,
1200
- content,
1201
- scope
1202
- });
1203
- } catch {
584
+ var SyncCancelledError;
585
+ var init_confirm = __esm({
586
+ "src/cli/confirm.ts"() {
587
+ "use strict";
588
+ SyncCancelledError = class extends Error {
589
+ constructor() {
590
+ super("Sync cancelled");
591
+ this.name = "SyncCancelledError";
1204
592
  }
1205
- }
593
+ };
1206
594
  }
1207
- }
1208
- async function scanFlatType(dir, type, ext, result) {
1209
- let entries;
595
+ });
596
+
597
+ // src/lib/tech-detect.ts
598
+ import { readFile as readFile5, access, readdir } from "node:fs/promises";
599
+ import { join as join5, relative } from "node:path";
600
+ async function fileExists(filePath) {
1210
601
  try {
1211
- entries = await readdir3(dir, { withFileTypes: true });
602
+ await access(filePath);
603
+ return true;
1212
604
  } catch {
1213
- return;
1214
- }
1215
- for (const entry of entries) {
1216
- if (entry.isFile() && entry.name.endsWith(ext)) {
1217
- const name = entry.name.slice(0, -ext.length);
1218
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1219
- const scope = extractScope(content, type);
1220
- const key = compositeKey(type, name, null);
1221
- result.set(key, { type, name, category: null, content, scope });
1222
- }
1223
- }
1224
- }
1225
- async function scanCategorizedType(dir, type, ext, result) {
1226
- let entries;
1227
- try {
1228
- entries = await readdir3(dir, { withFileTypes: true });
1229
- } catch {
1230
- return;
1231
- }
1232
- for (const entry of entries) {
1233
- if (entry.isDirectory()) {
1234
- const category = entry.name;
1235
- let subEntries;
1236
- try {
1237
- subEntries = await readdir3(join6(dir, category), {
1238
- withFileTypes: true
1239
- });
1240
- } catch {
1241
- continue;
1242
- }
1243
- for (const sub of subEntries) {
1244
- if (sub.isFile() && sub.name.endsWith(ext)) {
1245
- const name = sub.name.slice(0, -ext.length);
1246
- const content = await readFile6(
1247
- join6(dir, category, sub.name),
1248
- "utf-8"
1249
- );
1250
- const scope = extractScope(content, type);
1251
- const key = compositeKey(type, name, category);
1252
- result.set(key, { type, name, category, content, scope });
1253
- }
1254
- }
1255
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
1256
- const name = entry.name.slice(0, -ext.length);
1257
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1258
- const scope = extractScope(content, type);
1259
- const key = compositeKey(type, name, null);
1260
- result.set(key, { type, name, category: null, content, scope });
1261
- }
1262
- }
1263
- }
1264
- async function scanDocsRecursive(docsDir, result) {
1265
- await scanDocsDir(docsDir, docsDir, result);
1266
- }
1267
- async function scanDocsDir(baseDir, currentDir, result) {
1268
- let entries;
1269
- try {
1270
- entries = await readdir3(currentDir, { withFileTypes: true });
1271
- } catch {
1272
- return;
1273
- }
1274
- for (const entry of entries) {
1275
- if (entry.isDirectory()) {
1276
- await scanDocsDir(baseDir, join6(currentDir, entry.name), result);
1277
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1278
- const name = entry.name.slice(0, -3);
1279
- const content = await readFile6(join6(currentDir, entry.name), "utf-8");
1280
- const scope = extractScope(content, "docs");
1281
- const relDir = currentDir.slice(baseDir.length + 1);
1282
- const category = relDir || null;
1283
- const key = compositeKey("docs", name, category);
1284
- result.set(key, { type: "docs", name, category, content, scope });
1285
- }
1286
- }
1287
- }
1288
- async function scanTemplates(dir, result) {
1289
- let entries;
1290
- try {
1291
- entries = await readdir3(dir, { withFileTypes: true });
1292
- } catch {
1293
- return;
1294
- }
1295
- for (const entry of entries) {
1296
- if (entry.isFile() && extname(entry.name)) {
1297
- const content = await readFile6(join6(dir, entry.name), "utf-8");
1298
- const scope = extractScope(content, "template");
1299
- const key = compositeKey("template", entry.name, null);
1300
- result.set(key, {
1301
- type: "template",
1302
- name: entry.name,
1303
- category: null,
1304
- content,
1305
- scope
1306
- });
1307
- }
1308
- }
1309
- }
1310
- async function scanSettings(claudeDir, projectPath, result) {
1311
- const settingsPath = join6(claudeDir, "settings.json");
1312
- let raw;
1313
- try {
1314
- raw = await readFile6(settingsPath, "utf-8");
1315
- } catch {
1316
- return;
1317
- }
1318
- let parsed;
1319
- try {
1320
- parsed = JSON.parse(raw);
1321
- } catch {
1322
- return;
1323
- }
1324
- parsed = stripPermissionsAllow(parsed);
1325
- if (parsed.hooks && typeof parsed.hooks === "object") {
1326
- const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
1327
- const discovered = await discoverHooks(hooksDir);
1328
- if (discovered.size > 0) {
1329
- parsed.hooks = stripDiscoveredHooks(
1330
- parsed.hooks,
1331
- ".claude/hooks"
1332
- );
1333
- if (Object.keys(parsed.hooks).length === 0) {
1334
- delete parsed.hooks;
1335
- }
1336
- }
1337
- }
1338
- const content = JSON.stringify(parsed, null, 2) + "\n";
1339
- const key = compositeKey("settings", "settings", null);
1340
- result.set(key, {
1341
- type: "settings",
1342
- name: "settings",
1343
- category: null,
1344
- content,
1345
- scope: "shared"
1346
- });
1347
- }
1348
- var init_fileMapper = __esm({
1349
- "src/cli/fileMapper.ts"() {
1350
- "use strict";
1351
- init_settings_merge();
1352
- init_hook_registry();
1353
- }
1354
- });
1355
-
1356
- // src/cli/confirm.ts
1357
- var confirm_exports = {};
1358
- __export(confirm_exports, {
1359
- SyncCancelledError: () => SyncCancelledError,
1360
- confirmEach: () => confirmEach,
1361
- confirmProceed: () => confirmProceed,
1362
- promptChoice: () => promptChoice,
1363
- promptReviewMode: () => promptReviewMode,
1364
- reviewFilesOneByOne: () => reviewFilesOneByOne,
1365
- reviewFolder: () => reviewFolder
1366
- });
1367
- import { createInterface as createInterface2 } from "node:readline/promises";
1368
- import { stdin as stdin2, stdout as stdout2 } from "node:process";
1369
- function isAbortError(err) {
1370
- return err instanceof Error && err.code === "ABORT_ERR";
1371
- }
1372
- async function confirmProceed(message) {
1373
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1374
- try {
1375
- while (true) {
1376
- const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1377
- const a = answer.trim().toLowerCase();
1378
- if (a === "" || a === "y" || a === "yes") return true;
1379
- if (a === "n" || a === "no") return false;
1380
- console.log(
1381
- ` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
1382
- );
1383
- }
1384
- } catch (err) {
1385
- if (isAbortError(err)) throw new SyncCancelledError();
1386
- throw err;
1387
- } finally {
1388
- rl.close();
1389
- }
1390
- }
1391
- async function promptChoice(message, options) {
1392
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1393
- try {
1394
- const answer = await rl.question(message);
1395
- const a = answer.trim().toLowerCase();
1396
- return options.includes(a) ? a : options[0];
1397
- } catch (err) {
1398
- if (isAbortError(err)) throw new SyncCancelledError();
1399
- throw err;
1400
- } finally {
1401
- rl.close();
1402
- }
1403
- }
1404
- async function confirmEach(items, label) {
1405
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1406
- const accepted = [];
1407
- try {
1408
- for (const item of items) {
1409
- const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
1410
- const a = answer.trim().toLowerCase();
1411
- if (a === "a") {
1412
- accepted.push(item, ...items.slice(items.indexOf(item) + 1));
1413
- break;
1414
- }
1415
- if (a === "y" || a === "yes" || a === "") {
1416
- accepted.push(item);
1417
- }
1418
- }
1419
- } catch (err) {
1420
- if (isAbortError(err)) throw new SyncCancelledError();
1421
- throw err;
1422
- } finally {
1423
- rl.close();
1424
- }
1425
- return accepted;
1426
- }
1427
- function parseReviewAction(input) {
1428
- const a = input.trim().toLowerCase();
1429
- switch (a) {
1430
- case "d":
1431
- case "delete":
1432
- return { action: "delete", all: false, special: null };
1433
- case "p":
1434
- case "pull":
1435
- return { action: "pull", all: false, special: null };
1436
- case "s":
1437
- case "push":
1438
- return { action: "push", all: false, special: null };
1439
- case "k":
1440
- case "skip":
1441
- return { action: "skip", all: false, special: null };
1442
- case "da":
1443
- return { action: "delete", all: true, special: null };
1444
- case "pa":
1445
- return { action: "pull", all: true, special: null };
1446
- case "sa":
1447
- return { action: "push", all: true, special: null };
1448
- case "ka":
1449
- return { action: "skip", all: true, special: null };
1450
- case "v":
1451
- case "view":
1452
- return { action: null, all: false, special: "view" };
1453
- case "r":
1454
- case "recommended":
1455
- return { action: null, all: false, special: "recommended" };
1456
- case "":
1457
- return { action: null, all: false, special: "recommended" };
1458
- // Enter = recommended
1459
- default:
1460
- return { action: null, all: false, special: null };
1461
- }
1462
- }
1463
- function formatActionPrompt(recommended, includeView, includeRecommended) {
1464
- const actions = [
1465
- `[d]elete${recommended === "delete" ? "\u2605" : ""}`,
1466
- `[p]ull${recommended === "pull" ? "\u2605" : ""}`,
1467
- `pu[s]h${recommended === "push" ? "\u2605" : ""}`,
1468
- `s[k]ip${recommended === "skip" ? "\u2605" : ""}`
1469
- ];
1470
- if (includeView) actions.push("[v]iew");
1471
- if (includeRecommended) actions.push("[r]ecommended");
1472
- return actions.join(" ");
1473
- }
1474
- function showDiff(local, remote, displayPath) {
1475
- console.log(`
1476
- --- ${displayPath} (diff) ---`);
1477
- if (local === null && remote !== null) {
1478
- console.log(" (no local file \u2014 remote content below)");
1479
- for (const line of remote.split("\n").slice(0, 30)) {
1480
- console.log(` + ${line}`);
1481
- }
1482
- if (remote.split("\n").length > 30) console.log(" ... (truncated)");
1483
- } else if (local !== null && remote === null) {
1484
- console.log(" (no remote file \u2014 local content below)");
1485
- for (const line of local.split("\n").slice(0, 30)) {
1486
- console.log(` - ${line}`);
1487
- }
1488
- if (local.split("\n").length > 30) console.log(" ... (truncated)");
1489
- } else if (local !== null && remote !== null) {
1490
- const localLines = local.split("\n");
1491
- const remoteLines = remote.split("\n");
1492
- let shown = 0;
1493
- const maxLines = 40;
1494
- for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
1495
- const l = localLines[i];
1496
- const r = remoteLines[i];
1497
- if (l === r) {
1498
- console.log(` ${l ?? ""}`);
1499
- } else {
1500
- if (l !== void 0) console.log(` - ${l}`);
1501
- if (r !== void 0) console.log(` + ${r}`);
1502
- }
1503
- shown++;
1504
- }
1505
- if (Math.max(localLines.length, remoteLines.length) > maxLines) {
1506
- console.log(" ... (truncated)");
1507
- }
1508
- }
1509
- console.log();
1510
- }
1511
- async function promptReviewMode() {
1512
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1513
- try {
1514
- while (true) {
1515
- const answer = await rl.question(
1516
- " Review [o]ne-by-one or [f]older-by-folder? "
1517
- );
1518
- const a = answer.trim().toLowerCase();
1519
- if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
1520
- return "file";
1521
- if (a === "f" || a === "folder") return "folder";
1522
- console.log(
1523
- ` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
1524
- );
1525
- }
1526
- } catch (err) {
1527
- if (isAbortError(err)) throw new SyncCancelledError();
1528
- throw err;
1529
- } finally {
1530
- rl.close();
1531
- }
1532
- }
1533
- async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
1534
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1535
- const results = [];
1536
- try {
1537
- let applyAll = null;
1538
- for (const item of items) {
1539
- if (applyAll) {
1540
- results.push(applyAll);
1541
- continue;
1542
- }
1543
- const planned = plannedAction(item);
1544
- const rec = recommendedAction ? recommendedAction(item) : planned;
1545
- const hasContent = content != null;
1546
- const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
1547
- while (true) {
1548
- const answer = await rl.question(prompt);
1549
- const result = parseReviewAction(answer);
1550
- if (result.special === "view") {
1551
- if (content) {
1552
- showDiff(content.local(item), content.remote(item), label(item));
1553
- } else {
1554
- console.log(" No content available for diff.");
1555
- }
1556
- continue;
1557
- }
1558
- if (result.special === "recommended") {
1559
- results.push(rec);
1560
- break;
1561
- }
1562
- if (result.action === null) {
1563
- console.log(
1564
- ` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
1565
- );
1566
- continue;
1567
- }
1568
- results.push(result.action);
1569
- if (result.all) applyAll = result.action;
1570
- break;
1571
- }
1572
- }
1573
- } catch (err) {
1574
- if (isAbortError(err)) throw new SyncCancelledError();
1575
- throw err;
1576
- } finally {
1577
- rl.close();
1578
- }
1579
- return results;
1580
- }
1581
- async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
1582
- console.log(`
1583
- ${folderName} (${items.length} files):`);
1584
- for (const item of items) {
1585
- const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
1586
- const actionLabel = plannedAction(item);
1587
- const star = actionLabel === rec ? "\u2605" : "";
1588
- console.log(` ${label(item)} (${actionLabel}${star})`);
1589
- }
1590
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1591
- try {
1592
- while (true) {
1593
- const promptStr = ` Action for all: ${formatActionPrompt(
1594
- recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1595
- false,
1596
- true
1597
- )} [o]ne-by-one: `;
1598
- const answer = await rl.question(promptStr);
1599
- const a = answer.trim().toLowerCase();
1600
- if (a === "o" || a === "one-by-one") {
1601
- rl.close();
1602
- return reviewFilesOneByOne(
1603
- items,
1604
- label,
1605
- plannedAction,
1606
- recommendedAction,
1607
- content
1608
- );
1609
- }
1610
- if (a === "r" || a === "recommended") {
1611
- return items.map(
1612
- (item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
1613
- );
1614
- }
1615
- if (a === "v" || a === "view") {
1616
- if (content) {
1617
- for (const item of items) {
1618
- showDiff(content.local(item), content.remote(item), label(item));
1619
- }
1620
- } else {
1621
- console.log(" No content available for diff.");
1622
- }
1623
- continue;
1624
- }
1625
- const result = parseReviewAction(a);
1626
- if (result.action !== null) {
1627
- return items.map(() => result.action);
1628
- }
1629
- console.log(
1630
- ` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
1631
- recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1632
- false,
1633
- true
1634
- )} [o]ne-by-one`
1635
- );
1636
- }
1637
- } catch (err) {
1638
- if (isAbortError(err)) throw new SyncCancelledError();
1639
- throw err;
1640
- } finally {
1641
- rl.close();
1642
- }
1643
- }
1644
- var SyncCancelledError;
1645
- var init_confirm = __esm({
1646
- "src/cli/confirm.ts"() {
1647
- "use strict";
1648
- SyncCancelledError = class extends Error {
1649
- constructor() {
1650
- super("Sync cancelled");
1651
- this.name = "SyncCancelledError";
1652
- }
1653
- };
1654
- }
1655
- });
1656
-
1657
- // src/lib/tech-detect.ts
1658
- import { readFile as readFile7, access, readdir as readdir4 } from "node:fs/promises";
1659
- import { join as join7, relative } from "node:path";
1660
- async function fileExists(filePath) {
1661
- try {
1662
- await access(filePath);
1663
- return true;
1664
- } catch {
1665
- return false;
605
+ return false;
1666
606
  }
1667
607
  }
1668
608
  async function discoverMonorepoApps(projectPath) {
1669
609
  const apps = [];
1670
610
  const patterns = [];
1671
611
  try {
1672
- const raw = await readFile7(
1673
- join7(projectPath, "pnpm-workspace.yaml"),
612
+ const raw = await readFile5(
613
+ join5(projectPath, "pnpm-workspace.yaml"),
1674
614
  "utf-8"
1675
615
  );
1676
616
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1684,7 +624,7 @@ async function discoverMonorepoApps(projectPath) {
1684
624
  }
1685
625
  if (patterns.length === 0) {
1686
626
  try {
1687
- const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
627
+ const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
1688
628
  const pkg = JSON.parse(raw);
1689
629
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1690
630
  if (ws) patterns.push(...ws);
@@ -1694,14 +634,14 @@ async function discoverMonorepoApps(projectPath) {
1694
634
  for (const pattern of patterns) {
1695
635
  if (pattern.endsWith("/*")) {
1696
636
  const dir = pattern.slice(0, -2);
1697
- const absDir = join7(projectPath, dir);
637
+ const absDir = join5(projectPath, dir);
1698
638
  try {
1699
- const entries = await readdir4(absDir, { withFileTypes: true });
639
+ const entries = await readdir(absDir, { withFileTypes: true });
1700
640
  for (const entry of entries) {
1701
641
  if (entry.isDirectory()) {
1702
- const relPath = join7(dir, entry.name);
1703
- const absPath = join7(absDir, entry.name);
1704
- if (await fileExists(join7(absPath, "package.json"))) {
642
+ const relPath = join5(dir, entry.name);
643
+ const absPath = join5(absDir, entry.name);
644
+ if (await fileExists(join5(absPath, "package.json"))) {
1705
645
  apps.push({ name: entry.name, path: relPath, absPath });
1706
646
  }
1707
647
  }
@@ -1715,12 +655,12 @@ async function discoverMonorepoApps(projectPath) {
1715
655
  async function hasJsxFile(dir, depth = 0) {
1716
656
  if (depth > 6) return false;
1717
657
  try {
1718
- const entries = await readdir4(dir, { withFileTypes: true });
658
+ const entries = await readdir(dir, { withFileTypes: true });
1719
659
  for (const entry of entries) {
1720
660
  const name = entry.name;
1721
661
  if (entry.isDirectory()) {
1722
662
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1723
- if (await hasJsxFile(join7(dir, name), depth + 1)) return true;
663
+ if (await hasJsxFile(join5(dir, name), depth + 1)) return true;
1724
664
  } else if (entry.isFile()) {
1725
665
  if (JSX_TEST_PATTERN.test(name)) continue;
1726
666
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1739,7 +679,7 @@ async function hasJsxFile(dir, depth = 0) {
1739
679
  async function detectCapabilities(dirPath, pkgJson) {
1740
680
  const caps = /* @__PURE__ */ new Set();
1741
681
  for (const sub of JSX_SCAN_DIRS) {
1742
- if (await hasJsxFile(join7(dirPath, sub))) {
682
+ if (await hasJsxFile(join5(dirPath, sub))) {
1743
683
  caps.add("jsx");
1744
684
  break;
1745
685
  }
@@ -1761,7 +701,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1761
701
  }
1762
702
  }
1763
703
  }
1764
- if (!caps.has("node-server") && await fileExists(join7(dirPath, "src", "main.ts"))) {
704
+ if (!caps.has("node-server") && await fileExists(join5(dirPath, "src", "main.ts"))) {
1765
705
  caps.add("node-server");
1766
706
  }
1767
707
  if (pkgJson && pkgJson.bin) {
@@ -1777,7 +717,7 @@ async function detectFromDirectory(dirPath) {
1777
717
  const seen = /* @__PURE__ */ new Map();
1778
718
  let pkgJson = null;
1779
719
  try {
1780
- const raw = await readFile7(join7(dirPath, "package.json"), "utf-8");
720
+ const raw = await readFile5(join5(dirPath, "package.json"), "utf-8");
1781
721
  pkgJson = JSON.parse(raw);
1782
722
  const allDeps = {
1783
723
  ...pkgJson.dependencies ?? {},
@@ -1809,7 +749,7 @@ async function detectFromDirectory(dirPath) {
1809
749
  }
1810
750
  for (const { file, rule } of CONFIG_FILE_MAP) {
1811
751
  const key = rule.name.toLowerCase();
1812
- if (!seen.has(key) && await fileExists(join7(dirPath, file))) {
752
+ if (!seen.has(key) && await fileExists(join5(dirPath, file))) {
1813
753
  seen.set(key, { name: rule.name, category: rule.category });
1814
754
  }
1815
755
  }
@@ -1987,16 +927,16 @@ function categorizeDependency(depName) {
1987
927
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1988
928
  if (depth > 4) return [];
1989
929
  const results = [];
1990
- const pkgPath = join7(dir, "package.json");
930
+ const pkgPath = join5(dir, "package.json");
1991
931
  if (await fileExists(pkgPath)) {
1992
932
  results.push(pkgPath);
1993
933
  }
1994
934
  try {
1995
- const entries = await readdir4(dir, { withFileTypes: true });
935
+ const entries = await readdir(dir, { withFileTypes: true });
1996
936
  for (const entry of entries) {
1997
937
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1998
938
  const subResults = await findPackageJsonFiles(
1999
- join7(dir, entry.name),
939
+ join5(dir, entry.name),
2000
940
  projectPath,
2001
941
  depth + 1
2002
942
  );
@@ -2011,7 +951,7 @@ async function scanAllDependencies(projectPath) {
2011
951
  const dependencies = [];
2012
952
  for (const pkgPath of packageJsonPaths) {
2013
953
  try {
2014
- const raw = await readFile7(pkgPath, "utf-8");
954
+ const raw = await readFile5(pkgPath, "utf-8");
2015
955
  const pkg = JSON.parse(raw);
2016
956
  const sourcePath = relative(projectPath, pkgPath);
2017
957
  const depSections = [
@@ -2225,176 +1165,27 @@ var init_tech_detect = __esm({
2225
1165
  }
2226
1166
  });
2227
1167
 
2228
- // src/lib/server-detect.ts
2229
- function detectFramework(pkg) {
2230
- const deps = pkg.dependencies ?? {};
2231
- const devDeps = pkg.devDependencies ?? {};
2232
- const hasDep = (name) => name in deps || name in devDeps;
2233
- if (hasDep("next")) return "nextjs";
2234
- if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
2235
- if (hasDep("expo")) return "expo";
2236
- if (hasDep("vite")) return "vite";
2237
- if (hasDep("express")) return "express";
2238
- if (hasDep("@nestjs/core")) return "nestjs";
2239
- return "custom";
2240
- }
2241
- function detectPortFromScripts(pkg) {
2242
- const scripts = pkg.scripts;
2243
- if (!scripts?.dev) return null;
2244
- const parts = scripts.dev.split(/\s+/);
2245
- for (let i = 0; i < parts.length - 1; i++) {
2246
- if (parts[i] === "--port" || parts[i] === "-p") {
2247
- const next = parts[i + 1];
2248
- if (next) {
2249
- const port = parseInt(next, 10);
2250
- if (!isNaN(port)) return port;
1168
+ // src/lib/eslint-generator.ts
1169
+ import { createHash as createHash2 } from "node:crypto";
1170
+ function importedIdentifiers(importLines) {
1171
+ const names = /* @__PURE__ */ new Set();
1172
+ for (const line of importLines) {
1173
+ let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
1174
+ if (m) names.add(m[1]);
1175
+ m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
1176
+ if (m) names.add(m[1]);
1177
+ m = line.match(/^import\s*\{([^}]*)\}\s*from/);
1178
+ if (m) {
1179
+ for (const entry of m[1].split(",")) {
1180
+ const parts = entry.trim().split(/\s+as\s+/);
1181
+ const n = (parts[1] ?? parts[0]).trim();
1182
+ if (n) names.add(n);
2251
1183
  }
2252
1184
  }
1185
+ m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
1186
+ if (m) names.add(m[1]);
2253
1187
  }
2254
- return null;
2255
- }
2256
- var init_server_detect = __esm({
2257
- "src/lib/server-detect.ts"() {
2258
- "use strict";
2259
- }
2260
- });
2261
-
2262
- // src/lib/port-verify.ts
2263
- import { readFile as readFile8 } from "node:fs/promises";
2264
- async function verifyPorts(projectPath, portAllocations) {
2265
- const mismatches = [];
2266
- const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2267
- const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2268
- for (const pkgPath of packageJsonPaths) {
2269
- try {
2270
- const raw = await readFile8(pkgPath, "utf-8");
2271
- const pkg = JSON.parse(raw);
2272
- const scriptPort = detectPortFromScripts(pkg);
2273
- if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
2274
- const relativePath = pkgPath.replace(projectPath + "/", "");
2275
- const matchingAlloc = portAllocations.find(
2276
- (a) => a.label === getAppLabel(relativePath)
2277
- );
2278
- mismatches.push({
2279
- packageJsonPath: relativePath,
2280
- scriptPort,
2281
- allocation: matchingAlloc ?? null,
2282
- reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
2283
- });
2284
- }
2285
- } catch {
2286
- }
2287
- }
2288
- return mismatches;
2289
- }
2290
- function isDevServerScript(pkg) {
2291
- const scripts = pkg.scripts;
2292
- const raw = scripts?.dev;
2293
- if (!raw || typeof raw !== "string") return false;
2294
- const script = raw.trim().toLowerCase();
2295
- if (!script) return false;
2296
- for (const pattern of DEV_SERVER_BIN_PATTERNS) {
2297
- if (pattern.test(script)) return true;
2298
- }
2299
- const tokens = script.split(/\s+/);
2300
- for (const token of tokens) {
2301
- if (token === "--port" || token === "-p") return true;
2302
- if (token.startsWith("--port=")) return true;
2303
- }
2304
- return false;
2305
- }
2306
- function labelMatchesAppName(label, appName) {
2307
- if (!label || !appName) return false;
2308
- const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
2309
- const labelTokens = normalize(label).split(" ").filter(Boolean);
2310
- const appToken = normalize(appName);
2311
- if (!appToken) return false;
2312
- const appTokens = appToken.split(" ").filter(Boolean);
2313
- if (appTokens.length === 1) {
2314
- return labelTokens.includes(appTokens[0]);
2315
- }
2316
- for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
2317
- if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
2318
- }
2319
- return false;
2320
- }
2321
- async function findUnallocatedApps(projectPath, portAllocations) {
2322
- const apps = await discoverMonorepoApps(projectPath);
2323
- if (apps.length === 0) {
2324
- return [];
2325
- }
2326
- const unallocated = [];
2327
- for (const app of apps) {
2328
- if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
2329
- continue;
2330
- }
2331
- let pkg;
2332
- try {
2333
- const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
2334
- pkg = JSON.parse(raw);
2335
- } catch {
2336
- continue;
2337
- }
2338
- if (!isDevServerScript(pkg)) continue;
2339
- const framework = detectFramework(pkg);
2340
- const detectedPort = detectPortFromScripts(pkg);
2341
- const command = `pnpm --filter ${app.name} dev`;
2342
- unallocated.push({
2343
- name: app.name,
2344
- path: app.path,
2345
- framework,
2346
- detectedPort,
2347
- command
2348
- });
2349
- }
2350
- return unallocated;
2351
- }
2352
- function getAppLabel(relativePath) {
2353
- const parts = relativePath.split("/");
2354
- if (parts.length >= 3 && parts[0] === "apps") {
2355
- return parts[1];
2356
- }
2357
- return "root";
2358
- }
2359
- var DEV_SERVER_BIN_PATTERNS;
2360
- var init_port_verify = __esm({
2361
- "src/lib/port-verify.ts"() {
2362
- "use strict";
2363
- init_tech_detect();
2364
- init_server_detect();
2365
- DEV_SERVER_BIN_PATTERNS = [
2366
- /\bnext\s+dev\b/,
2367
- /\bnest\s+start\b/,
2368
- /\bvite\s+(?:dev|serve)\b/,
2369
- /\bvite\s+preview\b/,
2370
- /\bnuxt\s+dev\b/,
2371
- /\b(?:svelte-kit|sveltekit)\s+dev\b/,
2372
- /\bexpo\s+start\b/
2373
- ];
2374
- }
2375
- });
2376
-
2377
- // src/lib/eslint-generator.ts
2378
- import { createHash } from "node:crypto";
2379
- function importedIdentifiers(importLines) {
2380
- const names = /* @__PURE__ */ new Set();
2381
- for (const line of importLines) {
2382
- let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
2383
- if (m) names.add(m[1]);
2384
- m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
2385
- if (m) names.add(m[1]);
2386
- m = line.match(/^import\s*\{([^}]*)\}\s*from/);
2387
- if (m) {
2388
- for (const entry of m[1].split(",")) {
2389
- const parts = entry.trim().split(/\s+as\s+/);
2390
- const n = (parts[1] ?? parts[0]).trim();
2391
- if (n) names.add(n);
2392
- }
2393
- }
2394
- m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
2395
- if (m) names.add(m[1]);
2396
- }
2397
- return names;
1188
+ return names;
2398
1189
  }
2399
1190
  function parseFragment(fragment) {
2400
1191
  if (!fragment) return { imports: [], configComments: [] };
@@ -2443,7 +1234,7 @@ function collectDependencies(presets) {
2443
1234
  return deps;
2444
1235
  }
2445
1236
  function hashConfig(content) {
2446
- return createHash("sha256").update(content).digest("hex");
1237
+ return createHash2("sha256").update(content).digest("hex");
2447
1238
  }
2448
1239
  function buildRules(presets) {
2449
1240
  const merged = {};
@@ -2556,8 +1347,7 @@ function generateEslintConfig(input) {
2556
1347
  sections.push(
2557
1348
  "/**",
2558
1349
  " * ESLint flat config \u2014 generated by CodeByPlan CLI.",
2559
- " * Edit rule overrides via the web UI, then run `codebyplan eslint sync`.",
2560
- " * Manual edits will be detected as drift.",
1350
+ " * Edit rule overrides via the web UI, then run `codebyplan eslint init` to regenerate.",
2561
1351
  " */",
2562
1352
  ""
2563
1353
  );
@@ -2758,13 +1548,11 @@ var init_eslint_generator = __esm({
2758
1548
  var eslint_exports = {};
2759
1549
  __export(eslint_exports, {
2760
1550
  autoDetectIgnorePatterns: () => autoDetectIgnorePatterns,
2761
- checkEslintDrift: () => checkEslintDrift,
2762
1551
  eslintInit: () => eslintInit,
2763
- eslintSync: () => eslintSync,
2764
1552
  runEslint: () => runEslint
2765
1553
  });
2766
- import { readFile as readFile9, writeFile as writeFile4, access as access2, readdir as readdir5 } from "node:fs/promises";
2767
- import { join as join8, relative as relative2 } from "node:path";
1554
+ import { readFile as readFile6, writeFile as writeFile4, access as access2, readdir as readdir2 } from "node:fs/promises";
1555
+ import { join as join6, relative as relative2 } from "node:path";
2768
1556
  async function fileExists2(filePath) {
2769
1557
  try {
2770
1558
  await access2(filePath);
@@ -2775,12 +1563,12 @@ async function fileExists2(filePath) {
2775
1563
  }
2776
1564
  async function autoDetectIgnorePatterns(absPath) {
2777
1565
  const patterns = [];
2778
- if (await fileExists2(join8(absPath, "esbuild.js"))) {
1566
+ if (await fileExists2(join6(absPath, "esbuild.js"))) {
2779
1567
  patterns.push("esbuild.js");
2780
1568
  }
2781
1569
  let entries = [];
2782
1570
  try {
2783
- entries = await readdir5(absPath);
1571
+ entries = await readdir2(absPath);
2784
1572
  } catch (err) {
2785
1573
  console.error(
2786
1574
  ` autoDetectIgnorePatterns: failed to read ${absPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2795,19 +1583,19 @@ async function autoDetectIgnorePatterns(absPath) {
2795
1583
  }
2796
1584
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2797
1585
  const candidate = `vitest.config.${ext}`;
2798
- if (await fileExists2(join8(absPath, candidate))) {
1586
+ if (await fileExists2(join6(absPath, candidate))) {
2799
1587
  patterns.push(candidate);
2800
1588
  break;
2801
1589
  }
2802
1590
  }
2803
1591
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2804
1592
  const candidate = `vite.config.${ext}`;
2805
- if (await fileExists2(join8(absPath, candidate))) {
1593
+ if (await fileExists2(join6(absPath, candidate))) {
2806
1594
  patterns.push(candidate);
2807
1595
  break;
2808
1596
  }
2809
1597
  }
2810
- if (await fileExists2(join8(absPath, "tauri.conf.json"))) {
1598
+ if (await fileExists2(join6(absPath, "tauri.conf.json"))) {
2811
1599
  patterns.push("src-tauri/**");
2812
1600
  patterns.push("**/*.d.ts");
2813
1601
  }
@@ -2815,14 +1603,14 @@ async function autoDetectIgnorePatterns(absPath) {
2815
1603
  }
2816
1604
  function detectPackageManager(projectPath) {
2817
1605
  return (async () => {
2818
- if (await fileExists2(join8(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2819
- if (await fileExists2(join8(projectPath, "yarn.lock"))) return "yarn";
1606
+ if (await fileExists2(join6(projectPath, "pnpm-lock.yaml"))) return "pnpm";
1607
+ if (await fileExists2(join6(projectPath, "yarn.lock"))) return "yarn";
2820
1608
  return "npm";
2821
1609
  })();
2822
1610
  }
2823
1611
  async function getInstalledDeps(pkgJsonPath) {
2824
1612
  try {
2825
- const raw = await readFile9(pkgJsonPath, "utf-8");
1613
+ const raw = await readFile6(pkgJsonPath, "utf-8");
2826
1614
  const pkg = JSON.parse(raw);
2827
1615
  const all = /* @__PURE__ */ new Set();
2828
1616
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2935,7 +1723,7 @@ async function eslintInit(repoId, projectPath) {
2935
1723
  ignorePatterns: detectedIgnores
2936
1724
  });
2937
1725
  const hash = hashConfig(content);
2938
- const configPath = join8(target.absPath, "eslint.config.mjs");
1726
+ const configPath = join6(target.absPath, "eslint.config.mjs");
2939
1727
  configsToWrite.push({
2940
1728
  target,
2941
1729
  presets,
@@ -2957,11 +1745,11 @@ async function eslintInit(repoId, projectPath) {
2957
1745
  return;
2958
1746
  }
2959
1747
  const pm = await detectPackageManager(projectPath);
2960
- const rootPkgJsonPath = join8(projectPath, "package.json");
1748
+ const rootPkgJsonPath = join6(projectPath, "package.json");
2961
1749
  const installed = await getInstalledDeps(rootPkgJsonPath);
2962
1750
  if (isMonorepo) {
2963
1751
  for (const { target } of configsToWrite) {
2964
- const appPkgJson = join8(target.absPath, "package.json");
1752
+ const appPkgJson = join6(target.absPath, "package.json");
2965
1753
  const appDeps = await getInstalledDeps(appPkgJson);
2966
1754
  for (const dep of appDeps) {
2967
1755
  installed.add(dep);
@@ -2987,9 +1775,9 @@ async function eslintInit(repoId, projectPath) {
2987
1775
  Install ${missingPkgs.length} missing packages? [Y/n] `
2988
1776
  );
2989
1777
  if (confirmed) {
2990
- const { execSync } = await import("node:child_process");
1778
+ const { execSync: execSync3 } = await import("node:child_process");
2991
1779
  try {
2992
- execSync(installCmd, { cwd: projectPath, stdio: "inherit" });
1780
+ execSync3(installCmd, { cwd: projectPath, stdio: "inherit" });
2993
1781
  console.log(" Packages installed.\n");
2994
1782
  } catch (err) {
2995
1783
  console.error(
@@ -3013,7 +1801,7 @@ async function eslintInit(repoId, projectPath) {
3013
1801
  } of configsToWrite) {
3014
1802
  if (await fileExists2(configPath)) {
3015
1803
  try {
3016
- const existing = await readFile9(configPath, "utf-8");
1804
+ const existing = await readFile6(configPath, "utf-8");
3017
1805
  const existingHash = hashConfig(existing);
3018
1806
  if (existingHash === hash) {
3019
1807
  console.log(
@@ -3056,134 +1844,6 @@ async function eslintInit(repoId, projectPath) {
3056
1844
  }
3057
1845
  console.log("\n ESLint init complete.\n");
3058
1846
  }
3059
- async function eslintSync(repoId, projectPath) {
3060
- console.log("\n ESLint Sync");
3061
- console.log(` Repo: ${repoId}`);
3062
- console.log(` Path: ${projectPath}
3063
- `);
3064
- let configs;
3065
- try {
3066
- const res = await apiGet(
3067
- `/repos/${repoId}/eslint-config`
3068
- );
3069
- configs = res.data ?? [];
3070
- } catch {
3071
- console.log(
3072
- " No existing ESLint config found. Run `codebyplan eslint init` first.\n"
3073
- );
3074
- return;
3075
- }
3076
- if (configs.length === 0) {
3077
- console.log(
3078
- " No ESLint configs registered. Run `codebyplan eslint init` first.\n"
3079
- );
3080
- return;
3081
- }
3082
- let updatedCount = 0;
3083
- let skippedCount = 0;
3084
- let driftCount = 0;
3085
- for (const config of configs) {
3086
- const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
3087
- const configPath = join8(absPath, "eslint.config.mjs");
3088
- const detected = await detectTechStack(absPath);
3089
- const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
3090
- const capabilities = collectCapabilities(detected.flat);
3091
- const currentPresets = await resolvePresetsForTechStack(
3092
- techNames,
3093
- capabilities
3094
- );
3095
- const currentPresetIds = currentPresets.map((p) => p.id).sort();
3096
- const savedPresetIds = [...config.active_preset_ids ?? []].sort();
3097
- const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
3098
- if (!presetsChanged) {
3099
- if (await fileExists2(configPath)) {
3100
- try {
3101
- const currentContent = await readFile9(configPath, "utf-8");
3102
- const currentHash = hashConfig(currentContent);
3103
- if (config.generated_hash && currentHash !== config.generated_hash) {
3104
- console.log(
3105
- ` ${config.source_path}: drift detected (manually edited). Not overwriting.`
3106
- );
3107
- driftCount++;
3108
- continue;
3109
- }
3110
- skippedCount++;
3111
- continue;
3112
- } catch {
3113
- console.warn(
3114
- ` ${config.source_path}: config file unreadable, regenerating...`
3115
- );
3116
- }
3117
- } else {
3118
- console.log(
3119
- ` ${config.source_path}: config file missing, regenerating...`
3120
- );
3121
- }
3122
- }
3123
- if (presetsChanged) {
3124
- console.log(` ${config.source_path}: presets changed, regenerating...`);
3125
- }
3126
- const userOverrides = config.rule_overrides;
3127
- const detectedIgnores = await autoDetectIgnorePatterns(absPath);
3128
- const content = generateEslintConfig({
3129
- presets: currentPresets,
3130
- ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0,
3131
- ignorePatterns: detectedIgnores
3132
- });
3133
- try {
3134
- await writeFile4(configPath, content, "utf-8");
3135
- } catch (err) {
3136
- console.error(
3137
- ` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
3138
- );
3139
- continue;
3140
- }
3141
- const newHash = hashConfig(content);
3142
- try {
3143
- await apiPut(`/repos/${repoId}/eslint-config`, {
3144
- source_path: config.source_path,
3145
- preset_ids: currentPresetIds,
3146
- rule_overrides: userOverrides ?? {},
3147
- generated_hash: newHash
3148
- });
3149
- } catch (err) {
3150
- console.error(
3151
- ` Warning: Failed to update server: ${err instanceof Error ? err.message : String(err)}`
3152
- );
3153
- }
3154
- updatedCount++;
3155
- }
3156
- console.log(
3157
- `
3158
- Sync: ${updatedCount} updated, ${skippedCount} unchanged, ${driftCount} drift detected.
3159
- `
3160
- );
3161
- }
3162
- async function checkEslintDrift(repoId, projectPath) {
3163
- try {
3164
- const res = await apiGet(
3165
- `/repos/${repoId}/eslint-config`
3166
- );
3167
- const configs = res.data ?? [];
3168
- for (const config of configs) {
3169
- if (!config.generated_hash) continue;
3170
- const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
3171
- const configPath = join8(absPath, "eslint.config.mjs");
3172
- if (!await fileExists2(configPath)) continue;
3173
- try {
3174
- const content = await readFile9(configPath, "utf-8");
3175
- const currentHash = hashConfig(content);
3176
- if (currentHash !== config.generated_hash) {
3177
- return true;
3178
- }
3179
- } catch {
3180
- }
3181
- }
3182
- return false;
3183
- } catch {
3184
- return false;
3185
- }
3186
- }
3187
1847
  async function runEslint() {
3188
1848
  const subcommand = process.argv[3];
3189
1849
  const flags = parseFlags(4);
@@ -3194,14 +1854,10 @@ async function runEslint() {
3194
1854
  case "init":
3195
1855
  await eslintInit(repoId, projectPath);
3196
1856
  break;
3197
- case "sync":
3198
- await eslintSync(repoId, projectPath);
3199
- break;
3200
1857
  default:
3201
1858
  console.log(`
3202
1859
  Usage:
3203
1860
  codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
3204
- codebyplan eslint sync Regenerate if presets changed, detect drift
3205
1861
  `);
3206
1862
  break;
3207
1863
  }
@@ -3209,7 +1865,7 @@ async function runEslint() {
3209
1865
  var init_eslint = __esm({
3210
1866
  "src/cli/eslint.ts"() {
3211
1867
  "use strict";
3212
- init_config();
1868
+ init_flags();
3213
1869
  init_confirm();
3214
1870
  init_api();
3215
1871
  init_tech_detect();
@@ -3217,556 +1873,128 @@ var init_eslint = __esm({
3217
1873
  }
3218
1874
  });
3219
1875
 
3220
- // src/cli/sync.ts
3221
- var sync_exports = {};
3222
- __export(sync_exports, {
3223
- runSync: () => runSync
1876
+ // src/cli/resolve-worktree.ts
1877
+ var resolve_worktree_exports = {};
1878
+ __export(resolve_worktree_exports, {
1879
+ runResolveWorktree: () => runResolveWorktree
3224
1880
  });
3225
- import { createHash as createHash2 } from "node:crypto";
3226
- import { readFile as readFile10, writeFile as writeFile5, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
3227
- import { join as join9, dirname as dirname2 } from "node:path";
3228
- function contentHash(content) {
3229
- return createHash2("sha256").update(content).digest("hex");
1881
+ import { execSync as execSync2 } from "node:child_process";
1882
+ async function runResolveWorktree() {
1883
+ try {
1884
+ const projectPath = process.cwd();
1885
+ const found = await findCodebyplanConfig(projectPath);
1886
+ if (!found?.contents.repo_id) {
1887
+ process.exit(0);
1888
+ }
1889
+ const repoId = found.contents.repo_id;
1890
+ const deviceId = await getOrCreateDeviceId(projectPath);
1891
+ let branch = "";
1892
+ try {
1893
+ branch = execSync2("git symbolic-ref --short HEAD", {
1894
+ cwd: projectPath,
1895
+ encoding: "utf-8"
1896
+ }).trim();
1897
+ } catch {
1898
+ }
1899
+ const worktreeId = await resolveWorktreeId({
1900
+ repoId,
1901
+ repoPath: projectPath,
1902
+ branch,
1903
+ deviceId
1904
+ });
1905
+ if (worktreeId) {
1906
+ process.stdout.write(worktreeId);
1907
+ }
1908
+ process.exit(0);
1909
+ } catch (err) {
1910
+ if (process.env.CODEBYPLAN_DEBUG === "1") {
1911
+ const msg = err instanceof Error ? err.message : String(err);
1912
+ process.stderr.write(`resolve-worktree: ${msg}
1913
+ `);
1914
+ }
1915
+ process.exit(0);
1916
+ }
3230
1917
  }
3231
- async function runSync() {
1918
+ var init_resolve_worktree2 = __esm({
1919
+ "src/cli/resolve-worktree.ts"() {
1920
+ "use strict";
1921
+ init_flags();
1922
+ init_local_config();
1923
+ init_resolve_worktree();
1924
+ }
1925
+ });
1926
+
1927
+ // src/cli/config.ts
1928
+ var config_exports = {};
1929
+ __export(config_exports, {
1930
+ runConfig: () => runConfig
1931
+ });
1932
+ import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
1933
+ import { join as join7 } from "node:path";
1934
+ async function runConfig() {
3232
1935
  const flags = parseFlags(3);
3233
1936
  const dryRun = hasFlag("dry-run", 3);
3234
- const force = hasFlag("force", 3);
3235
- const fix = hasFlag("fix", 3);
3236
1937
  validateApiKey();
3237
1938
  const config = await resolveConfig(flags);
3238
1939
  const { repoId, projectPath } = config;
3239
1940
  console.log(`
3240
- CodeByPlan Sync`);
1941
+ CodeByPlan Config`);
3241
1942
  console.log(` Repo: ${repoId}`);
3242
1943
  console.log(` Path: ${projectPath}`);
3243
1944
  if (dryRun) console.log(` Mode: dry-run`);
3244
- if (force) console.log(` Mode: force`);
3245
1945
  console.log();
3246
- if (!dryRun) {
3247
- console.log(" Acquiring sync lock...");
1946
+ await syncConfigToFile(repoId, projectPath, dryRun);
1947
+ console.log("\n Config complete.\n");
1948
+ }
1949
+ async function syncConfigToFile(repoId, projectPath, dryRun) {
1950
+ const configPath = join7(projectPath, ".codebyplan.json");
1951
+ let currentConfig = {};
1952
+ try {
1953
+ const raw = await readFile7(configPath, "utf-8");
1954
+ currentConfig = JSON.parse(raw);
1955
+ } catch {
1956
+ currentConfig = { repo_id: repoId };
1957
+ }
1958
+ let resolvedWorktreeId;
1959
+ try {
1960
+ const deviceId = await getOrCreateDeviceId(projectPath);
1961
+ let branch = "main";
3248
1962
  try {
3249
- await apiPost("/sync/lock", {
3250
- repo_id: repoId,
3251
- locked_by: `cli-sync`,
3252
- reason: "Bidirectional sync",
3253
- ttl_minutes: 10
3254
- });
3255
- console.log(" Lock acquired.\n");
3256
- } catch (lockErr) {
3257
- const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
3258
- if (lockStatus.data.locked && lockStatus.data.lock) {
3259
- const lock = lockStatus.data.lock;
3260
- console.log(
3261
- ` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`
3262
- );
3263
- console.log(` Expires: ${lock.expires_at}`);
3264
- console.log(` Use --force to override, or wait for lock to expire.
3265
- `);
3266
- if (!force) return;
3267
- await apiPost("/sync/lock", {
3268
- repo_id: repoId,
3269
- locked_by: `cli-sync`,
3270
- reason: "Bidirectional sync (forced)",
3271
- ttl_minutes: 10
3272
- });
3273
- console.log(" Lock acquired (forced).\n");
3274
- } else {
3275
- throw lockErr;
3276
- }
1963
+ const { execSync: execSync3 } = await import("node:child_process");
1964
+ branch = execSync3("git symbolic-ref --short HEAD", {
1965
+ cwd: projectPath,
1966
+ encoding: "utf-8"
1967
+ }).trim();
1968
+ } catch {
1969
+ }
1970
+ const tupleId = await resolveWorktreeId({
1971
+ repoId,
1972
+ repoPath: projectPath,
1973
+ branch,
1974
+ deviceId
1975
+ });
1976
+ if (tupleId) {
1977
+ resolvedWorktreeId = tupleId;
1978
+ } else {
1979
+ resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
3277
1980
  }
1981
+ } catch (err) {
1982
+ const msg = err instanceof Error ? err.message : String(err);
1983
+ console.warn(
1984
+ ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
1985
+ );
3278
1986
  }
3279
- try {
3280
- await runSyncInner(repoId, projectPath, dryRun, force, fix);
3281
- } finally {
3282
- if (!dryRun) {
3283
- try {
3284
- await apiDelete("/sync/lock", { repo_id: repoId });
3285
- } catch (err) {
3286
- console.error(
3287
- ` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
3288
- );
3289
- }
3290
- }
3291
- }
3292
- }
3293
- async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
3294
- console.log(" Reading local and remote state...");
3295
- const claudeDir = join9(projectPath, ".claude");
3296
- let localFiles = /* @__PURE__ */ new Map();
3297
- try {
3298
- localFiles = await scanLocalFiles(claudeDir, projectPath);
3299
- } catch (err) {
3300
- console.warn(
3301
- ` Local file scan incomplete: ${err instanceof Error ? err.message : String(err)}`
3302
- );
3303
- }
3304
- const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
3305
- [
3306
- apiGet("/sync/defaults"),
3307
- apiGet("/sync/files", { repo_id: repoId }),
3308
- apiGet(`/repos/${repoId}`),
3309
- apiGet("/sync/state", {
3310
- repo_id: repoId
3311
- }),
3312
- apiGet("/sync/file-repos", {
3313
- repo_id: repoId
3314
- })
3315
- ]
3316
- );
3317
- const syncStartTime = Date.now();
3318
- const repoData = repoRes.data;
3319
- const remoteDefaults = flattenSyncData(defaultsRes.data);
3320
- const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
3321
- const fileRepoHashes = /* @__PURE__ */ new Map();
3322
- const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
3323
- for (const entry of fileReposRes.data ?? []) {
3324
- const baseKey = compositeKey(
3325
- entry.file_type,
3326
- entry.file_name,
3327
- entry.file_category
3328
- );
3329
- const scopedKey = `${baseKey}:${entry.file_scope}`;
3330
- fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
3331
- if (!fileRepoHashes.has(baseKey)) {
3332
- fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
3333
- }
3334
- if (entry.claude_file_id) {
3335
- fileRepoByClaudeFileId.set(
3336
- entry.claude_file_id,
3337
- entry.last_synced_content_hash
3338
- );
3339
- }
3340
- }
3341
- const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
3342
- console.log(
3343
- ` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
3344
- `
3345
- );
3346
- const plan = [];
3347
- const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
3348
- for (const key of allKeys) {
3349
- const local = localFiles.get(key);
3350
- const remote = remoteFiles.get(key);
3351
- if (local && !remote) {
3352
- plan.push({
3353
- key,
3354
- displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
3355
- action: "push",
3356
- recommended: "push",
3357
- localContent: local.content,
3358
- remoteContent: null,
3359
- pushContent: reverseSubstituteVariables(local.content, repoData),
3360
- filePath: getLocalFilePath(claudeDir, projectPath, {
3361
- type: local.type,
3362
- name: local.name,
3363
- category: local.category
3364
- }),
3365
- type: local.type,
3366
- name: local.name,
3367
- category: local.category,
3368
- scope: local.scope,
3369
- isHook: local.type === "hook",
3370
- claudeFileId: null
3371
- });
3372
- } else if (!local && remote) {
3373
- const remoteScope = remote.scope ?? "shared";
3374
- if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
3375
- continue;
3376
- }
3377
- const resolvedContent = substituteVariables(remote.content, repoData);
3378
- const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
3379
- const recommended = hadSyncedThisFile ? "delete" : "pull";
3380
- plan.push({
3381
- key,
3382
- displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
3383
- action: recommended,
3384
- recommended,
3385
- localContent: null,
3386
- remoteContent: resolvedContent,
3387
- pushContent: null,
3388
- filePath: getLocalFilePath(claudeDir, projectPath, remote),
3389
- type: remote.type,
3390
- name: remote.name,
3391
- category: remote.category ?? null,
3392
- scope: remote.scope ?? "shared",
3393
- isHook: remote.type === "hook",
3394
- claudeFileId: remote.id ?? null
3395
- });
3396
- } else if (local && remote) {
3397
- const remoteScope = remote.scope ?? "shared";
3398
- if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
3399
- continue;
3400
- }
3401
- const resolvedRemote = substituteVariables(remote.content, repoData);
3402
- if (local.content === resolvedRemote) {
3403
- continue;
3404
- }
3405
- const localHash = contentHash(local.content);
3406
- const scopedKey = `${key}:${local.scope}`;
3407
- const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
3408
- const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
3409
- let action;
3410
- if (force) {
3411
- action = "pull";
3412
- } else if (!localChanged) {
3413
- action = "pull";
3414
- } else if (lastSyncedHash === null) {
3415
- action = "conflict";
3416
- } else {
3417
- const remoteResolvedHash = contentHash(resolvedRemote);
3418
- const remoteChanged = remoteResolvedHash !== lastSyncedHash;
3419
- if (remoteChanged) {
3420
- action = "conflict";
3421
- } else {
3422
- action = "push";
3423
- }
3424
- }
3425
- plan.push({
3426
- key,
3427
- displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
3428
- action,
3429
- recommended: action === "conflict" ? "pull" : action,
3430
- localContent: local.content,
3431
- remoteContent: resolvedRemote,
3432
- pushContent: reverseSubstituteVariables(local.content, repoData),
3433
- filePath: getLocalFilePath(claudeDir, projectPath, remote),
3434
- type: local.type,
3435
- name: local.name,
3436
- category: local.category,
3437
- scope: local.scope,
3438
- isHook: local.type === "hook",
3439
- claudeFileId: remote.id ?? null
3440
- });
3441
- }
3442
- }
3443
- const pulls = plan.filter((p) => p.action === "pull");
3444
- const pushes = plan.filter((p) => p.action === "push");
3445
- const conflicts = plan.filter((p) => p.action === "conflict");
3446
- const contentPulls = pulls.filter((p) => p.localContent !== null);
3447
- const dbOnlyPull = plan.filter(
3448
- (p) => p.localContent === null && p.action === "pull"
3449
- );
3450
- const dbOnlyDelete = plan.filter(
3451
- (p) => p.localContent === null && p.action === "delete"
3452
- );
3453
- if (contentPulls.length > 0) {
3454
- console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
3455
- for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
3456
- }
3457
- if (pushes.length > 0) {
3458
- console.log(` Push (local \u2192 DB): ${pushes.length}`);
3459
- for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
3460
- }
3461
- if (dbOnlyPull.length > 0) {
3462
- console.log(`
3463
- DB-only (new, will pull): ${dbOnlyPull.length}`);
3464
- for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
3465
- }
3466
- if (dbOnlyDelete.length > 0) {
3467
- console.log(
3468
- `
3469
- DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
3470
- );
3471
- for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
3472
- }
3473
- if (conflicts.length > 0) {
3474
- console.log(`
3475
- Conflicts (both sides changed): ${conflicts.length}`);
3476
- for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
3477
- }
3478
- if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
3479
- console.log(" All .claude/ files in sync.");
3480
- }
3481
- if (plan.length > 0 && !dryRun) {
3482
- if (!force) {
3483
- const agreed = await confirmProceed(`
3484
- Agree with sync? [Y/n] `);
3485
- if (!agreed) {
3486
- const mode = await promptReviewMode();
3487
- const contentProvider = {
3488
- local: (p) => p.localContent,
3489
- remote: (p) => p.remoteContent
3490
- };
3491
- if (mode === "file") {
3492
- const actions = await reviewFilesOneByOne(
3493
- plan,
3494
- (p) => p.displayPath,
3495
- (p) => p.action,
3496
- (p) => p.recommended,
3497
- contentProvider
3498
- );
3499
- for (let i = 0; i < plan.length; i++) {
3500
- plan[i].action = actions[i];
3501
- }
3502
- } else {
3503
- const groups = groupByType(plan);
3504
- for (const [typeName, items] of groups) {
3505
- const actions = await reviewFolder(
3506
- typeName,
3507
- items,
3508
- (p) => p.displayPath,
3509
- (p) => p.action,
3510
- (p) => p.recommended,
3511
- contentProvider
3512
- );
3513
- for (let i = 0; i < items.length; i++) {
3514
- items[i].action = actions[i];
3515
- }
3516
- }
3517
- }
3518
- }
3519
- }
3520
- const toPull = plan.filter((p) => p.action === "pull");
3521
- const toPush = plan.filter((p) => p.action === "push");
3522
- const toDelete = plan.filter((p) => p.action === "delete");
3523
- const skipped = plan.filter((p) => p.action === "skip");
3524
- if (toPull.length + toPush.length + toDelete.length === 0) {
3525
- console.log("\n All items skipped \u2014 no changes applied.");
3526
- } else {
3527
- for (const p of toPull) {
3528
- if (p.filePath && p.remoteContent !== null) {
3529
- await mkdir2(dirname2(p.filePath), { recursive: true });
3530
- await writeFile5(p.filePath, p.remoteContent, "utf-8");
3531
- if (p.isHook) await chmod2(p.filePath, 493);
3532
- }
3533
- }
3534
- const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
3535
- type: p.type,
3536
- name: p.name,
3537
- category: p.category,
3538
- content: p.pushContent,
3539
- scope: p.scope
3540
- }));
3541
- if (toUpsert.length > 0) {
3542
- await apiPost("/sync/files", {
3543
- repo_id: repoId,
3544
- files: toUpsert,
3545
- changed_by_repo_id: repoId
3546
- });
3547
- }
3548
- if (toDelete.length > 0) {
3549
- const deleteKeys = toDelete.map((p) => ({
3550
- type: p.type,
3551
- name: p.name,
3552
- category: p.category
3553
- }));
3554
- await apiPost("/sync/files", {
3555
- repo_id: repoId,
3556
- delete_keys: deleteKeys
3557
- });
3558
- for (const p of toDelete) {
3559
- if (p.filePath) {
3560
- try {
3561
- await unlink2(p.filePath);
3562
- } catch (err) {
3563
- if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
3564
- console.error(
3565
- ` Warning: failed to delete ${p.filePath}: ${err.message}`
3566
- );
3567
- }
3568
- }
3569
- }
3570
- }
3571
- }
3572
- const syncDurationMs = Date.now() - syncStartTime;
3573
- await apiPost("/sync/state", {
3574
- repo_id: repoId,
3575
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
3576
- was_skipped: skipped.length > 0,
3577
- files_synced_count: toPull.length + toPush.length + toDelete.length,
3578
- files_pushed: toPush.length,
3579
- files_pulled: toPull.length,
3580
- files_deleted: toDelete.length,
3581
- files_skipped: skipped.length,
3582
- sync_duration_ms: syncDurationMs,
3583
- sync_version: getSyncVersion()
3584
- });
3585
- const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
3586
- const fileRepoUpdates = [];
3587
- for (const p of toPull) {
3588
- if (p.remoteContent !== null) {
3589
- fileRepoUpdates.push({
3590
- claude_file_id: p.claudeFileId ?? void 0,
3591
- file_type: p.type,
3592
- file_name: p.name,
3593
- file_category: p.category,
3594
- file_scope: p.scope,
3595
- last_synced_at: syncTimestamp,
3596
- last_synced_content_hash: contentHash(p.remoteContent),
3597
- sync_status: "synced"
3598
- });
3599
- }
3600
- }
3601
- for (const p of toPush) {
3602
- if (p.localContent !== null) {
3603
- fileRepoUpdates.push({
3604
- claude_file_id: p.claudeFileId ?? void 0,
3605
- file_type: p.type,
3606
- file_name: p.name,
3607
- file_category: p.category,
3608
- file_scope: p.scope,
3609
- last_synced_at: syncTimestamp,
3610
- last_synced_content_hash: contentHash(p.localContent),
3611
- sync_status: "synced"
3612
- });
3613
- }
3614
- }
3615
- if (fileRepoUpdates.length > 0) {
3616
- try {
3617
- await apiPost("/sync/file-repos", {
3618
- repo_id: repoId,
3619
- file_repos: fileRepoUpdates
3620
- });
3621
- } catch (err) {
3622
- console.warn(
3623
- ` Warning: failed to update file-repo tracking for ${fileRepoUpdates.length} files: ${err instanceof Error ? err.message : String(err)}`
3624
- );
3625
- }
3626
- }
3627
- console.log(
3628
- `
3629
- Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
3630
- );
3631
- }
3632
- const unresolvedConflicts = plan.filter(
3633
- (p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
3634
- );
3635
- if (unresolvedConflicts.length > 0) {
3636
- let stored = 0;
3637
- for (const p of unresolvedConflicts) {
3638
- try {
3639
- await apiPost("/sync/conflicts", {
3640
- repo_id: repoId,
3641
- claude_file_id: p.claudeFileId ?? void 0,
3642
- file_type: p.type,
3643
- file_name: p.name,
3644
- file_category: p.category,
3645
- file_scope: p.scope,
3646
- conflict_type: "both_modified",
3647
- local_content: p.localContent,
3648
- remote_content: p.remoteContent
3649
- });
3650
- stored++;
3651
- } catch (err) {
3652
- console.error(`Failed to store conflict for ${p.displayPath}:`, err);
3653
- }
3654
- }
3655
- if (stored > 0) {
3656
- console.log(
3657
- `
3658
- ${stored} conflict(s) stored in DB for later resolution.`
3659
- );
3660
- }
3661
- }
3662
- } else if (dryRun) {
3663
- console.log("\n (dry-run \u2014 no changes)");
3664
- }
3665
- console.log("\n Settings sync...");
3666
- await syncSettings(
3667
- claudeDir,
3668
- projectPath,
3669
- defaultsRes.data,
3670
- repoData,
3671
- dryRun
3672
- );
3673
- console.log(" Config sync...");
3674
- await syncConfig(repoId, projectPath, dryRun);
3675
- console.log(" Tech stack...");
3676
- await syncTechStack(repoId, projectPath, dryRun);
3677
- console.log(" ESLint config...");
3678
- await syncEslintDriftCheck(repoId, projectPath);
3679
- console.log(" Port verification...");
3680
- await syncPortVerification(repoId, projectPath, dryRun, fix);
3681
- console.log("\n Sync complete.\n");
3682
- }
3683
- async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
3684
- const settingsPath = join9(claudeDir, "settings.json");
3685
- const globalSettingsFiles = syncData.global_settings ?? [];
3686
- let globalSettings = {};
3687
- for (const gf of globalSettingsFiles) {
3688
- const parsed = JSON.parse(
3689
- substituteVariables(gf.content, repoData)
3690
- );
3691
- globalSettings = { ...globalSettings, ...parsed };
3692
- }
3693
- const repoSettingsFiles = syncData.settings ?? [];
3694
- let repoSettings = {};
3695
- for (const rf of repoSettingsFiles) {
3696
- repoSettings = JSON.parse(
3697
- substituteVariables(rf.content, repoData)
3698
- );
3699
- }
3700
- const combinedTemplate = mergeGlobalAndRepoSettings(
3701
- globalSettings,
3702
- repoSettings
3703
- );
3704
- const hooksDir = join9(projectPath, ".claude", "hooks");
3705
- const discovered = await discoverHooks(hooksDir);
3706
- let localSettings = {};
3707
- try {
3708
- const raw = await readFile10(settingsPath, "utf-8");
3709
- localSettings = JSON.parse(raw);
3710
- } catch {
3711
- }
3712
- let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
3713
- merged = stripPermissionsAllow(merged);
3714
- if (discovered.size > 0) {
3715
- merged.hooks = mergeDiscoveredHooks(
3716
- merged.hooks ?? {},
3717
- discovered
3718
- );
3719
- }
3720
- const mergedContent = JSON.stringify(merged, null, 2) + "\n";
3721
- let currentContent = "";
3722
- try {
3723
- currentContent = await readFile10(settingsPath, "utf-8");
3724
- } catch {
3725
- }
3726
- if (currentContent === mergedContent) {
3727
- console.log(" Settings up to date.");
3728
- return;
3729
- }
3730
- if (dryRun) {
3731
- console.log(" Settings would be updated (dry-run).");
3732
- return;
3733
- }
3734
- await mkdir2(dirname2(settingsPath), { recursive: true });
3735
- await writeFile5(settingsPath, mergedContent, "utf-8");
3736
- console.log(" Updated settings.json");
3737
- }
3738
- async function syncConfig(repoId, projectPath, dryRun) {
3739
- const configPath = join9(projectPath, ".codebyplan.json");
3740
- let currentConfig = {};
3741
- try {
3742
- const raw = await readFile10(configPath, "utf-8");
3743
- currentConfig = JSON.parse(raw);
3744
- } catch {
3745
- currentConfig = { repo_id: repoId };
3746
- }
3747
- let resolvedWorktreeId;
3748
- try {
3749
- resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath);
3750
- } catch (err) {
3751
- const msg = err instanceof Error ? err.message : String(err);
3752
- console.warn(
3753
- ` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
3754
- );
3755
- }
3756
- if (resolvedWorktreeId && currentConfig.worktree_id !== resolvedWorktreeId) {
3757
- currentConfig = { ...currentConfig, worktree_id: resolvedWorktreeId };
3758
- }
3759
- const repoRes = await apiGet(`/repos/${repoId}`);
3760
- const repo = repoRes.data;
3761
- let portAllocations = [];
1987
+ const repoRes = await apiGet(`/repos/${repoId}`);
1988
+ const repo = repoRes.data;
1989
+ let portAllocations = [];
3762
1990
  try {
3763
1991
  const portsRes = await apiGet(
3764
1992
  `/port-allocations`,
3765
1993
  { repo_id: repoId }
3766
1994
  );
3767
1995
  const allAllocations = portsRes.data ?? [];
3768
- const worktreeId2 = currentConfig.worktree_id;
3769
- const filtered = worktreeId2 ? allAllocations.filter((a) => a.worktree_id === worktreeId2) : allAllocations.filter((a) => !a.worktree_id);
1996
+ const filteredByWorktree = resolvedWorktreeId;
1997
+ const filtered = filteredByWorktree ? allAllocations.filter((a) => a.worktree_id === filteredByWorktree) : allAllocations.filter((a) => !a.worktree_id);
3770
1998
  const ALLOWED_FIELDS = [
3771
1999
  "id",
3772
2000
  "repo_id",
@@ -3794,7 +2022,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
3794
2022
  ` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
3795
2023
  );
3796
2024
  }
3797
- const worktreeId = currentConfig.worktree_id;
2025
+ const worktreeId = resolvedWorktreeId;
3798
2026
  const matchingAlloc = portAllocations[0];
3799
2027
  const defaultBranchConfig = {
3800
2028
  protected: ["main", "development"],
@@ -3805,7 +2033,9 @@ async function syncConfig(repoId, projectPath, dryRun) {
3805
2033
  const branchConfig = repo.branch_config ?? defaultBranchConfig;
3806
2034
  const newConfig = {
3807
2035
  repo_id: repoId,
3808
- ...worktreeId ? { worktree_id: worktreeId } : {},
2036
+ // worktree_id is intentionally omitted it is never persisted in
2037
+ // .codebyplan.json (CHK-108). The in-memory worktreeId is used only
2038
+ // for server_port / server_type resolution immediately below.
3809
2039
  server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
3810
2040
  server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
3811
2041
  git_branch: repo.git_branch ?? "development",
@@ -3816,68 +2046,194 @@ async function syncConfig(repoId, projectPath, dryRun) {
3816
2046
  const currentJson = JSON.stringify(currentConfig, null, 2);
3817
2047
  const newJson = JSON.stringify(newConfig, null, 2);
3818
2048
  if (currentJson === newJson) {
3819
- console.log(" Config up to date.");
2049
+ console.log(" Config up to date.");
3820
2050
  return;
3821
2051
  }
3822
2052
  if (dryRun) {
3823
- console.log(" Config would be updated (dry-run).");
2053
+ console.log(" Config would be updated (dry-run).");
3824
2054
  return;
3825
2055
  }
3826
2056
  await writeFile5(configPath, newJson + "\n", "utf-8");
3827
- console.log(" Updated .codebyplan.json");
2057
+ console.log(" Updated .codebyplan.json");
3828
2058
  }
3829
- async function syncTechStack(repoId, projectPath, dryRun) {
3830
- try {
3831
- const { dependencies } = await scanAllDependencies(projectPath);
3832
- if (dependencies.length === 0) {
3833
- console.log(" No dependencies found.");
3834
- return;
3835
- }
3836
- const sourcePaths = new Set(dependencies.map((d) => d.source_path));
3837
- console.log(
3838
- ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
3839
- );
3840
- if (!dryRun) {
3841
- const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
3842
- if (result.data.stale_removed > 0) {
3843
- console.log(
3844
- ` ${result.data.stale_removed} stale dependencies removed`
3845
- );
2059
+ var init_config = __esm({
2060
+ "src/cli/config.ts"() {
2061
+ "use strict";
2062
+ init_flags();
2063
+ init_api();
2064
+ init_resolve_worktree();
2065
+ init_local_config();
2066
+ }
2067
+ });
2068
+
2069
+ // src/lib/server-detect.ts
2070
+ function detectFramework(pkg) {
2071
+ const deps = pkg.dependencies ?? {};
2072
+ const devDeps = pkg.devDependencies ?? {};
2073
+ const hasDep = (name) => name in deps || name in devDeps;
2074
+ if (hasDep("next")) return "nextjs";
2075
+ if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
2076
+ if (hasDep("expo")) return "expo";
2077
+ if (hasDep("vite")) return "vite";
2078
+ if (hasDep("express")) return "express";
2079
+ if (hasDep("@nestjs/core")) return "nestjs";
2080
+ return "custom";
2081
+ }
2082
+ function detectPortFromScripts(pkg) {
2083
+ const scripts = pkg.scripts;
2084
+ if (!scripts?.dev) return null;
2085
+ const parts = scripts.dev.split(/\s+/);
2086
+ for (let i = 0; i < parts.length - 1; i++) {
2087
+ if (parts[i] === "--port" || parts[i] === "-p") {
2088
+ const next = parts[i + 1];
2089
+ if (next) {
2090
+ const port = parseInt(next, 10);
2091
+ if (!isNaN(port)) return port;
3846
2092
  }
3847
2093
  }
3848
- const detected = await detectTechStack(projectPath);
3849
- if (detected.flat.length > 0) {
3850
- const repoRes = await apiGet(`/repos/${repoId}`);
3851
- const remote = parseTechStackResult(repoRes.data.tech_stack);
3852
- const { merged, added } = mergeTechStack(remote, detected);
3853
- if (added.length > 0) {
3854
- console.log(` ${added.length} new tech entries`);
3855
- if (!dryRun) {
3856
- await apiPut(`/repos/${repoId}`, { tech_stack: merged });
3857
- }
2094
+ }
2095
+ return null;
2096
+ }
2097
+ var init_server_detect = __esm({
2098
+ "src/lib/server-detect.ts"() {
2099
+ "use strict";
2100
+ }
2101
+ });
2102
+
2103
+ // src/lib/port-verify.ts
2104
+ import { readFile as readFile8 } from "node:fs/promises";
2105
+ async function verifyPorts(projectPath, portAllocations) {
2106
+ const mismatches = [];
2107
+ const allocatedPorts = new Set(portAllocations.map((a) => a.port));
2108
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
2109
+ for (const pkgPath of packageJsonPaths) {
2110
+ try {
2111
+ const raw = await readFile8(pkgPath, "utf-8");
2112
+ const pkg = JSON.parse(raw);
2113
+ const scriptPort = detectPortFromScripts(pkg);
2114
+ if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
2115
+ const relativePath = pkgPath.replace(projectPath + "/", "");
2116
+ const matchingAlloc = portAllocations.find(
2117
+ (a) => a.label === getAppLabel(relativePath)
2118
+ );
2119
+ mismatches.push({
2120
+ packageJsonPath: relativePath,
2121
+ scriptPort,
2122
+ allocation: matchingAlloc ?? null,
2123
+ reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
2124
+ });
3858
2125
  }
2126
+ } catch {
3859
2127
  }
3860
- } catch (err) {
3861
- console.warn(
3862
- ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
3863
- );
3864
2128
  }
2129
+ return mismatches;
3865
2130
  }
3866
- async function syncEslintDriftCheck(repoId, projectPath) {
3867
- try {
3868
- const hasDrift = await checkEslintDrift(repoId, projectPath);
3869
- if (hasDrift) {
3870
- console.log(
3871
- " ESLint config drift detected. Run `codebyplan eslint sync` to update."
3872
- );
3873
- } else {
3874
- console.log(" ESLint configs up to date.");
2131
+ function isDevServerScript(pkg) {
2132
+ const scripts = pkg.scripts;
2133
+ const raw = scripts?.dev;
2134
+ if (!raw || typeof raw !== "string") return false;
2135
+ const script = raw.trim().toLowerCase();
2136
+ if (!script) return false;
2137
+ for (const pattern of DEV_SERVER_BIN_PATTERNS) {
2138
+ if (pattern.test(script)) return true;
2139
+ }
2140
+ const tokens = script.split(/\s+/);
2141
+ for (const token of tokens) {
2142
+ if (token === "--port" || token === "-p") return true;
2143
+ if (token.startsWith("--port=")) return true;
2144
+ }
2145
+ return false;
2146
+ }
2147
+ function labelMatchesAppName(label, appName) {
2148
+ if (!label || !appName) return false;
2149
+ const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
2150
+ const labelTokens = normalize(label).split(" ").filter(Boolean);
2151
+ const appToken = normalize(appName);
2152
+ if (!appToken) return false;
2153
+ const appTokens = appToken.split(" ").filter(Boolean);
2154
+ if (appTokens.length === 1) {
2155
+ return labelTokens.includes(appTokens[0]);
2156
+ }
2157
+ for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
2158
+ if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
2159
+ }
2160
+ return false;
2161
+ }
2162
+ async function findUnallocatedApps(projectPath, portAllocations) {
2163
+ const apps = await discoverMonorepoApps(projectPath);
2164
+ if (apps.length === 0) {
2165
+ return [];
2166
+ }
2167
+ const unallocated = [];
2168
+ for (const app of apps) {
2169
+ if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
2170
+ continue;
3875
2171
  }
3876
- } catch (error) {
3877
- console.warn(" ESLint drift check skipped:", error);
2172
+ let pkg;
2173
+ try {
2174
+ const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
2175
+ pkg = JSON.parse(raw);
2176
+ } catch {
2177
+ continue;
2178
+ }
2179
+ if (!isDevServerScript(pkg)) continue;
2180
+ const framework = detectFramework(pkg);
2181
+ const detectedPort = detectPortFromScripts(pkg);
2182
+ const command = `pnpm --filter ${app.name} dev`;
2183
+ unallocated.push({
2184
+ name: app.name,
2185
+ path: app.path,
2186
+ framework,
2187
+ detectedPort,
2188
+ command
2189
+ });
3878
2190
  }
2191
+ return unallocated;
3879
2192
  }
3880
- async function syncPortVerification(repoId, projectPath, dryRun, fix) {
2193
+ function getAppLabel(relativePath) {
2194
+ const parts = relativePath.split("/");
2195
+ if (parts.length >= 3 && parts[0] === "apps") {
2196
+ return parts[1];
2197
+ }
2198
+ return "root";
2199
+ }
2200
+ var DEV_SERVER_BIN_PATTERNS;
2201
+ var init_port_verify = __esm({
2202
+ "src/lib/port-verify.ts"() {
2203
+ "use strict";
2204
+ init_tech_detect();
2205
+ init_server_detect();
2206
+ DEV_SERVER_BIN_PATTERNS = [
2207
+ /\bnext\s+dev\b/,
2208
+ /\bnest\s+start\b/,
2209
+ /\bvite\s+(?:dev|serve)\b/,
2210
+ /\bvite\s+preview\b/,
2211
+ /\bnuxt\s+dev\b/,
2212
+ /\b(?:svelte-kit|sveltekit)\s+dev\b/,
2213
+ /\bexpo\s+start\b/
2214
+ ];
2215
+ }
2216
+ });
2217
+
2218
+ // src/cli/ports.ts
2219
+ var ports_exports = {};
2220
+ __export(ports_exports, {
2221
+ runPorts: () => runPorts
2222
+ });
2223
+ async function runPorts() {
2224
+ const flags = parseFlags(3);
2225
+ const dryRun = hasFlag("dry-run", 3);
2226
+ const fix = hasFlag("fix", 3);
2227
+ validateApiKey();
2228
+ const config = await resolveConfig(flags);
2229
+ const { repoId, projectPath } = config;
2230
+ console.log(`
2231
+ CodeByPlan Ports`);
2232
+ console.log(` Repo: ${repoId}`);
2233
+ console.log(` Path: ${projectPath}`);
2234
+ if (dryRun) console.log(` Mode: dry-run`);
2235
+ if (fix) console.log(` Mode: fix`);
2236
+ console.log();
3881
2237
  try {
3882
2238
  const portsRes = await apiGet(
3883
2239
  `/port-allocations`,
@@ -3885,22 +2241,23 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
3885
2241
  );
3886
2242
  const allocations = portsRes.data ?? [];
3887
2243
  if (allocations.length === 0) {
3888
- console.log(" No port allocations found \u2014 skipping verification.");
2244
+ console.log(" No port allocations found \u2014 skipping verification.");
2245
+ console.log("\n Ports complete.\n");
3889
2246
  return;
3890
2247
  }
3891
2248
  const mismatches = await verifyPorts(projectPath, allocations);
3892
2249
  if (mismatches.length > 0) {
3893
- console.log(` Port mismatches: ${mismatches.length}`);
2250
+ console.log(` Port mismatches: ${mismatches.length}`);
3894
2251
  for (const m of mismatches) {
3895
- console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
2252
+ console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
3896
2253
  }
3897
2254
  }
3898
2255
  const unallocated = await findUnallocatedApps(projectPath, allocations);
3899
2256
  if (unallocated.length > 0) {
3900
- console.log(` Unallocated apps: ${unallocated.length}`);
2257
+ console.log(` Unallocated apps: ${unallocated.length}`);
3901
2258
  for (const app of unallocated) {
3902
2259
  console.log(
3903
- ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
2260
+ ` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
3904
2261
  );
3905
2262
  }
3906
2263
  if (fix && !dryRun) {
@@ -3918,11 +2275,11 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
3918
2275
  command: app.command,
3919
2276
  working_dir: app.path
3920
2277
  });
3921
- console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
2278
+ console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
3922
2279
  } catch (err) {
3923
2280
  const msg = err instanceof Error ? err.message : String(err);
3924
2281
  console.log(
3925
- ` Failed to create allocation for ${app.name}: ${msg}`
2282
+ ` Failed to create allocation for ${app.name}: ${msg}`
3926
2283
  );
3927
2284
  }
3928
2285
  if (app.detectedPort && app.detectedPort >= nextPort) {
@@ -3930,123 +2287,180 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
3930
2287
  }
3931
2288
  }
3932
2289
  } else if (fix && dryRun) {
3933
- console.log(" (dry-run \u2014 would create allocations with --fix)");
2290
+ console.log(" (dry-run \u2014 would create allocations with --fix)");
3934
2291
  } else {
3935
- console.log(" Run with --fix to auto-create allocations.");
2292
+ console.log(" Run with --fix to auto-create allocations.");
3936
2293
  }
3937
2294
  }
3938
2295
  if (mismatches.length === 0 && unallocated.length === 0) {
3939
- console.log(" Ports verified.");
2296
+ console.log(" Ports verified.");
3940
2297
  }
3941
2298
  } catch (err) {
3942
2299
  console.warn(
3943
- ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
2300
+ ` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
3944
2301
  );
3945
2302
  }
2303
+ console.log("\n Ports complete.\n");
3946
2304
  }
3947
- function groupByType(items) {
3948
- const groups = /* @__PURE__ */ new Map();
3949
- const typeLabels = {
3950
- command: "Commands",
3951
- agent: "Agents",
3952
- skill: "Skills",
3953
- rule: "Rules",
3954
- hook: "Hooks",
3955
- template: "Templates",
3956
- settings: "Settings",
3957
- context: "Context",
3958
- docs_stack: "Stack Docs",
3959
- docs: "Docs"
3960
- };
3961
- for (const item of items) {
3962
- const label = typeLabels[item.type] ?? item.type;
3963
- const group = groups.get(label) ?? [];
3964
- group.push(item);
3965
- groups.set(label, group);
2305
+ var init_ports = __esm({
2306
+ "src/cli/ports.ts"() {
2307
+ "use strict";
2308
+ init_flags();
2309
+ init_api();
2310
+ init_port_verify();
3966
2311
  }
3967
- return groups;
3968
- }
3969
- function getLocalFilePath(claudeDir, projectPath, remote) {
3970
- const typeConfig2 = {
3971
- command: { dir: "commands", ext: ".md" },
3972
- agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
3973
- skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
3974
- rule: { dir: "rules", ext: ".md" },
3975
- hook: { dir: "hooks", ext: ".sh" },
3976
- template: { dir: "templates", ext: "" },
3977
- context: { dir: "context", ext: ".md" },
3978
- docs_stack: { dir: join9("docs", "stack"), ext: ".md" },
3979
- docs: { dir: "docs", ext: ".md" },
3980
- claude_md: { dir: "", ext: "" },
3981
- settings: { dir: "", ext: "" }
3982
- };
3983
- if (remote.type === "claude_md") return join9(projectPath, "CLAUDE.md");
3984
- if (remote.type === "settings") return join9(claudeDir, "settings.json");
3985
- const cfg = typeConfig2[remote.type];
3986
- if (!cfg) return join9(claudeDir, remote.name);
3987
- const typeDir = remote.type === "command" ? join9(claudeDir, cfg.dir, "cbp") : join9(claudeDir, cfg.dir);
3988
- if (cfg.subfolder)
3989
- return join9(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
3990
- if (remote.type === "command" && remote.category)
3991
- return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3992
- if (remote.type === "template") return join9(typeDir, remote.name);
3993
- if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
3994
- return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3995
- return join9(typeDir, `${remote.name}${cfg.ext}`);
2312
+ });
2313
+
2314
+ // src/lib/migrate-local-config.ts
2315
+ import { readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
2316
+ import { join as join8 } from "node:path";
2317
+ function sharedConfigPath(projectPath) {
2318
+ return join8(projectPath, ".codebyplan.json");
3996
2319
  }
3997
- function getSyncVersion() {
2320
+ async function needsLocalMigration(projectPath) {
3998
2321
  try {
3999
- return "1.4.3";
2322
+ const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
2323
+ const parsed = JSON.parse(raw);
2324
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2325
+ return false;
2326
+ }
2327
+ const cfg = parsed;
2328
+ if (typeof cfg.worktree_id !== "string" || cfg.worktree_id === "") {
2329
+ return false;
2330
+ }
2331
+ const local = await readLocalConfig(projectPath);
2332
+ if (local?.device_id) {
2333
+ return false;
2334
+ }
2335
+ return true;
4000
2336
  } catch {
4001
- return "unknown";
2337
+ return false;
4002
2338
  }
4003
2339
  }
4004
- function flattenSyncData(data) {
4005
- const result = /* @__PURE__ */ new Map();
4006
- const typeMap = {
4007
- commands: "command",
4008
- agents: "agent",
4009
- skills: "skill",
4010
- rules: "rule",
4011
- hooks: "hook",
4012
- templates: "template",
4013
- settings: "settings",
4014
- contexts: "context",
4015
- docs_stack: "docs_stack",
4016
- docs: "docs"
2340
+ async function runLocalMigration(projectPath) {
2341
+ const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
2342
+ const parsed = JSON.parse(raw);
2343
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2344
+ throw new Error(
2345
+ ".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
2346
+ );
2347
+ }
2348
+ const cfg = parsed;
2349
+ const hadWorktreeId = "worktree_id" in cfg;
2350
+ const localBefore = await readLocalConfig(projectPath);
2351
+ const localWillBeCreated = !localBefore?.device_id;
2352
+ const device_id = await getOrCreateDeviceId(projectPath);
2353
+ const cleaned = { ...cfg };
2354
+ delete cleaned.worktree_id;
2355
+ await writeFile6(
2356
+ sharedConfigPath(projectPath),
2357
+ JSON.stringify(cleaned, null, 2) + "\n",
2358
+ "utf-8"
2359
+ );
2360
+ const files_changed = [".codebyplan.json"];
2361
+ if (localWillBeCreated) files_changed.push(".codebyplan.local.json");
2362
+ return {
2363
+ migrated: true,
2364
+ was_dirty: hadWorktreeId || localWillBeCreated,
2365
+ files_changed,
2366
+ device_id
4017
2367
  };
4018
- for (const [syncKey, typeName] of Object.entries(typeMap)) {
4019
- const files = data[syncKey] ?? [];
4020
- for (const file of files) {
4021
- const key = compositeKey(typeName, file.name, file.category ?? null);
4022
- result.set(key, {
4023
- id: file.id,
4024
- type: typeName,
4025
- name: file.name,
4026
- content: file.content,
4027
- category: file.category,
4028
- updated_at: file.updated_at,
4029
- content_hash: file.content_hash,
4030
- scope: file.scope
4031
- });
2368
+ }
2369
+ var init_migrate_local_config = __esm({
2370
+ "src/lib/migrate-local-config.ts"() {
2371
+ "use strict";
2372
+ init_local_config();
2373
+ }
2374
+ });
2375
+
2376
+ // src/cli/tech-stack.ts
2377
+ var tech_stack_exports = {};
2378
+ __export(tech_stack_exports, {
2379
+ runTechStack: () => runTechStack
2380
+ });
2381
+ async function runTechStack() {
2382
+ const flags = parseFlags(3);
2383
+ const dryRun = hasFlag("dry-run", 3);
2384
+ validateApiKey();
2385
+ const config = await resolveConfig(flags);
2386
+ const { repoId, projectPath } = config;
2387
+ console.log(`
2388
+ CodeByPlan Tech Stack`);
2389
+ console.log(` Repo: ${repoId}`);
2390
+ console.log(` Path: ${projectPath}`);
2391
+ if (dryRun) console.log(` Mode: dry-run`);
2392
+ console.log();
2393
+ if (dryRun) {
2394
+ try {
2395
+ if (await needsLocalMigration(projectPath)) {
2396
+ console.log(
2397
+ ` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
2398
+ );
2399
+ }
2400
+ } catch {
2401
+ }
2402
+ } else {
2403
+ try {
2404
+ if (await needsLocalMigration(projectPath)) {
2405
+ const result = await runLocalMigration(projectPath);
2406
+ console.log(
2407
+ ` Migrated .codebyplan.json -> moved worktree_id to gitignored .codebyplan.local.json (device_id=${result.device_id.slice(0, 8)})`
2408
+ );
2409
+ console.log(
2410
+ ` Suggest /cbp-git-commit to stage the cleaned shared file.`
2411
+ );
2412
+ }
2413
+ } catch (err) {
2414
+ console.warn(
2415
+ ` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
2416
+ );
2417
+ }
2418
+ }
2419
+ try {
2420
+ const { dependencies } = await scanAllDependencies(projectPath);
2421
+ if (dependencies.length === 0) {
2422
+ console.log(" No dependencies found.");
2423
+ console.log("\n Tech stack complete.\n");
2424
+ return;
2425
+ }
2426
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
2427
+ console.log(
2428
+ ` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
2429
+ );
2430
+ if (!dryRun) {
2431
+ const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
2432
+ if (result.data.stale_removed > 0) {
2433
+ console.log(
2434
+ ` ${result.data.stale_removed} stale dependencies removed`
2435
+ );
2436
+ }
4032
2437
  }
2438
+ const detected = await detectTechStack(projectPath);
2439
+ if (detected.flat.length > 0) {
2440
+ const repoRes = await apiGet(`/repos/${repoId}`);
2441
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
2442
+ const { merged, added } = mergeTechStack(remote, detected);
2443
+ if (added.length > 0) {
2444
+ console.log(` ${added.length} new tech entries`);
2445
+ if (!dryRun) {
2446
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
2447
+ }
2448
+ }
2449
+ }
2450
+ } catch (err) {
2451
+ console.warn(
2452
+ ` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
2453
+ );
4033
2454
  }
4034
- return result;
2455
+ console.log("\n Tech stack complete.\n");
4035
2456
  }
4036
- var init_sync = __esm({
4037
- "src/cli/sync.ts"() {
2457
+ var init_tech_stack = __esm({
2458
+ "src/cli/tech-stack.ts"() {
4038
2459
  "use strict";
4039
- init_config();
4040
- init_fileMapper();
4041
- init_confirm();
2460
+ init_flags();
4042
2461
  init_api();
4043
- init_variables();
4044
2462
  init_tech_detect();
4045
- init_settings_merge();
4046
- init_hook_registry();
4047
- init_port_verify();
4048
- init_resolve_worktree();
4049
- init_eslint();
2463
+ init_migrate_local_config();
4050
2464
  }
4051
2465
  });
4052
2466
 
@@ -4086,20 +2500,6 @@ void (async () => {
4086
2500
  await runSetup2();
4087
2501
  process.exit(0);
4088
2502
  }
4089
- if (arg === "sync") {
4090
- const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
4091
- const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
4092
- try {
4093
- await runSync2();
4094
- } catch (err) {
4095
- if (err instanceof SyncCancelledError2) {
4096
- console.log("\n Sync cancelled.\n");
4097
- process.exit(0);
4098
- }
4099
- throw err;
4100
- }
4101
- process.exit(0);
4102
- }
4103
2503
  if (arg === "eslint") {
4104
2504
  const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
4105
2505
  const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
@@ -4114,27 +2514,58 @@ void (async () => {
4114
2514
  }
4115
2515
  process.exit(0);
4116
2516
  }
2517
+ if (arg === "resolve-worktree") {
2518
+ const { runResolveWorktree: runResolveWorktree2 } = await Promise.resolve().then(() => (init_resolve_worktree2(), resolve_worktree_exports));
2519
+ await runResolveWorktree2();
2520
+ process.exit(0);
2521
+ }
2522
+ if (arg === "config") {
2523
+ const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2524
+ await runConfig2();
2525
+ process.exit(0);
2526
+ }
2527
+ if (arg === "ports") {
2528
+ const { runPorts: runPorts2 } = await Promise.resolve().then(() => (init_ports(), ports_exports));
2529
+ await runPorts2();
2530
+ process.exit(0);
2531
+ }
2532
+ if (arg === "tech-stack") {
2533
+ const { runTechStack: runTechStack2 } = await Promise.resolve().then(() => (init_tech_stack(), tech_stack_exports));
2534
+ await runTechStack2();
2535
+ process.exit(0);
2536
+ }
4117
2537
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
4118
2538
  console.log(`
4119
2539
  CodeByPlan CLI v${VERSION}
4120
2540
 
4121
2541
  Usage:
4122
- codebyplan setup Interactive setup (API key + project init + first sync)
4123
- codebyplan sync Bidirectional sync (pull + push + config)
4124
- codebyplan eslint ESLint config management (init, sync)
4125
- codebyplan help Show this help message
4126
- codebyplan --version Print version
2542
+ codebyplan setup Interactive setup (API key + project init)
2543
+ codebyplan config Sync repo config from DB to .codebyplan.json
2544
+ codebyplan ports Verify port allocations against local package.json scripts
2545
+ codebyplan tech-stack Detect and sync tech stack dependencies
2546
+ codebyplan eslint ESLint config management (init)
2547
+ codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
2548
+ codebyplan help Show this help message
2549
+ codebyplan --version Print version
4127
2550
 
4128
- Sync options:
2551
+ Config options:
2552
+ --path <dir> Project root directory (default: cwd)
2553
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
2554
+ --dry-run Preview changes without writing
2555
+
2556
+ Ports options:
4129
2557
  --path <dir> Project root directory (default: cwd)
4130
2558
  --repo-id <uuid> Repository ID (or set via .codebyplan.json)
4131
2559
  --dry-run Preview changes without writing
4132
- --force Skip confirmation and conflict prompts
4133
2560
  --fix Auto-create missing port allocations
4134
2561
 
2562
+ Tech stack options:
2563
+ --path <dir> Project root directory (default: cwd)
2564
+ --repo-id <uuid> Repository ID (or set via .codebyplan.json)
2565
+ --dry-run Preview changes without writing
2566
+
4135
2567
  ESLint commands:
4136
2568
  codebyplan eslint init Detect tech stack, resolve presets, generate configs
4137
- codebyplan eslint sync Regenerate if presets changed, detect drift
4138
2569
 
4139
2570
  MCP Server:
4140
2571
  Claude Code connects to CodeByPlan via remote MCP: