@codebyplan/cli 3.0.3 → 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.
- package/dist/cli.js +415 -56
- 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
|
|
40
|
+
VERSION = "3.2.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,90 @@ async function confirmProceed(message) {
|
|
|
1055
1063
|
rl.close();
|
|
1056
1064
|
}
|
|
1057
1065
|
}
|
|
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() {
|
|
1096
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1097
|
+
try {
|
|
1098
|
+
const answer = await rl.question(" Review [o]ne-by-one or [f]older-by-folder? ");
|
|
1099
|
+
const a = answer.trim().toLowerCase();
|
|
1100
|
+
return a === "f" || a === "folder" ? "folder" : "file";
|
|
1101
|
+
} finally {
|
|
1102
|
+
rl.close();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async function reviewFilesOneByOne(items, label, plannedAction) {
|
|
1106
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1107
|
+
const results = [];
|
|
1108
|
+
try {
|
|
1109
|
+
let applyAll = null;
|
|
1110
|
+
for (const item of items) {
|
|
1111
|
+
if (applyAll) {
|
|
1112
|
+
results.push(applyAll);
|
|
1113
|
+
continue;
|
|
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;
|
|
1122
|
+
}
|
|
1123
|
+
} finally {
|
|
1124
|
+
rl.close();
|
|
1125
|
+
}
|
|
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);
|
|
1149
|
+
}
|
|
1058
1150
|
var init_confirm = __esm({
|
|
1059
1151
|
"src/cli/confirm.ts"() {
|
|
1060
1152
|
"use strict";
|
|
@@ -1062,7 +1154,7 @@ var init_confirm = __esm({
|
|
|
1062
1154
|
});
|
|
1063
1155
|
|
|
1064
1156
|
// src/lib/tech-detect.ts
|
|
1065
|
-
import { readFile as readFile6, access } from "node:fs/promises";
|
|
1157
|
+
import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
|
|
1066
1158
|
import { join as join6 } from "node:path";
|
|
1067
1159
|
async function fileExists(filePath) {
|
|
1068
1160
|
try {
|
|
@@ -1072,15 +1164,56 @@ async function fileExists(filePath) {
|
|
|
1072
1164
|
return false;
|
|
1073
1165
|
}
|
|
1074
1166
|
}
|
|
1075
|
-
async function
|
|
1167
|
+
async function discoverMonorepoApps(projectPath) {
|
|
1168
|
+
const apps = [];
|
|
1169
|
+
const patterns = [];
|
|
1170
|
+
try {
|
|
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) {
|
|
1076
1212
|
const seen = /* @__PURE__ */ new Map();
|
|
1077
1213
|
try {
|
|
1078
|
-
const raw = await readFile6(join6(
|
|
1214
|
+
const raw = await readFile6(join6(dirPath, "package.json"), "utf-8");
|
|
1079
1215
|
const pkg = JSON.parse(raw);
|
|
1080
|
-
const allDeps = {
|
|
1081
|
-
...pkg.dependencies,
|
|
1082
|
-
...pkg.devDependencies
|
|
1083
|
-
};
|
|
1216
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1084
1217
|
for (const depName of Object.keys(allDeps)) {
|
|
1085
1218
|
const rule = PACKAGE_MAP[depName];
|
|
1086
1219
|
if (rule) {
|
|
@@ -1088,13 +1221,23 @@ async function detectTechStack(projectPath) {
|
|
|
1088
1221
|
if (!seen.has(key)) {
|
|
1089
1222
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
1090
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
|
+
}
|
|
1091
1234
|
}
|
|
1092
1235
|
}
|
|
1093
1236
|
} catch {
|
|
1094
1237
|
}
|
|
1095
1238
|
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
1096
1239
|
const key = rule.name.toLowerCase();
|
|
1097
|
-
if (!seen.has(key) && await fileExists(join6(
|
|
1240
|
+
if (!seen.has(key) && await fileExists(join6(dirPath, file))) {
|
|
1098
1241
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
1099
1242
|
}
|
|
1100
1243
|
}
|
|
@@ -1104,33 +1247,95 @@ async function detectTechStack(projectPath) {
|
|
|
1104
1247
|
return a.name.localeCompare(b.name);
|
|
1105
1248
|
});
|
|
1106
1249
|
}
|
|
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 });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const flatMap = /* @__PURE__ */ new Map();
|
|
1261
|
+
for (const entry of repo) {
|
|
1262
|
+
flatMap.set(entry.name.toLowerCase(), entry);
|
|
1263
|
+
}
|
|
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) => {
|
|
1273
|
+
const catCmp = a.category.localeCompare(b.category);
|
|
1274
|
+
if (catCmp !== 0) return catCmp;
|
|
1275
|
+
return a.name.localeCompare(b.name);
|
|
1276
|
+
});
|
|
1277
|
+
return { repo, apps, flat };
|
|
1278
|
+
}
|
|
1107
1279
|
function mergeTechStack(remote, detected) {
|
|
1280
|
+
const remoteResult = Array.isArray(remote) ? { repo: remote, apps: [], flat: remote } : remote;
|
|
1108
1281
|
const seen = /* @__PURE__ */ new Map();
|
|
1109
|
-
for (const entry of
|
|
1282
|
+
for (const entry of remoteResult.flat) {
|
|
1110
1283
|
seen.set(entry.name.toLowerCase(), entry);
|
|
1111
1284
|
}
|
|
1112
1285
|
const added = [];
|
|
1113
|
-
for (const entry of detected) {
|
|
1286
|
+
for (const entry of detected.flat) {
|
|
1114
1287
|
const key = entry.name.toLowerCase();
|
|
1115
1288
|
if (!seen.has(key)) {
|
|
1116
1289
|
seen.set(key, entry);
|
|
1117
1290
|
added.push(entry);
|
|
1118
1291
|
}
|
|
1119
1292
|
}
|
|
1120
|
-
const
|
|
1293
|
+
const flat = Array.from(seen.values()).sort((a, b) => {
|
|
1121
1294
|
const catCmp = a.category.localeCompare(b.category);
|
|
1122
1295
|
if (catCmp !== 0) return catCmp;
|
|
1123
1296
|
return a.name.localeCompare(b.name);
|
|
1124
1297
|
});
|
|
1298
|
+
const merged = {
|
|
1299
|
+
repo: detected.repo,
|
|
1300
|
+
apps: detected.apps,
|
|
1301
|
+
flat
|
|
1302
|
+
};
|
|
1125
1303
|
return { merged, added };
|
|
1126
1304
|
}
|
|
1127
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) {
|
|
1128
1317
|
if (!Array.isArray(raw)) return [];
|
|
1129
1318
|
return raw.filter(
|
|
1130
|
-
(item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.
|
|
1131
|
-
)
|
|
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 };
|
|
1132
1337
|
}
|
|
1133
|
-
var PACKAGE_MAP, CONFIG_FILE_MAP;
|
|
1338
|
+
var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP;
|
|
1134
1339
|
var init_tech_detect = __esm({
|
|
1135
1340
|
"src/lib/tech-detect.ts"() {
|
|
1136
1341
|
"use strict";
|
|
@@ -1173,17 +1378,50 @@ var init_tech_detect = __esm({
|
|
|
1173
1378
|
playwright: { name: "Playwright", category: "testing" },
|
|
1174
1379
|
"@playwright/test": { name: "Playwright", category: "testing" },
|
|
1175
1380
|
cypress: { name: "Cypress", category: "testing" },
|
|
1381
|
+
supertest: { name: "Supertest", category: "testing" },
|
|
1176
1382
|
// Build tools
|
|
1177
1383
|
turbo: { name: "Turborepo", category: "build" },
|
|
1178
1384
|
vite: { name: "Vite", category: "build" },
|
|
1179
1385
|
webpack: { name: "Webpack", category: "build" },
|
|
1180
1386
|
esbuild: { name: "esbuild", category: "build" },
|
|
1181
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" },
|
|
1182
1393
|
// Tools
|
|
1183
1394
|
eslint: { name: "ESLint", category: "tool" },
|
|
1184
1395
|
prettier: { name: "Prettier", category: "tool" },
|
|
1185
|
-
"@biomejs/biome": { name: "Biome", category: "tool" }
|
|
1186
|
-
|
|
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
|
+
];
|
|
1187
1425
|
CONFIG_FILE_MAP = [
|
|
1188
1426
|
{ file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
|
|
1189
1427
|
{ file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
|
|
@@ -1195,7 +1433,13 @@ var init_tech_detect = __esm({
|
|
|
1195
1433
|
{ file: "docker-compose.yml", rule: { name: "Docker", category: "deployment" } },
|
|
1196
1434
|
{ file: "docker-compose.yaml", rule: { name: "Docker", category: "deployment" } },
|
|
1197
1435
|
{ file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
|
|
1198
|
-
{ 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" } }
|
|
1199
1443
|
];
|
|
1200
1444
|
}
|
|
1201
1445
|
});
|
|
@@ -1205,7 +1449,7 @@ var sync_exports = {};
|
|
|
1205
1449
|
__export(sync_exports, {
|
|
1206
1450
|
runSync: () => runSync
|
|
1207
1451
|
});
|
|
1208
|
-
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
1452
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
|
|
1209
1453
|
import { join as join7, dirname as dirname2 } from "node:path";
|
|
1210
1454
|
async function runSync() {
|
|
1211
1455
|
const flags = parseFlags(3);
|
|
@@ -1252,7 +1496,7 @@ async function runSync() {
|
|
|
1252
1496
|
localContent: local.content,
|
|
1253
1497
|
remoteContent: null,
|
|
1254
1498
|
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
1255
|
-
filePath:
|
|
1499
|
+
filePath: getLocalFilePath(claudeDir, projectPath, { type: local.type, name: local.name, category: local.category }),
|
|
1256
1500
|
type: local.type,
|
|
1257
1501
|
name: local.name,
|
|
1258
1502
|
category: local.category,
|
|
@@ -1295,7 +1539,7 @@ async function runSync() {
|
|
|
1295
1539
|
action,
|
|
1296
1540
|
localContent: local.content,
|
|
1297
1541
|
remoteContent: resolvedRemote,
|
|
1298
|
-
pushContent:
|
|
1542
|
+
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
1299
1543
|
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
1300
1544
|
type: local.type,
|
|
1301
1545
|
name: local.name,
|
|
@@ -1306,48 +1550,106 @@ async function runSync() {
|
|
|
1306
1550
|
}
|
|
1307
1551
|
const pulls = plan.filter((p) => p.action === "pull");
|
|
1308
1552
|
const pushes = plan.filter((p) => p.action === "push");
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1553
|
+
const remoteOnly = plan.filter((p) => p.action === "pull" && p.localContent === null);
|
|
1554
|
+
const contentPulls = pulls.filter((p) => p.localContent !== null);
|
|
1555
|
+
if (contentPulls.length > 0) {
|
|
1556
|
+
console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
|
|
1557
|
+
for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
|
|
1312
1558
|
}
|
|
1313
1559
|
if (pushes.length > 0) {
|
|
1314
1560
|
console.log(` Push (local \u2192 DB): ${pushes.length}`);
|
|
1315
1561
|
for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
|
|
1316
1562
|
}
|
|
1317
|
-
if (
|
|
1563
|
+
if (remoteOnly.length > 0) {
|
|
1564
|
+
console.log(`
|
|
1565
|
+
DB-only (not on disk): ${remoteOnly.length}`);
|
|
1566
|
+
for (const p of remoteOnly) console.log(` \u2715 ${p.displayPath}`);
|
|
1567
|
+
}
|
|
1568
|
+
if (contentPulls.length === 0 && pushes.length === 0 && remoteOnly.length === 0) {
|
|
1318
1569
|
console.log(" All .claude/ files in sync.");
|
|
1319
1570
|
}
|
|
1320
1571
|
if (plan.length > 0 && !dryRun) {
|
|
1321
|
-
if (!force
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (!
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
+
}
|
|
1599
|
+
}
|
|
1327
1600
|
}
|
|
1328
1601
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
|
|
1617
|
+
type: p.type,
|
|
1618
|
+
name: p.name,
|
|
1619
|
+
category: p.category,
|
|
1620
|
+
content: p.pushContent
|
|
1621
|
+
}));
|
|
1622
|
+
if (toUpsert.length > 0) {
|
|
1623
|
+
await apiPost("/sync/files", {
|
|
1624
|
+
repo_id: repoId,
|
|
1625
|
+
files: toUpsert
|
|
1626
|
+
});
|
|
1334
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
|
+
);
|
|
1335
1652
|
}
|
|
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
|
-
});
|
|
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
1653
|
} else if (dryRun) {
|
|
1352
1654
|
console.log("\n (dry-run \u2014 no changes)");
|
|
1353
1655
|
}
|
|
@@ -1449,14 +1751,17 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
1449
1751
|
async function syncTechStack(repoId, projectPath, dryRun) {
|
|
1450
1752
|
try {
|
|
1451
1753
|
const detected = await detectTechStack(projectPath);
|
|
1452
|
-
if (detected.length === 0) {
|
|
1754
|
+
if (detected.flat.length === 0) {
|
|
1453
1755
|
console.log(" No tech stack detected.");
|
|
1454
1756
|
return;
|
|
1455
1757
|
}
|
|
1456
1758
|
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
1457
|
-
const remote =
|
|
1759
|
+
const remote = parseTechStackResult(repoRes.data.tech_stack);
|
|
1458
1760
|
const { merged, added } = mergeTechStack(remote, detected);
|
|
1459
|
-
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
|
+
}
|
|
1460
1765
|
for (const entry of added) {
|
|
1461
1766
|
console.log(` + ${entry.name} (${entry.category})`);
|
|
1462
1767
|
}
|
|
@@ -1467,6 +1772,25 @@ async function syncTechStack(repoId, projectPath, dryRun) {
|
|
|
1467
1772
|
console.log(" Tech stack detection skipped.");
|
|
1468
1773
|
}
|
|
1469
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
|
+
}
|
|
1470
1794
|
function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
1471
1795
|
const typeConfig2 = {
|
|
1472
1796
|
command: { dir: "commands", ext: ".md" },
|
|
@@ -24031,17 +24355,52 @@ function registerWriteTools(server) {
|
|
|
24031
24355
|
}, async ({ repo_id, project_path }) => {
|
|
24032
24356
|
try {
|
|
24033
24357
|
const syncResult = await executeSyncToLocal({ repoId: repo_id, projectPath: project_path });
|
|
24034
|
-
const { byType, totals } = syncResult;
|
|
24358
|
+
const { byType, totals, dbOnlyFiles } = syncResult;
|
|
24035
24359
|
const summary = {
|
|
24036
24360
|
...byType,
|
|
24037
24361
|
totals: { created: totals.created, updated: totals.updated, deleted: totals.deleted },
|
|
24038
24362
|
message: totals.created + totals.updated + totals.deleted === 0 ? "All files up to date" : `Synced: ${totals.created} created, ${totals.updated} updated, ${totals.deleted} deleted`
|
|
24039
24363
|
};
|
|
24364
|
+
if (dbOnlyFiles.length > 0) {
|
|
24365
|
+
summary.db_only_files = dbOnlyFiles;
|
|
24366
|
+
summary.message += `
|
|
24367
|
+
|
|
24368
|
+
${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.`;
|
|
24369
|
+
}
|
|
24040
24370
|
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
24041
24371
|
} catch (err) {
|
|
24042
24372
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24043
24373
|
}
|
|
24044
24374
|
});
|
|
24375
|
+
server.registerTool("delete_claude_files", {
|
|
24376
|
+
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.",
|
|
24377
|
+
inputSchema: {
|
|
24378
|
+
repo_id: external_exports.string().uuid().describe("Repository ID"),
|
|
24379
|
+
files: external_exports.array(external_exports.object({
|
|
24380
|
+
type: external_exports.string().describe("File type: command, agent, skill, rule, hook, template, docs_stack"),
|
|
24381
|
+
name: external_exports.string().describe("File name"),
|
|
24382
|
+
category: external_exports.string().nullable().optional().describe("Category (for commands: e.g. 'development/checkpoint')")
|
|
24383
|
+
})).describe("Files to soft-delete from DB")
|
|
24384
|
+
}
|
|
24385
|
+
}, async ({ repo_id, files }) => {
|
|
24386
|
+
try {
|
|
24387
|
+
const res = await apiPost("/sync/files", {
|
|
24388
|
+
repo_id,
|
|
24389
|
+
delete_keys: files
|
|
24390
|
+
});
|
|
24391
|
+
return {
|
|
24392
|
+
content: [{
|
|
24393
|
+
type: "text",
|
|
24394
|
+
text: JSON.stringify({
|
|
24395
|
+
deleted: res.data.deleted,
|
|
24396
|
+
message: `Soft-deleted ${res.data.deleted} file(s) from DB. Run sync_claude_files to clean up local copies.`
|
|
24397
|
+
}, null, 2)
|
|
24398
|
+
}]
|
|
24399
|
+
};
|
|
24400
|
+
} catch (err) {
|
|
24401
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24402
|
+
}
|
|
24403
|
+
});
|
|
24045
24404
|
server.registerTool("update_session_state", {
|
|
24046
24405
|
description: "Update session state for a repo. Actions: activate (deactivates other repos), deactivate, pause, refresh, clear_refresh.",
|
|
24047
24406
|
inputSchema: {
|