planmode 0.1.5 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command16 } from "commander";
5
5
 
6
6
  // src/commands/install.ts
7
7
  import { Command } from "commander";
@@ -399,6 +399,18 @@ function removeImport(planName, projectDir = process.cwd()) {
399
399
  const updated = content.split("\n").filter((line) => line.trim() !== importLine).join("\n");
400
400
  fs5.writeFileSync(claudeMdPath, updated, "utf-8");
401
401
  }
402
+ function listImports(projectDir = process.cwd()) {
403
+ const claudeMdPath = getClaudeMdPath(projectDir);
404
+ if (!fs5.existsSync(claudeMdPath)) return [];
405
+ const content = fs5.readFileSync(claudeMdPath, "utf-8");
406
+ const importRegex = /^-\s*@plans\/(.+)\.md$/gm;
407
+ const imports = [];
408
+ let match;
409
+ while ((match = importRegex.exec(content)) !== null) {
410
+ imports.push(match[1]);
411
+ }
412
+ return imports;
413
+ }
402
414
 
403
415
  // src/lib/manifest.ts
404
416
  import fs6 from "fs";
@@ -581,38 +593,96 @@ var YELLOW = "\x1B[33m";
581
593
  var CYAN = "\x1B[36m";
582
594
  var DIM = "\x1B[2m";
583
595
  var BOLD = "\x1B[1m";
596
+ function stripAnsi(str) {
597
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
598
+ }
599
+ var capturing = false;
600
+ var captured = [];
584
601
  var logger = {
602
+ capture() {
603
+ capturing = true;
604
+ captured = [];
605
+ },
606
+ flush() {
607
+ const messages = captured;
608
+ captured = [];
609
+ capturing = false;
610
+ return messages;
611
+ },
612
+ isCapturing() {
613
+ return capturing;
614
+ },
585
615
  info(msg) {
586
- console.log(`${CYAN}info${RESET} ${msg}`);
616
+ const text = `info ${msg}`;
617
+ if (capturing) {
618
+ captured.push(stripAnsi(text));
619
+ } else {
620
+ console.log(`${CYAN}info${RESET} ${msg}`);
621
+ }
587
622
  },
588
623
  success(msg) {
589
- console.log(`${GREEN}\u2713${RESET} ${msg}`);
624
+ const text = `\u2713 ${msg}`;
625
+ if (capturing) {
626
+ captured.push(stripAnsi(text));
627
+ } else {
628
+ console.log(`${GREEN}\u2713${RESET} ${msg}`);
629
+ }
590
630
  },
591
631
  warn(msg) {
592
- console.log(`${YELLOW}warn${RESET} ${msg}`);
632
+ const text = `warn ${msg}`;
633
+ if (capturing) {
634
+ captured.push(stripAnsi(text));
635
+ } else {
636
+ console.log(`${YELLOW}warn${RESET} ${msg}`);
637
+ }
593
638
  },
594
639
  error(msg) {
595
- console.error(`${RED}error${RESET} ${msg}`);
640
+ const text = `error ${msg}`;
641
+ if (capturing) {
642
+ captured.push(stripAnsi(text));
643
+ } else {
644
+ console.error(`${RED}error${RESET} ${msg}`);
645
+ }
596
646
  },
597
647
  dim(msg) {
598
- console.log(`${DIM}${msg}${RESET}`);
648
+ if (capturing) {
649
+ captured.push(msg);
650
+ } else {
651
+ console.log(`${DIM}${msg}${RESET}`);
652
+ }
599
653
  },
600
654
  bold(msg) {
601
- console.log(`${BOLD}${msg}${RESET}`);
655
+ if (capturing) {
656
+ captured.push(msg);
657
+ } else {
658
+ console.log(`${BOLD}${msg}${RESET}`);
659
+ }
602
660
  },
603
661
  table(headers, rows) {
604
662
  const colWidths = headers.map(
605
663
  (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
606
664
  );
607
665
  const header = headers.map((h, i) => h.toUpperCase().padEnd(colWidths[i])).join(" ");
608
- console.log(` ${DIM}${header}${RESET}`);
609
- for (const row of rows) {
610
- const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
611
- console.log(` ${line}`);
666
+ if (capturing) {
667
+ captured.push(` ${header}`);
668
+ for (const row of rows) {
669
+ const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
670
+ captured.push(` ${line}`);
671
+ }
672
+ } else {
673
+ console.log(` ${DIM}${header}${RESET}`);
674
+ for (const row of rows) {
675
+ const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
676
+ console.log(` ${line}`);
677
+ }
612
678
  }
613
679
  },
614
680
  blank() {
615
- console.log();
681
+ if (capturing) {
682
+ captured.push("");
683
+ } else {
684
+ console.log();
685
+ }
616
686
  }
617
687
  };
618
688
 
@@ -695,13 +765,26 @@ async function installPackage(packageName, options = {}) {
695
765
  logger.warn(`Overwriting ${installPath} with new content`);
696
766
  }
697
767
  }
768
+ const computedHash = contentHash(content);
769
+ if (versionMeta.content_hash && computedHash !== versionMeta.content_hash) {
770
+ logger.warn(
771
+ `Content hash mismatch for ${packageName}@${version}. Expected ${versionMeta.content_hash.slice(0, 20)}..., got ${computedHash.slice(0, 20)}... The package content may have been modified after review.`
772
+ );
773
+ }
698
774
  fs7.mkdirSync(path7.dirname(fullPath), { recursive: true });
699
775
  fs7.writeFileSync(fullPath, content, "utf-8");
700
776
  logger.success(`Installed ${packageName}@${version} \u2192 ${installPath}`);
701
777
  trackDownload(packageName);
702
778
  if (type === "plan") {
703
779
  addImport(packageName, projectDir);
704
- logger.dim(`Added @import to CLAUDE.md`);
780
+ logger.dim(`Added @plans/${packageName}.md to CLAUDE.md`);
781
+ logger.dim(`Claude Code will automatically see this plan in your next conversation.`);
782
+ }
783
+ if (type === "rule") {
784
+ logger.dim(`Rule is active \u2014 Claude Code auto-loads all files in .claude/rules/.`);
785
+ }
786
+ if (type === "prompt") {
787
+ logger.dim(`Run it with: planmode run ${packageName}`);
705
788
  }
706
789
  const hash = contentHash(content);
707
790
  const entry = {
@@ -905,144 +988,143 @@ var runCommand = new Command4("run").description("Run a templated prompt and out
905
988
 
906
989
  // src/commands/publish.ts
907
990
  import { Command as Command5 } from "commander";
908
- var publishCommand = new Command5("publish").description("Publish the current directory as a package to the registry").action(async () => {
991
+
992
+ // src/lib/publisher.ts
993
+ async function publishPackage(options = {}) {
994
+ const cwd = options.projectDir ?? process.cwd();
995
+ const token = options.token ?? getGitHubToken();
996
+ if (!token) {
997
+ throw new Error("Not authenticated. Run `planmode login` first.");
998
+ }
999
+ logger.info("Reading planmode.yaml...");
1000
+ const manifest = readManifest(cwd);
1001
+ const errors = validateManifest(manifest, true);
1002
+ if (errors.length > 0) {
1003
+ throw new Error(`Invalid manifest:
1004
+ ${errors.map((e) => ` - ${e}`).join("\n")}`);
1005
+ }
1006
+ const remoteUrl = await getRemoteUrl(cwd);
1007
+ if (!remoteUrl) {
1008
+ throw new Error("No git remote found. Push your code to GitHub first.");
1009
+ }
1010
+ const sha = await getHeadSha(cwd);
1011
+ const tag = `v${manifest.version}`;
1012
+ logger.info(`Creating tag ${tag}...`);
909
1013
  try {
910
- const cwd = process.cwd();
911
- const token = getGitHubToken();
912
- if (!token) {
913
- logger.error("Not authenticated. Run `planmode login` first.");
914
- process.exit(1);
915
- }
916
- logger.info("Reading planmode.yaml...");
917
- const manifest = readManifest(cwd);
918
- const errors = validateManifest(manifest, true);
919
- if (errors.length > 0) {
920
- logger.error("Invalid manifest:");
921
- for (const err of errors) {
922
- console.log(` - ${err}`);
923
- }
924
- process.exit(1);
925
- }
926
- const remoteUrl = await getRemoteUrl(cwd);
927
- if (!remoteUrl) {
928
- logger.error("No git remote found. Push your code to GitHub first.");
929
- process.exit(1);
930
- }
931
- const sha = await getHeadSha(cwd);
932
- const tag = `v${manifest.version}`;
933
- logger.info(`Creating tag ${tag}...`);
934
- try {
935
- await createTag(cwd, tag);
936
- } catch {
937
- logger.dim(`Tag ${tag} already exists, using existing`);
938
- }
939
- try {
940
- await pushTag(cwd, tag);
941
- logger.success(`Pushed tag ${tag}`);
942
- } catch {
943
- logger.dim(`Tag ${tag} already pushed`);
944
- }
945
- logger.info("Submitting to registry...");
946
- const headers = {
947
- Authorization: `Bearer ${token}`,
948
- Accept: "application/vnd.github.v3+json",
949
- "User-Agent": "planmode-cli",
950
- "Content-Type": "application/json"
951
- };
952
- await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
953
- method: "POST",
954
- headers
955
- });
956
- const userRes = await fetch("https://api.github.com/user", { headers });
957
- const user = await userRes.json();
958
- const metadataContent = JSON.stringify(
959
- {
960
- name: manifest.name,
961
- description: manifest.description,
962
- author: manifest.author,
963
- license: manifest.license,
964
- repository: remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, ""),
965
- category: manifest.category ?? "other",
966
- tags: manifest.tags ?? [],
967
- type: manifest.type,
968
- models: manifest.models ?? [],
969
- latest_version: manifest.version,
970
- versions: [manifest.version],
971
- downloads: 0,
972
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
973
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
974
- dependencies: manifest.dependencies,
975
- variables: manifest.variables
976
- },
977
- null,
978
- 2
979
- );
980
- const versionContent = JSON.stringify(
981
- {
982
- version: manifest.version,
983
- published_at: (/* @__PURE__ */ new Date()).toISOString(),
984
- source: {
985
- repository: remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, ""),
986
- tag,
987
- sha
988
- },
989
- files: ["planmode.yaml", manifest.content_file ?? "inline"],
990
- content_hash: `sha256:${sha.slice(0, 16)}`
1014
+ await createTag(cwd, tag);
1015
+ } catch {
1016
+ logger.dim(`Tag ${tag} already exists, using existing`);
1017
+ }
1018
+ try {
1019
+ await pushTag(cwd, tag);
1020
+ logger.success(`Pushed tag ${tag}`);
1021
+ } catch {
1022
+ logger.dim(`Tag ${tag} already pushed`);
1023
+ }
1024
+ logger.info("Submitting to registry...");
1025
+ const headers = {
1026
+ Authorization: `Bearer ${token}`,
1027
+ Accept: "application/vnd.github.v3+json",
1028
+ "User-Agent": "planmode-cli",
1029
+ "Content-Type": "application/json"
1030
+ };
1031
+ await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
1032
+ method: "POST",
1033
+ headers
1034
+ });
1035
+ const userRes = await fetch("https://api.github.com/user", { headers });
1036
+ if (!userRes.ok) {
1037
+ throw new Error("Failed to authenticate with GitHub. Check your token.");
1038
+ }
1039
+ const user = await userRes.json();
1040
+ const repoPath = remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, "");
1041
+ const metadataContent = JSON.stringify(
1042
+ {
1043
+ name: manifest.name,
1044
+ description: manifest.description,
1045
+ author: manifest.author,
1046
+ license: manifest.license,
1047
+ repository: repoPath,
1048
+ category: manifest.category ?? "other",
1049
+ tags: manifest.tags ?? [],
1050
+ type: manifest.type,
1051
+ models: manifest.models ?? [],
1052
+ latest_version: manifest.version,
1053
+ versions: [manifest.version],
1054
+ downloads: 0,
1055
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1056
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1057
+ dependencies: manifest.dependencies,
1058
+ variables: manifest.variables
1059
+ },
1060
+ null,
1061
+ 2
1062
+ );
1063
+ const versionContent = JSON.stringify(
1064
+ {
1065
+ version: manifest.version,
1066
+ published_at: (/* @__PURE__ */ new Date()).toISOString(),
1067
+ source: {
1068
+ repository: repoPath,
1069
+ tag,
1070
+ sha
991
1071
  },
992
- null,
993
- 2
994
- );
995
- const branchName = `add-${manifest.name}-${manifest.version}`;
996
- const refRes = await fetch(
997
- `https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
998
- { headers }
999
- );
1000
- if (!refRes.ok) {
1001
- logger.error("Failed to access registry fork. Make sure the fork exists.");
1002
- process.exit(1);
1003
- }
1004
- const refData = await refRes.json();
1005
- const baseSha = refData.object.sha;
1006
- await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
1007
- method: "POST",
1072
+ files: ["planmode.yaml", manifest.content_file ?? "inline"],
1073
+ content_hash: `sha256:${sha.slice(0, 16)}`
1074
+ },
1075
+ null,
1076
+ 2
1077
+ );
1078
+ const branchName = `add-${manifest.name}-${manifest.version}`;
1079
+ const refRes = await fetch(
1080
+ `https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
1081
+ { headers }
1082
+ );
1083
+ if (!refRes.ok) {
1084
+ throw new Error("Failed to access registry fork. Make sure the fork exists.");
1085
+ }
1086
+ const refData = await refRes.json();
1087
+ const baseSha = refData.object.sha;
1088
+ await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
1089
+ method: "POST",
1090
+ headers,
1091
+ body: JSON.stringify({
1092
+ ref: `refs/heads/${branchName}`,
1093
+ sha: baseSha
1094
+ })
1095
+ });
1096
+ await fetch(
1097
+ `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
1098
+ {
1099
+ method: "PUT",
1008
1100
  headers,
1009
1101
  body: JSON.stringify({
1010
- ref: `refs/heads/${branchName}`,
1011
- sha: baseSha
1102
+ message: `Add ${manifest.name}@${manifest.version}`,
1103
+ content: Buffer.from(metadataContent).toString("base64"),
1104
+ branch: branchName
1012
1105
  })
1013
- });
1014
- await fetch(
1015
- `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
1016
- {
1017
- method: "PUT",
1018
- headers,
1019
- body: JSON.stringify({
1020
- message: `Add ${manifest.name}@${manifest.version}`,
1021
- content: Buffer.from(metadataContent).toString("base64"),
1022
- branch: branchName
1023
- })
1024
- }
1025
- );
1026
- await fetch(
1027
- `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
1028
- {
1029
- method: "PUT",
1030
- headers,
1031
- body: JSON.stringify({
1032
- message: `Add ${manifest.name}@${manifest.version} version metadata`,
1033
- content: Buffer.from(versionContent).toString("base64"),
1034
- branch: branchName
1035
- })
1036
- }
1037
- );
1038
- const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
1039
- method: "POST",
1106
+ }
1107
+ );
1108
+ await fetch(
1109
+ `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
1110
+ {
1111
+ method: "PUT",
1040
1112
  headers,
1041
1113
  body: JSON.stringify({
1042
- title: `Add ${manifest.name}@${manifest.version}`,
1043
- head: `${user.login}:${branchName}`,
1044
- base: "main",
1045
- body: `## New package: ${manifest.name}
1114
+ message: `Add ${manifest.name}@${manifest.version} version metadata`,
1115
+ content: Buffer.from(versionContent).toString("base64"),
1116
+ branch: branchName
1117
+ })
1118
+ }
1119
+ );
1120
+ const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
1121
+ method: "POST",
1122
+ headers,
1123
+ body: JSON.stringify({
1124
+ title: `Add ${manifest.name}@${manifest.version}`,
1125
+ head: `${user.login}:${branchName}`,
1126
+ base: "main",
1127
+ body: `## New package: ${manifest.name}
1046
1128
 
1047
1129
  - **Type:** ${manifest.type}
1048
1130
  - **Version:** ${manifest.version}
@@ -1050,18 +1132,27 @@ var publishCommand = new Command5("publish").description("Publish the current di
1050
1132
  - **Author:** ${manifest.author}
1051
1133
 
1052
1134
  Submitted via \`planmode publish\`.`
1053
- })
1054
- });
1055
- if (prRes.ok) {
1056
- const pr = await prRes.json();
1057
- logger.blank();
1058
- logger.success(`Published ${manifest.name}@${manifest.version}`);
1059
- logger.info(`PR: ${pr.html_url}`);
1060
- } else {
1061
- const err = await prRes.text();
1062
- logger.error(`Failed to create PR: ${err}`);
1063
- process.exit(1);
1064
- }
1135
+ })
1136
+ });
1137
+ if (!prRes.ok) {
1138
+ const err = await prRes.text();
1139
+ throw new Error(`Failed to create PR: ${err}`);
1140
+ }
1141
+ const pr = await prRes.json();
1142
+ logger.success(`Published ${manifest.name}@${manifest.version}`);
1143
+ logger.info(`PR: ${pr.html_url}`);
1144
+ return {
1145
+ prUrl: pr.html_url,
1146
+ packageName: manifest.name,
1147
+ version: manifest.version
1148
+ };
1149
+ }
1150
+
1151
+ // src/commands/publish.ts
1152
+ var publishCommand = new Command5("publish").description("Publish the current directory as a package to the registry").action(async () => {
1153
+ try {
1154
+ logger.blank();
1155
+ const result = await publishPackage();
1065
1156
  logger.blank();
1066
1157
  } catch (err) {
1067
1158
  logger.error(err.message);
@@ -1181,9 +1272,114 @@ var infoCommand = new Command8("info").description("Show detailed info about a p
1181
1272
 
1182
1273
  // src/commands/init.ts
1183
1274
  import { Command as Command9 } from "commander";
1275
+
1276
+ // src/lib/init.ts
1184
1277
  import fs9 from "fs";
1185
1278
  import path9 from "path";
1186
1279
  import { stringify as stringify3 } from "yaml";
1280
+
1281
+ // src/lib/templates.ts
1282
+ function getPlanTemplate(name) {
1283
+ return `# ${name}
1284
+
1285
+ ## Prerequisites
1286
+
1287
+ - List any tools, dependencies, or setup required before starting
1288
+
1289
+ ## Steps
1290
+
1291
+ 1. **Step one** \u2014 Description of what to do first
1292
+ 2. **Step two** \u2014 Description of what to do next
1293
+ 3. **Step three** \u2014 Description of the final step
1294
+
1295
+ ## Verification
1296
+
1297
+ - [ ] Verify step one completed successfully
1298
+ - [ ] Verify step two completed successfully
1299
+ - [ ] Verify the final result works as expected
1300
+ `;
1301
+ }
1302
+ function getRuleTemplate(name) {
1303
+ return `# ${name}
1304
+
1305
+ ## Code Style
1306
+
1307
+ - Follow consistent naming conventions
1308
+ - Keep functions small and focused
1309
+
1310
+ ## Best Practices
1311
+
1312
+ - Prefer composition over inheritance
1313
+ - Write self-documenting code
1314
+
1315
+ ## Avoid
1316
+
1317
+ - Do not use deprecated APIs
1318
+ - Do not ignore error handling
1319
+ `;
1320
+ }
1321
+ function getPromptTemplate(name) {
1322
+ return `# ${name}
1323
+
1324
+ {{description}}
1325
+
1326
+ ## Context
1327
+
1328
+ Provide any relevant context here.
1329
+
1330
+ ## Requirements
1331
+
1332
+ - Requirement one
1333
+ - Requirement two
1334
+
1335
+ ## Output Format
1336
+
1337
+ Describe the expected output format.
1338
+ `;
1339
+ }
1340
+
1341
+ // src/lib/init.ts
1342
+ function createPackage(options) {
1343
+ const {
1344
+ name,
1345
+ type,
1346
+ description,
1347
+ author,
1348
+ license = "MIT",
1349
+ tags = [],
1350
+ category = "other",
1351
+ projectDir = process.cwd()
1352
+ } = options;
1353
+ const manifest = {
1354
+ name,
1355
+ version: "1.0.0",
1356
+ type,
1357
+ description,
1358
+ author,
1359
+ license
1360
+ };
1361
+ if (tags.length > 0) manifest["tags"] = tags;
1362
+ manifest["category"] = category;
1363
+ const contentFile = `${type}.md`;
1364
+ manifest["content_file"] = contentFile;
1365
+ const yamlContent = stringify3(manifest);
1366
+ const manifestPath = path9.join(projectDir, "planmode.yaml");
1367
+ fs9.writeFileSync(manifestPath, yamlContent, "utf-8");
1368
+ const stubs = {
1369
+ plan: getPlanTemplate(name),
1370
+ rule: getRuleTemplate(name),
1371
+ prompt: getPromptTemplate(name)
1372
+ };
1373
+ const contentPath = path9.join(projectDir, contentFile);
1374
+ fs9.writeFileSync(contentPath, stubs[type] ?? stubs["plan"], "utf-8");
1375
+ return {
1376
+ files: ["planmode.yaml", contentFile],
1377
+ manifestPath,
1378
+ contentPath
1379
+ };
1380
+ }
1381
+
1382
+ // src/commands/init.ts
1187
1383
  async function prompt(question) {
1188
1384
  const { createInterface } = await import("readline");
1189
1385
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -1211,46 +1407,23 @@ var initCommand = new Command9("init").description("Initialize a new package in
1211
1407
  const license = await prompt("License [MIT]: ") || "MIT";
1212
1408
  const tagsInput = await prompt("Tags (comma-separated): ");
1213
1409
  const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim().toLowerCase()) : [];
1214
- const category = await prompt("Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ") || "other";
1215
- const manifest = {
1410
+ const category = await prompt(
1411
+ "Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: "
1412
+ ) || "other";
1413
+ const result = createPackage({
1216
1414
  name,
1217
- version: "1.0.0",
1218
1415
  type,
1219
1416
  description,
1220
1417
  author,
1221
- license
1222
- };
1223
- if (tags.length > 0) manifest["tags"] = tags;
1224
- manifest["category"] = category;
1225
- const contentFile = `${type}.md`;
1226
- manifest["content_file"] = contentFile;
1227
- const yamlContent = stringify3(manifest);
1228
- fs9.writeFileSync(path9.join(process.cwd(), "planmode.yaml"), yamlContent, "utf-8");
1229
- logger.success("Created planmode.yaml");
1230
- const stubs = {
1231
- plan: `# ${name}
1232
-
1233
- 1. First step
1234
- 2. Second step
1235
- 3. Third step
1236
- `,
1237
- rule: `- Rule one
1238
- - Rule two
1239
- - Rule three
1240
- `,
1241
- prompt: `Write your prompt here.
1242
-
1243
- Use {{variable_name}} for template variables.
1244
- `
1245
- };
1246
- fs9.writeFileSync(
1247
- path9.join(process.cwd(), contentFile),
1248
- stubs[type] ?? stubs["plan"],
1249
- "utf-8"
1250
- );
1251
- logger.success(`Created ${contentFile}`);
1418
+ license,
1419
+ tags,
1420
+ category
1421
+ });
1422
+ logger.success(`Created ${result.files.join(", ")}`);
1252
1423
  logger.blank();
1253
- logger.info(`Edit ${contentFile}, then run \`planmode publish\` when ready.`);
1424
+ logger.info(
1425
+ `Edit ${result.files[1]}, then run \`planmode publish\` when ready.`
1426
+ );
1254
1427
  logger.blank();
1255
1428
  } catch (err) {
1256
1429
  logger.error(err.message);
@@ -1302,9 +1475,821 @@ var loginCommand = new Command10("login").description("Configure GitHub authenti
1302
1475
  logger.success(`Authenticated as ${user.login}`);
1303
1476
  });
1304
1477
 
1478
+ // src/commands/mcp.ts
1479
+ import { Command as Command11 } from "commander";
1480
+ import { execSync as execSync2 } from "child_process";
1481
+ var mcpCommand = new Command11("mcp").description("Manage MCP server registration with Claude Code");
1482
+ mcpCommand.command("setup").description("Register the planmode MCP server with Claude Code").action(() => {
1483
+ try {
1484
+ execSync2("claude mcp add --transport stdio planmode -- planmode-mcp", {
1485
+ stdio: "inherit"
1486
+ });
1487
+ logger.success("Planmode MCP server registered with Claude Code.");
1488
+ logger.dim("Claude Code can now use planmode tools directly.");
1489
+ } catch (err) {
1490
+ logger.error(
1491
+ "Failed to register MCP server. Make sure Claude Code CLI is installed."
1492
+ );
1493
+ process.exit(1);
1494
+ }
1495
+ });
1496
+ mcpCommand.command("remove").description("Remove the planmode MCP server from Claude Code").action(() => {
1497
+ try {
1498
+ execSync2("claude mcp remove planmode", { stdio: "inherit" });
1499
+ logger.success("Planmode MCP server removed from Claude Code.");
1500
+ } catch (err) {
1501
+ logger.error(
1502
+ "Failed to remove MCP server. Make sure Claude Code CLI is installed."
1503
+ );
1504
+ process.exit(1);
1505
+ }
1506
+ });
1507
+
1508
+ // src/commands/doctor.ts
1509
+ import { Command as Command12 } from "commander";
1510
+
1511
+ // src/lib/doctor.ts
1512
+ import fs10 from "fs";
1513
+ import path10 from "path";
1514
+ import crypto2 from "crypto";
1515
+ function computeHash(content) {
1516
+ return `sha256:${crypto2.createHash("sha256").update(content).digest("hex")}`;
1517
+ }
1518
+ function runDoctor(projectDir = process.cwd()) {
1519
+ const issues = [];
1520
+ const lockfile = readLockfile(projectDir);
1521
+ const entries = Object.entries(lockfile.packages);
1522
+ for (const [name, entry] of entries) {
1523
+ const fullPath = path10.join(projectDir, entry.installed_to);
1524
+ if (!fs10.existsSync(fullPath)) {
1525
+ issues.push({
1526
+ severity: "error",
1527
+ message: `Missing file for "${name}": ${entry.installed_to}`,
1528
+ fix: `Run \`planmode install ${name}\` to reinstall`
1529
+ });
1530
+ continue;
1531
+ }
1532
+ const content = fs10.readFileSync(fullPath, "utf-8");
1533
+ const actualHash = computeHash(content);
1534
+ if (actualHash !== entry.content_hash) {
1535
+ issues.push({
1536
+ severity: "warning",
1537
+ message: `Content hash mismatch for "${name}" at ${entry.installed_to}`,
1538
+ fix: "File was modified locally. Run `planmode update " + name + "` to restore, or ignore if intentional"
1539
+ });
1540
+ }
1541
+ }
1542
+ const claudeMdPath = path10.join(projectDir, "CLAUDE.md");
1543
+ const imports = listImports(projectDir);
1544
+ const installedPlans = entries.filter(([, entry]) => entry.type === "plan").map(([name]) => name);
1545
+ for (const planName of installedPlans) {
1546
+ if (!imports.includes(planName)) {
1547
+ issues.push({
1548
+ severity: "error",
1549
+ message: `Plan "${planName}" is installed but missing from CLAUDE.md imports`,
1550
+ fix: `Add \`- @plans/${planName}.md\` to the # Planmode section of CLAUDE.md`
1551
+ });
1552
+ }
1553
+ }
1554
+ for (const importName of imports) {
1555
+ if (!installedPlans.includes(importName)) {
1556
+ const filePath = path10.join(projectDir, "plans", `${importName}.md`);
1557
+ if (!fs10.existsSync(filePath)) {
1558
+ issues.push({
1559
+ severity: "error",
1560
+ message: `CLAUDE.md imports "${importName}" but the file doesn't exist at plans/${importName}.md`,
1561
+ fix: `Run \`planmode install ${importName}\` or remove the import from CLAUDE.md`
1562
+ });
1563
+ } else {
1564
+ issues.push({
1565
+ severity: "warning",
1566
+ message: `CLAUDE.md imports "${importName}" but it's not tracked in planmode.lock`,
1567
+ fix: "This plan was added manually. No action needed unless you want lockfile tracking."
1568
+ });
1569
+ }
1570
+ }
1571
+ }
1572
+ if (installedPlans.length > 0 && !fs10.existsSync(claudeMdPath)) {
1573
+ issues.push({
1574
+ severity: "error",
1575
+ message: "CLAUDE.md is missing but plans are installed",
1576
+ fix: "Run `planmode install <any-plan>` to recreate it, or create it manually with a # Planmode section"
1577
+ });
1578
+ }
1579
+ const plansDir = path10.join(projectDir, "plans");
1580
+ if (fs10.existsSync(plansDir)) {
1581
+ const planFiles = fs10.readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1582
+ for (const file of planFiles) {
1583
+ const name = file.replace(/\.md$/, "");
1584
+ if (!lockfile.packages[name]) {
1585
+ issues.push({
1586
+ severity: "warning",
1587
+ message: `Untracked plan file: plans/${file}`,
1588
+ fix: "This file isn't managed by planmode. Ignore if intentional."
1589
+ });
1590
+ }
1591
+ }
1592
+ }
1593
+ return {
1594
+ issues,
1595
+ packagesChecked: entries.length,
1596
+ healthy: issues.filter((i) => i.severity === "error").length === 0
1597
+ };
1598
+ }
1599
+
1600
+ // src/commands/doctor.ts
1601
+ var doctorCommand = new Command12("doctor").description("Check project health: verify installed packages, imports, and file integrity").action(() => {
1602
+ const result = runDoctor();
1603
+ logger.blank();
1604
+ logger.bold(`Checked ${result.packagesChecked} package(s)`);
1605
+ logger.blank();
1606
+ if (result.issues.length === 0) {
1607
+ logger.success("Everything looks good. No issues found.");
1608
+ logger.blank();
1609
+ return;
1610
+ }
1611
+ const errors = result.issues.filter((i) => i.severity === "error");
1612
+ const warnings = result.issues.filter((i) => i.severity === "warning");
1613
+ for (const issue of errors) {
1614
+ logger.error(issue.message);
1615
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
1616
+ }
1617
+ for (const issue of warnings) {
1618
+ logger.warn(issue.message);
1619
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
1620
+ }
1621
+ logger.blank();
1622
+ if (errors.length > 0) {
1623
+ logger.error(`${errors.length} error(s), ${warnings.length} warning(s)`);
1624
+ } else {
1625
+ logger.warn(`${warnings.length} warning(s)`);
1626
+ }
1627
+ logger.blank();
1628
+ if (errors.length > 0) {
1629
+ process.exit(1);
1630
+ }
1631
+ });
1632
+
1633
+ // src/commands/test.ts
1634
+ import { Command as Command13 } from "commander";
1635
+
1636
+ // src/lib/tester.ts
1637
+ async function testPackage(projectDir = process.cwd()) {
1638
+ const issues = [];
1639
+ const checks = [];
1640
+ let manifest;
1641
+ try {
1642
+ manifest = readManifest(projectDir);
1643
+ checks.push({ name: "Manifest parses", passed: true });
1644
+ } catch (err) {
1645
+ issues.push({
1646
+ severity: "error",
1647
+ check: "Manifest parses",
1648
+ message: err.message
1649
+ });
1650
+ checks.push({ name: "Manifest parses", passed: false });
1651
+ return { issues, passed: false, checks };
1652
+ }
1653
+ const errors = validateManifest(manifest, true);
1654
+ if (errors.length === 0) {
1655
+ checks.push({ name: "Manifest valid for publishing", passed: true });
1656
+ } else {
1657
+ for (const err of errors) {
1658
+ issues.push({
1659
+ severity: "error",
1660
+ check: "Manifest valid for publishing",
1661
+ message: err
1662
+ });
1663
+ }
1664
+ checks.push({ name: "Manifest valid for publishing", passed: false });
1665
+ }
1666
+ let content;
1667
+ try {
1668
+ content = readPackageContent(projectDir, manifest);
1669
+ if (content.trim().length === 0) {
1670
+ issues.push({
1671
+ severity: "warning",
1672
+ check: "Content is non-empty",
1673
+ message: "Content file is empty"
1674
+ });
1675
+ checks.push({ name: "Content is non-empty", passed: false });
1676
+ } else {
1677
+ checks.push({ name: "Content is non-empty", passed: true });
1678
+ }
1679
+ } catch (err) {
1680
+ issues.push({
1681
+ severity: "error",
1682
+ check: "Content readable",
1683
+ message: err.message
1684
+ });
1685
+ checks.push({ name: "Content readable", passed: false });
1686
+ }
1687
+ if (content && manifest.variables && Object.keys(manifest.variables).length > 0) {
1688
+ try {
1689
+ const missingDefaults = [];
1690
+ for (const [name, def] of Object.entries(manifest.variables)) {
1691
+ if (def.required && def.default === void 0) {
1692
+ missingDefaults.push(name);
1693
+ }
1694
+ }
1695
+ if (missingDefaults.length > 0) {
1696
+ issues.push({
1697
+ severity: "warning",
1698
+ check: "Required variables have defaults",
1699
+ message: `Required variables without defaults: ${missingDefaults.join(", ")}. Users must provide these at install time.`
1700
+ });
1701
+ checks.push({ name: "Required variables have defaults", passed: false });
1702
+ } else {
1703
+ checks.push({ name: "Required variables have defaults", passed: true });
1704
+ }
1705
+ const values = collectVariableValues(manifest.variables, {});
1706
+ renderTemplate(content, values);
1707
+ checks.push({ name: "Template renders with defaults", passed: true });
1708
+ } catch (err) {
1709
+ issues.push({
1710
+ severity: "error",
1711
+ check: "Template renders with defaults",
1712
+ message: err.message
1713
+ });
1714
+ checks.push({ name: "Template renders with defaults", passed: false });
1715
+ }
1716
+ }
1717
+ if (manifest.dependencies) {
1718
+ const allDeps = [
1719
+ ...manifest.dependencies.rules ?? [],
1720
+ ...manifest.dependencies.plans ?? []
1721
+ ];
1722
+ for (const dep of allDeps) {
1723
+ const depName = dep.includes("@") ? dep.split("@")[0] : dep;
1724
+ try {
1725
+ const results = await searchPackages(depName);
1726
+ const found = results.some((r) => r.name === depName);
1727
+ if (found) {
1728
+ checks.push({ name: `Dependency "${depName}" exists`, passed: true });
1729
+ } else {
1730
+ issues.push({
1731
+ severity: "warning",
1732
+ check: `Dependency "${depName}" exists`,
1733
+ message: `Dependency "${depName}" not found in registry. It may not be published yet.`
1734
+ });
1735
+ checks.push({ name: `Dependency "${depName}" exists`, passed: false });
1736
+ }
1737
+ } catch {
1738
+ issues.push({
1739
+ severity: "warning",
1740
+ check: `Dependency "${depName}" exists`,
1741
+ message: `Could not check registry for "${depName}" (network error)`
1742
+ });
1743
+ checks.push({ name: `Dependency "${depName}" exists`, passed: false });
1744
+ }
1745
+ }
1746
+ }
1747
+ if (content) {
1748
+ const sizeKb = Buffer.byteLength(content, "utf-8") / 1024;
1749
+ if (sizeKb > 100) {
1750
+ issues.push({
1751
+ severity: "warning",
1752
+ check: "Content size reasonable",
1753
+ message: `Content is ${sizeKb.toFixed(1)}KB. Large packages may hit token limits in AI models.`
1754
+ });
1755
+ checks.push({ name: "Content size reasonable", passed: false });
1756
+ } else {
1757
+ checks.push({ name: "Content size reasonable", passed: true });
1758
+ }
1759
+ }
1760
+ const hasErrors = issues.some((i) => i.severity === "error");
1761
+ return { issues, passed: !hasErrors, checks };
1762
+ }
1763
+
1764
+ // src/commands/test.ts
1765
+ var testCommand = new Command13("test").description("Test the current package before publishing: validate manifest, render templates, check dependencies").action(async () => {
1766
+ try {
1767
+ logger.blank();
1768
+ logger.bold("Testing package...");
1769
+ logger.blank();
1770
+ const result = await testPackage();
1771
+ for (const check of result.checks) {
1772
+ if (check.passed) {
1773
+ logger.success(check.name);
1774
+ } else {
1775
+ const issue = result.issues.find((i) => i.check === check.name);
1776
+ if (issue?.severity === "error") {
1777
+ logger.error(`${check.name}: ${issue.message}`);
1778
+ } else if (issue) {
1779
+ logger.warn(`${check.name}: ${issue.message}`);
1780
+ }
1781
+ }
1782
+ }
1783
+ logger.blank();
1784
+ if (result.passed) {
1785
+ logger.success(`All checks passed. Ready to publish.`);
1786
+ } else {
1787
+ const errors = result.issues.filter((i) => i.severity === "error");
1788
+ const warnings = result.issues.filter((i) => i.severity === "warning");
1789
+ logger.error(`${errors.length} error(s), ${warnings.length} warning(s). Fix errors before publishing.`);
1790
+ }
1791
+ logger.blank();
1792
+ if (!result.passed) {
1793
+ process.exit(1);
1794
+ }
1795
+ } catch (err) {
1796
+ logger.error(err.message);
1797
+ process.exit(1);
1798
+ }
1799
+ });
1800
+
1801
+ // src/commands/record.ts
1802
+ import { Command as Command14 } from "commander";
1803
+ import fs12 from "fs";
1804
+ import path12 from "path";
1805
+
1806
+ // src/lib/recorder.ts
1807
+ import fs11 from "fs";
1808
+ import path11 from "path";
1809
+ import { simpleGit as simpleGit2 } from "simple-git";
1810
+ import { stringify as stringify4 } from "yaml";
1811
+ var RECORDING_FILE = ".planmode-recording";
1812
+ function startRecording(projectDir = process.cwd()) {
1813
+ const git = simpleGit2(projectDir);
1814
+ const recordingPath = path11.join(projectDir, RECORDING_FILE);
1815
+ if (fs11.existsSync(recordingPath)) {
1816
+ const existing = fs11.readFileSync(recordingPath, "utf-8").trim();
1817
+ throw new Error(
1818
+ `Recording already in progress (started at ${existing}). Run \`planmode record stop\` first.`
1819
+ );
1820
+ }
1821
+ return recordingPath;
1822
+ }
1823
+ async function startRecordingAsync(projectDir = process.cwd()) {
1824
+ const recordingPath = startRecording(projectDir);
1825
+ const git = simpleGit2(projectDir);
1826
+ const log = await git.log({ n: 1 });
1827
+ const sha = log.latest?.hash;
1828
+ if (!sha) {
1829
+ throw new Error("No commits found in this repository.");
1830
+ }
1831
+ fs11.writeFileSync(recordingPath, sha, "utf-8");
1832
+ return sha;
1833
+ }
1834
+ function isRecording(projectDir = process.cwd()) {
1835
+ return fs11.existsSync(path11.join(projectDir, RECORDING_FILE));
1836
+ }
1837
+ async function stopRecording(projectDir = process.cwd(), options = {}) {
1838
+ const recordingPath = path11.join(projectDir, RECORDING_FILE);
1839
+ if (!fs11.existsSync(recordingPath)) {
1840
+ throw new Error("No recording in progress. Run `planmode record start` first.");
1841
+ }
1842
+ const startSha = fs11.readFileSync(recordingPath, "utf-8").trim();
1843
+ const git = simpleGit2(projectDir);
1844
+ const log = await git.log({ from: startSha, to: "HEAD" });
1845
+ if (log.total === 0) {
1846
+ fs11.unlinkSync(recordingPath);
1847
+ throw new Error("No commits since recording started. Nothing to capture.");
1848
+ }
1849
+ const commits = [...log.all].reverse();
1850
+ const steps = [];
1851
+ const allFilesChanged = /* @__PURE__ */ new Set();
1852
+ for (const commit of commits) {
1853
+ const diff = await git.diffSummary([`${commit.hash}~1`, commit.hash]).catch(
1854
+ () => (
1855
+ // First commit in range might not have a parent in range
1856
+ git.diffSummary([startSha, commit.hash])
1857
+ )
1858
+ );
1859
+ const filesChanged = diff.files.map((f) => f.file);
1860
+ filesChanged.forEach((f) => allFilesChanged.add(f));
1861
+ const firstLine = commit.message.split("\n")[0].trim();
1862
+ const body = commit.message.split("\n").slice(1).join("\n").trim();
1863
+ steps.push({
1864
+ title: firstLine,
1865
+ message: body || firstLine,
1866
+ filesChanged,
1867
+ sha: commit.hash.slice(0, 7)
1868
+ });
1869
+ }
1870
+ const planName = options.name || inferPlanName(steps);
1871
+ const planContent = generatePlanContent(planName, steps);
1872
+ const manifestContent = generateManifest(planName, options.author || "");
1873
+ fs11.unlinkSync(recordingPath);
1874
+ return {
1875
+ steps,
1876
+ planContent,
1877
+ manifestContent,
1878
+ totalCommits: commits.length,
1879
+ totalFilesChanged: allFilesChanged.size
1880
+ };
1881
+ }
1882
+ function inferPlanName(steps) {
1883
+ const words = steps.map((s) => s.title.toLowerCase()).join(" ").replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !["the", "and", "for", "add", "fix", "update", "set"].includes(w));
1884
+ const counts = /* @__PURE__ */ new Map();
1885
+ for (const word of words) {
1886
+ counts.set(word, (counts.get(word) || 0) + 1);
1887
+ }
1888
+ const topWords = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w);
1889
+ return topWords.length > 0 ? topWords.join("-") + "-setup" : "recorded-plan";
1890
+ }
1891
+ function generatePlanContent(name, steps) {
1892
+ const lines = [];
1893
+ lines.push(`# ${name}`);
1894
+ lines.push("");
1895
+ lines.push("## Steps");
1896
+ lines.push("");
1897
+ for (let i = 0; i < steps.length; i++) {
1898
+ const step = steps[i];
1899
+ lines.push(`### ${i + 1}. ${step.title}`);
1900
+ lines.push("");
1901
+ if (step.message !== step.title) {
1902
+ lines.push(step.message);
1903
+ lines.push("");
1904
+ }
1905
+ if (step.filesChanged.length > 0) {
1906
+ lines.push("**Files changed:**");
1907
+ for (const file of step.filesChanged) {
1908
+ lines.push(`- \`${file}\``);
1909
+ }
1910
+ lines.push("");
1911
+ }
1912
+ }
1913
+ lines.push("## Verification");
1914
+ lines.push("");
1915
+ lines.push("- [ ] All steps completed successfully");
1916
+ lines.push("- [ ] Application builds without errors");
1917
+ lines.push("- [ ] Tests pass");
1918
+ lines.push("");
1919
+ return lines.join("\n");
1920
+ }
1921
+ function generateManifest(name, author) {
1922
+ const manifest = {
1923
+ name,
1924
+ version: "1.0.0",
1925
+ type: "plan",
1926
+ description: `Plan recorded from git history`,
1927
+ author: author || "unknown",
1928
+ license: "MIT",
1929
+ category: "other",
1930
+ content_file: "plan.md"
1931
+ };
1932
+ return stringify4(manifest);
1933
+ }
1934
+
1935
+ // src/commands/record.ts
1936
+ var recordCommand = new Command14("record").description("Record git activity and generate a plan from commits");
1937
+ recordCommand.command("start").description("Start recording \u2014 saves current HEAD as the starting point").action(async () => {
1938
+ try {
1939
+ logger.blank();
1940
+ const sha = await startRecordingAsync();
1941
+ logger.success(`Recording started at ${sha.slice(0, 7)}`);
1942
+ logger.dim("Work normally. When done, run `planmode record stop` to generate a plan.");
1943
+ logger.blank();
1944
+ } catch (err) {
1945
+ logger.error(err.message);
1946
+ process.exit(1);
1947
+ }
1948
+ });
1949
+ recordCommand.command("stop").description("Stop recording and generate a plan from commits since start").option("--name <name>", "Package name (auto-inferred if not provided)").option("--author <author>", "Author GitHub username").option("--dir <dir>", "Output directory for the generated package (default: current directory)").action(async (options) => {
1950
+ try {
1951
+ logger.blank();
1952
+ logger.info("Analyzing commits...");
1953
+ const result = await stopRecording(process.cwd(), {
1954
+ name: options.name,
1955
+ author: options.author
1956
+ });
1957
+ const outDir = options.dir ?? process.cwd();
1958
+ fs12.mkdirSync(outDir, { recursive: true });
1959
+ fs12.writeFileSync(path12.join(outDir, "planmode.yaml"), result.manifestContent, "utf-8");
1960
+ fs12.writeFileSync(path12.join(outDir, "plan.md"), result.planContent, "utf-8");
1961
+ logger.success(`Generated plan from ${result.totalCommits} commit(s) (${result.totalFilesChanged} files changed)`);
1962
+ logger.blank();
1963
+ for (let i = 0; i < result.steps.length; i++) {
1964
+ const step = result.steps[i];
1965
+ logger.dim(` ${i + 1}. ${step.title} (${step.filesChanged.length} files)`);
1966
+ }
1967
+ logger.blank();
1968
+ logger.success("Created planmode.yaml and plan.md");
1969
+ logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
1970
+ logger.blank();
1971
+ } catch (err) {
1972
+ logger.error(err.message);
1973
+ process.exit(1);
1974
+ }
1975
+ });
1976
+ recordCommand.command("status").description("Check if a recording is in progress").action(() => {
1977
+ if (isRecording()) {
1978
+ logger.info("Recording is in progress. Run `planmode record stop` to generate a plan.");
1979
+ } else {
1980
+ logger.info("No recording in progress.");
1981
+ }
1982
+ });
1983
+
1984
+ // src/commands/snapshot.ts
1985
+ import { Command as Command15 } from "commander";
1986
+ import fs14 from "fs";
1987
+ import path14 from "path";
1988
+
1989
+ // src/lib/snapshot.ts
1990
+ import fs13 from "fs";
1991
+ import path13 from "path";
1992
+ import { stringify as stringify5 } from "yaml";
1993
+ var CONFIG_FILES = {
1994
+ "tsconfig.json": "TypeScript",
1995
+ "tsconfig.base.json": "TypeScript (base)",
1996
+ ".eslintrc": "ESLint",
1997
+ ".eslintrc.js": "ESLint",
1998
+ ".eslintrc.json": "ESLint",
1999
+ "eslint.config.js": "ESLint (flat config)",
2000
+ "eslint.config.mjs": "ESLint (flat config)",
2001
+ ".prettierrc": "Prettier",
2002
+ ".prettierrc.json": "Prettier",
2003
+ "prettier.config.js": "Prettier",
2004
+ "tailwind.config.js": "Tailwind CSS",
2005
+ "tailwind.config.ts": "Tailwind CSS",
2006
+ "tailwind.config.mjs": "Tailwind CSS",
2007
+ "postcss.config.js": "PostCSS",
2008
+ "postcss.config.mjs": "PostCSS",
2009
+ "next.config.js": "Next.js",
2010
+ "next.config.mjs": "Next.js",
2011
+ "next.config.ts": "Next.js",
2012
+ "vite.config.ts": "Vite",
2013
+ "vite.config.js": "Vite",
2014
+ "astro.config.mjs": "Astro",
2015
+ "astro.config.ts": "Astro",
2016
+ "svelte.config.js": "SvelteKit",
2017
+ "nuxt.config.ts": "Nuxt",
2018
+ "remix.config.js": "Remix",
2019
+ "webpack.config.js": "Webpack",
2020
+ "rollup.config.js": "Rollup",
2021
+ "vitest.config.ts": "Vitest",
2022
+ "jest.config.js": "Jest",
2023
+ "jest.config.ts": "Jest",
2024
+ "docker-compose.yml": "Docker Compose",
2025
+ "docker-compose.yaml": "Docker Compose",
2026
+ "Dockerfile": "Docker",
2027
+ ".dockerignore": "Docker",
2028
+ "prisma/schema.prisma": "Prisma",
2029
+ "drizzle.config.ts": "Drizzle ORM",
2030
+ ".env.example": "Environment variables",
2031
+ ".github/workflows": "GitHub Actions",
2032
+ "vercel.json": "Vercel",
2033
+ "netlify.toml": "Netlify",
2034
+ "wrangler.toml": "Cloudflare Workers",
2035
+ "fly.toml": "Fly.io"
2036
+ };
2037
+ function takeSnapshot(projectDir = process.cwd(), options = {}) {
2038
+ const data = analyzeProject(projectDir);
2039
+ if (options.name) {
2040
+ data.name = options.name;
2041
+ }
2042
+ const planContent = generatePlanFromSnapshot(data);
2043
+ const manifestContent = generateManifestFromSnapshot(data, options.author || "");
2044
+ return { planContent, manifestContent, data };
2045
+ }
2046
+ function analyzeProject(projectDir) {
2047
+ let name = path13.basename(projectDir) + "-setup";
2048
+ const dependencies = {};
2049
+ const devDependencies = {};
2050
+ const scripts = {};
2051
+ const pkgPath = path13.join(projectDir, "package.json");
2052
+ if (fs13.existsSync(pkgPath)) {
2053
+ try {
2054
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
2055
+ if (pkg.name) name = pkg.name + "-setup";
2056
+ if (pkg.dependencies) Object.assign(dependencies, pkg.dependencies);
2057
+ if (pkg.devDependencies) Object.assign(devDependencies, pkg.devDependencies);
2058
+ if (pkg.scripts) Object.assign(scripts, pkg.scripts);
2059
+ } catch {
2060
+ }
2061
+ }
2062
+ const detectedTools = [];
2063
+ for (const [file, toolName] of Object.entries(CONFIG_FILES)) {
2064
+ const fullPath = path13.join(projectDir, file);
2065
+ if (fs13.existsSync(fullPath)) {
2066
+ detectedTools.push({ name: toolName, file });
2067
+ }
2068
+ }
2069
+ const structure = getDirectoryStructure(projectDir, 2);
2070
+ const framework = detectFramework(dependencies, devDependencies);
2071
+ name = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2072
+ return { name, dependencies, devDependencies, detectedTools, structure, scripts, framework };
2073
+ }
2074
+ function detectFramework(deps, devDeps) {
2075
+ const all = { ...deps, ...devDeps };
2076
+ if (all["next"]) return "Next.js";
2077
+ if (all["astro"]) return "Astro";
2078
+ if (all["@sveltejs/kit"]) return "SvelteKit";
2079
+ if (all["nuxt"]) return "Nuxt";
2080
+ if (all["@remix-run/react"]) return "Remix";
2081
+ if (all["vue"]) return "Vue";
2082
+ if (all["react"]) return "React";
2083
+ if (all["express"]) return "Express";
2084
+ if (all["fastify"]) return "Fastify";
2085
+ if (all["hono"]) return "Hono";
2086
+ return null;
2087
+ }
2088
+ function getDirectoryStructure(dir, maxDepth, depth = 0) {
2089
+ const SKIP = /* @__PURE__ */ new Set([
2090
+ "node_modules",
2091
+ "dist",
2092
+ ".git",
2093
+ ".next",
2094
+ ".nuxt",
2095
+ ".svelte-kit",
2096
+ ".astro",
2097
+ ".vercel",
2098
+ ".netlify",
2099
+ "build",
2100
+ "coverage",
2101
+ "__pycache__",
2102
+ ".turbo",
2103
+ ".cache"
2104
+ ]);
2105
+ const results = [];
2106
+ try {
2107
+ const entries = fs13.readdirSync(dir, { withFileTypes: true });
2108
+ for (const entry of entries) {
2109
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
2110
+ if (SKIP.has(entry.name)) continue;
2111
+ const indent = " ".repeat(depth);
2112
+ if (entry.isDirectory()) {
2113
+ results.push(`${indent}${entry.name}/`);
2114
+ if (depth < maxDepth) {
2115
+ results.push(...getDirectoryStructure(path13.join(dir, entry.name), maxDepth, depth + 1));
2116
+ }
2117
+ } else {
2118
+ results.push(`${indent}${entry.name}`);
2119
+ }
2120
+ }
2121
+ } catch {
2122
+ }
2123
+ return results;
2124
+ }
2125
+ function generatePlanFromSnapshot(data) {
2126
+ const lines = [];
2127
+ lines.push(`# ${data.name}`);
2128
+ lines.push("");
2129
+ if (data.framework) {
2130
+ lines.push(`Set up a ${data.framework} project with the following tools and configuration.`);
2131
+ } else {
2132
+ lines.push("Set up a project with the following tools and configuration.");
2133
+ }
2134
+ lines.push("");
2135
+ lines.push("## Prerequisites");
2136
+ lines.push("");
2137
+ lines.push("- Node.js 20+");
2138
+ if (Object.keys(data.dependencies).length > 0 || Object.keys(data.devDependencies).length > 0) {
2139
+ lines.push("- npm or your preferred package manager");
2140
+ }
2141
+ const toolNames = [...new Set(data.detectedTools.map((t) => t.name))];
2142
+ if (toolNames.includes("Docker") || toolNames.includes("Docker Compose")) {
2143
+ lines.push("- Docker");
2144
+ }
2145
+ if (toolNames.includes("Prisma")) {
2146
+ lines.push("- A PostgreSQL database (or update the Prisma schema for your database)");
2147
+ }
2148
+ lines.push("");
2149
+ lines.push("## Steps");
2150
+ lines.push("");
2151
+ let stepNum = 1;
2152
+ if (data.framework) {
2153
+ lines.push(`### ${stepNum}. Create ${data.framework} project`);
2154
+ lines.push("");
2155
+ lines.push(`Initialize a new ${data.framework} project.`);
2156
+ lines.push("");
2157
+ stepNum++;
2158
+ }
2159
+ const depNames = Object.keys(data.dependencies);
2160
+ const devDepNames = Object.keys(data.devDependencies);
2161
+ if (depNames.length > 0) {
2162
+ lines.push(`### ${stepNum}. Install dependencies`);
2163
+ lines.push("");
2164
+ lines.push("```bash");
2165
+ lines.push(`npm install ${depNames.join(" ")}`);
2166
+ lines.push("```");
2167
+ lines.push("");
2168
+ stepNum++;
2169
+ }
2170
+ if (devDepNames.length > 0) {
2171
+ lines.push(`### ${stepNum}. Install dev dependencies`);
2172
+ lines.push("");
2173
+ lines.push("```bash");
2174
+ lines.push(`npm install -D ${devDepNames.join(" ")}`);
2175
+ lines.push("```");
2176
+ lines.push("");
2177
+ stepNum++;
2178
+ }
2179
+ for (const tool of data.detectedTools) {
2180
+ if (tool.name === data.framework) continue;
2181
+ if (tool.name === "Environment variables") continue;
2182
+ lines.push(`### ${stepNum}. Configure ${tool.name}`);
2183
+ lines.push("");
2184
+ lines.push(`Create or update \`${tool.file}\` with the appropriate configuration.`);
2185
+ lines.push("");
2186
+ stepNum++;
2187
+ }
2188
+ if (data.detectedTools.some((t) => t.name === "Environment variables")) {
2189
+ lines.push(`### ${stepNum}. Set up environment variables`);
2190
+ lines.push("");
2191
+ lines.push("Copy `.env.example` to `.env` and fill in the values:");
2192
+ lines.push("");
2193
+ lines.push("```bash");
2194
+ lines.push("cp .env.example .env");
2195
+ lines.push("```");
2196
+ lines.push("");
2197
+ stepNum++;
2198
+ }
2199
+ if (Object.keys(data.scripts).length > 0) {
2200
+ lines.push(`### ${stepNum}. Available scripts`);
2201
+ lines.push("");
2202
+ for (const [name, cmd] of Object.entries(data.scripts)) {
2203
+ lines.push(`- \`npm run ${name}\` \u2014 \`${cmd}\``);
2204
+ }
2205
+ lines.push("");
2206
+ stepNum++;
2207
+ }
2208
+ if (data.structure.length > 0) {
2209
+ lines.push("## Project Structure");
2210
+ lines.push("");
2211
+ lines.push("```");
2212
+ for (const line of data.structure.slice(0, 40)) {
2213
+ lines.push(line);
2214
+ }
2215
+ if (data.structure.length > 40) {
2216
+ lines.push(" ...");
2217
+ }
2218
+ lines.push("```");
2219
+ lines.push("");
2220
+ }
2221
+ lines.push("## Verification");
2222
+ lines.push("");
2223
+ lines.push("- [ ] All dependencies installed without errors");
2224
+ lines.push("- [ ] Configuration files are in place");
2225
+ if (data.scripts["build"]) lines.push("- [ ] `npm run build` succeeds");
2226
+ if (data.scripts["test"]) lines.push("- [ ] `npm run test` passes");
2227
+ if (data.scripts["dev"]) lines.push("- [ ] `npm run dev` starts without errors");
2228
+ lines.push("");
2229
+ return lines.join("\n");
2230
+ }
2231
+ function generateManifestFromSnapshot(data, author) {
2232
+ const tags = [];
2233
+ if (data.framework) tags.push(data.framework.toLowerCase().replace(/[^a-z0-9]/g, ""));
2234
+ const toolTags = data.detectedTools.map((t) => t.name.toLowerCase().replace(/[^a-z0-9]/g, "-")).filter((t) => t.length > 1);
2235
+ tags.push(...[...new Set(toolTags)].slice(0, 8));
2236
+ const category = detectCategory(data);
2237
+ const manifest = {
2238
+ name: data.name,
2239
+ version: "1.0.0",
2240
+ type: "plan",
2241
+ description: data.framework ? `Set up a ${data.framework} project with ${data.detectedTools.map((t) => t.name).slice(0, 3).join(", ")}` : `Project setup with ${data.detectedTools.map((t) => t.name).slice(0, 3).join(", ")}`,
2242
+ author: author || "unknown",
2243
+ license: "MIT",
2244
+ tags: tags.slice(0, 10),
2245
+ category,
2246
+ content_file: "plan.md"
2247
+ };
2248
+ return stringify5(manifest);
2249
+ }
2250
+ function detectCategory(data) {
2251
+ const all = { ...data.dependencies, ...data.devDependencies };
2252
+ if (all["react"] || all["vue"] || all["svelte"] || all["next"] || all["astro"]) return "frontend";
2253
+ if (all["express"] || all["fastify"] || all["hono"] || all["koa"]) return "backend";
2254
+ if (data.detectedTools.some((t) => t.name === "Docker" || t.name === "Docker Compose")) return "devops";
2255
+ if (all["prisma"] || all["drizzle-orm"] || all["typeorm"]) return "database";
2256
+ return "other";
2257
+ }
2258
+
2259
+ // src/commands/snapshot.ts
2260
+ var snapshotCommand = new Command15("snapshot").description("Analyze the current project and generate a plan that recreates this setup").option("--name <name>", "Package name (auto-inferred from project)").option("--author <author>", "Author GitHub username").option("--dir <dir>", "Output directory for the generated package (default: current directory)").action((options) => {
2261
+ try {
2262
+ logger.blank();
2263
+ logger.info("Analyzing project...");
2264
+ const result = takeSnapshot(process.cwd(), {
2265
+ name: options.name,
2266
+ author: options.author
2267
+ });
2268
+ const outDir = options.dir ?? process.cwd();
2269
+ fs14.mkdirSync(outDir, { recursive: true });
2270
+ fs14.writeFileSync(path14.join(outDir, "planmode.yaml"), result.manifestContent, "utf-8");
2271
+ fs14.writeFileSync(path14.join(outDir, "plan.md"), result.planContent, "utf-8");
2272
+ logger.blank();
2273
+ logger.success(`Snapshot: ${result.data.name}`);
2274
+ if (result.data.framework) {
2275
+ logger.dim(` Framework: ${result.data.framework}`);
2276
+ }
2277
+ logger.dim(` Dependencies: ${Object.keys(result.data.dependencies).length}`);
2278
+ logger.dim(` Dev dependencies: ${Object.keys(result.data.devDependencies).length}`);
2279
+ logger.dim(` Tools detected: ${result.data.detectedTools.map((t) => t.name).join(", ") || "none"}`);
2280
+ logger.blank();
2281
+ logger.success("Created planmode.yaml and plan.md");
2282
+ logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
2283
+ logger.blank();
2284
+ } catch (err) {
2285
+ logger.error(err.message);
2286
+ process.exit(1);
2287
+ }
2288
+ });
2289
+
1305
2290
  // src/index.ts
1306
- var program = new Command11();
1307
- program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.1.5");
2291
+ var program = new Command16();
2292
+ program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.2.1");
1308
2293
  program.addCommand(installCommand);
1309
2294
  program.addCommand(uninstallCommand);
1310
2295
  program.addCommand(searchCommand);
@@ -1315,4 +2300,9 @@ program.addCommand(listCommand);
1315
2300
  program.addCommand(infoCommand);
1316
2301
  program.addCommand(initCommand);
1317
2302
  program.addCommand(loginCommand);
2303
+ program.addCommand(mcpCommand);
2304
+ program.addCommand(doctorCommand);
2305
+ program.addCommand(testCommand);
2306
+ program.addCommand(recordCommand);
2307
+ program.addCommand(snapshotCommand);
1318
2308
  program.parse();