@codebyplan/cli 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +392 -36
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ var VERSION, PACKAGE_NAME;
37
37
  var init_version = __esm({
38
38
  "src/lib/version.ts"() {
39
39
  "use strict";
40
- VERSION = "3.0.2";
40
+ VERSION = "3.1.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -465,6 +465,7 @@ async function executeSyncToLocal(options) {
465
465
  const worktree = await isGitWorktree(projectPath);
466
466
  const byType = {};
467
467
  const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
468
+ const dbOnlyFiles = [];
468
469
  for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
469
470
  if (worktree && typeName === "command") {
470
471
  byType["commands"] = { created: [], updated: [], deleted: [], unchanged: [] };
@@ -489,6 +490,13 @@ async function executeSyncToLocal(options) {
489
490
  const fullPath = join2(targetDir, relPath);
490
491
  const localContent = localFiles.get(relPath);
491
492
  if (localContent === void 0) {
493
+ const remoteFile = remoteFiles.find((f) => f.name === name);
494
+ dbOnlyFiles.push({
495
+ type: typeName,
496
+ name,
497
+ category: remoteFile?.category ?? null,
498
+ localPath: fullPath
499
+ });
492
500
  if (!dryRun) {
493
501
  await mkdir(dirname(fullPath), { recursive: true });
494
502
  await writeFile(fullPath, content, "utf-8");
@@ -659,7 +667,7 @@ async function executeSyncToLocal(options) {
659
667
  if (!dryRun) {
660
668
  await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
661
669
  }
662
- return { byType, totals };
670
+ return { byType, totals, dbOnlyFiles };
663
671
  }
664
672
  var typeConfig, syncKeyToType;
665
673
  var init_sync_engine = __esm({
@@ -1055,6 +1063,36 @@ async function confirmProceed(message) {
1055
1063
  rl.close();
1056
1064
  }
1057
1065
  }
1066
+ async function promptChoice(message, options) {
1067
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1068
+ try {
1069
+ const answer = await rl.question(message);
1070
+ const a = answer.trim().toLowerCase();
1071
+ return options.includes(a) ? a : options[0];
1072
+ } finally {
1073
+ rl.close();
1074
+ }
1075
+ }
1076
+ async function confirmEach(items, label) {
1077
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1078
+ const accepted = [];
1079
+ try {
1080
+ for (const item of items) {
1081
+ const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
1082
+ const a = answer.trim().toLowerCase();
1083
+ if (a === "a") {
1084
+ accepted.push(item, ...items.slice(items.indexOf(item) + 1));
1085
+ break;
1086
+ }
1087
+ if (a === "y" || a === "yes" || a === "") {
1088
+ accepted.push(item);
1089
+ }
1090
+ }
1091
+ } finally {
1092
+ rl.close();
1093
+ }
1094
+ return accepted;
1095
+ }
1058
1096
  var init_confirm = __esm({
1059
1097
  "src/cli/confirm.ts"() {
1060
1098
  "use strict";
@@ -1062,7 +1100,7 @@ var init_confirm = __esm({
1062
1100
  });
1063
1101
 
1064
1102
  // src/lib/tech-detect.ts
1065
- import { readFile as readFile6, access } from "node:fs/promises";
1103
+ import { readFile as readFile6, readdir as readdir4, access } from "node:fs/promises";
1066
1104
  import { join as join6 } from "node:path";
1067
1105
  async function fileExists(filePath) {
1068
1106
  try {
@@ -1072,10 +1110,9 @@ async function fileExists(filePath) {
1072
1110
  return false;
1073
1111
  }
1074
1112
  }
1075
- async function detectTechStack(projectPath) {
1076
- const seen = /* @__PURE__ */ new Map();
1113
+ async function scanPackageJson(pkgPath, seen) {
1077
1114
  try {
1078
- const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
1115
+ const raw = await readFile6(pkgPath, "utf-8");
1079
1116
  const pkg = JSON.parse(raw);
1080
1117
  const allDeps = {
1081
1118
  ...pkg.dependencies,
@@ -1092,12 +1129,42 @@ async function detectTechStack(projectPath) {
1092
1129
  }
1093
1130
  } catch {
1094
1131
  }
1132
+ }
1133
+ async function scanConfigFiles(dir, seen) {
1095
1134
  for (const { file, rule } of CONFIG_FILE_MAP) {
1096
1135
  const key = rule.name.toLowerCase();
1097
- if (!seen.has(key) && await fileExists(join6(projectPath, file))) {
1136
+ if (!seen.has(key) && await fileExists(join6(dir, file))) {
1098
1137
  seen.set(key, { name: rule.name, category: rule.category });
1099
1138
  }
1100
1139
  }
1140
+ }
1141
+ async function detectWorkspaceDirs(projectPath) {
1142
+ const isTurbo = await fileExists(join6(projectPath, "turbo.json"));
1143
+ const isPnpm = await fileExists(join6(projectPath, "pnpm-workspace.yaml"));
1144
+ if (!isTurbo && !isPnpm) return [];
1145
+ const dirs = [];
1146
+ for (const parent of ["apps", "packages"]) {
1147
+ try {
1148
+ const entries = await readdir4(join6(projectPath, parent), { withFileTypes: true });
1149
+ for (const entry of entries) {
1150
+ if (entry.isDirectory()) {
1151
+ dirs.push(join6(projectPath, parent, entry.name));
1152
+ }
1153
+ }
1154
+ } catch {
1155
+ }
1156
+ }
1157
+ return dirs;
1158
+ }
1159
+ async function detectTechStack(projectPath) {
1160
+ const seen = /* @__PURE__ */ new Map();
1161
+ await scanPackageJson(join6(projectPath, "package.json"), seen);
1162
+ await scanConfigFiles(projectPath, seen);
1163
+ const workspaceDirs = await detectWorkspaceDirs(projectPath);
1164
+ for (const dir of workspaceDirs) {
1165
+ await scanPackageJson(join6(dir, "package.json"), seen);
1166
+ await scanConfigFiles(dir, seen);
1167
+ }
1101
1168
  return Array.from(seen.values()).sort((a, b) => {
1102
1169
  const catCmp = a.category.localeCompare(b.category);
1103
1170
  if (catCmp !== 0) return catCmp;
@@ -1200,13 +1267,126 @@ var init_tech_detect = __esm({
1200
1267
  }
1201
1268
  });
1202
1269
 
1270
+ // src/lib/server-detect.ts
1271
+ import { readFile as readFile7, readdir as readdir5, access as access2 } from "node:fs/promises";
1272
+ import { join as join7 } from "node:path";
1273
+ async function fileExists2(filePath) {
1274
+ try {
1275
+ await access2(filePath);
1276
+ return true;
1277
+ } catch {
1278
+ return false;
1279
+ }
1280
+ }
1281
+ function detectPackageManager(dir) {
1282
+ return (async () => {
1283
+ if (await fileExists2(join7(dir, "pnpm-lock.yaml"))) return "pnpm";
1284
+ if (await fileExists2(join7(dir, "yarn.lock"))) return "yarn";
1285
+ return "npm";
1286
+ })();
1287
+ }
1288
+ function detectFramework(pkg) {
1289
+ const deps = pkg.dependencies ?? {};
1290
+ const devDeps = pkg.devDependencies ?? {};
1291
+ const hasDep = (name) => name in deps || name in devDeps;
1292
+ if (hasDep("next")) return "nextjs";
1293
+ if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
1294
+ if (hasDep("expo")) return "expo";
1295
+ if (hasDep("vite")) return "vite";
1296
+ if (hasDep("express")) return "express";
1297
+ if (hasDep("@nestjs/core")) return "nestjs";
1298
+ return "custom";
1299
+ }
1300
+ function detectPortFromScripts(pkg) {
1301
+ const scripts = pkg.scripts;
1302
+ if (!scripts?.dev) return null;
1303
+ const parts = scripts.dev.split(/\s+/);
1304
+ for (let i = 0; i < parts.length - 1; i++) {
1305
+ if (parts[i] === "--port" || parts[i] === "-p") {
1306
+ const next = parts[i + 1];
1307
+ if (next) {
1308
+ const port = parseInt(next, 10);
1309
+ if (!isNaN(port)) return port;
1310
+ }
1311
+ }
1312
+ }
1313
+ return null;
1314
+ }
1315
+ async function isMonorepo(dir) {
1316
+ return await fileExists2(join7(dir, "turbo.json")) || await fileExists2(join7(dir, "pnpm-workspace.yaml"));
1317
+ }
1318
+ async function detectServers(projectPath) {
1319
+ let pkg;
1320
+ try {
1321
+ const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
1322
+ pkg = JSON.parse(raw);
1323
+ } catch {
1324
+ return {
1325
+ name: "unknown",
1326
+ isMonorepo: false,
1327
+ packageManager: "npm",
1328
+ servers: []
1329
+ };
1330
+ }
1331
+ const rawName = pkg.name ?? "unknown";
1332
+ const name = rawName.startsWith("@") ? projectPath.split("/").pop() ?? rawName : rawName;
1333
+ const pkgManager = await detectPackageManager(projectPath);
1334
+ const mono = await isMonorepo(projectPath);
1335
+ const servers = [];
1336
+ if (mono) {
1337
+ const appsDir = join7(projectPath, "apps");
1338
+ try {
1339
+ const entries = await readdir5(appsDir, { withFileTypes: true });
1340
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
1341
+ for (const entry of sorted) {
1342
+ if (!entry.isDirectory()) continue;
1343
+ const appPkgPath = join7(appsDir, entry.name, "package.json");
1344
+ try {
1345
+ const appRaw = await readFile7(appPkgPath, "utf-8");
1346
+ const appPkg = JSON.parse(appRaw);
1347
+ const appName = entry.name;
1348
+ const framework = detectFramework(appPkg);
1349
+ const port = detectPortFromScripts(appPkg);
1350
+ let command;
1351
+ switch (pkgManager) {
1352
+ case "pnpm":
1353
+ command = `pnpm --filter ${appName} dev`;
1354
+ break;
1355
+ case "yarn":
1356
+ command = `yarn workspace ${appName} dev`;
1357
+ break;
1358
+ default:
1359
+ command = `npm run dev -w apps/${appName}`;
1360
+ break;
1361
+ }
1362
+ servers.push({ label: appName, port, command, server_type: framework });
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ } catch {
1367
+ }
1368
+ } else {
1369
+ const framework = detectFramework(pkg);
1370
+ const port = detectPortFromScripts(pkg);
1371
+ const command = `${pkgManager} run dev`;
1372
+ servers.push({ label: "dev", port, command, server_type: framework });
1373
+ }
1374
+ return { name, isMonorepo: mono, packageManager: pkgManager, servers };
1375
+ }
1376
+ var init_server_detect = __esm({
1377
+ "src/lib/server-detect.ts"() {
1378
+ "use strict";
1379
+ }
1380
+ });
1381
+
1203
1382
  // src/cli/sync.ts
1204
1383
  var sync_exports = {};
1205
1384
  __export(sync_exports, {
1206
1385
  runSync: () => runSync
1207
1386
  });
1208
- import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1209
- import { join as join7, dirname as dirname2 } from "node:path";
1387
+ import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1388
+ import { homedir as homedir2 } from "node:os";
1389
+ import { join as join8, dirname as dirname2 } from "node:path";
1210
1390
  async function runSync() {
1211
1391
  const flags = parseFlags(3);
1212
1392
  const dryRun = hasFlag("dry-run", 3);
@@ -1222,7 +1402,7 @@ async function runSync() {
1222
1402
  if (force) console.log(` Mode: force`);
1223
1403
  console.log();
1224
1404
  console.log(" Reading local and remote state...");
1225
- const claudeDir = join7(projectPath, ".claude");
1405
+ const claudeDir = join8(projectPath, ".claude");
1226
1406
  let localFiles = /* @__PURE__ */ new Map();
1227
1407
  try {
1228
1408
  localFiles = await scanLocalFiles(claudeDir, projectPath);
@@ -1306,27 +1486,61 @@ async function runSync() {
1306
1486
  }
1307
1487
  const pulls = plan.filter((p) => p.action === "pull");
1308
1488
  const pushes = plan.filter((p) => p.action === "push");
1309
- if (pulls.length > 0) {
1310
- console.log(` Pull (DB \u2192 local): ${pulls.length}`);
1311
- for (const p of pulls) console.log(` \u2193 ${p.displayPath}`);
1489
+ const remoteOnly = plan.filter((p) => p.action === "pull" && p.localContent === null);
1490
+ const contentPulls = pulls.filter((p) => p.localContent !== null);
1491
+ if (contentPulls.length > 0) {
1492
+ console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
1493
+ for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
1312
1494
  }
1313
1495
  if (pushes.length > 0) {
1314
1496
  console.log(` Push (local \u2192 DB): ${pushes.length}`);
1315
1497
  for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
1316
1498
  }
1317
- if (pulls.length === 0 && pushes.length === 0) {
1499
+ if (remoteOnly.length > 0) {
1500
+ console.log(`
1501
+ DB-only (not on disk): ${remoteOnly.length}`);
1502
+ for (const p of remoteOnly) console.log(` \u2715 ${p.displayPath}`);
1503
+ }
1504
+ if (contentPulls.length === 0 && pushes.length === 0 && remoteOnly.length === 0) {
1318
1505
  console.log(" All .claude/ files in sync.");
1319
1506
  }
1320
1507
  if (plan.length > 0 && !dryRun) {
1321
- if (!force && pulls.length + pushes.length > 0) {
1508
+ let toDelete = [];
1509
+ let toPull = contentPulls;
1510
+ if (remoteOnly.length > 0) {
1322
1511
  console.log();
1323
- const confirmed = await confirmProceed();
1324
- if (!confirmed) {
1325
- console.log(" Cancelled.\n");
1326
- return;
1512
+ console.log(` ${remoteOnly.length} file(s) exist in DB but not locally.`);
1513
+ const choice = await promptChoice(
1514
+ " Delete from DB? [a]ll / [o]ne-by-one / [p]ull instead: ",
1515
+ ["a", "o", "p"]
1516
+ );
1517
+ if (choice === "a") {
1518
+ toDelete = remoteOnly;
1519
+ } else if (choice === "o") {
1520
+ toDelete = await confirmEach(
1521
+ remoteOnly,
1522
+ (p) => p.displayPath
1523
+ );
1524
+ const deleteKeys = new Set(toDelete.map((d) => d.key));
1525
+ const pullBack = remoteOnly.filter((p) => !deleteKeys.has(p.key));
1526
+ toPull = [...toPull, ...pullBack];
1527
+ } else {
1528
+ toPull = [...toPull, ...remoteOnly];
1327
1529
  }
1328
1530
  }
1329
- for (const p of pulls) {
1531
+ if (toPull.length + pushes.length + toDelete.length > 0 && !force) {
1532
+ if (toPull.length > 0 || pushes.length > 0) {
1533
+ const confirmed = await confirmProceed(
1534
+ `
1535
+ Apply ${toPull.length} pull(s), ${pushes.length} push(es), ${toDelete.length} deletion(s)? [Y/n] `
1536
+ );
1537
+ if (!confirmed) {
1538
+ console.log(" Cancelled.\n");
1539
+ return;
1540
+ }
1541
+ }
1542
+ }
1543
+ for (const p of toPull) {
1330
1544
  if (p.filePath && p.remoteContent !== null) {
1331
1545
  await mkdir2(dirname2(p.filePath), { recursive: true });
1332
1546
  await writeFile3(p.filePath, p.remoteContent, "utf-8");
@@ -1345,9 +1559,20 @@ async function runSync() {
1345
1559
  files: toUpsert
1346
1560
  });
1347
1561
  }
1562
+ if (toDelete.length > 0) {
1563
+ const deleteKeys = toDelete.map((p) => ({
1564
+ type: p.type,
1565
+ name: p.name,
1566
+ category: p.category
1567
+ }));
1568
+ await apiPost("/sync/files", {
1569
+ repo_id: repoId,
1570
+ delete_keys: deleteKeys
1571
+ });
1572
+ }
1348
1573
  await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1349
1574
  console.log(`
1350
- Applied: ${pulls.length} pulled, ${pushes.length} pushed`);
1575
+ Applied: ${toPull.length} pulled, ${pushes.length} pushed, ${toDelete.length} deleted from DB`);
1351
1576
  } else if (dryRun) {
1352
1577
  console.log("\n (dry-run \u2014 no changes)");
1353
1578
  }
@@ -1357,10 +1582,12 @@ async function runSync() {
1357
1582
  await syncConfig(repoId, projectPath, dryRun);
1358
1583
  console.log(" Tech stack...");
1359
1584
  await syncTechStack(repoId, projectPath, dryRun);
1585
+ console.log(" Desktop server configs...");
1586
+ await syncDesktopConfigs(repoId, projectPath, repoData, dryRun);
1360
1587
  console.log("\n Sync complete.\n");
1361
1588
  }
1362
1589
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
1363
- const settingsPath = join7(claudeDir, "settings.json");
1590
+ const settingsPath = join8(claudeDir, "settings.json");
1364
1591
  const globalSettingsFiles = syncData.global_settings ?? [];
1365
1592
  let globalSettings = {};
1366
1593
  for (const gf of globalSettingsFiles) {
@@ -1373,11 +1600,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1373
1600
  repoSettings = JSON.parse(substituteVariables(rf.content, repoData));
1374
1601
  }
1375
1602
  const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
1376
- const hooksDir = join7(projectPath, ".claude", "hooks");
1603
+ const hooksDir = join8(projectPath, ".claude", "hooks");
1377
1604
  const discovered = await discoverHooks(hooksDir);
1378
1605
  let localSettings = {};
1379
1606
  try {
1380
- const raw = await readFile7(settingsPath, "utf-8");
1607
+ const raw = await readFile8(settingsPath, "utf-8");
1381
1608
  localSettings = JSON.parse(raw);
1382
1609
  } catch {
1383
1610
  }
@@ -1392,7 +1619,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1392
1619
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
1393
1620
  let currentContent = "";
1394
1621
  try {
1395
- currentContent = await readFile7(settingsPath, "utf-8");
1622
+ currentContent = await readFile8(settingsPath, "utf-8");
1396
1623
  } catch {
1397
1624
  }
1398
1625
  if (currentContent === mergedContent) {
@@ -1408,10 +1635,10 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1408
1635
  console.log(" Updated settings.json");
1409
1636
  }
1410
1637
  async function syncConfig(repoId, projectPath, dryRun) {
1411
- const configPath = join7(projectPath, ".codebyplan.json");
1638
+ const configPath = join8(projectPath, ".codebyplan.json");
1412
1639
  let currentConfig = {};
1413
1640
  try {
1414
- const raw = await readFile7(configPath, "utf-8");
1641
+ const raw = await readFile8(configPath, "utf-8");
1415
1642
  currentConfig = JSON.parse(raw);
1416
1643
  } catch {
1417
1644
  currentConfig = { repo_id: repoId };
@@ -1478,15 +1705,15 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
1478
1705
  claude_md: { dir: "", ext: "" },
1479
1706
  settings: { dir: "", ext: "" }
1480
1707
  };
1481
- if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
1482
- if (remote.type === "settings") return join7(claudeDir, "settings.json");
1708
+ if (remote.type === "claude_md") return join8(projectPath, "CLAUDE.md");
1709
+ if (remote.type === "settings") return join8(claudeDir, "settings.json");
1483
1710
  const cfg = typeConfig2[remote.type];
1484
- if (!cfg) return join7(claudeDir, remote.name);
1485
- const typeDir = remote.type === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
1486
- if (cfg.subfolder) return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
1487
- if (remote.type === "command" && remote.category) return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
1488
- if (remote.type === "template") return join7(typeDir, remote.name);
1489
- return join7(typeDir, `${remote.name}${cfg.ext}`);
1711
+ if (!cfg) return join8(claudeDir, remote.name);
1712
+ const typeDir = remote.type === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
1713
+ if (cfg.subfolder) return join8(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
1714
+ if (remote.type === "command" && remote.category) return join8(typeDir, remote.category, `${remote.name}${cfg.ext}`);
1715
+ if (remote.type === "template") return join8(typeDir, remote.name);
1716
+ return join8(typeDir, `${remote.name}${cfg.ext}`);
1490
1717
  }
1491
1718
  function flattenSyncData(data) {
1492
1719
  const result = /* @__PURE__ */ new Map();
@@ -1514,6 +1741,99 @@ function flattenSyncData(data) {
1514
1741
  }
1515
1742
  return result;
1516
1743
  }
1744
+ async function syncDesktopConfigs(repoId, projectPath, repoData, dryRun) {
1745
+ try {
1746
+ const cbpDir = join8(homedir2(), ".codebyplan");
1747
+ const configsPath = join8(cbpDir, "server-configs.json");
1748
+ let configFile = { repos: [] };
1749
+ try {
1750
+ const raw = await readFile8(configsPath, "utf-8");
1751
+ configFile = JSON.parse(raw);
1752
+ } catch {
1753
+ }
1754
+ const detection = await detectServers(projectPath);
1755
+ let portAllocations = [];
1756
+ try {
1757
+ const localConfigRaw = await readFile8(join8(projectPath, ".codebyplan.json"), "utf-8");
1758
+ const localConfig = JSON.parse(localConfigRaw);
1759
+ portAllocations = localConfig.port_allocations ?? [];
1760
+ } catch {
1761
+ }
1762
+ const rootAllocations = portAllocations.filter((a) => !a.worktree_id);
1763
+ const worktreeAllocations = portAllocations.filter((a) => a.worktree_id);
1764
+ const matchDetected = (alloc) => {
1765
+ let matched = detection.servers.find(
1766
+ (s) => alloc.label.toLowerCase().includes(s.label.toLowerCase())
1767
+ );
1768
+ if (!matched) {
1769
+ matched = detection.servers.find(
1770
+ (s) => s.server_type === alloc.server_type
1771
+ );
1772
+ }
1773
+ return {
1774
+ label: alloc.label,
1775
+ port: alloc.port,
1776
+ command: matched?.command ?? "",
1777
+ server_type: alloc.server_type,
1778
+ auto_start: alloc.auto_start
1779
+ };
1780
+ };
1781
+ let servers;
1782
+ if (rootAllocations.length > 0) {
1783
+ servers = rootAllocations.map(matchDetected);
1784
+ } else {
1785
+ servers = detection.servers.map((s) => ({
1786
+ label: s.label,
1787
+ port: s.port,
1788
+ command: s.command,
1789
+ server_type: s.server_type,
1790
+ auto_start: "off"
1791
+ }));
1792
+ }
1793
+ const worktreeGroups = /* @__PURE__ */ new Map();
1794
+ for (const alloc of worktreeAllocations) {
1795
+ const wId = alloc.worktree_id;
1796
+ if (!worktreeGroups.has(wId)) worktreeGroups.set(wId, []);
1797
+ worktreeGroups.get(wId).push(alloc);
1798
+ }
1799
+ const worktrees = Array.from(worktreeGroups.entries()).map(
1800
+ ([worktreeId, allocs]) => {
1801
+ const firstLabel = allocs[0]?.label ?? "";
1802
+ const parenMatch = firstLabel.match(/\(([^)]+)\)/);
1803
+ const worktreeName = parenMatch?.[1] ?? worktreeId;
1804
+ return {
1805
+ name: worktreeName,
1806
+ path: "",
1807
+ // Path is managed by the desktop app
1808
+ cloud_id: worktreeId,
1809
+ servers: allocs.map(matchDetected)
1810
+ };
1811
+ }
1812
+ );
1813
+ const repoEntry = {
1814
+ name: repoData.name,
1815
+ path: projectPath,
1816
+ servers,
1817
+ cloud_id: repoId,
1818
+ ...worktrees.length > 0 ? { worktrees } : {}
1819
+ };
1820
+ const existingIndex = configFile.repos.findIndex((r) => r.cloud_id === repoId);
1821
+ if (existingIndex >= 0) {
1822
+ configFile.repos[existingIndex] = repoEntry;
1823
+ } else {
1824
+ configFile.repos.push(repoEntry);
1825
+ }
1826
+ if (dryRun) {
1827
+ console.log(" Desktop server configs would be updated (dry-run).");
1828
+ return;
1829
+ }
1830
+ await mkdir2(cbpDir, { recursive: true });
1831
+ await writeFile3(configsPath, JSON.stringify(configFile, null, 2) + "\n", "utf-8");
1832
+ console.log(` Updated server-configs.json (${servers.length} server(s), ${worktrees.length} worktree(s))`);
1833
+ } catch {
1834
+ console.log(" Desktop server config sync skipped.");
1835
+ }
1836
+ }
1517
1837
  var init_sync = __esm({
1518
1838
  "src/cli/sync.ts"() {
1519
1839
  "use strict";
@@ -1523,6 +1843,7 @@ var init_sync = __esm({
1523
1843
  init_api();
1524
1844
  init_variables();
1525
1845
  init_tech_detect();
1846
+ init_server_detect();
1526
1847
  init_settings_merge();
1527
1848
  init_hook_registry();
1528
1849
  }
@@ -24031,17 +24352,52 @@ function registerWriteTools(server) {
24031
24352
  }, async ({ repo_id, project_path }) => {
24032
24353
  try {
24033
24354
  const syncResult = await executeSyncToLocal({ repoId: repo_id, projectPath: project_path });
24034
- const { byType, totals } = syncResult;
24355
+ const { byType, totals, dbOnlyFiles } = syncResult;
24035
24356
  const summary = {
24036
24357
  ...byType,
24037
24358
  totals: { created: totals.created, updated: totals.updated, deleted: totals.deleted },
24038
24359
  message: totals.created + totals.updated + totals.deleted === 0 ? "All files up to date" : `Synced: ${totals.created} created, ${totals.updated} updated, ${totals.deleted} deleted`
24039
24360
  };
24361
+ if (dbOnlyFiles.length > 0) {
24362
+ summary.db_only_files = dbOnlyFiles;
24363
+ summary.message += `
24364
+
24365
+ ${dbOnlyFiles.length} file(s) exist in DB but were missing locally (recreated). Use delete_claude_files to remove them from DB if they were intentionally deleted.`;
24366
+ }
24040
24367
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
24041
24368
  } catch (err) {
24042
24369
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24043
24370
  }
24044
24371
  });
24372
+ server.registerTool("delete_claude_files", {
24373
+ description: "Soft-delete claude files from the CodeByPlan DB. Use after sync_claude_files reports db_only_files that should be removed. Each file is identified by type, name, and optional category.",
24374
+ inputSchema: {
24375
+ repo_id: external_exports.string().uuid().describe("Repository ID"),
24376
+ files: external_exports.array(external_exports.object({
24377
+ type: external_exports.string().describe("File type: command, agent, skill, rule, hook, template, docs_stack"),
24378
+ name: external_exports.string().describe("File name"),
24379
+ category: external_exports.string().nullable().optional().describe("Category (for commands: e.g. 'development/checkpoint')")
24380
+ })).describe("Files to soft-delete from DB")
24381
+ }
24382
+ }, async ({ repo_id, files }) => {
24383
+ try {
24384
+ const res = await apiPost("/sync/files", {
24385
+ repo_id,
24386
+ delete_keys: files
24387
+ });
24388
+ return {
24389
+ content: [{
24390
+ type: "text",
24391
+ text: JSON.stringify({
24392
+ deleted: res.data.deleted,
24393
+ message: `Soft-deleted ${res.data.deleted} file(s) from DB. Run sync_claude_files to clean up local copies.`
24394
+ }, null, 2)
24395
+ }]
24396
+ };
24397
+ } catch (err) {
24398
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24399
+ }
24400
+ });
24045
24401
  server.registerTool("update_session_state", {
24046
24402
  description: "Update session state for a repo. Actions: activate (deactivates other repos), deactivate, pause, refresh, clear_refresh.",
24047
24403
  inputSchema: {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
7
- "@codebyplan/cli": "dist/index.js",
8
- "codebyplan": "dist/index.js"
7
+ "@codebyplan/cli": "dist/cli.js",
8
+ "codebyplan": "dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "dist/cli.js",