@codebyplan/cli 3.0.1 → 3.0.2

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 +196 -170
  2. package/package.json +1 -1
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.1";
40
+ VERSION = "3.0.2";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -1042,79 +1042,11 @@ var init_fileMapper = __esm({
1042
1042
  }
1043
1043
  });
1044
1044
 
1045
- // src/cli/conflict.ts
1045
+ // src/cli/confirm.ts
1046
1046
  import { createInterface as createInterface2 } from "node:readline/promises";
1047
1047
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
1048
- async function resolveConflicts(conflicts) {
1049
- const resolutions = /* @__PURE__ */ new Map();
1050
- if (conflicts.length === 0) return resolutions;
1051
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1052
- try {
1053
- console.log(`
1054
- ${conflicts.length} conflict(s) found:
1055
- `);
1056
- let applyToAll = null;
1057
- for (let i = 0; i < conflicts.length; i++) {
1058
- const conflict = conflicts[i];
1059
- if (applyToAll) {
1060
- resolutions.set(conflict.key, applyToAll);
1061
- continue;
1062
- }
1063
- console.log(` Conflict ${i + 1}/${conflicts.length}: ${conflict.displayPath}`);
1064
- if (conflict.localModified) {
1065
- console.log(` Local: modified ${conflict.localModified}`);
1066
- }
1067
- if (conflict.remoteModified) {
1068
- console.log(` Remote: modified ${conflict.remoteModified}`);
1069
- }
1070
- const remaining = conflicts.length - i;
1071
- const prompt = remaining > 1 ? " [L] Push local [R] Keep remote [S] Skip [LA/RA/SA] Apply to all: " : " [L] Push local [R] Keep remote [S] Skip: ";
1072
- const answer = (await rl.question(prompt)).trim().toUpperCase();
1073
- switch (answer) {
1074
- case "L":
1075
- resolutions.set(conflict.key, "local");
1076
- break;
1077
- case "R":
1078
- resolutions.set(conflict.key, "remote");
1079
- break;
1080
- case "LA":
1081
- resolutions.set(conflict.key, "local");
1082
- applyToAll = "local";
1083
- break;
1084
- case "RA":
1085
- resolutions.set(conflict.key, "remote");
1086
- applyToAll = "remote";
1087
- break;
1088
- case "SA":
1089
- resolutions.set(conflict.key, "skip");
1090
- applyToAll = "skip";
1091
- break;
1092
- default:
1093
- resolutions.set(conflict.key, "skip");
1094
- break;
1095
- }
1096
- }
1097
- if (applyToAll) {
1098
- const appliedCount = conflicts.length - [...resolutions.values()].filter((_, idx) => idx === 0).length;
1099
- console.log(`
1100
- Applied "${applyToAll}" to remaining conflicts.`);
1101
- }
1102
- } finally {
1103
- rl.close();
1104
- }
1105
- return resolutions;
1106
- }
1107
- var init_conflict = __esm({
1108
- "src/cli/conflict.ts"() {
1109
- "use strict";
1110
- }
1111
- });
1112
-
1113
- // src/cli/confirm.ts
1114
- import { createInterface as createInterface3 } from "node:readline/promises";
1115
- import { stdin as stdin3, stdout as stdout3 } from "node:process";
1116
1048
  async function confirmProceed(message) {
1117
- const rl = createInterface3({ input: stdin3, output: stdout3 });
1049
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1118
1050
  try {
1119
1051
  const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1120
1052
  const a = answer.trim().toLowerCase();
@@ -1273,8 +1205,8 @@ var sync_exports = {};
1273
1205
  __export(sync_exports, {
1274
1206
  runSync: () => runSync
1275
1207
  });
1276
- import { readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
1277
- import { join as join7 } from "node:path";
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";
1278
1210
  async function runSync() {
1279
1211
  const flags = parseFlags(3);
1280
1212
  const dryRun = hasFlag("dry-run", 3);
@@ -1289,118 +1221,191 @@ async function runSync() {
1289
1221
  if (dryRun) console.log(` Mode: dry-run`);
1290
1222
  if (force) console.log(` Mode: force`);
1291
1223
  console.log();
1292
- console.log(" Phase 1: Pull (DB \u2192 local)...");
1293
- const pullResult = await executeSyncToLocal({ repoId, projectPath, dryRun });
1294
- const pullChanges = pullResult.totals.created + pullResult.totals.updated + pullResult.totals.deleted;
1295
- if (pullChanges > 0) {
1296
- for (const [typeKey, result] of Object.entries(pullResult.byType)) {
1297
- for (const name of result.created) console.log(` + ${typeKey}/${name}`);
1298
- for (const name of result.updated) console.log(` ~ ${typeKey}/${name}`);
1299
- for (const name of result.deleted) console.log(` - ${typeKey}/${name}`);
1300
- }
1301
- console.log(` ${pullResult.totals.created} created, ${pullResult.totals.updated} updated, ${pullResult.totals.deleted} deleted`);
1302
- } else {
1303
- console.log(" All files up to date.");
1304
- }
1305
- console.log("\n Phase 2: Push (local \u2192 DB)...");
1306
- await executePush(repoId, projectPath, dryRun, force);
1307
- console.log("\n Phase 3: Config sync...");
1308
- await syncConfig(repoId, projectPath, dryRun);
1309
- console.log("\n Phase 4: Tech stack...");
1310
- await syncTechStack(repoId, projectPath, dryRun);
1311
- console.log("\n Sync complete.\n");
1312
- }
1313
- async function executePush(repoId, projectPath, dryRun, force) {
1224
+ console.log(" Reading local and remote state...");
1314
1225
  const claudeDir = join7(projectPath, ".claude");
1315
- let localFiles;
1226
+ let localFiles = /* @__PURE__ */ new Map();
1316
1227
  try {
1317
1228
  localFiles = await scanLocalFiles(claudeDir, projectPath);
1318
1229
  } catch {
1319
- console.log(" No .claude/ directory found. Skipping push.");
1320
- return;
1321
1230
  }
1322
- const [syncRes, repoRes] = await Promise.all([
1231
+ const [defaultsRes, repoSyncRes, repoRes] = await Promise.all([
1232
+ apiGet("/sync/defaults"),
1323
1233
  apiGet("/sync/files", { repo_id: repoId }),
1324
1234
  apiGet(`/repos/${repoId}`)
1325
1235
  ]);
1326
1236
  const repoData = repoRes.data;
1327
- const remoteFiles = flattenSyncData(syncRes.data);
1328
- const toUpsert = [];
1329
- const toDelete = [];
1330
- const conflicts = [];
1331
- const conflictLocalRecords = /* @__PURE__ */ new Map();
1332
- for (const [key, local] of localFiles) {
1237
+ const remoteDefaults = flattenSyncData(defaultsRes.data);
1238
+ const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
1239
+ const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
1240
+ console.log(` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
1241
+ `);
1242
+ const plan = [];
1243
+ const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
1244
+ for (const key of allKeys) {
1245
+ const local = localFiles.get(key);
1333
1246
  const remote = remoteFiles.get(key);
1334
- if (!remote) {
1335
- toUpsert.push({ ...local, content: reverseSubstituteVariables(local.content, repoData) });
1336
- } else {
1247
+ if (local && !remote) {
1248
+ plan.push({
1249
+ key,
1250
+ displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1251
+ action: "push",
1252
+ localContent: local.content,
1253
+ remoteContent: null,
1254
+ pushContent: reverseSubstituteVariables(local.content, repoData),
1255
+ filePath: null,
1256
+ type: local.type,
1257
+ name: local.name,
1258
+ category: local.category,
1259
+ isHook: local.type === "hook"
1260
+ });
1261
+ } else if (!local && remote) {
1262
+ const resolvedContent = substituteVariables(remote.content, repoData);
1263
+ plan.push({
1264
+ key,
1265
+ displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
1266
+ action: "pull",
1267
+ localContent: null,
1268
+ remoteContent: resolvedContent,
1269
+ pushContent: null,
1270
+ filePath: getLocalFilePath(claudeDir, projectPath, remote),
1271
+ type: remote.type,
1272
+ name: remote.name,
1273
+ category: remote.category ?? null,
1274
+ isHook: remote.type === "hook"
1275
+ });
1276
+ } else if (local && remote) {
1337
1277
  const resolvedRemote = substituteVariables(remote.content, repoData);
1338
- if (local.content !== resolvedRemote) {
1339
- const reversed = { ...local, content: reverseSubstituteVariables(local.content, repoData) };
1340
- if (force) {
1341
- toUpsert.push(reversed);
1342
- } else {
1343
- conflicts.push({
1344
- key,
1345
- displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1346
- remoteModified: remote.updated_at
1347
- });
1348
- conflictLocalRecords.set(key, reversed);
1349
- }
1278
+ if (local.content === resolvedRemote) {
1279
+ continue;
1350
1280
  }
1281
+ const repoSyncAt = repoRes.data.claude_sync_at;
1282
+ const remoteUpdatedAt = remote.updated_at;
1283
+ const remoteChanged = remoteUpdatedAt && repoSyncAt ? new Date(remoteUpdatedAt) > new Date(repoSyncAt) : true;
1284
+ let action;
1285
+ if (remoteChanged && force) {
1286
+ action = "pull";
1287
+ } else if (!remoteChanged) {
1288
+ action = "push";
1289
+ } else {
1290
+ action = "pull";
1291
+ }
1292
+ plan.push({
1293
+ key,
1294
+ displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1295
+ action,
1296
+ localContent: local.content,
1297
+ remoteContent: resolvedRemote,
1298
+ pushContent: action === "push" ? reverseSubstituteVariables(local.content, repoData) : null,
1299
+ filePath: getLocalFilePath(claudeDir, projectPath, remote),
1300
+ type: local.type,
1301
+ name: local.name,
1302
+ category: local.category,
1303
+ isHook: local.type === "hook"
1304
+ });
1351
1305
  }
1352
1306
  }
1353
- for (const [key, remote] of remoteFiles) {
1354
- if (!localFiles.has(key)) {
1355
- toDelete.push({ type: remote.type, name: remote.name, category: remote.category ?? null });
1356
- }
1307
+ const pulls = plan.filter((p) => p.action === "pull");
1308
+ 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}`);
1312
+ }
1313
+ if (pushes.length > 0) {
1314
+ console.log(` Push (local \u2192 DB): ${pushes.length}`);
1315
+ for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
1357
1316
  }
1358
- if (conflicts.length > 0) {
1359
- const resolutions = await resolveConflicts(conflicts);
1360
- for (const [key, resolution] of resolutions) {
1361
- if (resolution === "local") {
1362
- const local = conflictLocalRecords.get(key);
1363
- if (local) toUpsert.push(local);
1317
+ if (pulls.length === 0 && pushes.length === 0) {
1318
+ console.log(" All .claude/ files in sync.");
1319
+ }
1320
+ if (plan.length > 0 && !dryRun) {
1321
+ if (!force && pulls.length + pushes.length > 0) {
1322
+ console.log();
1323
+ const confirmed = await confirmProceed();
1324
+ if (!confirmed) {
1325
+ console.log(" Cancelled.\n");
1326
+ return;
1364
1327
  }
1365
1328
  }
1366
- }
1367
- if (toUpsert.length > 0) {
1368
- for (const f of toUpsert) {
1369
- console.log(` + ${f.type}/${f.category ? f.category + "/" : ""}${f.name}`);
1329
+ for (const p of pulls) {
1330
+ if (p.filePath && p.remoteContent !== null) {
1331
+ await mkdir2(dirname2(p.filePath), { recursive: true });
1332
+ await writeFile3(p.filePath, p.remoteContent, "utf-8");
1333
+ if (p.isHook) await chmod2(p.filePath, 493);
1334
+ }
1370
1335
  }
1371
- }
1372
- if (toDelete.length > 0) {
1373
- for (const d of toDelete) {
1374
- console.log(` - ${d.type}/${d.category ? d.category + "/" : ""}${d.name}`);
1336
+ const toUpsert = pushes.filter((p) => p.pushContent !== null).map((p) => ({
1337
+ type: p.type,
1338
+ name: p.name,
1339
+ category: p.category,
1340
+ content: p.pushContent
1341
+ }));
1342
+ if (toUpsert.length > 0) {
1343
+ await apiPost("/sync/files", {
1344
+ repo_id: repoId,
1345
+ files: toUpsert
1346
+ });
1375
1347
  }
1348
+ await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1349
+ console.log(`
1350
+ Applied: ${pulls.length} pulled, ${pushes.length} pushed`);
1351
+ } else if (dryRun) {
1352
+ console.log("\n (dry-run \u2014 no changes)");
1353
+ }
1354
+ console.log("\n Settings sync...");
1355
+ await syncSettings(claudeDir, projectPath, defaultsRes.data, repoData, dryRun);
1356
+ console.log(" Config sync...");
1357
+ await syncConfig(repoId, projectPath, dryRun);
1358
+ console.log(" Tech stack...");
1359
+ await syncTechStack(repoId, projectPath, dryRun);
1360
+ console.log("\n Sync complete.\n");
1361
+ }
1362
+ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
1363
+ const settingsPath = join7(claudeDir, "settings.json");
1364
+ const globalSettingsFiles = syncData.global_settings ?? [];
1365
+ let globalSettings = {};
1366
+ for (const gf of globalSettingsFiles) {
1367
+ const parsed = JSON.parse(substituteVariables(gf.content, repoData));
1368
+ globalSettings = { ...globalSettings, ...parsed };
1369
+ }
1370
+ const repoSettingsFiles = syncData.settings ?? [];
1371
+ let repoSettings = {};
1372
+ for (const rf of repoSettingsFiles) {
1373
+ repoSettings = JSON.parse(substituteVariables(rf.content, repoData));
1374
+ }
1375
+ const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
1376
+ const hooksDir = join7(projectPath, ".claude", "hooks");
1377
+ const discovered = await discoverHooks(hooksDir);
1378
+ let localSettings = {};
1379
+ try {
1380
+ const raw = await readFile7(settingsPath, "utf-8");
1381
+ localSettings = JSON.parse(raw);
1382
+ } catch {
1383
+ }
1384
+ let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
1385
+ merged = stripPermissionsAllow(merged);
1386
+ if (discovered.size > 0) {
1387
+ merged.hooks = mergeDiscoveredHooks(
1388
+ merged.hooks ?? {},
1389
+ discovered
1390
+ );
1391
+ }
1392
+ const mergedContent = JSON.stringify(merged, null, 2) + "\n";
1393
+ let currentContent = "";
1394
+ try {
1395
+ currentContent = await readFile7(settingsPath, "utf-8");
1396
+ } catch {
1376
1397
  }
1377
- if (toUpsert.length === 0 && toDelete.length === 0) {
1378
- console.log(" Nothing to push.");
1398
+ if (currentContent === mergedContent) {
1399
+ console.log(" Settings up to date.");
1379
1400
  return;
1380
1401
  }
1381
1402
  if (dryRun) {
1382
- console.log(" (dry-run \u2014 no changes)");
1403
+ console.log(" Settings would be updated (dry-run).");
1383
1404
  return;
1384
1405
  }
1385
- if (!force) {
1386
- const confirmed = await confirmProceed();
1387
- if (!confirmed) {
1388
- console.log(" Cancelled.");
1389
- return;
1390
- }
1391
- }
1392
- const result = await apiPost("/sync/files", {
1393
- repo_id: repoId,
1394
- files: toUpsert.map((f) => ({
1395
- type: f.type,
1396
- name: f.name,
1397
- category: f.category,
1398
- content: f.content
1399
- })),
1400
- delete_keys: toDelete
1401
- });
1402
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1403
- console.log(` ${result.data.upserted} upserted, ${result.data.deleted} deleted`);
1406
+ await mkdir2(dirname2(settingsPath), { recursive: true });
1407
+ await writeFile3(settingsPath, mergedContent, "utf-8");
1408
+ console.log(" Updated settings.json");
1404
1409
  }
1405
1410
  async function syncConfig(repoId, projectPath, dryRun) {
1406
1411
  const configPath = join7(projectPath, ".codebyplan.json");
@@ -1462,6 +1467,27 @@ async function syncTechStack(repoId, projectPath, dryRun) {
1462
1467
  console.log(" Tech stack detection skipped.");
1463
1468
  }
1464
1469
  }
1470
+ function getLocalFilePath(claudeDir, projectPath, remote) {
1471
+ const typeConfig2 = {
1472
+ command: { dir: "commands", ext: ".md" },
1473
+ agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
1474
+ skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
1475
+ rule: { dir: "rules", ext: ".md" },
1476
+ hook: { dir: "hooks", ext: ".sh" },
1477
+ template: { dir: "templates", ext: "" },
1478
+ claude_md: { dir: "", ext: "" },
1479
+ settings: { dir: "", ext: "" }
1480
+ };
1481
+ if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
1482
+ if (remote.type === "settings") return join7(claudeDir, "settings.json");
1483
+ 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}`);
1490
+ }
1465
1491
  function flattenSyncData(data) {
1466
1492
  const result = /* @__PURE__ */ new Map();
1467
1493
  const typeMap = {
@@ -1493,12 +1519,12 @@ var init_sync = __esm({
1493
1519
  "use strict";
1494
1520
  init_config();
1495
1521
  init_fileMapper();
1496
- init_conflict();
1497
1522
  init_confirm();
1498
1523
  init_api();
1499
- init_sync_engine();
1500
1524
  init_variables();
1501
1525
  init_tech_detect();
1526
+ init_settings_merge();
1527
+ init_hook_registry();
1502
1528
  }
1503
1529
  });
1504
1530
 
@@ -23316,11 +23342,11 @@ async function createPR(options) {
23316
23342
  }
23317
23343
  } catch {
23318
23344
  }
23319
- const { stdout: stdout4 } = await exec(
23345
+ const { stdout: stdout3 } = await exec(
23320
23346
  `gh pr create --head "${head}" --base "${base}" --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`,
23321
23347
  { cwd: repoPath }
23322
23348
  );
23323
- const prUrl = stdout4.trim();
23349
+ const prUrl = stdout3.trim();
23324
23350
  const prNumber = parseInt(prUrl.split("/").pop() ?? "0", 10);
23325
23351
  return { pr_url: prUrl, pr_number: prNumber || null };
23326
23352
  } catch (err) {
@@ -23331,11 +23357,11 @@ async function createPR(options) {
23331
23357
  async function mergePR(options) {
23332
23358
  const { repoPath, prNumber, mergeMethod } = options;
23333
23359
  try {
23334
- const { stdout: stdout4 } = await exec(
23360
+ const { stdout: stdout3 } = await exec(
23335
23361
  `gh pr merge ${prNumber} --${mergeMethod} --delete-branch`,
23336
23362
  { cwd: repoPath }
23337
23363
  );
23338
- return { merged: true, message: stdout4.trim() || `PR #${prNumber} merged via ${mergeMethod}` };
23364
+ return { merged: true, message: stdout3.trim() || `PR #${prNumber} merged via ${mergeMethod}` };
23339
23365
  } catch (err) {
23340
23366
  const errorMessage = err instanceof Error ? err.message : String(err);
23341
23367
  return { merged: false, message: "Merge failed", error: errorMessage };
@@ -23343,11 +23369,11 @@ async function mergePR(options) {
23343
23369
  }
23344
23370
  async function getPRStatus(repoPath, prNumber) {
23345
23371
  try {
23346
- const { stdout: stdout4 } = await exec(
23372
+ const { stdout: stdout3 } = await exec(
23347
23373
  `gh pr view ${prNumber} --json state,mergeable,title,url,number`,
23348
23374
  { cwd: repoPath }
23349
23375
  );
23350
- const pr = JSON.parse(stdout4.trim());
23376
+ const pr = JSON.parse(stdout3.trim());
23351
23377
  return {
23352
23378
  state: pr.state,
23353
23379
  mergeable: pr.mergeable,
@@ -24192,7 +24218,7 @@ function registerFileGenTools(server) {
24192
24218
  }
24193
24219
  const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
24194
24220
  await exec2(addCmd, { cwd: repo.path });
24195
- const { stdout: stdout4, stderr } = await exec2(
24221
+ const { stdout: stdout3, stderr } = await exec2(
24196
24222
  `git commit -m "${message.replace(/"/g, '\\"')}"`,
24197
24223
  { cwd: repo.path }
24198
24224
  );
@@ -24203,7 +24229,7 @@ function registerFileGenTools(server) {
24203
24229
  text: JSON.stringify({
24204
24230
  status: "committed",
24205
24231
  branch: branch.trim(),
24206
- output: stdout4.trim(),
24232
+ output: stdout3.trim(),
24207
24233
  warnings: stderr.trim() || void 0
24208
24234
  }, null, 2)
24209
24235
  }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {