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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { simpleGit } from "simple-git";
|
|
4
|
+
import { stringify } from "yaml";
|
|
5
|
+
|
|
6
|
+
const RECORDING_FILE = ".planmode-recording";
|
|
7
|
+
|
|
8
|
+
export interface RecordingStep {
|
|
9
|
+
title: string;
|
|
10
|
+
message: string;
|
|
11
|
+
filesChanged: string[];
|
|
12
|
+
sha: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RecordingResult {
|
|
16
|
+
steps: RecordingStep[];
|
|
17
|
+
planContent: string;
|
|
18
|
+
manifestContent: string;
|
|
19
|
+
totalCommits: number;
|
|
20
|
+
totalFilesChanged: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function startRecording(projectDir: string = process.cwd()): string {
|
|
24
|
+
const git = simpleGit(projectDir);
|
|
25
|
+
// We need to do this synchronously-ish, so we write a marker file
|
|
26
|
+
// The actual SHA will be resolved in the async wrapper
|
|
27
|
+
const recordingPath = path.join(projectDir, RECORDING_FILE);
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(recordingPath)) {
|
|
30
|
+
const existing = fs.readFileSync(recordingPath, "utf-8").trim();
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Recording already in progress (started at ${existing}). Run \`planmode record stop\` first.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return recordingPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function startRecordingAsync(projectDir: string = process.cwd()): Promise<string> {
|
|
40
|
+
const recordingPath = startRecording(projectDir);
|
|
41
|
+
const git = simpleGit(projectDir);
|
|
42
|
+
const log = await git.log({ n: 1 });
|
|
43
|
+
const sha = log.latest?.hash;
|
|
44
|
+
|
|
45
|
+
if (!sha) {
|
|
46
|
+
throw new Error("No commits found in this repository.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(recordingPath, sha, "utf-8");
|
|
50
|
+
return sha;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isRecording(projectDir: string = process.cwd()): boolean {
|
|
54
|
+
return fs.existsSync(path.join(projectDir, RECORDING_FILE));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function stopRecording(
|
|
58
|
+
projectDir: string = process.cwd(),
|
|
59
|
+
options: { name?: string; author?: string } = {},
|
|
60
|
+
): Promise<RecordingResult> {
|
|
61
|
+
const recordingPath = path.join(projectDir, RECORDING_FILE);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(recordingPath)) {
|
|
64
|
+
throw new Error("No recording in progress. Run `planmode record start` first.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startSha = fs.readFileSync(recordingPath, "utf-8").trim();
|
|
68
|
+
const git = simpleGit(projectDir);
|
|
69
|
+
|
|
70
|
+
// Get commits since the start SHA
|
|
71
|
+
const log = await git.log({ from: startSha, to: "HEAD" });
|
|
72
|
+
|
|
73
|
+
if (log.total === 0) {
|
|
74
|
+
fs.unlinkSync(recordingPath);
|
|
75
|
+
throw new Error("No commits since recording started. Nothing to capture.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Process each commit into a step (oldest first)
|
|
79
|
+
const commits = [...log.all].reverse();
|
|
80
|
+
const steps: RecordingStep[] = [];
|
|
81
|
+
const allFilesChanged = new Set<string>();
|
|
82
|
+
|
|
83
|
+
for (const commit of commits) {
|
|
84
|
+
// Get files changed in this commit
|
|
85
|
+
const diff = await git.diffSummary([`${commit.hash}~1`, commit.hash]).catch(() =>
|
|
86
|
+
// First commit in range might not have a parent in range
|
|
87
|
+
git.diffSummary([startSha, commit.hash]),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const filesChanged = diff.files.map((f) => f.file);
|
|
91
|
+
filesChanged.forEach((f) => allFilesChanged.add(f));
|
|
92
|
+
|
|
93
|
+
// Clean up commit message for use as step title
|
|
94
|
+
const firstLine = commit.message.split("\n")[0]!.trim();
|
|
95
|
+
const body = commit.message.split("\n").slice(1).join("\n").trim();
|
|
96
|
+
|
|
97
|
+
steps.push({
|
|
98
|
+
title: firstLine,
|
|
99
|
+
message: body || firstLine,
|
|
100
|
+
filesChanged,
|
|
101
|
+
sha: commit.hash.slice(0, 7),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate plan content
|
|
106
|
+
const planName = options.name || inferPlanName(steps);
|
|
107
|
+
const planContent = generatePlanContent(planName, steps);
|
|
108
|
+
const manifestContent = generateManifest(planName, options.author || "");
|
|
109
|
+
|
|
110
|
+
// Clean up recording file
|
|
111
|
+
fs.unlinkSync(recordingPath);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
steps,
|
|
115
|
+
planContent,
|
|
116
|
+
manifestContent,
|
|
117
|
+
totalCommits: commits.length,
|
|
118
|
+
totalFilesChanged: allFilesChanged.size,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inferPlanName(steps: RecordingStep[]): string {
|
|
123
|
+
// Try to infer a name from the commit messages
|
|
124
|
+
const words = steps
|
|
125
|
+
.map((s) => s.title.toLowerCase())
|
|
126
|
+
.join(" ")
|
|
127
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
128
|
+
.split(/\s+/)
|
|
129
|
+
.filter((w) => w.length > 2 && !["the", "and", "for", "add", "fix", "update", "set"].includes(w));
|
|
130
|
+
|
|
131
|
+
// Take the most common meaningful words
|
|
132
|
+
const counts = new Map<string, number>();
|
|
133
|
+
for (const word of words) {
|
|
134
|
+
counts.set(word, (counts.get(word) || 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const topWords = [...counts.entries()]
|
|
138
|
+
.sort((a, b) => b[1] - a[1])
|
|
139
|
+
.slice(0, 3)
|
|
140
|
+
.map(([w]) => w);
|
|
141
|
+
|
|
142
|
+
return topWords.length > 0 ? topWords.join("-") + "-setup" : "recorded-plan";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function generatePlanContent(name: string, steps: RecordingStep[]): string {
|
|
146
|
+
const lines: string[] = [];
|
|
147
|
+
|
|
148
|
+
lines.push(`# ${name}`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("## Steps");
|
|
151
|
+
lines.push("");
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < steps.length; i++) {
|
|
154
|
+
const step = steps[i]!;
|
|
155
|
+
lines.push(`### ${i + 1}. ${step.title}`);
|
|
156
|
+
lines.push("");
|
|
157
|
+
|
|
158
|
+
if (step.message !== step.title) {
|
|
159
|
+
lines.push(step.message);
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (step.filesChanged.length > 0) {
|
|
164
|
+
lines.push("**Files changed:**");
|
|
165
|
+
for (const file of step.filesChanged) {
|
|
166
|
+
lines.push(`- \`${file}\``);
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push("## Verification");
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push("- [ ] All steps completed successfully");
|
|
175
|
+
lines.push("- [ ] Application builds without errors");
|
|
176
|
+
lines.push("- [ ] Tests pass");
|
|
177
|
+
lines.push("");
|
|
178
|
+
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function generateManifest(name: string, author: string): string {
|
|
183
|
+
const manifest: Record<string, unknown> = {
|
|
184
|
+
name,
|
|
185
|
+
version: "1.0.0",
|
|
186
|
+
type: "plan",
|
|
187
|
+
description: `Plan recorded from git history`,
|
|
188
|
+
author: author || "unknown",
|
|
189
|
+
license: "MIT",
|
|
190
|
+
category: "other",
|
|
191
|
+
content_file: "plan.md",
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return stringify(manifest);
|
|
195
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
|
|
5
|
+
// Config files to detect and describe
|
|
6
|
+
const CONFIG_FILES: Record<string, string> = {
|
|
7
|
+
"tsconfig.json": "TypeScript",
|
|
8
|
+
"tsconfig.base.json": "TypeScript (base)",
|
|
9
|
+
".eslintrc": "ESLint",
|
|
10
|
+
".eslintrc.js": "ESLint",
|
|
11
|
+
".eslintrc.json": "ESLint",
|
|
12
|
+
"eslint.config.js": "ESLint (flat config)",
|
|
13
|
+
"eslint.config.mjs": "ESLint (flat config)",
|
|
14
|
+
".prettierrc": "Prettier",
|
|
15
|
+
".prettierrc.json": "Prettier",
|
|
16
|
+
"prettier.config.js": "Prettier",
|
|
17
|
+
"tailwind.config.js": "Tailwind CSS",
|
|
18
|
+
"tailwind.config.ts": "Tailwind CSS",
|
|
19
|
+
"tailwind.config.mjs": "Tailwind CSS",
|
|
20
|
+
"postcss.config.js": "PostCSS",
|
|
21
|
+
"postcss.config.mjs": "PostCSS",
|
|
22
|
+
"next.config.js": "Next.js",
|
|
23
|
+
"next.config.mjs": "Next.js",
|
|
24
|
+
"next.config.ts": "Next.js",
|
|
25
|
+
"vite.config.ts": "Vite",
|
|
26
|
+
"vite.config.js": "Vite",
|
|
27
|
+
"astro.config.mjs": "Astro",
|
|
28
|
+
"astro.config.ts": "Astro",
|
|
29
|
+
"svelte.config.js": "SvelteKit",
|
|
30
|
+
"nuxt.config.ts": "Nuxt",
|
|
31
|
+
"remix.config.js": "Remix",
|
|
32
|
+
"webpack.config.js": "Webpack",
|
|
33
|
+
"rollup.config.js": "Rollup",
|
|
34
|
+
"vitest.config.ts": "Vitest",
|
|
35
|
+
"jest.config.js": "Jest",
|
|
36
|
+
"jest.config.ts": "Jest",
|
|
37
|
+
"docker-compose.yml": "Docker Compose",
|
|
38
|
+
"docker-compose.yaml": "Docker Compose",
|
|
39
|
+
"Dockerfile": "Docker",
|
|
40
|
+
".dockerignore": "Docker",
|
|
41
|
+
"prisma/schema.prisma": "Prisma",
|
|
42
|
+
"drizzle.config.ts": "Drizzle ORM",
|
|
43
|
+
".env.example": "Environment variables",
|
|
44
|
+
".github/workflows": "GitHub Actions",
|
|
45
|
+
"vercel.json": "Vercel",
|
|
46
|
+
"netlify.toml": "Netlify",
|
|
47
|
+
"wrangler.toml": "Cloudflare Workers",
|
|
48
|
+
"fly.toml": "Fly.io",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
interface SnapshotData {
|
|
52
|
+
name: string;
|
|
53
|
+
dependencies: Record<string, string>;
|
|
54
|
+
devDependencies: Record<string, string>;
|
|
55
|
+
detectedTools: { name: string; file: string }[];
|
|
56
|
+
structure: string[];
|
|
57
|
+
scripts: Record<string, string>;
|
|
58
|
+
framework: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SnapshotResult {
|
|
62
|
+
planContent: string;
|
|
63
|
+
manifestContent: string;
|
|
64
|
+
data: SnapshotData;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function takeSnapshot(
|
|
68
|
+
projectDir: string = process.cwd(),
|
|
69
|
+
options: { name?: string; author?: string } = {},
|
|
70
|
+
): SnapshotResult {
|
|
71
|
+
const data = analyzeProject(projectDir);
|
|
72
|
+
|
|
73
|
+
if (options.name) {
|
|
74
|
+
data.name = options.name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const planContent = generatePlanFromSnapshot(data);
|
|
78
|
+
const manifestContent = generateManifestFromSnapshot(data, options.author || "");
|
|
79
|
+
|
|
80
|
+
return { planContent, manifestContent, data };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function analyzeProject(projectDir: string): SnapshotData {
|
|
84
|
+
let name = path.basename(projectDir) + "-setup";
|
|
85
|
+
const dependencies: Record<string, string> = {};
|
|
86
|
+
const devDependencies: Record<string, string> = {};
|
|
87
|
+
const scripts: Record<string, string> = {};
|
|
88
|
+
|
|
89
|
+
// Read package.json
|
|
90
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
91
|
+
if (fs.existsSync(pkgPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
94
|
+
if (pkg.name) name = pkg.name + "-setup";
|
|
95
|
+
if (pkg.dependencies) Object.assign(dependencies, pkg.dependencies);
|
|
96
|
+
if (pkg.devDependencies) Object.assign(devDependencies, pkg.devDependencies);
|
|
97
|
+
if (pkg.scripts) Object.assign(scripts, pkg.scripts);
|
|
98
|
+
} catch {
|
|
99
|
+
// Skip if invalid JSON
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Detect config files / tools
|
|
104
|
+
const detectedTools: { name: string; file: string }[] = [];
|
|
105
|
+
for (const [file, toolName] of Object.entries(CONFIG_FILES)) {
|
|
106
|
+
const fullPath = path.join(projectDir, file);
|
|
107
|
+
if (fs.existsSync(fullPath)) {
|
|
108
|
+
detectedTools.push({ name: toolName, file });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get directory structure (top 2 levels, skip node_modules/dist/.git)
|
|
113
|
+
const structure = getDirectoryStructure(projectDir, 2);
|
|
114
|
+
|
|
115
|
+
// Detect primary framework
|
|
116
|
+
const framework = detectFramework(dependencies, devDependencies);
|
|
117
|
+
|
|
118
|
+
// Sanitize name for planmode
|
|
119
|
+
name = name
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
122
|
+
.replace(/-+/g, "-")
|
|
123
|
+
.replace(/^-|-$/g, "");
|
|
124
|
+
|
|
125
|
+
return { name, dependencies, devDependencies, detectedTools, structure, scripts, framework };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function detectFramework(
|
|
129
|
+
deps: Record<string, string>,
|
|
130
|
+
devDeps: Record<string, string>,
|
|
131
|
+
): string | null {
|
|
132
|
+
const all = { ...deps, ...devDeps };
|
|
133
|
+
if (all["next"]) return "Next.js";
|
|
134
|
+
if (all["astro"]) return "Astro";
|
|
135
|
+
if (all["@sveltejs/kit"]) return "SvelteKit";
|
|
136
|
+
if (all["nuxt"]) return "Nuxt";
|
|
137
|
+
if (all["@remix-run/react"]) return "Remix";
|
|
138
|
+
if (all["vue"]) return "Vue";
|
|
139
|
+
if (all["react"]) return "React";
|
|
140
|
+
if (all["express"]) return "Express";
|
|
141
|
+
if (all["fastify"]) return "Fastify";
|
|
142
|
+
if (all["hono"]) return "Hono";
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getDirectoryStructure(dir: string, maxDepth: number, depth = 0): string[] {
|
|
147
|
+
const SKIP = new Set([
|
|
148
|
+
"node_modules", "dist", ".git", ".next", ".nuxt", ".svelte-kit",
|
|
149
|
+
".astro", ".vercel", ".netlify", "build", "coverage", "__pycache__",
|
|
150
|
+
".turbo", ".cache",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const results: string[] = [];
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
159
|
+
if (SKIP.has(entry.name)) continue;
|
|
160
|
+
|
|
161
|
+
const indent = " ".repeat(depth);
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
results.push(`${indent}${entry.name}/`);
|
|
164
|
+
if (depth < maxDepth) {
|
|
165
|
+
results.push(...getDirectoryStructure(path.join(dir, entry.name), maxDepth, depth + 1));
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
results.push(`${indent}${entry.name}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Skip unreadable directories
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function generatePlanFromSnapshot(data: SnapshotData): string {
|
|
179
|
+
const lines: string[] = [];
|
|
180
|
+
|
|
181
|
+
lines.push(`# ${data.name}`);
|
|
182
|
+
lines.push("");
|
|
183
|
+
|
|
184
|
+
if (data.framework) {
|
|
185
|
+
lines.push(`Set up a ${data.framework} project with the following tools and configuration.`);
|
|
186
|
+
} else {
|
|
187
|
+
lines.push("Set up a project with the following tools and configuration.");
|
|
188
|
+
}
|
|
189
|
+
lines.push("");
|
|
190
|
+
|
|
191
|
+
// Prerequisites
|
|
192
|
+
lines.push("## Prerequisites");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("- Node.js 20+");
|
|
195
|
+
if (Object.keys(data.dependencies).length > 0 || Object.keys(data.devDependencies).length > 0) {
|
|
196
|
+
lines.push("- npm or your preferred package manager");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const toolNames = [...new Set(data.detectedTools.map((t) => t.name))];
|
|
200
|
+
if (toolNames.includes("Docker") || toolNames.includes("Docker Compose")) {
|
|
201
|
+
lines.push("- Docker");
|
|
202
|
+
}
|
|
203
|
+
if (toolNames.includes("Prisma")) {
|
|
204
|
+
lines.push("- A PostgreSQL database (or update the Prisma schema for your database)");
|
|
205
|
+
}
|
|
206
|
+
lines.push("");
|
|
207
|
+
|
|
208
|
+
// Steps
|
|
209
|
+
lines.push("## Steps");
|
|
210
|
+
lines.push("");
|
|
211
|
+
|
|
212
|
+
let stepNum = 1;
|
|
213
|
+
|
|
214
|
+
// Step: create project
|
|
215
|
+
if (data.framework) {
|
|
216
|
+
lines.push(`### ${stepNum}. Create ${data.framework} project`);
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(`Initialize a new ${data.framework} project.`);
|
|
219
|
+
lines.push("");
|
|
220
|
+
stepNum++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Step: install dependencies
|
|
224
|
+
const depNames = Object.keys(data.dependencies);
|
|
225
|
+
const devDepNames = Object.keys(data.devDependencies);
|
|
226
|
+
|
|
227
|
+
if (depNames.length > 0) {
|
|
228
|
+
lines.push(`### ${stepNum}. Install dependencies`);
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push("```bash");
|
|
231
|
+
lines.push(`npm install ${depNames.join(" ")}`);
|
|
232
|
+
lines.push("```");
|
|
233
|
+
lines.push("");
|
|
234
|
+
stepNum++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (devDepNames.length > 0) {
|
|
238
|
+
lines.push(`### ${stepNum}. Install dev dependencies`);
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push("```bash");
|
|
241
|
+
lines.push(`npm install -D ${devDepNames.join(" ")}`);
|
|
242
|
+
lines.push("```");
|
|
243
|
+
lines.push("");
|
|
244
|
+
stepNum++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Steps: configure each detected tool
|
|
248
|
+
for (const tool of data.detectedTools) {
|
|
249
|
+
// Avoid duplicating framework config if already mentioned
|
|
250
|
+
if (tool.name === data.framework) continue;
|
|
251
|
+
// Skip generic files
|
|
252
|
+
if (tool.name === "Environment variables") continue;
|
|
253
|
+
|
|
254
|
+
lines.push(`### ${stepNum}. Configure ${tool.name}`);
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push(`Create or update \`${tool.file}\` with the appropriate configuration.`);
|
|
257
|
+
lines.push("");
|
|
258
|
+
stepNum++;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Step: environment variables
|
|
262
|
+
if (data.detectedTools.some((t) => t.name === "Environment variables")) {
|
|
263
|
+
lines.push(`### ${stepNum}. Set up environment variables`);
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push("Copy `.env.example` to `.env` and fill in the values:");
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("```bash");
|
|
268
|
+
lines.push("cp .env.example .env");
|
|
269
|
+
lines.push("```");
|
|
270
|
+
lines.push("");
|
|
271
|
+
stepNum++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Step: available scripts
|
|
275
|
+
if (Object.keys(data.scripts).length > 0) {
|
|
276
|
+
lines.push(`### ${stepNum}. Available scripts`);
|
|
277
|
+
lines.push("");
|
|
278
|
+
for (const [name, cmd] of Object.entries(data.scripts)) {
|
|
279
|
+
lines.push(`- \`npm run ${name}\` — \`${cmd}\``);
|
|
280
|
+
}
|
|
281
|
+
lines.push("");
|
|
282
|
+
stepNum++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Project structure
|
|
286
|
+
if (data.structure.length > 0) {
|
|
287
|
+
lines.push("## Project Structure");
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push("```");
|
|
290
|
+
for (const line of data.structure.slice(0, 40)) {
|
|
291
|
+
lines.push(line);
|
|
292
|
+
}
|
|
293
|
+
if (data.structure.length > 40) {
|
|
294
|
+
lines.push(" ...");
|
|
295
|
+
}
|
|
296
|
+
lines.push("```");
|
|
297
|
+
lines.push("");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Verification
|
|
301
|
+
lines.push("## Verification");
|
|
302
|
+
lines.push("");
|
|
303
|
+
lines.push("- [ ] All dependencies installed without errors");
|
|
304
|
+
lines.push("- [ ] Configuration files are in place");
|
|
305
|
+
if (data.scripts["build"]) lines.push("- [ ] `npm run build` succeeds");
|
|
306
|
+
if (data.scripts["test"]) lines.push("- [ ] `npm run test` passes");
|
|
307
|
+
if (data.scripts["dev"]) lines.push("- [ ] `npm run dev` starts without errors");
|
|
308
|
+
lines.push("");
|
|
309
|
+
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function generateManifestFromSnapshot(data: SnapshotData, author: string): string {
|
|
314
|
+
const tags: string[] = [];
|
|
315
|
+
if (data.framework) tags.push(data.framework.toLowerCase().replace(/[^a-z0-9]/g, ""));
|
|
316
|
+
|
|
317
|
+
const toolTags = data.detectedTools
|
|
318
|
+
.map((t) => t.name.toLowerCase().replace(/[^a-z0-9]/g, "-"))
|
|
319
|
+
.filter((t) => t.length > 1);
|
|
320
|
+
tags.push(...[...new Set(toolTags)].slice(0, 8));
|
|
321
|
+
|
|
322
|
+
const category = detectCategory(data);
|
|
323
|
+
|
|
324
|
+
const manifest: Record<string, unknown> = {
|
|
325
|
+
name: data.name,
|
|
326
|
+
version: "1.0.0",
|
|
327
|
+
type: "plan",
|
|
328
|
+
description: data.framework
|
|
329
|
+
? `Set up a ${data.framework} project with ${data.detectedTools.map((t) => t.name).slice(0, 3).join(", ")}`
|
|
330
|
+
: `Project setup with ${data.detectedTools.map((t) => t.name).slice(0, 3).join(", ")}`,
|
|
331
|
+
author: author || "unknown",
|
|
332
|
+
license: "MIT",
|
|
333
|
+
tags: tags.slice(0, 10),
|
|
334
|
+
category,
|
|
335
|
+
content_file: "plan.md",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return stringify(manifest);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function detectCategory(data: SnapshotData): string {
|
|
342
|
+
const all = { ...data.dependencies, ...data.devDependencies };
|
|
343
|
+
if (all["react"] || all["vue"] || all["svelte"] || all["next"] || all["astro"]) return "frontend";
|
|
344
|
+
if (all["express"] || all["fastify"] || all["hono"] || all["koa"]) return "backend";
|
|
345
|
+
if (data.detectedTools.some((t) => t.name === "Docker" || t.name === "Docker Compose")) return "devops";
|
|
346
|
+
if (all["prisma"] || all["drizzle-orm"] || all["typeorm"]) return "database";
|
|
347
|
+
return "other";
|
|
348
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getPlanTemplate(name: string): string {
|
|
2
|
+
return `# ${name}
|
|
3
|
+
|
|
4
|
+
## Prerequisites
|
|
5
|
+
|
|
6
|
+
- List any tools, dependencies, or setup required before starting
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
1. **Step one** — Description of what to do first
|
|
11
|
+
2. **Step two** — Description of what to do next
|
|
12
|
+
3. **Step three** — Description of the final step
|
|
13
|
+
|
|
14
|
+
## Verification
|
|
15
|
+
|
|
16
|
+
- [ ] Verify step one completed successfully
|
|
17
|
+
- [ ] Verify step two completed successfully
|
|
18
|
+
- [ ] Verify the final result works as expected
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getRuleTemplate(name: string): string {
|
|
23
|
+
return `# ${name}
|
|
24
|
+
|
|
25
|
+
## Code Style
|
|
26
|
+
|
|
27
|
+
- Follow consistent naming conventions
|
|
28
|
+
- Keep functions small and focused
|
|
29
|
+
|
|
30
|
+
## Best Practices
|
|
31
|
+
|
|
32
|
+
- Prefer composition over inheritance
|
|
33
|
+
- Write self-documenting code
|
|
34
|
+
|
|
35
|
+
## Avoid
|
|
36
|
+
|
|
37
|
+
- Do not use deprecated APIs
|
|
38
|
+
- Do not ignore error handling
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPromptTemplate(name: string): string {
|
|
43
|
+
return `# ${name}
|
|
44
|
+
|
|
45
|
+
{{description}}
|
|
46
|
+
|
|
47
|
+
## Context
|
|
48
|
+
|
|
49
|
+
Provide any relevant context here.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Requirement one
|
|
54
|
+
- Requirement two
|
|
55
|
+
|
|
56
|
+
## Output Format
|
|
57
|
+
|
|
58
|
+
Describe the expected output format.
|
|
59
|
+
`;
|
|
60
|
+
}
|