planmode 0.1.5 → 0.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/index.js +1183 -193
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +2435 -0
- package/package.json +6 -3
- package/src/commands/doctor.ts +43 -0
- package/src/commands/init.ts +17 -36
- package/src/commands/mcp.ts +39 -0
- package/src/commands/publish.ts +3 -191
- package/src/commands/record.ts +76 -0
- package/src/commands/snapshot.ts +46 -0
- package/src/commands/test.ts +45 -0
- package/src/index.ts +11 -1
- package/src/lib/doctor.ts +123 -0
- package/src/lib/init.ts +71 -0
- package/src/lib/installer.ts +20 -1
- package/src/lib/logger.ts +74 -11
- package/src/lib/publisher.ts +203 -0
- package/src/lib/recorder.ts +195 -0
- package/src/lib/snapshot.ts +348 -0
- package/src/lib/templates.ts +60 -0
- package/src/lib/tester.ts +162 -0
- package/src/mcp.ts +853 -0
- package/src/types/index.ts +2 -0
- package/tsup.config.ts +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
+
if (capturing) {
|
|
649
|
+
captured.push(msg);
|
|
650
|
+
} else {
|
|
651
|
+
console.log(`${DIM}${msg}${RESET}`);
|
|
652
|
+
}
|
|
599
653
|
},
|
|
600
654
|
bold(msg) {
|
|
601
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
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
|
-
|
|
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 @
|
|
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
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
logger.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
repository:
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1102
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
1103
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
1104
|
+
branch: branchName
|
|
1012
1105
|
})
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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(
|
|
1215
|
-
|
|
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
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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(
|
|
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
|
|
1307
|
-
program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.
|
|
2291
|
+
var program = new Command16();
|
|
2292
|
+
program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.2.0");
|
|
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();
|