pmpt-cli 1.6.0 → 1.7.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/README.md +6 -26
- package/dist/commands/browse.js +1 -1
- package/dist/commands/clone.js +11 -2
- package/dist/commands/import.js +25 -7
- package/dist/commands/init.js +143 -11
- package/dist/commands/plan.js +2 -27
- package/dist/commands/publish.js +11 -0
- package/dist/commands/recover.js +222 -0
- package/dist/index.js +8 -2
- package/dist/lib/clipboard.js +25 -0
- package/dist/lib/pmptFile.js +29 -2
- package/dist/lib/scanner.js +289 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,30 +20,9 @@ No coding required. No complex setup. Just answer 5 questions.
|
|
|
20
20
|
|
|
21
21
|
## Demo
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
┌ pmpt — Let's plan your product!
|
|
27
|
-
│
|
|
28
|
-
◆ What should we call your project?
|
|
29
|
-
│ my-budget-app
|
|
30
|
-
│
|
|
31
|
-
◆ What would you like to build with AI?
|
|
32
|
-
│ A personal budget tracking app for freelancers
|
|
33
|
-
│
|
|
34
|
-
◆ Any additional context AI should know? (optional)
|
|
35
|
-
│ Simple UI, mobile-friendly, works offline
|
|
36
|
-
│
|
|
37
|
-
◆ Key features to include?
|
|
38
|
-
│ Expense tracking; Income categories; Monthly reports; Export to CSV
|
|
39
|
-
│
|
|
40
|
-
◆ Preferred tech stack? (optional)
|
|
41
|
-
│ React, Node.js
|
|
42
|
-
│
|
|
43
|
-
└ Done! AI prompt copied to clipboard.
|
|
44
|
-
|
|
45
|
-
→ Open Claude / ChatGPT / Cursor → Ctrl+V → Start building!
|
|
46
|
-
```
|
|
23
|
+
<img src="./demo.gif" alt="pmpt plan demo" width="600" />
|
|
24
|
+
|
|
25
|
+
> Answer 5 questions → AI prompt auto-generated & copied to clipboard → Paste into Claude Code / Codex / Cursor → Start building!
|
|
47
26
|
|
|
48
27
|
---
|
|
49
28
|
|
|
@@ -69,7 +48,7 @@ pmpt init
|
|
|
69
48
|
# 3. Answer 5 questions → AI prompt auto-generated & copied
|
|
70
49
|
pmpt plan
|
|
71
50
|
|
|
72
|
-
# 4. Paste into Claude /
|
|
51
|
+
# 4. Paste into Claude Code / Codex / Cursor → Build your product!
|
|
73
52
|
|
|
74
53
|
# 5. Save your progress anytime
|
|
75
54
|
pmpt save
|
|
@@ -124,13 +103,14 @@ The generated prompt is **automatically copied to your clipboard**. Just paste i
|
|
|
124
103
|
| `pmpt squash v2 v5` | Merge versions v2–v5 into one |
|
|
125
104
|
| `pmpt export` | Export project as `.pmpt` file |
|
|
126
105
|
| `pmpt import <file>` | Import from `.pmpt` file |
|
|
106
|
+
| `pmpt recover` | Recover damaged pmpt.md via AI-generated prompt |
|
|
127
107
|
|
|
128
108
|
### Platform
|
|
129
109
|
|
|
130
110
|
| Command | Description |
|
|
131
111
|
|---------|-------------|
|
|
132
112
|
| `pmpt login` | Authenticate via GitHub (one-time) |
|
|
133
|
-
| `pmpt publish` | Publish your project
|
|
113
|
+
| `pmpt publish` | Publish your project (requires pmpt.md) |
|
|
134
114
|
| `pmpt edit` | Edit published project metadata (description, tags, category) |
|
|
135
115
|
| `pmpt unpublish` | Remove a published project from pmptwiki |
|
|
136
116
|
| `pmpt clone <slug>` | Clone and reproduce someone's project |
|
package/dist/commands/browse.js
CHANGED
|
@@ -64,7 +64,7 @@ export async function cmdBrowse() {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
if (action === 'url') {
|
|
67
|
-
const url = `https://pmptwiki.com/
|
|
67
|
+
const url = `https://pmptwiki.com/p/${project.slug}`;
|
|
68
68
|
p.log.info(`URL: ${url}`);
|
|
69
69
|
p.log.message(`Download: ${project.downloadUrl}`);
|
|
70
70
|
p.log.message(`\npmpt clone ${project.slug} — clone via terminal`);
|
package/dist/commands/clone.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
2
|
+
import { join, dirname, resolve, sep } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
4
4
|
import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
|
|
5
|
-
import { validatePmptFile } from '../lib/pmptFile.js';
|
|
5
|
+
import { validatePmptFile, isSafeFilename } from '../lib/pmptFile.js';
|
|
6
6
|
import { fetchPmptFile } from '../lib/api.js';
|
|
7
7
|
/**
|
|
8
8
|
* Restore history from .pmpt data (shared with import command)
|
|
@@ -15,7 +15,12 @@ export function restoreHistory(historyDir, history) {
|
|
|
15
15
|
const snapshotDir = join(historyDir, snapshotName);
|
|
16
16
|
mkdirSync(snapshotDir, { recursive: true });
|
|
17
17
|
for (const [filename, content] of Object.entries(version.files)) {
|
|
18
|
+
if (!isSafeFilename(filename))
|
|
19
|
+
continue; // skip unsafe filenames
|
|
18
20
|
const filePath = join(snapshotDir, filename);
|
|
21
|
+
// Double-check resolved path stays within snapshot dir
|
|
22
|
+
if (!resolve(filePath).startsWith(resolve(snapshotDir) + sep))
|
|
23
|
+
continue;
|
|
19
24
|
const fileDir = dirname(filePath);
|
|
20
25
|
if (fileDir !== snapshotDir) {
|
|
21
26
|
mkdirSync(fileDir, { recursive: true });
|
|
@@ -36,7 +41,11 @@ export function restoreHistory(historyDir, history) {
|
|
|
36
41
|
export function restoreDocs(docsDir, docs) {
|
|
37
42
|
mkdirSync(docsDir, { recursive: true });
|
|
38
43
|
for (const [filename, content] of Object.entries(docs)) {
|
|
44
|
+
if (!isSafeFilename(filename))
|
|
45
|
+
continue; // skip unsafe filenames
|
|
39
46
|
const filePath = join(docsDir, filename);
|
|
47
|
+
if (!resolve(filePath).startsWith(resolve(docsDir) + sep))
|
|
48
|
+
continue;
|
|
40
49
|
const fileDir = dirname(filePath);
|
|
41
50
|
if (fileDir !== docsDir) {
|
|
42
51
|
mkdirSync(fileDir, { recursive: true });
|
package/dist/commands/import.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import { resolve, join, dirname } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { resolve, join, dirname, sep } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
|
|
4
4
|
import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
|
|
5
|
-
import { validatePmptFile } from '../lib/pmptFile.js';
|
|
5
|
+
import { validatePmptFile, isSafeFilename } from '../lib/pmptFile.js';
|
|
6
6
|
/**
|
|
7
7
|
* Restore history from .pmpt file
|
|
8
8
|
*/
|
|
@@ -13,9 +13,13 @@ function restoreHistory(historyDir, history) {
|
|
|
13
13
|
const snapshotName = `v${version.version}-${timestamp}`;
|
|
14
14
|
const snapshotDir = join(historyDir, snapshotName);
|
|
15
15
|
mkdirSync(snapshotDir, { recursive: true });
|
|
16
|
-
// Write files
|
|
16
|
+
// Write files (with path traversal protection)
|
|
17
17
|
for (const [filename, content] of Object.entries(version.files)) {
|
|
18
|
+
if (!isSafeFilename(filename))
|
|
19
|
+
continue;
|
|
18
20
|
const filePath = join(snapshotDir, filename);
|
|
21
|
+
if (!resolve(filePath).startsWith(resolve(snapshotDir) + sep))
|
|
22
|
+
continue;
|
|
19
23
|
const fileDir = dirname(filePath);
|
|
20
24
|
if (fileDir !== snapshotDir) {
|
|
21
25
|
mkdirSync(fileDir, { recursive: true });
|
|
@@ -38,7 +42,11 @@ function restoreHistory(historyDir, history) {
|
|
|
38
42
|
function restoreDocs(docsDir, docs) {
|
|
39
43
|
mkdirSync(docsDir, { recursive: true });
|
|
40
44
|
for (const [filename, content] of Object.entries(docs)) {
|
|
45
|
+
if (!isSafeFilename(filename))
|
|
46
|
+
continue;
|
|
41
47
|
const filePath = join(docsDir, filename);
|
|
48
|
+
if (!resolve(filePath).startsWith(resolve(docsDir) + sep))
|
|
49
|
+
continue;
|
|
42
50
|
const fileDir = dirname(filePath);
|
|
43
51
|
if (fileDir !== docsDir) {
|
|
44
52
|
mkdirSync(fileDir, { recursive: true });
|
|
@@ -112,13 +120,23 @@ export async function cmdImport(pmptFile, options) {
|
|
|
112
120
|
}
|
|
113
121
|
const importSpinner = p.spinner();
|
|
114
122
|
importSpinner.start('Importing project...');
|
|
123
|
+
const pmptDir = getConfigDir(projectPath);
|
|
124
|
+
const historyDir = getHistoryDir(projectPath);
|
|
125
|
+
const docsDir = getDocsDir(projectPath);
|
|
126
|
+
// --force: clean existing history and docs before restoring
|
|
127
|
+
if (options?.force && isInitialized(projectPath)) {
|
|
128
|
+
if (existsSync(historyDir))
|
|
129
|
+
rmSync(historyDir, { recursive: true, force: true });
|
|
130
|
+
if (existsSync(docsDir))
|
|
131
|
+
rmSync(docsDir, { recursive: true, force: true });
|
|
132
|
+
const planPath = join(pmptDir, 'plan-progress.json');
|
|
133
|
+
if (existsSync(planPath))
|
|
134
|
+
rmSync(planPath, { force: true });
|
|
135
|
+
}
|
|
115
136
|
// Initialize project if not exists
|
|
116
137
|
if (!isInitialized(projectPath)) {
|
|
117
138
|
initializeProject(projectPath, { trackGit: true });
|
|
118
139
|
}
|
|
119
|
-
const pmptDir = getConfigDir(projectPath);
|
|
120
|
-
const historyDir = getHistoryDir(projectPath);
|
|
121
|
-
const docsDir = getDocsDir(projectPath);
|
|
122
140
|
// Restore history
|
|
123
141
|
restoreHistory(historyDir, pmptData.history);
|
|
124
142
|
// Restore docs
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { initializeProject, isInitialized } from '../lib/config.js';
|
|
5
5
|
import { isGitRepo, getGitInfo, formatGitInfo } from '../lib/git.js';
|
|
6
6
|
import { cmdPlan } from './plan.js';
|
|
7
|
+
import { scanProject, scanResultToAnswers } from '../lib/scanner.js';
|
|
8
|
+
import { savePlanDocuments, initPlanProgress, savePlanProgress } from '../lib/plan.js';
|
|
9
|
+
import { copyToClipboard } from '../lib/clipboard.js';
|
|
7
10
|
export async function cmdInit(path, options) {
|
|
8
11
|
p.intro('pmpt init');
|
|
9
12
|
const projectPath = path ? resolve(path) : process.cwd();
|
|
@@ -12,7 +15,16 @@ export async function cmdInit(path, options) {
|
|
|
12
15
|
process.exit(1);
|
|
13
16
|
}
|
|
14
17
|
if (isInitialized(projectPath)) {
|
|
15
|
-
p.
|
|
18
|
+
p.log.warn('Project already initialized.');
|
|
19
|
+
p.log.message('');
|
|
20
|
+
p.log.info('Available commands:');
|
|
21
|
+
p.log.message(' pmpt plan — Generate or view AI prompt');
|
|
22
|
+
p.log.message(' pmpt save — Save a snapshot');
|
|
23
|
+
p.log.message(' pmpt watch — Auto-save on file changes');
|
|
24
|
+
p.log.message(' pmpt history — View version history');
|
|
25
|
+
p.log.message('');
|
|
26
|
+
p.log.message('To reinitialize, remove .pmpt/ and run `pmpt init` again.');
|
|
27
|
+
p.outro('');
|
|
16
28
|
process.exit(0);
|
|
17
29
|
}
|
|
18
30
|
// Detect Git repository
|
|
@@ -102,17 +114,137 @@ export async function cmdInit(path, options) {
|
|
|
102
114
|
notes.push(' pmpt watch # Auto-save on changes');
|
|
103
115
|
notes.push(' pmpt history # View versions');
|
|
104
116
|
p.note(notes.join('\n'), 'Project Info');
|
|
105
|
-
//
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// Scan for existing project
|
|
118
|
+
const scanResult = scanProject(projectPath);
|
|
119
|
+
if (scanResult.isExistingProject) {
|
|
120
|
+
// Show scan summary
|
|
121
|
+
const scanNotes = [];
|
|
122
|
+
if (scanResult.packageInfo) {
|
|
123
|
+
scanNotes.push(`Package: ${scanResult.packageInfo.name}`);
|
|
124
|
+
if (scanResult.packageInfo.description) {
|
|
125
|
+
scanNotes.push(`Description: ${scanResult.packageInfo.description}`);
|
|
126
|
+
}
|
|
127
|
+
scanNotes.push(`Dependencies: ${scanResult.packageInfo.dependencies.length} production, ${scanResult.packageInfo.devDependencies.length} dev`);
|
|
128
|
+
}
|
|
129
|
+
if (scanResult.detectedFramework) {
|
|
130
|
+
scanNotes.push(`Framework: ${scanResult.detectedFramework}`);
|
|
131
|
+
}
|
|
132
|
+
if (scanResult.directoryStructure.length > 0) {
|
|
133
|
+
scanNotes.push(`Structure: ${scanResult.directoryStructure.map((d) => d + '/').join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
if (scanResult.gitSummary) {
|
|
136
|
+
const gs = scanResult.gitSummary;
|
|
137
|
+
let gitLine = `Git: ${gs.totalCommits} commits`;
|
|
138
|
+
if (gs.firstCommitDate) {
|
|
139
|
+
gitLine += ` since ${gs.firstCommitDate.split('T')[0]}`;
|
|
140
|
+
}
|
|
141
|
+
gitLine += `, ${gs.contributors} contributor(s)`;
|
|
142
|
+
scanNotes.push(gitLine);
|
|
143
|
+
}
|
|
144
|
+
p.note(scanNotes.join('\n'), 'Existing Project Detected');
|
|
145
|
+
const scanChoice = await p.select({
|
|
146
|
+
message: 'Auto-generate plan from detected project files?',
|
|
147
|
+
options: [
|
|
148
|
+
{ value: 'auto', label: 'Auto-generate plan', hint: 'Recommended — instant AI prompt from project analysis' },
|
|
149
|
+
{ value: 'manual', label: 'Manual planning', hint: '5 questions interactive flow' },
|
|
150
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
if (p.isCancel(scanChoice)) {
|
|
154
|
+
p.cancel('Cancelled');
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
if (scanChoice === 'auto') {
|
|
158
|
+
// Ask for project description
|
|
159
|
+
const defaultDesc = scanResult.readmeDescription
|
|
160
|
+
|| scanResult.packageInfo?.description
|
|
161
|
+
|| '';
|
|
162
|
+
const userDesc = await p.text({
|
|
163
|
+
message: 'Briefly describe what this project does:',
|
|
164
|
+
defaultValue: defaultDesc || undefined,
|
|
165
|
+
placeholder: defaultDesc || 'e.g., A web app for sharing AI project histories',
|
|
166
|
+
});
|
|
167
|
+
if (p.isCancel(userDesc)) {
|
|
168
|
+
p.cancel('Cancelled');
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
const s2 = p.spinner();
|
|
172
|
+
s2.start('Scanning project and generating plan...');
|
|
173
|
+
const answers = scanResultToAnswers(scanResult, userDesc);
|
|
174
|
+
const { planPath, promptPath } = savePlanDocuments(projectPath, answers);
|
|
175
|
+
const progress = initPlanProgress(projectPath);
|
|
176
|
+
progress.completed = true;
|
|
177
|
+
progress.answers = answers;
|
|
178
|
+
savePlanProgress(projectPath, progress);
|
|
179
|
+
s2.stop('Plan generated!');
|
|
180
|
+
p.log.message('');
|
|
181
|
+
p.log.success('Two documents have been created:');
|
|
182
|
+
p.log.message('');
|
|
183
|
+
const docExplanation = [
|
|
184
|
+
`1. plan.md — Your product overview`,
|
|
185
|
+
` Location: ${planPath}`,
|
|
186
|
+
'',
|
|
187
|
+
`2. pmpt.md — AI prompt (THE IMPORTANT ONE!)`,
|
|
188
|
+
` Copy this to Claude Code / Codex / Cursor`,
|
|
189
|
+
` Location: ${promptPath}`,
|
|
190
|
+
];
|
|
191
|
+
p.note(docExplanation.join('\n'), 'Auto-generated from project scan');
|
|
192
|
+
// Copy to clipboard
|
|
193
|
+
const content = readFileSync(promptPath, 'utf-8');
|
|
194
|
+
const copied = copyToClipboard(content);
|
|
195
|
+
if (copied) {
|
|
196
|
+
p.log.message('');
|
|
197
|
+
p.log.success('AI prompt copied to clipboard!');
|
|
198
|
+
p.log.message('');
|
|
199
|
+
const banner = [
|
|
200
|
+
'',
|
|
201
|
+
'┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓',
|
|
202
|
+
'┃ ┃',
|
|
203
|
+
'┃ 📋 NEXT STEP ┃',
|
|
204
|
+
'┃ ┃',
|
|
205
|
+
'┃ Open your AI coding tool and press: ┃',
|
|
206
|
+
'┃ ┃',
|
|
207
|
+
'┃ ⌘ + V (Mac) ┃',
|
|
208
|
+
'┃ Ctrl + V (Windows/Linux) ┃',
|
|
209
|
+
'┃ ┃',
|
|
210
|
+
'┃ Your project context is ready! 🚀 ┃',
|
|
211
|
+
'┃ ┃',
|
|
212
|
+
'┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
|
|
213
|
+
'',
|
|
214
|
+
];
|
|
215
|
+
console.log(banner.join('\n'));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
p.log.warn('Could not copy to clipboard.');
|
|
219
|
+
p.log.info(`Read it at: ${promptPath}`);
|
|
220
|
+
}
|
|
221
|
+
p.log.info('Tips:');
|
|
222
|
+
p.log.message(' pmpt plan — View or edit your AI prompt');
|
|
223
|
+
p.log.message(' pmpt save — Save a snapshot anytime');
|
|
224
|
+
p.log.message(' pmpt watch — Auto-save on file changes');
|
|
225
|
+
p.outro('Ready to go!');
|
|
226
|
+
}
|
|
227
|
+
else if (scanChoice === 'manual') {
|
|
228
|
+
p.log.message('');
|
|
229
|
+
await cmdPlan(projectPath);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
p.outro('Ready! Run `pmpt plan` when you want to start.');
|
|
233
|
+
}
|
|
113
234
|
}
|
|
114
235
|
else {
|
|
115
|
-
|
|
236
|
+
// New/empty project — original flow
|
|
237
|
+
const startPlan = await p.confirm({
|
|
238
|
+
message: 'Start planning? (Generate AI prompt with 5 quick questions)',
|
|
239
|
+
initialValue: true,
|
|
240
|
+
});
|
|
241
|
+
if (!p.isCancel(startPlan) && startPlan) {
|
|
242
|
+
p.log.message('');
|
|
243
|
+
await cmdPlan(projectPath);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
p.outro('Ready! Run `pmpt plan` when you want to start.');
|
|
247
|
+
}
|
|
116
248
|
}
|
|
117
249
|
}
|
|
118
250
|
catch (error) {
|
package/dist/commands/plan.js
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
4
|
import { isInitialized } from '../lib/config.js';
|
|
5
|
+
import { copyToClipboard } from '../lib/clipboard.js';
|
|
6
6
|
import { cmdWatch } from './watch.js';
|
|
7
7
|
import { PLAN_QUESTIONS, getPlanProgress, initPlanProgress, savePlanProgress, savePlanDocuments, } from '../lib/plan.js';
|
|
8
|
-
// Cross-platform clipboard copy
|
|
9
|
-
function copyToClipboard(text) {
|
|
10
|
-
try {
|
|
11
|
-
const platform = process.platform;
|
|
12
|
-
if (platform === 'darwin') {
|
|
13
|
-
execSync('pbcopy', { input: text });
|
|
14
|
-
}
|
|
15
|
-
else if (platform === 'win32') {
|
|
16
|
-
execSync('clip', { input: text });
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
// Linux - try xclip or xsel
|
|
20
|
-
try {
|
|
21
|
-
execSync('xclip -selection clipboard', { input: text });
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
execSync('xsel --clipboard --input', { input: text });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
8
|
export async function cmdPlan(path, options) {
|
|
34
9
|
const projectPath = path ? resolve(path) : process.cwd();
|
|
35
10
|
// Check initialization
|
|
@@ -185,7 +160,7 @@ export async function cmdPlan(path, options) {
|
|
|
185
160
|
` Location: ${planPath}`,
|
|
186
161
|
'',
|
|
187
162
|
`2. pmpt.md — AI prompt (THE IMPORTANT ONE!)`,
|
|
188
|
-
` • Copy this to Claude/
|
|
163
|
+
` • Copy this to Claude Code / Codex / Cursor`,
|
|
189
164
|
` • AI will help you build step by step`,
|
|
190
165
|
` • AI will update this doc as you progress`,
|
|
191
166
|
` Location: ${promptPath}`,
|
package/dist/commands/publish.js
CHANGED
|
@@ -42,6 +42,17 @@ export async function cmdPublish(path) {
|
|
|
42
42
|
p.outro('');
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
// Validate pmpt.md exists and has content
|
|
46
|
+
const pmptMdPath = join(getDocsDir(projectPath), 'pmpt.md');
|
|
47
|
+
if (!existsSync(pmptMdPath)) {
|
|
48
|
+
p.log.error('pmpt.md not found. Run `pmpt plan` to generate it first.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const pmptMdContent = readFileSync(pmptMdPath, 'utf-8').trim();
|
|
52
|
+
if (pmptMdContent.length === 0) {
|
|
53
|
+
p.log.error('pmpt.md is empty. Run `pmpt plan` to generate content.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
45
56
|
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
46
57
|
// Try to load existing published data for prefill
|
|
47
58
|
let existing;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, basename } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { isInitialized, getDocsDir } from '../lib/config.js';
|
|
6
|
+
import { getAllSnapshots, resolveFileContent } from '../lib/history.js';
|
|
7
|
+
import { getPlanProgress } from '../lib/plan.js';
|
|
8
|
+
import { copyToClipboard } from '../lib/clipboard.js';
|
|
9
|
+
export async function cmdRecover(path) {
|
|
10
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
11
|
+
if (!isInitialized(projectPath)) {
|
|
12
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
p.intro('pmpt recover');
|
|
16
|
+
const docsDir = getDocsDir(projectPath);
|
|
17
|
+
const pmptMdPath = join(docsDir, 'pmpt.md');
|
|
18
|
+
const planMdPath = join(docsDir, 'plan.md');
|
|
19
|
+
// Check current state
|
|
20
|
+
const currentExists = existsSync(pmptMdPath);
|
|
21
|
+
const currentContent = currentExists ? readFileSync(pmptMdPath, 'utf-8').trim() : '';
|
|
22
|
+
if (currentExists && currentContent.length > 100) {
|
|
23
|
+
const proceed = await p.confirm({
|
|
24
|
+
message: `pmpt.md exists (${currentContent.length} chars). Overwrite with recovered version?`,
|
|
25
|
+
initialValue: false,
|
|
26
|
+
});
|
|
27
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
28
|
+
p.cancel('Cancelled');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Gather all available context
|
|
33
|
+
const context = [];
|
|
34
|
+
// 1. Plan progress answers
|
|
35
|
+
const planProgress = getPlanProgress(projectPath);
|
|
36
|
+
if (planProgress?.answers) {
|
|
37
|
+
const a = planProgress.answers;
|
|
38
|
+
context.push('## Project Plan Answers');
|
|
39
|
+
if (a.projectName)
|
|
40
|
+
context.push(`- **Project Name**: ${a.projectName}`);
|
|
41
|
+
if (a.productIdea)
|
|
42
|
+
context.push(`- **Product Idea**: ${a.productIdea}`);
|
|
43
|
+
if (a.coreFeatures)
|
|
44
|
+
context.push(`- **Core Features**: ${a.coreFeatures}`);
|
|
45
|
+
if (a.techStack)
|
|
46
|
+
context.push(`- **Tech Stack**: ${a.techStack}`);
|
|
47
|
+
if (a.additionalContext)
|
|
48
|
+
context.push(`- **Additional Context**: ${a.additionalContext}`);
|
|
49
|
+
context.push('');
|
|
50
|
+
}
|
|
51
|
+
// 2. plan.md content
|
|
52
|
+
if (existsSync(planMdPath)) {
|
|
53
|
+
const planMd = readFileSync(planMdPath, 'utf-8').trim();
|
|
54
|
+
if (planMd) {
|
|
55
|
+
context.push('## Current plan.md');
|
|
56
|
+
context.push('```markdown');
|
|
57
|
+
context.push(planMd);
|
|
58
|
+
context.push('```');
|
|
59
|
+
context.push('');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 3. Last known good pmpt.md from history
|
|
63
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
64
|
+
let lastPmptMd = null;
|
|
65
|
+
let lastPmptVersion = 0;
|
|
66
|
+
if (snapshots.length > 0) {
|
|
67
|
+
for (let i = snapshots.length - 1; i >= 0; i--) {
|
|
68
|
+
const content = resolveFileContent(snapshots, i, 'pmpt.md');
|
|
69
|
+
if (content && content.trim().length > 50) {
|
|
70
|
+
lastPmptMd = content;
|
|
71
|
+
lastPmptVersion = snapshots[i].version;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (lastPmptMd) {
|
|
77
|
+
context.push(`## Last Known pmpt.md (from v${lastPmptVersion})`);
|
|
78
|
+
context.push('```markdown');
|
|
79
|
+
context.push(lastPmptMd);
|
|
80
|
+
context.push('```');
|
|
81
|
+
context.push('');
|
|
82
|
+
}
|
|
83
|
+
// 4. package.json info
|
|
84
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
85
|
+
if (existsSync(pkgPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
88
|
+
context.push('## package.json');
|
|
89
|
+
const fields = [];
|
|
90
|
+
if (pkg.name)
|
|
91
|
+
fields.push(`- **name**: ${pkg.name}`);
|
|
92
|
+
if (pkg.description)
|
|
93
|
+
fields.push(`- **description**: ${pkg.description}`);
|
|
94
|
+
if (pkg.dependencies)
|
|
95
|
+
fields.push(`- **dependencies**: ${Object.keys(pkg.dependencies).join(', ')}`);
|
|
96
|
+
if (pkg.devDependencies)
|
|
97
|
+
fields.push(`- **devDependencies**: ${Object.keys(pkg.devDependencies).join(', ')}`);
|
|
98
|
+
if (pkg.scripts)
|
|
99
|
+
fields.push(`- **scripts**: ${Object.keys(pkg.scripts).join(', ')}`);
|
|
100
|
+
context.push(fields.join('\n'));
|
|
101
|
+
context.push('');
|
|
102
|
+
}
|
|
103
|
+
catch { /* skip */ }
|
|
104
|
+
}
|
|
105
|
+
// 5. Directory structure
|
|
106
|
+
const dirEntries = readdirTopLevel(projectPath);
|
|
107
|
+
if (dirEntries.length > 0) {
|
|
108
|
+
context.push('## Project Structure');
|
|
109
|
+
context.push(dirEntries.join('\n'));
|
|
110
|
+
context.push('');
|
|
111
|
+
}
|
|
112
|
+
if (context.length === 0) {
|
|
113
|
+
p.log.error('No project context found. Nothing to recover from.');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
// Show what we found
|
|
117
|
+
const sources = [];
|
|
118
|
+
if (planProgress?.answers)
|
|
119
|
+
sources.push('plan answers');
|
|
120
|
+
if (existsSync(planMdPath))
|
|
121
|
+
sources.push('plan.md');
|
|
122
|
+
if (lastPmptMd)
|
|
123
|
+
sources.push(`history v${lastPmptVersion}`);
|
|
124
|
+
if (existsSync(pkgPath))
|
|
125
|
+
sources.push('package.json');
|
|
126
|
+
p.log.info(`Found context: ${sources.join(', ')}`);
|
|
127
|
+
// Build recovery prompt
|
|
128
|
+
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
129
|
+
const prompt = `# pmpt.md Recovery Request
|
|
130
|
+
|
|
131
|
+
I need you to regenerate the \`.pmpt/docs/pmpt.md\` file for my project "${projectName}".
|
|
132
|
+
|
|
133
|
+
This file is the main AI prompt document — it contains the product development context that gets pasted into AI tools (Claude Code, Codex, Cursor, etc.) to continue development.
|
|
134
|
+
|
|
135
|
+
## Available Context
|
|
136
|
+
|
|
137
|
+
${context.join('\n')}
|
|
138
|
+
|
|
139
|
+
## Instructions
|
|
140
|
+
|
|
141
|
+
Based on the context above, regenerate \`.pmpt/docs/pmpt.md\` with the following structure:
|
|
142
|
+
|
|
143
|
+
\`\`\`
|
|
144
|
+
# {Project Name} — Product Development Request
|
|
145
|
+
|
|
146
|
+
## What I Want to Build
|
|
147
|
+
{describe the product idea}
|
|
148
|
+
|
|
149
|
+
## Key Features
|
|
150
|
+
- {feature 1}
|
|
151
|
+
- {feature 2}
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
## Tech Stack Preferences
|
|
155
|
+
{detected or specified tech stack}
|
|
156
|
+
|
|
157
|
+
## Additional Context
|
|
158
|
+
{any extra context}
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
Please help me build this product based on the requirements above.
|
|
163
|
+
|
|
164
|
+
1. First, review the requirements and ask if anything is unclear.
|
|
165
|
+
2. Propose a technical architecture.
|
|
166
|
+
3. Outline the implementation steps.
|
|
167
|
+
4. Start coding from the first step.
|
|
168
|
+
|
|
169
|
+
I'll confirm progress at each step before moving to the next.
|
|
170
|
+
|
|
171
|
+
## Documentation Rule
|
|
172
|
+
|
|
173
|
+
**Important:** Update this document (located at \`.pmpt/docs/pmpt.md\`) at these moments:
|
|
174
|
+
- When architecture or tech decisions are finalized
|
|
175
|
+
- When a feature is implemented (mark as done)
|
|
176
|
+
- When a development phase is completed
|
|
177
|
+
- When requirements change or new decisions are made
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
${lastPmptMd ? 'Use the "Last Known pmpt.md" as the primary reference — update it rather than starting from scratch.' : 'Generate a fresh pmpt.md based on the available context.'}
|
|
181
|
+
|
|
182
|
+
Write the content directly to \`.pmpt/docs/pmpt.md\`. After writing, run \`pmpt save\` to create a snapshot.`;
|
|
183
|
+
// Copy to clipboard
|
|
184
|
+
const copied = copyToClipboard(prompt);
|
|
185
|
+
if (copied) {
|
|
186
|
+
p.log.success('Recovery prompt copied to clipboard!');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
p.log.warn('Could not copy to clipboard. Prompt printed below:');
|
|
190
|
+
console.log('\n' + prompt + '\n');
|
|
191
|
+
}
|
|
192
|
+
p.note([
|
|
193
|
+
'1. Paste the prompt into your AI tool (Claude Code, Cursor, etc.)',
|
|
194
|
+
'2. The AI will regenerate .pmpt/docs/pmpt.md',
|
|
195
|
+
'3. Run `pmpt save` to snapshot the recovered file',
|
|
196
|
+
].join('\n'), 'Next Steps');
|
|
197
|
+
p.outro('');
|
|
198
|
+
}
|
|
199
|
+
function readdirTopLevel(projectPath) {
|
|
200
|
+
const ignore = new Set([
|
|
201
|
+
'node_modules', '.git', '.pmpt', 'dist', 'build', '.next',
|
|
202
|
+
'.nuxt', '.astro', '.vercel', '.cache', 'coverage', '.turbo',
|
|
203
|
+
]);
|
|
204
|
+
try {
|
|
205
|
+
const entries = readdirSync(projectPath, { encoding: 'utf-8' });
|
|
206
|
+
return entries
|
|
207
|
+
.filter(e => !ignore.has(e) && !e.startsWith('.'))
|
|
208
|
+
.slice(0, 30)
|
|
209
|
+
.map(e => {
|
|
210
|
+
try {
|
|
211
|
+
const stat = statSync(join(projectPath, e));
|
|
212
|
+
return stat.isDirectory() ? `${e}/` : e;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return e;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdEdit } from './commands/edit.js';
|
|
|
15
15
|
import { cmdUnpublish } from './commands/unpublish.js';
|
|
16
16
|
import { cmdClone } from './commands/clone.js';
|
|
17
17
|
import { cmdBrowse } from './commands/browse.js';
|
|
18
|
+
import { cmdRecover } from './commands/recover.js';
|
|
18
19
|
const program = new Command();
|
|
19
20
|
program
|
|
20
21
|
.name('pmpt')
|
|
@@ -34,6 +35,7 @@ Examples:
|
|
|
34
35
|
$ pmpt publish Publish project to pmptwiki
|
|
35
36
|
$ pmpt clone <slug> Clone a project from pmptwiki
|
|
36
37
|
$ pmpt browse Browse published projects
|
|
38
|
+
$ pmpt recover Recover damaged pmpt.md via AI
|
|
37
39
|
|
|
38
40
|
Documentation: https://pmptwiki.com
|
|
39
41
|
`);
|
|
@@ -66,12 +68,12 @@ program
|
|
|
66
68
|
.action(cmdSquash);
|
|
67
69
|
program
|
|
68
70
|
.command('export [path]')
|
|
69
|
-
.description('Export project history as a shareable
|
|
71
|
+
.description('Export project history as a shareable .pmpt file')
|
|
70
72
|
.option('-o, --output <file>', 'Output file path')
|
|
71
73
|
.action(cmdExport);
|
|
72
74
|
program
|
|
73
75
|
.command('import <file>')
|
|
74
|
-
.description('Import project from
|
|
76
|
+
.description('Import project from .pmpt file')
|
|
75
77
|
.option('-f, --force', 'Overwrite existing project')
|
|
76
78
|
.action(cmdImport);
|
|
77
79
|
program
|
|
@@ -112,4 +114,8 @@ program
|
|
|
112
114
|
.command('browse')
|
|
113
115
|
.description('Browse and search published projects')
|
|
114
116
|
.action(cmdBrowse);
|
|
117
|
+
program
|
|
118
|
+
.command('recover [path]')
|
|
119
|
+
.description('Generate a recovery prompt to regenerate pmpt.md via AI')
|
|
120
|
+
.action(cmdRecover);
|
|
115
121
|
program.parse();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
export function copyToClipboard(text) {
|
|
3
|
+
try {
|
|
4
|
+
const platform = process.platform;
|
|
5
|
+
if (platform === 'darwin') {
|
|
6
|
+
execSync('pbcopy', { input: text });
|
|
7
|
+
}
|
|
8
|
+
else if (platform === 'win32') {
|
|
9
|
+
execSync('clip', { input: text });
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
// Linux - try xclip or xsel
|
|
13
|
+
try {
|
|
14
|
+
execSync('xclip -selection clipboard', { input: text });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
execSync('xsel --clipboard --input', { input: text });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/lib/pmptFile.js
CHANGED
|
@@ -4,9 +4,36 @@
|
|
|
4
4
|
* Single JSON file format for sharing pmpt projects.
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { resolve, relative } from 'path';
|
|
7
8
|
// Schema version
|
|
8
9
|
export const SCHEMA_VERSION = '1.0';
|
|
9
10
|
export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
11
|
+
/**
|
|
12
|
+
* Validate that a filename is safe (no path traversal).
|
|
13
|
+
* Rejects absolute paths, ".." components, and backslash paths.
|
|
14
|
+
*/
|
|
15
|
+
export function isSafeFilename(filename) {
|
|
16
|
+
if (!filename || filename.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
// Reject absolute paths and backslashes
|
|
19
|
+
if (/^[/\\]/.test(filename) || /\\/.test(filename))
|
|
20
|
+
return false;
|
|
21
|
+
// Reject .. path components
|
|
22
|
+
const parts = filename.split('/');
|
|
23
|
+
if (parts.some(p => p === '..' || p === '.'))
|
|
24
|
+
return false;
|
|
25
|
+
// Double check: resolve and verify it stays within parent
|
|
26
|
+
const base = '/safe';
|
|
27
|
+
const resolved = resolve(base, filename);
|
|
28
|
+
const rel = relative(base, resolved);
|
|
29
|
+
if (rel.startsWith('..') || resolve(rel) !== resolve(base, rel))
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Safe filename zod refinement
|
|
34
|
+
const safeFilenameKey = z.string().refine(isSafeFilename, {
|
|
35
|
+
message: 'Unsafe filename detected (path traversal)',
|
|
36
|
+
});
|
|
10
37
|
// Git info schema
|
|
11
38
|
const GitInfoSchema = z.object({
|
|
12
39
|
commit: z.string(),
|
|
@@ -20,7 +47,7 @@ const VersionSchema = z.object({
|
|
|
20
47
|
version: z.number().min(1),
|
|
21
48
|
timestamp: z.string(),
|
|
22
49
|
summary: z.string().optional(),
|
|
23
|
-
files: z.record(
|
|
50
|
+
files: z.record(safeFilenameKey, z.string()), // filename -> content
|
|
24
51
|
git: GitInfoSchema,
|
|
25
52
|
});
|
|
26
53
|
// Plan answers schema
|
|
@@ -46,7 +73,7 @@ export const PmptFileSchema = z.object({
|
|
|
46
73
|
guide: z.string().optional(),
|
|
47
74
|
meta: MetaSchema,
|
|
48
75
|
plan: PlanSchema,
|
|
49
|
-
docs: z.record(
|
|
76
|
+
docs: z.record(safeFilenameKey, z.string()).optional(), // current docs
|
|
50
77
|
history: z.array(VersionSchema),
|
|
51
78
|
});
|
|
52
79
|
/**
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { isGitRepo } from './git.js';
|
|
5
|
+
// ── Scanner Functions ──
|
|
6
|
+
function git(path, args) {
|
|
7
|
+
try {
|
|
8
|
+
return execSync(`git ${args}`, {
|
|
9
|
+
cwd: path,
|
|
10
|
+
encoding: 'utf-8',
|
|
11
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
12
|
+
}).trim();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function scanPackageJson(projectPath) {
|
|
19
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
20
|
+
if (!existsSync(pkgPath))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
return {
|
|
25
|
+
name: raw.name || basename(projectPath),
|
|
26
|
+
description: raw.description || null,
|
|
27
|
+
dependencies: Object.keys(raw.dependencies || {}),
|
|
28
|
+
devDependencies: Object.keys(raw.devDependencies || {}),
|
|
29
|
+
scripts: Object.keys(raw.scripts || {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function scanReadme(projectPath) {
|
|
37
|
+
const candidates = ['README.md', 'readme.md', 'Readme.md'];
|
|
38
|
+
let content = null;
|
|
39
|
+
for (const name of candidates) {
|
|
40
|
+
const filePath = join(projectPath, name);
|
|
41
|
+
if (existsSync(filePath)) {
|
|
42
|
+
try {
|
|
43
|
+
content = readFileSync(filePath, 'utf-8');
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!content)
|
|
52
|
+
return null;
|
|
53
|
+
// Split by paragraph and find the first meaningful one
|
|
54
|
+
const paragraphs = content.split(/\n\n+/);
|
|
55
|
+
for (const p of paragraphs) {
|
|
56
|
+
const trimmed = p.trim();
|
|
57
|
+
// Skip headings, badges, images, empty lines, HTML tags
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
continue;
|
|
60
|
+
if (trimmed.startsWith('#'))
|
|
61
|
+
continue;
|
|
62
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('!'))
|
|
63
|
+
continue;
|
|
64
|
+
if (trimmed.startsWith('<'))
|
|
65
|
+
continue;
|
|
66
|
+
if (trimmed.startsWith('```'))
|
|
67
|
+
continue;
|
|
68
|
+
// Found a text paragraph
|
|
69
|
+
const cleaned = trimmed.replace(/\n/g, ' ').trim();
|
|
70
|
+
return cleaned.length > 500 ? cleaned.slice(0, 500) + '...' : cleaned;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const FRAMEWORK_CONFIGS = [
|
|
75
|
+
['astro.config.*', 'Astro'],
|
|
76
|
+
['next.config.*', 'Next.js'],
|
|
77
|
+
['nuxt.config.*', 'Nuxt'],
|
|
78
|
+
['svelte.config.*', 'SvelteKit'],
|
|
79
|
+
['remix.config.*', 'Remix'],
|
|
80
|
+
['gatsby-config.*', 'Gatsby'],
|
|
81
|
+
['angular.json', 'Angular'],
|
|
82
|
+
['vite.config.*', 'Vite'],
|
|
83
|
+
];
|
|
84
|
+
const DEP_FRAMEWORKS = [
|
|
85
|
+
['next', 'Next.js'],
|
|
86
|
+
['nuxt', 'Nuxt'],
|
|
87
|
+
['@angular/core', 'Angular'],
|
|
88
|
+
['svelte', 'Svelte'],
|
|
89
|
+
['vue', 'Vue'],
|
|
90
|
+
['react', 'React'],
|
|
91
|
+
['express', 'Express'],
|
|
92
|
+
['fastify', 'Fastify'],
|
|
93
|
+
['@nestjs/core', 'NestJS'],
|
|
94
|
+
['hono', 'Hono'],
|
|
95
|
+
['django', 'Django'],
|
|
96
|
+
['flask', 'Flask'],
|
|
97
|
+
];
|
|
98
|
+
function detectFramework(projectPath, packageInfo) {
|
|
99
|
+
// Check config files first
|
|
100
|
+
for (const [pattern, name] of FRAMEWORK_CONFIGS) {
|
|
101
|
+
if (pattern.includes('*')) {
|
|
102
|
+
const base = pattern.replace('.*', '');
|
|
103
|
+
try {
|
|
104
|
+
const files = readdirSync(projectPath);
|
|
105
|
+
if (files.some((f) => f.startsWith(base)))
|
|
106
|
+
return name;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// ignore
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (existsSync(join(projectPath, pattern)))
|
|
114
|
+
return name;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Fall back to dependencies
|
|
118
|
+
if (packageInfo) {
|
|
119
|
+
const allDeps = [...packageInfo.dependencies, ...packageInfo.devDependencies];
|
|
120
|
+
for (const [dep, name] of DEP_FRAMEWORKS) {
|
|
121
|
+
if (allDeps.includes(dep))
|
|
122
|
+
return name;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const MEANINGFUL_DIRS = new Set([
|
|
128
|
+
'src', 'app', 'pages', 'components', 'routes', 'views',
|
|
129
|
+
'lib', 'utils', 'helpers', 'hooks', 'stores', 'store',
|
|
130
|
+
'api', 'server', 'services', 'middleware',
|
|
131
|
+
'public', 'static', 'assets', 'styles', 'css',
|
|
132
|
+
'tests', '__tests__', 'test', 'spec',
|
|
133
|
+
'scripts', 'config', 'database', 'db', 'models',
|
|
134
|
+
'layouts', 'templates',
|
|
135
|
+
]);
|
|
136
|
+
function scanDirectoryStructure(projectPath) {
|
|
137
|
+
try {
|
|
138
|
+
const entries = readdirSync(projectPath);
|
|
139
|
+
return entries
|
|
140
|
+
.filter((name) => {
|
|
141
|
+
if (name.startsWith('.'))
|
|
142
|
+
return false;
|
|
143
|
+
if (name === 'node_modules' || name === 'dist' || name === 'build')
|
|
144
|
+
return false;
|
|
145
|
+
try {
|
|
146
|
+
return statSync(join(projectPath, name)).isDirectory();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
.filter((name) => MEANINGFUL_DIRS.has(name))
|
|
153
|
+
.sort();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function scanGitHistory(projectPath) {
|
|
160
|
+
if (!isGitRepo(projectPath))
|
|
161
|
+
return null;
|
|
162
|
+
const countStr = git(projectPath, 'rev-list --count HEAD');
|
|
163
|
+
if (!countStr)
|
|
164
|
+
return null;
|
|
165
|
+
const totalCommits = parseInt(countStr, 10);
|
|
166
|
+
if (isNaN(totalCommits) || totalCommits === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const recentRaw = git(projectPath, 'log --oneline -5 --format=%s');
|
|
169
|
+
const recentCommits = recentRaw
|
|
170
|
+
? recentRaw.split('\n').filter(Boolean)
|
|
171
|
+
: [];
|
|
172
|
+
const firstCommitDate = git(projectPath, 'log --reverse --format=%cI -1') || null;
|
|
173
|
+
let contributors = 1;
|
|
174
|
+
const shortlogRaw = git(projectPath, 'shortlog -sn HEAD');
|
|
175
|
+
if (shortlogRaw) {
|
|
176
|
+
contributors = shortlogRaw.split('\n').filter(Boolean).length;
|
|
177
|
+
}
|
|
178
|
+
return { totalCommits, recentCommits, firstCommitDate, contributors };
|
|
179
|
+
}
|
|
180
|
+
// ── Main Functions ──
|
|
181
|
+
export function scanProject(projectPath) {
|
|
182
|
+
const packageInfo = scanPackageJson(projectPath);
|
|
183
|
+
const readmeDescription = scanReadme(projectPath);
|
|
184
|
+
const detectedFramework = detectFramework(projectPath, packageInfo);
|
|
185
|
+
const directoryStructure = scanDirectoryStructure(projectPath);
|
|
186
|
+
const gitSummary = scanGitHistory(projectPath);
|
|
187
|
+
const isExistingProject = packageInfo !== null ||
|
|
188
|
+
readmeDescription !== null ||
|
|
189
|
+
directoryStructure.length > 0 ||
|
|
190
|
+
(gitSummary !== null && gitSummary.totalCommits > 0);
|
|
191
|
+
return {
|
|
192
|
+
isExistingProject,
|
|
193
|
+
packageInfo,
|
|
194
|
+
readmeDescription,
|
|
195
|
+
detectedFramework,
|
|
196
|
+
directoryStructure,
|
|
197
|
+
gitSummary,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function scanResultToAnswers(result, userDescription) {
|
|
201
|
+
// projectName
|
|
202
|
+
const projectName = result.packageInfo?.name || basename(process.cwd());
|
|
203
|
+
// productIdea — user's own description
|
|
204
|
+
const productIdea = userDescription;
|
|
205
|
+
// additionalContext — scanned technical info
|
|
206
|
+
const contextParts = [];
|
|
207
|
+
contextParts.push('Existing project with established codebase.');
|
|
208
|
+
if (result.detectedFramework) {
|
|
209
|
+
contextParts.push(`- Framework: ${result.detectedFramework}`);
|
|
210
|
+
}
|
|
211
|
+
if (result.directoryStructure.length > 0) {
|
|
212
|
+
contextParts.push(`- Project structure: ${result.directoryStructure.map((d) => d + '/').join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
if (result.gitSummary) {
|
|
215
|
+
const gs = result.gitSummary;
|
|
216
|
+
let gitLine = `- Git history: ${gs.totalCommits} commits`;
|
|
217
|
+
if (gs.firstCommitDate) {
|
|
218
|
+
gitLine += ` since ${gs.firstCommitDate.split('T')[0]}`;
|
|
219
|
+
}
|
|
220
|
+
gitLine += `, ${gs.contributors} contributor(s)`;
|
|
221
|
+
contextParts.push(gitLine);
|
|
222
|
+
if (gs.recentCommits.length > 0) {
|
|
223
|
+
contextParts.push(`- Recent work: ${gs.recentCommits.map((c) => `"${c}"`).join(', ')}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const additionalContext = contextParts.join('\n');
|
|
227
|
+
// coreFeatures — from scripts and directory structure
|
|
228
|
+
const featureParts = [];
|
|
229
|
+
if (result.packageInfo?.scripts.length) {
|
|
230
|
+
for (const script of result.packageInfo.scripts) {
|
|
231
|
+
featureParts.push(`${script} (npm script)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (result.directoryStructure.length > 0) {
|
|
235
|
+
const dirHints = {
|
|
236
|
+
pages: 'Page routing',
|
|
237
|
+
routes: 'Route handling',
|
|
238
|
+
components: 'Component library',
|
|
239
|
+
api: 'API layer',
|
|
240
|
+
server: 'Server-side logic',
|
|
241
|
+
tests: 'Test suite',
|
|
242
|
+
__tests__: 'Test suite',
|
|
243
|
+
test: 'Test suite',
|
|
244
|
+
models: 'Data models',
|
|
245
|
+
database: 'Database layer',
|
|
246
|
+
db: 'Database layer',
|
|
247
|
+
middleware: 'Middleware',
|
|
248
|
+
layouts: 'Layout system',
|
|
249
|
+
};
|
|
250
|
+
for (const dir of result.directoryStructure) {
|
|
251
|
+
if (dirHints[dir]) {
|
|
252
|
+
featureParts.push(`${dirHints[dir]} (${dir}/ directory)`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const coreFeatures = featureParts.length > 0
|
|
257
|
+
? featureParts.join('; ')
|
|
258
|
+
: 'Existing project features';
|
|
259
|
+
// techStack — from framework + dependencies
|
|
260
|
+
const stackParts = [];
|
|
261
|
+
if (result.detectedFramework) {
|
|
262
|
+
stackParts.push(result.detectedFramework);
|
|
263
|
+
}
|
|
264
|
+
if (result.packageInfo) {
|
|
265
|
+
// Add top production dependencies (exclude framework already listed)
|
|
266
|
+
const frameworkLower = result.detectedFramework?.toLowerCase() || '';
|
|
267
|
+
const topDeps = result.packageInfo.dependencies
|
|
268
|
+
.filter((d) => !d.startsWith('@types/') && !d.toLowerCase().includes(frameworkLower))
|
|
269
|
+
.slice(0, 8);
|
|
270
|
+
stackParts.push(...topDeps);
|
|
271
|
+
// Add notable dev deps
|
|
272
|
+
const notableDevDeps = ['typescript', 'eslint', 'prettier', 'jest', 'vitest', 'mocha', 'tailwindcss'];
|
|
273
|
+
for (const d of result.packageInfo.devDependencies) {
|
|
274
|
+
if (notableDevDeps.includes(d) && !stackParts.includes(d)) {
|
|
275
|
+
stackParts.push(d);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const techStack = stackParts.length > 0
|
|
280
|
+
? stackParts.join(', ')
|
|
281
|
+
: '';
|
|
282
|
+
return {
|
|
283
|
+
projectName,
|
|
284
|
+
productIdea,
|
|
285
|
+
additionalContext,
|
|
286
|
+
coreFeatures,
|
|
287
|
+
techStack,
|
|
288
|
+
};
|
|
289
|
+
}
|