@codebyplan/cli 3.1.0 → 3.2.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 +353 -350
  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.1.0";
40
+ VERSION = "3.2.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -1063,35 +1063,89 @@ async function confirmProceed(message) {
1063
1063
  rl.close();
1064
1064
  }
1065
1065
  }
1066
- async function promptChoice(message, options) {
1066
+ function parseReviewAction(input, fallback) {
1067
+ const a = input.trim().toLowerCase();
1068
+ switch (a) {
1069
+ case "d":
1070
+ case "delete":
1071
+ return { action: "delete", all: false };
1072
+ case "p":
1073
+ case "pull":
1074
+ return { action: "pull", all: false };
1075
+ case "s":
1076
+ case "push":
1077
+ return { action: "push", all: false };
1078
+ case "k":
1079
+ case "skip":
1080
+ return { action: "skip", all: false };
1081
+ case "da":
1082
+ return { action: "delete", all: true };
1083
+ case "pa":
1084
+ return { action: "pull", all: true };
1085
+ case "sa":
1086
+ return { action: "push", all: true };
1087
+ case "ka":
1088
+ return { action: "skip", all: true };
1089
+ case "":
1090
+ return { action: fallback, all: false };
1091
+ default:
1092
+ return { action: fallback, all: false };
1093
+ }
1094
+ }
1095
+ async function promptReviewMode() {
1067
1096
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1068
1097
  try {
1069
- const answer = await rl.question(message);
1098
+ const answer = await rl.question(" Review [o]ne-by-one or [f]older-by-folder? ");
1070
1099
  const a = answer.trim().toLowerCase();
1071
- return options.includes(a) ? a : options[0];
1100
+ return a === "f" || a === "folder" ? "folder" : "file";
1072
1101
  } finally {
1073
1102
  rl.close();
1074
1103
  }
1075
1104
  }
1076
- async function confirmEach(items, label) {
1105
+ async function reviewFilesOneByOne(items, label, plannedAction) {
1077
1106
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1078
- const accepted = [];
1107
+ const results = [];
1079
1108
  try {
1109
+ let applyAll = null;
1080
1110
  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);
1111
+ if (applyAll) {
1112
+ results.push(applyAll);
1113
+ continue;
1089
1114
  }
1115
+ const planned = plannedAction(item);
1116
+ const answer = await rl.question(
1117
+ ` ${label(item)} (${planned}) \u2014 [d]elete [p]ull pu[s]h s[k]ip: `
1118
+ );
1119
+ const { action, all } = parseReviewAction(answer, planned);
1120
+ results.push(action);
1121
+ if (all) applyAll = action;
1090
1122
  }
1091
1123
  } finally {
1092
1124
  rl.close();
1093
1125
  }
1094
- return accepted;
1126
+ return results;
1127
+ }
1128
+ async function reviewFolder(folderName, items, label, plannedAction) {
1129
+ console.log(`
1130
+ ${folderName} (${items.length} files):`);
1131
+ for (const item of items) {
1132
+ console.log(` ${label(item)} (${plannedAction(item)})`);
1133
+ }
1134
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1135
+ let answer;
1136
+ try {
1137
+ answer = await rl.question(
1138
+ ` Action for all: [d]elete [p]ull pu[s]h s[k]ip [o]ne-by-one: `
1139
+ );
1140
+ } finally {
1141
+ rl.close();
1142
+ }
1143
+ const a = answer.trim().toLowerCase();
1144
+ if (a === "o" || a === "one-by-one") {
1145
+ return reviewFilesOneByOne(items, label, plannedAction);
1146
+ }
1147
+ const { action } = parseReviewAction(a, "skip");
1148
+ return items.map(() => action);
1095
1149
  }
1096
1150
  var init_confirm = __esm({
1097
1151
  "src/cli/confirm.ts"() {
@@ -1100,7 +1154,7 @@ var init_confirm = __esm({
1100
1154
  });
1101
1155
 
1102
1156
  // src/lib/tech-detect.ts
1103
- import { readFile as readFile6, readdir as readdir4, access } from "node:fs/promises";
1157
+ import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
1104
1158
  import { join as join6 } from "node:path";
1105
1159
  async function fileExists(filePath) {
1106
1160
  try {
@@ -1110,14 +1164,56 @@ async function fileExists(filePath) {
1110
1164
  return false;
1111
1165
  }
1112
1166
  }
1113
- async function scanPackageJson(pkgPath, seen) {
1167
+ async function discoverMonorepoApps(projectPath) {
1168
+ const apps = [];
1169
+ const patterns = [];
1114
1170
  try {
1115
- const raw = await readFile6(pkgPath, "utf-8");
1171
+ const raw = await readFile6(join6(projectPath, "pnpm-workspace.yaml"), "utf-8");
1172
+ const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
1173
+ if (matches) {
1174
+ for (const m of matches) {
1175
+ const pattern = m.replace(/^\s*-\s*['"]?/, "").replace(/['"]?\s*$/, "").trim();
1176
+ if (pattern) patterns.push(pattern);
1177
+ }
1178
+ }
1179
+ } catch {
1180
+ }
1181
+ if (patterns.length === 0) {
1182
+ try {
1183
+ const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
1184
+ const pkg = JSON.parse(raw);
1185
+ const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1186
+ if (ws) patterns.push(...ws);
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ for (const pattern of patterns) {
1191
+ if (pattern.endsWith("/*")) {
1192
+ const dir = pattern.slice(0, -2);
1193
+ const absDir = join6(projectPath, dir);
1194
+ try {
1195
+ const entries = await readdir4(absDir, { withFileTypes: true });
1196
+ for (const entry of entries) {
1197
+ if (entry.isDirectory()) {
1198
+ const relPath = join6(dir, entry.name);
1199
+ const absPath = join6(absDir, entry.name);
1200
+ if (await fileExists(join6(absPath, "package.json"))) {
1201
+ apps.push({ name: entry.name, path: relPath, absPath });
1202
+ }
1203
+ }
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ }
1209
+ return apps;
1210
+ }
1211
+ async function detectFromDirectory(dirPath) {
1212
+ const seen = /* @__PURE__ */ new Map();
1213
+ try {
1214
+ const raw = await readFile6(join6(dirPath, "package.json"), "utf-8");
1116
1215
  const pkg = JSON.parse(raw);
1117
- const allDeps = {
1118
- ...pkg.dependencies,
1119
- ...pkg.devDependencies
1120
- };
1216
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1121
1217
  for (const depName of Object.keys(allDeps)) {
1122
1218
  const rule = PACKAGE_MAP[depName];
1123
1219
  if (rule) {
@@ -1125,79 +1221,121 @@ async function scanPackageJson(pkgPath, seen) {
1125
1221
  if (!seen.has(key)) {
1126
1222
  seen.set(key, { name: rule.name, category: rule.category });
1127
1223
  }
1224
+ continue;
1225
+ }
1226
+ for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
1227
+ if (depName.startsWith(prefix)) {
1228
+ const key = prefixRule.name.toLowerCase();
1229
+ if (!seen.has(key)) {
1230
+ seen.set(key, { name: prefixRule.name, category: prefixRule.category });
1231
+ }
1232
+ break;
1233
+ }
1128
1234
  }
1129
1235
  }
1130
1236
  } catch {
1131
1237
  }
1132
- }
1133
- async function scanConfigFiles(dir, seen) {
1134
1238
  for (const { file, rule } of CONFIG_FILE_MAP) {
1135
1239
  const key = rule.name.toLowerCase();
1136
- if (!seen.has(key) && await fileExists(join6(dir, file))) {
1240
+ if (!seen.has(key) && await fileExists(join6(dirPath, file))) {
1137
1241
  seen.set(key, { name: rule.name, category: rule.category });
1138
1242
  }
1139
1243
  }
1244
+ return Array.from(seen.values()).sort((a, b) => {
1245
+ const catCmp = a.category.localeCompare(b.category);
1246
+ if (catCmp !== 0) return catCmp;
1247
+ return a.name.localeCompare(b.name);
1248
+ });
1140
1249
  }
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 {
1250
+ async function detectTechStack(projectPath) {
1251
+ const repo = await detectFromDirectory(projectPath);
1252
+ const discoveredApps = await discoverMonorepoApps(projectPath);
1253
+ const apps = [];
1254
+ for (const app of discoveredApps) {
1255
+ const stack = await detectFromDirectory(app.absPath);
1256
+ if (stack.length > 0) {
1257
+ apps.push({ name: app.name, path: app.path, stack });
1155
1258
  }
1156
1259
  }
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);
1260
+ const flatMap = /* @__PURE__ */ new Map();
1261
+ for (const entry of repo) {
1262
+ flatMap.set(entry.name.toLowerCase(), entry);
1167
1263
  }
1168
- return Array.from(seen.values()).sort((a, b) => {
1264
+ for (const app of apps) {
1265
+ for (const entry of app.stack) {
1266
+ const key = entry.name.toLowerCase();
1267
+ if (!flatMap.has(key)) {
1268
+ flatMap.set(key, entry);
1269
+ }
1270
+ }
1271
+ }
1272
+ const flat = Array.from(flatMap.values()).sort((a, b) => {
1169
1273
  const catCmp = a.category.localeCompare(b.category);
1170
1274
  if (catCmp !== 0) return catCmp;
1171
1275
  return a.name.localeCompare(b.name);
1172
1276
  });
1277
+ return { repo, apps, flat };
1173
1278
  }
1174
1279
  function mergeTechStack(remote, detected) {
1280
+ const remoteResult = Array.isArray(remote) ? { repo: remote, apps: [], flat: remote } : remote;
1175
1281
  const seen = /* @__PURE__ */ new Map();
1176
- for (const entry of remote) {
1282
+ for (const entry of remoteResult.flat) {
1177
1283
  seen.set(entry.name.toLowerCase(), entry);
1178
1284
  }
1179
1285
  const added = [];
1180
- for (const entry of detected) {
1286
+ for (const entry of detected.flat) {
1181
1287
  const key = entry.name.toLowerCase();
1182
1288
  if (!seen.has(key)) {
1183
1289
  seen.set(key, entry);
1184
1290
  added.push(entry);
1185
1291
  }
1186
1292
  }
1187
- const merged = Array.from(seen.values()).sort((a, b) => {
1293
+ const flat = Array.from(seen.values()).sort((a, b) => {
1188
1294
  const catCmp = a.category.localeCompare(b.category);
1189
1295
  if (catCmp !== 0) return catCmp;
1190
1296
  return a.name.localeCompare(b.name);
1191
1297
  });
1298
+ const merged = {
1299
+ repo: detected.repo,
1300
+ apps: detected.apps,
1301
+ flat
1302
+ };
1192
1303
  return { merged, added };
1193
1304
  }
1194
1305
  function parseTechStack(raw) {
1306
+ if (Array.isArray(raw)) {
1307
+ return raw.filter(
1308
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
1309
+ );
1310
+ }
1311
+ if (typeof raw === "object" && raw !== null && "flat" in raw) {
1312
+ return parseTechStack(raw.flat);
1313
+ }
1314
+ return [];
1315
+ }
1316
+ function parseAppTechStacks(raw) {
1195
1317
  if (!Array.isArray(raw)) return [];
1196
1318
  return raw.filter(
1197
- (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
1198
- );
1319
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.path === "string" && Array.isArray(item.stack)
1320
+ ).map((item) => ({
1321
+ name: item.name,
1322
+ path: item.path,
1323
+ stack: parseTechStack(item.stack)
1324
+ }));
1325
+ }
1326
+ function parseTechStackResult(raw) {
1327
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && "flat" in raw) {
1328
+ const obj = raw;
1329
+ return {
1330
+ repo: parseTechStack(obj.repo),
1331
+ apps: parseAppTechStacks(obj.apps),
1332
+ flat: parseTechStack(obj.flat)
1333
+ };
1334
+ }
1335
+ const flat = parseTechStack(raw);
1336
+ return { repo: flat, apps: [], flat };
1199
1337
  }
1200
- var PACKAGE_MAP, CONFIG_FILE_MAP;
1338
+ var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP;
1201
1339
  var init_tech_detect = __esm({
1202
1340
  "src/lib/tech-detect.ts"() {
1203
1341
  "use strict";
@@ -1240,17 +1378,50 @@ var init_tech_detect = __esm({
1240
1378
  playwright: { name: "Playwright", category: "testing" },
1241
1379
  "@playwright/test": { name: "Playwright", category: "testing" },
1242
1380
  cypress: { name: "Cypress", category: "testing" },
1381
+ supertest: { name: "Supertest", category: "testing" },
1243
1382
  // Build tools
1244
1383
  turbo: { name: "Turborepo", category: "build" },
1245
1384
  vite: { name: "Vite", category: "build" },
1246
1385
  webpack: { name: "Webpack", category: "build" },
1247
1386
  esbuild: { name: "esbuild", category: "build" },
1248
1387
  rollup: { name: "Rollup", category: "build" },
1388
+ nx: { name: "Nx", category: "build" },
1389
+ lerna: { name: "Lerna", category: "build" },
1390
+ tsup: { name: "tsup", category: "build" },
1391
+ "@swc/core": { name: "SWC", category: "build" },
1392
+ parcel: { name: "Parcel", category: "build" },
1249
1393
  // Tools
1250
1394
  eslint: { name: "ESLint", category: "tool" },
1251
1395
  prettier: { name: "Prettier", category: "tool" },
1252
- "@biomejs/biome": { name: "Biome", category: "tool" }
1253
- };
1396
+ "@biomejs/biome": { name: "Biome", category: "tool" },
1397
+ storybook: { name: "Storybook", category: "tool" },
1398
+ // Component libs
1399
+ "@mui/material": { name: "MUI", category: "component-lib" },
1400
+ "@chakra-ui/react": { name: "Chakra UI", category: "component-lib" },
1401
+ "@mantine/core": { name: "Mantine", category: "component-lib" },
1402
+ // GraphQL
1403
+ graphql: { name: "GraphQL", category: "graphql" },
1404
+ "@apollo/client": { name: "Apollo Client", category: "graphql" },
1405
+ urql: { name: "urql", category: "graphql" },
1406
+ "graphql-request": { name: "graphql-request", category: "graphql" },
1407
+ // Documentation
1408
+ typedoc: { name: "TypeDoc", category: "documentation" },
1409
+ "@docusaurus/core": { name: "Docusaurus", category: "documentation" },
1410
+ vitepress: { name: "VitePress", category: "documentation" },
1411
+ // Code quality
1412
+ husky: { name: "Husky", category: "quality" },
1413
+ "lint-staged": { name: "lint-staged", category: "quality" },
1414
+ commitlint: { name: "commitlint", category: "quality" },
1415
+ "@commitlint/cli": { name: "commitlint", category: "quality" },
1416
+ // Mobile
1417
+ "react-native": { name: "React Native", category: "mobile" },
1418
+ expo: { name: "Expo", category: "mobile" }
1419
+ };
1420
+ PACKAGE_PREFIX_MAP = [
1421
+ { prefix: "@radix-ui/", rule: { name: "Radix UI", category: "component-lib" } },
1422
+ { prefix: "@storybook/", rule: { name: "Storybook", category: "tool" } },
1423
+ { prefix: "@testing-library/", rule: { name: "Testing Library", category: "testing" } }
1424
+ ];
1254
1425
  CONFIG_FILE_MAP = [
1255
1426
  { file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
1256
1427
  { file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
@@ -1262,131 +1433,24 @@ var init_tech_detect = __esm({
1262
1433
  { file: "docker-compose.yml", rule: { name: "Docker", category: "deployment" } },
1263
1434
  { file: "docker-compose.yaml", rule: { name: "Docker", category: "deployment" } },
1264
1435
  { file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
1265
- { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } }
1436
+ { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } },
1437
+ { file: ".storybook/main.js", rule: { name: "Storybook", category: "tool" } },
1438
+ { file: ".storybook/main.ts", rule: { name: "Storybook", category: "tool" } },
1439
+ { file: ".storybook/main.mjs", rule: { name: "Storybook", category: "tool" } },
1440
+ { file: "components.json", rule: { name: "shadcn/ui", category: "component-lib" } },
1441
+ { file: "nx.json", rule: { name: "Nx", category: "build" } },
1442
+ { file: "lerna.json", rule: { name: "Lerna", category: "build" } }
1266
1443
  ];
1267
1444
  }
1268
1445
  });
1269
1446
 
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
-
1382
1447
  // src/cli/sync.ts
1383
1448
  var sync_exports = {};
1384
1449
  __export(sync_exports, {
1385
1450
  runSync: () => runSync
1386
1451
  });
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";
1452
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
1453
+ import { join as join7, dirname as dirname2 } from "node:path";
1390
1454
  async function runSync() {
1391
1455
  const flags = parseFlags(3);
1392
1456
  const dryRun = hasFlag("dry-run", 3);
@@ -1402,7 +1466,7 @@ async function runSync() {
1402
1466
  if (force) console.log(` Mode: force`);
1403
1467
  console.log();
1404
1468
  console.log(" Reading local and remote state...");
1405
- const claudeDir = join8(projectPath, ".claude");
1469
+ const claudeDir = join7(projectPath, ".claude");
1406
1470
  let localFiles = /* @__PURE__ */ new Map();
1407
1471
  try {
1408
1472
  localFiles = await scanLocalFiles(claudeDir, projectPath);
@@ -1432,7 +1496,7 @@ async function runSync() {
1432
1496
  localContent: local.content,
1433
1497
  remoteContent: null,
1434
1498
  pushContent: reverseSubstituteVariables(local.content, repoData),
1435
- filePath: null,
1499
+ filePath: getLocalFilePath(claudeDir, projectPath, { type: local.type, name: local.name, category: local.category }),
1436
1500
  type: local.type,
1437
1501
  name: local.name,
1438
1502
  category: local.category,
@@ -1475,7 +1539,7 @@ async function runSync() {
1475
1539
  action,
1476
1540
  localContent: local.content,
1477
1541
  remoteContent: resolvedRemote,
1478
- pushContent: action === "push" ? reverseSubstituteVariables(local.content, repoData) : null,
1542
+ pushContent: reverseSubstituteVariables(local.content, repoData),
1479
1543
  filePath: getLocalFilePath(claudeDir, projectPath, remote),
1480
1544
  type: local.type,
1481
1545
  name: local.name,
@@ -1505,74 +1569,87 @@ async function runSync() {
1505
1569
  console.log(" All .claude/ files in sync.");
1506
1570
  }
1507
1571
  if (plan.length > 0 && !dryRun) {
1508
- let toDelete = [];
1509
- let toPull = contentPulls;
1510
- if (remoteOnly.length > 0) {
1511
- console.log();
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];
1529
- }
1530
- }
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;
1572
+ if (!force) {
1573
+ const agreed = await confirmProceed(`
1574
+ Agree with sync? [Y/n] `);
1575
+ if (!agreed) {
1576
+ const mode = await promptReviewMode();
1577
+ if (mode === "file") {
1578
+ const actions = await reviewFilesOneByOne(
1579
+ plan,
1580
+ (p) => p.displayPath,
1581
+ (p) => p.action
1582
+ );
1583
+ for (let i = 0; i < plan.length; i++) {
1584
+ plan[i].action = actions[i];
1585
+ }
1586
+ } else {
1587
+ const groups = groupByType(plan);
1588
+ for (const [typeName, items] of groups) {
1589
+ const actions = await reviewFolder(
1590
+ typeName,
1591
+ items,
1592
+ (p) => p.displayPath,
1593
+ (p) => p.action
1594
+ );
1595
+ for (let i = 0; i < items.length; i++) {
1596
+ items[i].action = actions[i];
1597
+ }
1598
+ }
1540
1599
  }
1541
1600
  }
1542
1601
  }
1543
- for (const p of toPull) {
1544
- if (p.filePath && p.remoteContent !== null) {
1545
- await mkdir2(dirname2(p.filePath), { recursive: true });
1546
- await writeFile3(p.filePath, p.remoteContent, "utf-8");
1547
- if (p.isHook) await chmod2(p.filePath, 493);
1602
+ const toPull = plan.filter((p) => p.action === "pull");
1603
+ const toPush = plan.filter((p) => p.action === "push");
1604
+ const toDelete = plan.filter((p) => p.action === "delete");
1605
+ const skipped = plan.filter((p) => p.action === "skip");
1606
+ if (toPull.length + toPush.length + toDelete.length === 0) {
1607
+ console.log("\n All items skipped \u2014 no changes applied.");
1608
+ } else {
1609
+ for (const p of toPull) {
1610
+ if (p.filePath && p.remoteContent !== null) {
1611
+ await mkdir2(dirname2(p.filePath), { recursive: true });
1612
+ await writeFile3(p.filePath, p.remoteContent, "utf-8");
1613
+ if (p.isHook) await chmod2(p.filePath, 493);
1614
+ }
1548
1615
  }
1549
- }
1550
- const toUpsert = pushes.filter((p) => p.pushContent !== null).map((p) => ({
1551
- type: p.type,
1552
- name: p.name,
1553
- category: p.category,
1554
- content: p.pushContent
1555
- }));
1556
- if (toUpsert.length > 0) {
1557
- await apiPost("/sync/files", {
1558
- repo_id: repoId,
1559
- files: toUpsert
1560
- });
1561
- }
1562
- if (toDelete.length > 0) {
1563
- const deleteKeys = toDelete.map((p) => ({
1616
+ const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
1564
1617
  type: p.type,
1565
1618
  name: p.name,
1566
- category: p.category
1619
+ category: p.category,
1620
+ content: p.pushContent
1567
1621
  }));
1568
- await apiPost("/sync/files", {
1569
- repo_id: repoId,
1570
- delete_keys: deleteKeys
1571
- });
1622
+ if (toUpsert.length > 0) {
1623
+ await apiPost("/sync/files", {
1624
+ repo_id: repoId,
1625
+ files: toUpsert
1626
+ });
1627
+ }
1628
+ if (toDelete.length > 0) {
1629
+ const deleteKeys = toDelete.map((p) => ({
1630
+ type: p.type,
1631
+ name: p.name,
1632
+ category: p.category
1633
+ }));
1634
+ await apiPost("/sync/files", {
1635
+ repo_id: repoId,
1636
+ delete_keys: deleteKeys
1637
+ });
1638
+ for (const p of toDelete) {
1639
+ if (p.filePath) {
1640
+ try {
1641
+ await unlink2(p.filePath);
1642
+ } catch {
1643
+ }
1644
+ }
1645
+ }
1646
+ }
1647
+ await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1648
+ console.log(
1649
+ `
1650
+ Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
1651
+ );
1572
1652
  }
1573
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1574
- console.log(`
1575
- Applied: ${toPull.length} pulled, ${pushes.length} pushed, ${toDelete.length} deleted from DB`);
1576
1653
  } else if (dryRun) {
1577
1654
  console.log("\n (dry-run \u2014 no changes)");
1578
1655
  }
@@ -1582,12 +1659,10 @@ async function runSync() {
1582
1659
  await syncConfig(repoId, projectPath, dryRun);
1583
1660
  console.log(" Tech stack...");
1584
1661
  await syncTechStack(repoId, projectPath, dryRun);
1585
- console.log(" Desktop server configs...");
1586
- await syncDesktopConfigs(repoId, projectPath, repoData, dryRun);
1587
1662
  console.log("\n Sync complete.\n");
1588
1663
  }
1589
1664
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
1590
- const settingsPath = join8(claudeDir, "settings.json");
1665
+ const settingsPath = join7(claudeDir, "settings.json");
1591
1666
  const globalSettingsFiles = syncData.global_settings ?? [];
1592
1667
  let globalSettings = {};
1593
1668
  for (const gf of globalSettingsFiles) {
@@ -1600,11 +1675,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1600
1675
  repoSettings = JSON.parse(substituteVariables(rf.content, repoData));
1601
1676
  }
1602
1677
  const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
1603
- const hooksDir = join8(projectPath, ".claude", "hooks");
1678
+ const hooksDir = join7(projectPath, ".claude", "hooks");
1604
1679
  const discovered = await discoverHooks(hooksDir);
1605
1680
  let localSettings = {};
1606
1681
  try {
1607
- const raw = await readFile8(settingsPath, "utf-8");
1682
+ const raw = await readFile7(settingsPath, "utf-8");
1608
1683
  localSettings = JSON.parse(raw);
1609
1684
  } catch {
1610
1685
  }
@@ -1619,7 +1694,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1619
1694
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
1620
1695
  let currentContent = "";
1621
1696
  try {
1622
- currentContent = await readFile8(settingsPath, "utf-8");
1697
+ currentContent = await readFile7(settingsPath, "utf-8");
1623
1698
  } catch {
1624
1699
  }
1625
1700
  if (currentContent === mergedContent) {
@@ -1635,10 +1710,10 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
1635
1710
  console.log(" Updated settings.json");
1636
1711
  }
1637
1712
  async function syncConfig(repoId, projectPath, dryRun) {
1638
- const configPath = join8(projectPath, ".codebyplan.json");
1713
+ const configPath = join7(projectPath, ".codebyplan.json");
1639
1714
  let currentConfig = {};
1640
1715
  try {
1641
- const raw = await readFile8(configPath, "utf-8");
1716
+ const raw = await readFile7(configPath, "utf-8");
1642
1717
  currentConfig = JSON.parse(raw);
1643
1718
  } catch {
1644
1719
  currentConfig = { repo_id: repoId };
@@ -1676,14 +1751,17 @@ async function syncConfig(repoId, projectPath, dryRun) {
1676
1751
  async function syncTechStack(repoId, projectPath, dryRun) {
1677
1752
  try {
1678
1753
  const detected = await detectTechStack(projectPath);
1679
- if (detected.length === 0) {
1754
+ if (detected.flat.length === 0) {
1680
1755
  console.log(" No tech stack detected.");
1681
1756
  return;
1682
1757
  }
1683
1758
  const repoRes = await apiGet(`/repos/${repoId}`);
1684
- const remote = parseTechStack(repoRes.data.tech_stack);
1759
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
1685
1760
  const { merged, added } = mergeTechStack(remote, detected);
1686
- console.log(` ${detected.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1761
+ console.log(` ${detected.flat.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1762
+ if (detected.apps.length > 0) {
1763
+ console.log(` Apps: ${detected.apps.map((a) => a.name).join(", ")}`);
1764
+ }
1687
1765
  for (const entry of added) {
1688
1766
  console.log(` + ${entry.name} (${entry.category})`);
1689
1767
  }
@@ -1694,6 +1772,25 @@ async function syncTechStack(repoId, projectPath, dryRun) {
1694
1772
  console.log(" Tech stack detection skipped.");
1695
1773
  }
1696
1774
  }
1775
+ function groupByType(items) {
1776
+ const groups = /* @__PURE__ */ new Map();
1777
+ const typeLabels = {
1778
+ command: "Commands",
1779
+ agent: "Agents",
1780
+ skill: "Skills",
1781
+ rule: "Rules",
1782
+ hook: "Hooks",
1783
+ template: "Templates",
1784
+ settings: "Settings"
1785
+ };
1786
+ for (const item of items) {
1787
+ const label = typeLabels[item.type] ?? item.type;
1788
+ const group = groups.get(label) ?? [];
1789
+ group.push(item);
1790
+ groups.set(label, group);
1791
+ }
1792
+ return groups;
1793
+ }
1697
1794
  function getLocalFilePath(claudeDir, projectPath, remote) {
1698
1795
  const typeConfig2 = {
1699
1796
  command: { dir: "commands", ext: ".md" },
@@ -1705,15 +1802,15 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
1705
1802
  claude_md: { dir: "", ext: "" },
1706
1803
  settings: { dir: "", ext: "" }
1707
1804
  };
1708
- if (remote.type === "claude_md") return join8(projectPath, "CLAUDE.md");
1709
- if (remote.type === "settings") return join8(claudeDir, "settings.json");
1805
+ if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
1806
+ if (remote.type === "settings") return join7(claudeDir, "settings.json");
1710
1807
  const cfg = typeConfig2[remote.type];
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}`);
1808
+ if (!cfg) return join7(claudeDir, remote.name);
1809
+ const typeDir = remote.type === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
1810
+ if (cfg.subfolder) return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
1811
+ if (remote.type === "command" && remote.category) return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
1812
+ if (remote.type === "template") return join7(typeDir, remote.name);
1813
+ return join7(typeDir, `${remote.name}${cfg.ext}`);
1717
1814
  }
1718
1815
  function flattenSyncData(data) {
1719
1816
  const result = /* @__PURE__ */ new Map();
@@ -1741,99 +1838,6 @@ function flattenSyncData(data) {
1741
1838
  }
1742
1839
  return result;
1743
1840
  }
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
- }
1837
1841
  var init_sync = __esm({
1838
1842
  "src/cli/sync.ts"() {
1839
1843
  "use strict";
@@ -1843,7 +1847,6 @@ var init_sync = __esm({
1843
1847
  init_api();
1844
1848
  init_variables();
1845
1849
  init_tech_detect();
1846
- init_server_detect();
1847
1850
  init_settings_merge();
1848
1851
  init_hook_registry();
1849
1852
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {