@codebyplan/cli 2.0.0 → 2.0.1

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 +222 -31
  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 = "2.0.0";
40
+ VERSION = "2.0.1";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -262,8 +262,14 @@ async function request(method, path, options) {
262
262
  let code;
263
263
  try {
264
264
  const body = await res.json();
265
- if (body.error) message = body.error;
266
- if (body.code) code = body.code;
265
+ if (body.error && typeof body.error === "object") {
266
+ const err = body.error;
267
+ if (err.message) message = err.message;
268
+ if (err.code) code = err.code;
269
+ } else if (typeof body.error === "string") {
270
+ message = body.error;
271
+ }
272
+ if (body.code && typeof body.code === "string") code = body.code;
267
273
  } catch {
268
274
  }
269
275
  const apiError = new ApiError(message, res.status, code);
@@ -727,6 +733,145 @@ var init_confirm = __esm({
727
733
  }
728
734
  });
729
735
 
736
+ // src/lib/tech-detect.ts
737
+ import { readFile as readFile4, access } from "node:fs/promises";
738
+ import { join as join4 } from "node:path";
739
+ async function fileExists(filePath) {
740
+ try {
741
+ await access(filePath);
742
+ return true;
743
+ } catch {
744
+ return false;
745
+ }
746
+ }
747
+ async function detectTechStack(projectPath) {
748
+ const seen = /* @__PURE__ */ new Map();
749
+ try {
750
+ const raw = await readFile4(join4(projectPath, "package.json"), "utf-8");
751
+ const pkg = JSON.parse(raw);
752
+ const allDeps = {
753
+ ...pkg.dependencies,
754
+ ...pkg.devDependencies
755
+ };
756
+ for (const depName of Object.keys(allDeps)) {
757
+ const rule = PACKAGE_MAP[depName];
758
+ if (rule) {
759
+ const key = rule.name.toLowerCase();
760
+ if (!seen.has(key)) {
761
+ seen.set(key, { name: rule.name, category: rule.category });
762
+ }
763
+ }
764
+ }
765
+ } catch {
766
+ }
767
+ for (const { file, rule } of CONFIG_FILE_MAP) {
768
+ const key = rule.name.toLowerCase();
769
+ if (!seen.has(key) && await fileExists(join4(projectPath, file))) {
770
+ seen.set(key, { name: rule.name, category: rule.category });
771
+ }
772
+ }
773
+ return Array.from(seen.values()).sort((a, b) => {
774
+ const catCmp = a.category.localeCompare(b.category);
775
+ if (catCmp !== 0) return catCmp;
776
+ return a.name.localeCompare(b.name);
777
+ });
778
+ }
779
+ function mergeTechStack(remote, detected) {
780
+ const seen = /* @__PURE__ */ new Map();
781
+ for (const entry of remote) {
782
+ seen.set(entry.name.toLowerCase(), entry);
783
+ }
784
+ const added = [];
785
+ for (const entry of detected) {
786
+ const key = entry.name.toLowerCase();
787
+ if (!seen.has(key)) {
788
+ seen.set(key, entry);
789
+ added.push(entry);
790
+ }
791
+ }
792
+ const merged = Array.from(seen.values()).sort((a, b) => {
793
+ const catCmp = a.category.localeCompare(b.category);
794
+ if (catCmp !== 0) return catCmp;
795
+ return a.name.localeCompare(b.name);
796
+ });
797
+ return { merged, added };
798
+ }
799
+ function parseTechStack(raw) {
800
+ if (!Array.isArray(raw)) return [];
801
+ return raw.filter(
802
+ (item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
803
+ );
804
+ }
805
+ var PACKAGE_MAP, CONFIG_FILE_MAP;
806
+ var init_tech_detect = __esm({
807
+ "src/lib/tech-detect.ts"() {
808
+ "use strict";
809
+ PACKAGE_MAP = {
810
+ // Frameworks
811
+ next: { name: "Next.js", category: "framework" },
812
+ nuxt: { name: "Nuxt", category: "framework" },
813
+ gatsby: { name: "Gatsby", category: "framework" },
814
+ express: { name: "Express", category: "framework" },
815
+ fastify: { name: "Fastify", category: "framework" },
816
+ hono: { name: "Hono", category: "framework" },
817
+ "@remix-run/node": { name: "Remix", category: "framework" },
818
+ svelte: { name: "Svelte", category: "framework" },
819
+ astro: { name: "Astro", category: "framework" },
820
+ "@angular/core": { name: "Angular", category: "framework" },
821
+ // Libraries (UI)
822
+ react: { name: "React", category: "framework" },
823
+ vue: { name: "Vue", category: "framework" },
824
+ "solid-js": { name: "Solid", category: "framework" },
825
+ preact: { name: "Preact", category: "framework" },
826
+ // Languages (detected via devDeps)
827
+ typescript: { name: "TypeScript", category: "language" },
828
+ // Styling
829
+ tailwindcss: { name: "Tailwind CSS", category: "styling" },
830
+ sass: { name: "SCSS", category: "styling" },
831
+ "styled-components": { name: "styled-components", category: "styling" },
832
+ "@emotion/react": { name: "Emotion", category: "styling" },
833
+ // Database
834
+ prisma: { name: "Prisma", category: "database" },
835
+ "@prisma/client": { name: "Prisma", category: "database" },
836
+ "drizzle-orm": { name: "Drizzle", category: "database" },
837
+ "@supabase/supabase-js": { name: "Supabase", category: "database" },
838
+ mongoose: { name: "MongoDB", category: "database" },
839
+ typeorm: { name: "TypeORM", category: "database" },
840
+ knex: { name: "Knex", category: "database" },
841
+ // Testing
842
+ jest: { name: "Jest", category: "testing" },
843
+ vitest: { name: "Vitest", category: "testing" },
844
+ mocha: { name: "Mocha", category: "testing" },
845
+ playwright: { name: "Playwright", category: "testing" },
846
+ "@playwright/test": { name: "Playwright", category: "testing" },
847
+ cypress: { name: "Cypress", category: "testing" },
848
+ // Build tools
849
+ turbo: { name: "Turborepo", category: "build" },
850
+ vite: { name: "Vite", category: "build" },
851
+ webpack: { name: "Webpack", category: "build" },
852
+ esbuild: { name: "esbuild", category: "build" },
853
+ rollup: { name: "Rollup", category: "build" },
854
+ // Tools
855
+ eslint: { name: "ESLint", category: "tool" },
856
+ prettier: { name: "Prettier", category: "tool" },
857
+ "@biomejs/biome": { name: "Biome", category: "tool" }
858
+ };
859
+ CONFIG_FILE_MAP = [
860
+ { file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
861
+ { file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
862
+ { file: "next.config.mjs", rule: { name: "Next.js", category: "framework" } },
863
+ { file: "next.config.ts", rule: { name: "Next.js", category: "framework" } },
864
+ { file: "tailwind.config.js", rule: { name: "Tailwind CSS", category: "styling" } },
865
+ { file: "tailwind.config.ts", rule: { name: "Tailwind CSS", category: "styling" } },
866
+ { file: "turbo.json", rule: { name: "Turborepo", category: "build" } },
867
+ { file: "docker-compose.yml", rule: { name: "Docker", category: "deployment" } },
868
+ { file: "docker-compose.yaml", rule: { name: "Docker", category: "deployment" } },
869
+ { file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
870
+ { file: "vercel.json", rule: { name: "Vercel", category: "deployment" } }
871
+ ];
872
+ }
873
+ });
874
+
730
875
  // src/cli/pull.ts
731
876
  var pull_exports = {};
732
877
  __export(pull_exports, {
@@ -801,6 +946,7 @@ async function runPull() {
801
946
  if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
802
947
  if (force) console.log(` Mode: force (no confirmation prompt)`);
803
948
  console.log();
949
+ const repoRes = await apiGet(`/repos/${repoId}`);
804
950
  const result = await executePull({ repoId, projectPath, dryRun, force });
805
951
  console.log();
806
952
  if (result.created + result.updated + result.deleted === 0) {
@@ -811,8 +957,29 @@ async function runPull() {
811
957
  if (dryRun) {
812
958
  console.log(" (dry-run \u2014 no changes were made)");
813
959
  }
960
+ printTechStack(repoRes.data.tech_stack);
814
961
  await printSyncStatus(repoId);
815
962
  }
963
+ function printTechStack(techStackRaw) {
964
+ try {
965
+ const entries = parseTechStack(techStackRaw);
966
+ if (entries.length === 0) return;
967
+ const grouped = /* @__PURE__ */ new Map();
968
+ for (const entry of entries) {
969
+ const list = grouped.get(entry.category) ?? [];
970
+ list.push(entry);
971
+ grouped.set(entry.category, list);
972
+ }
973
+ console.log();
974
+ console.log(" Tech stack:");
975
+ for (const [category, items] of grouped) {
976
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
977
+ const names = items.map((e) => e.name).join(", ");
978
+ console.log(` ${label}: ${names}`);
979
+ }
980
+ } catch {
981
+ }
982
+ }
816
983
  async function printSyncStatus(currentRepoId) {
817
984
  try {
818
985
  const statusRes = await apiGet("/sync/status");
@@ -842,6 +1009,7 @@ var init_pull = __esm({
842
1009
  init_api();
843
1010
  init_sync_engine();
844
1011
  init_confirm();
1012
+ init_tech_detect();
845
1013
  displayTypeMap = {
846
1014
  commands: "command",
847
1015
  agents: "agent",
@@ -854,19 +1022,19 @@ var init_pull = __esm({
854
1022
  });
855
1023
 
856
1024
  // src/cli/fileMapper.ts
857
- import { readdir as readdir3, readFile as readFile4 } from "node:fs/promises";
858
- import { join as join4, extname } from "node:path";
1025
+ import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
1026
+ import { join as join5, extname } from "node:path";
859
1027
  function compositeKey(type, name, category) {
860
1028
  return category ? `${type}:${category}/${name}` : `${type}:${name}`;
861
1029
  }
862
1030
  async function scanLocalFiles(claudeDir) {
863
1031
  const result = /* @__PURE__ */ new Map();
864
- await scanCommands(join4(claudeDir, "commands", "cbp"), result);
865
- await scanSubfolderType(join4(claudeDir, "agents"), "agent", "AGENT.md", result);
866
- await scanSubfolderType(join4(claudeDir, "skills"), "skill", "SKILL.md", result);
867
- await scanFlatType(join4(claudeDir, "rules"), "rule", ".md", result);
868
- await scanFlatType(join4(claudeDir, "hooks"), "hook", ".sh", result);
869
- await scanTemplates(join4(claudeDir, "templates"), result);
1032
+ await scanCommands(join5(claudeDir, "commands", "cbp"), result);
1033
+ await scanSubfolderType(join5(claudeDir, "agents"), "agent", "AGENT.md", result);
1034
+ await scanSubfolderType(join5(claudeDir, "skills"), "skill", "SKILL.md", result);
1035
+ await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
1036
+ await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
1037
+ await scanTemplates(join5(claudeDir, "templates"), result);
870
1038
  return result;
871
1039
  }
872
1040
  async function scanCommands(dir, result) {
@@ -881,10 +1049,10 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
881
1049
  }
882
1050
  for (const entry of entries) {
883
1051
  if (entry.isDirectory()) {
884
- await scanCommandsRecursive(baseDir, join4(currentDir, entry.name), result);
1052
+ await scanCommandsRecursive(baseDir, join5(currentDir, entry.name), result);
885
1053
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
886
1054
  const name = entry.name.slice(0, -3);
887
- const content = await readFile4(join4(currentDir, entry.name), "utf-8");
1055
+ const content = await readFile5(join5(currentDir, entry.name), "utf-8");
888
1056
  const relDir = currentDir.slice(baseDir.length + 1);
889
1057
  const category = relDir || null;
890
1058
  const key = compositeKey("command", name, category);
@@ -901,9 +1069,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
901
1069
  }
902
1070
  for (const entry of entries) {
903
1071
  if (entry.isDirectory()) {
904
- const filePath = join4(dir, entry.name, fileName);
1072
+ const filePath = join5(dir, entry.name, fileName);
905
1073
  try {
906
- const content = await readFile4(filePath, "utf-8");
1074
+ const content = await readFile5(filePath, "utf-8");
907
1075
  const key = compositeKey(type, entry.name, null);
908
1076
  result.set(key, { type, name: entry.name, category: null, content });
909
1077
  } catch {
@@ -921,7 +1089,7 @@ async function scanFlatType(dir, type, ext, result) {
921
1089
  for (const entry of entries) {
922
1090
  if (entry.isFile() && entry.name.endsWith(ext)) {
923
1091
  const name = entry.name.slice(0, -ext.length);
924
- const content = await readFile4(join4(dir, entry.name), "utf-8");
1092
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
925
1093
  const key = compositeKey(type, name, null);
926
1094
  result.set(key, { type, name, category: null, content });
927
1095
  }
@@ -936,7 +1104,7 @@ async function scanTemplates(dir, result) {
936
1104
  }
937
1105
  for (const entry of entries) {
938
1106
  if (entry.isFile() && extname(entry.name)) {
939
- const content = await readFile4(join4(dir, entry.name), "utf-8");
1107
+ const content = await readFile5(join5(dir, entry.name), "utf-8");
940
1108
  const key = compositeKey("template", entry.name, null);
941
1109
  result.set(key, { type: "template", name: entry.name, category: null, content });
942
1110
  }
@@ -1022,7 +1190,7 @@ __export(push_exports, {
1022
1190
  runPush: () => runPush
1023
1191
  });
1024
1192
  import { stat as stat2 } from "node:fs/promises";
1025
- import { join as join5 } from "node:path";
1193
+ import { join as join6 } from "node:path";
1026
1194
  async function runPush() {
1027
1195
  const flags = parseFlags(3);
1028
1196
  const dryRun = hasFlag("dry-run", 3);
@@ -1037,7 +1205,7 @@ async function runPush() {
1037
1205
  if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
1038
1206
  if (force) console.log(` Mode: force (no conflict prompts)`);
1039
1207
  console.log();
1040
- const claudeDir = join5(projectPath, ".claude");
1208
+ const claudeDir = join6(projectPath, ".claude");
1041
1209
  try {
1042
1210
  await stat2(claudeDir);
1043
1211
  } catch {
@@ -1108,7 +1276,11 @@ async function runPush() {
1108
1276
  }
1109
1277
  }
1110
1278
  if (toUpsert.length === 0 && toDelete.length === 0) {
1111
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1279
+ if (!dryRun) {
1280
+ const repoUpdate2 = { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() };
1281
+ await pushTechStack(projectPath, repoRes.data.tech_stack, repoUpdate2);
1282
+ await apiPut(`/repos/${repoId}`, repoUpdate2);
1283
+ }
1112
1284
  console.log(" Everything is in sync. Nothing to push.\n");
1113
1285
  return;
1114
1286
  }
@@ -1134,10 +1306,28 @@ async function runPush() {
1134
1306
  })),
1135
1307
  delete_keys: toDelete
1136
1308
  });
1137
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
1309
+ const repoUpdate = { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() };
1310
+ await pushTechStack(projectPath, repoRes.data.tech_stack, repoUpdate);
1311
+ await apiPut(`/repos/${repoId}`, repoUpdate);
1138
1312
  console.log(` Done: ${result.data.upserted} upserted, ${result.data.deleted} deleted
1139
1313
  `);
1140
1314
  }
1315
+ async function pushTechStack(projectPath, remoteTechStack, repoUpdate) {
1316
+ try {
1317
+ const detected = await detectTechStack(projectPath);
1318
+ if (detected.length === 0) return;
1319
+ const remote = parseTechStack(remoteTechStack);
1320
+ const { merged, added } = mergeTechStack(remote, detected);
1321
+ console.log(` Tech stack: ${detected.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1322
+ for (const entry of added) {
1323
+ console.log(` + ${entry.name} (${entry.category})`);
1324
+ }
1325
+ if (added.length > 0) {
1326
+ repoUpdate.tech_stack = merged;
1327
+ }
1328
+ } catch {
1329
+ }
1330
+ }
1141
1331
  function flattenSyncData(data) {
1142
1332
  const result = /* @__PURE__ */ new Map();
1143
1333
  const typeMap = {
@@ -1176,6 +1366,7 @@ var init_push = __esm({
1176
1366
  init_confirm();
1177
1367
  init_api();
1178
1368
  init_variables();
1369
+ init_tech_detect();
1179
1370
  }
1180
1371
  });
1181
1372
 
@@ -1187,7 +1378,7 @@ __export(init_exports, {
1187
1378
  import { createInterface as createInterface4 } from "node:readline/promises";
1188
1379
  import { stdin as stdin4, stdout as stdout4 } from "node:process";
1189
1380
  import { writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1190
- import { join as join6, dirname as dirname2 } from "node:path";
1381
+ import { join as join7, dirname as dirname2 } from "node:path";
1191
1382
  async function runInit() {
1192
1383
  const flags = parseFlags(3);
1193
1384
  const projectPath = flags["path"] ?? process.cwd();
@@ -1226,7 +1417,7 @@ async function runInit() {
1226
1417
  if (match) worktreeId = match.id;
1227
1418
  } catch {
1228
1419
  }
1229
- const configPath = join6(projectPath, ".codebyplan.json");
1420
+ const configPath = join7(projectPath, ".codebyplan.json");
1230
1421
  const configData = { repo_id: repoId };
1231
1422
  if (worktreeId) configData.worktree_id = worktreeId;
1232
1423
  const configContent = JSON.stringify(configData, null, 2) + "\n";
@@ -1236,17 +1427,17 @@ async function runInit() {
1236
1427
  if (seedAnswer === "" || seedAnswer === "y" || seedAnswer === "yes") {
1237
1428
  let getFilePath3 = function(typeName, file) {
1238
1429
  const cfg = typeConfig2[typeName];
1239
- const typeDir = typeName === "command" ? join6(claudeDir, cfg.dir, "cbp") : join6(claudeDir, cfg.dir);
1430
+ const typeDir = typeName === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
1240
1431
  if (cfg.subfolder) {
1241
- return join6(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
1432
+ return join7(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
1242
1433
  }
1243
1434
  if (typeName === "command" && file.category) {
1244
- return join6(typeDir, file.category, `${file.name}${cfg.ext}`);
1435
+ return join7(typeDir, file.category, `${file.name}${cfg.ext}`);
1245
1436
  }
1246
1437
  if (typeName === "template") {
1247
- return join6(typeDir, file.name);
1438
+ return join7(typeDir, file.name);
1248
1439
  }
1249
- return join6(typeDir, `${file.name}${cfg.ext}`);
1440
+ return join7(typeDir, `${file.name}${cfg.ext}`);
1250
1441
  };
1251
1442
  var getFilePath2 = getFilePath3;
1252
1443
  console.log("\n Fetching default files...");
@@ -1261,7 +1452,7 @@ async function runInit() {
1261
1452
  printNextSteps(projectPath);
1262
1453
  return;
1263
1454
  }
1264
- const claudeDir = join6(projectPath, ".claude");
1455
+ const claudeDir = join7(projectPath, ".claude");
1265
1456
  let written = 0;
1266
1457
  const typeConfig2 = {
1267
1458
  command: { dir: "commands", ext: ".md" },
@@ -1293,14 +1484,14 @@ async function runInit() {
1293
1484
  ...defaultsData.claude_md ?? []
1294
1485
  ];
1295
1486
  for (const file of specialFiles) {
1296
- const targetPath = join6(projectPath, "CLAUDE.md");
1487
+ const targetPath = join7(projectPath, "CLAUDE.md");
1297
1488
  await mkdir2(dirname2(targetPath), { recursive: true });
1298
1489
  await writeFile2(targetPath, file.content, "utf-8");
1299
1490
  written++;
1300
1491
  }
1301
1492
  const settingsFiles = defaultsData.settings ?? [];
1302
1493
  for (const file of settingsFiles) {
1303
- const targetPath = join6(claudeDir, "settings.json");
1494
+ const targetPath = join7(claudeDir, "settings.json");
1304
1495
  await mkdir2(dirname2(targetPath), { recursive: true });
1305
1496
  await writeFile2(targetPath, file.content, "utf-8");
1306
1497
  written++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {