@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.
- package/dist/cli.js +392 -36
- 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
|
|
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
|
|
1076
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1113
|
+
async function scanPackageJson(pkgPath, seen) {
|
|
1077
1114
|
try {
|
|
1078
|
-
const raw = await readFile6(
|
|
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(
|
|
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
|
|
1209
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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 (
|
|
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
|
-
|
|
1508
|
+
let toDelete = [];
|
|
1509
|
+
let toPull = contentPulls;
|
|
1510
|
+
if (remoteOnly.length > 0) {
|
|
1322
1511
|
console.log();
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
1638
|
+
const configPath = join8(projectPath, ".codebyplan.json");
|
|
1412
1639
|
let currentConfig = {};
|
|
1413
1640
|
try {
|
|
1414
|
-
const raw = await
|
|
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
|
|
1482
|
-
if (remote.type === "settings") return
|
|
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
|
|
1485
|
-
const typeDir = remote.type === "command" ?
|
|
1486
|
-
if (cfg.subfolder) return
|
|
1487
|
-
if (remote.type === "command" && remote.category) return
|
|
1488
|
-
if (remote.type === "template") return
|
|
1489
|
-
return
|
|
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
|
|
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/
|
|
8
|
-
"codebyplan": "dist/
|
|
7
|
+
"@codebyplan/cli": "dist/cli.js",
|
|
8
|
+
"codebyplan": "dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/cli.js",
|