pmpt-cli 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/export.js +206 -0
- package/dist/commands/import.js +152 -0
- package/dist/index.js +16 -2
- package/dist/lib/plan.js +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, join, basename } from 'path';
|
|
3
|
+
import { existsSync, writeFileSync, statSync } from 'fs';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { isInitialized, getConfigDir, getHistoryDir, loadConfig } from '../lib/config.js';
|
|
6
|
+
import { getAllSnapshots } from '../lib/history.js';
|
|
7
|
+
import { getPlanProgress } from '../lib/plan.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a zip file without external dependencies
|
|
10
|
+
* Uses native zip command on macOS/Linux, PowerShell on Windows
|
|
11
|
+
*/
|
|
12
|
+
function createZip(sourceDir, outputPath) {
|
|
13
|
+
try {
|
|
14
|
+
const platform = process.platform;
|
|
15
|
+
if (platform === 'win32') {
|
|
16
|
+
// PowerShell Compress-Archive
|
|
17
|
+
execSync(`powershell -command "Compress-Archive -Path '${sourceDir}/*' -DestinationPath '${outputPath}'"`, {
|
|
18
|
+
stdio: 'pipe',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Unix zip command
|
|
23
|
+
execSync(`cd "${sourceDir}" && zip -r "${outputPath}" .`, {
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate metadata markdown file
|
|
35
|
+
*/
|
|
36
|
+
function generateMetadataMarkdown(projectPath, snapshots, planProgress, config) {
|
|
37
|
+
const answers = planProgress?.answers || {};
|
|
38
|
+
const projectName = answers.projectName || basename(projectPath);
|
|
39
|
+
// Timeline info
|
|
40
|
+
const firstSnapshot = snapshots[0];
|
|
41
|
+
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
42
|
+
const startDate = firstSnapshot?.timestamp || 'N/A';
|
|
43
|
+
const lastDate = lastSnapshot?.timestamp || 'N/A';
|
|
44
|
+
// Git info from last snapshot
|
|
45
|
+
const gitInfo = lastSnapshot?.git;
|
|
46
|
+
// All unique files across snapshots
|
|
47
|
+
const allFiles = new Set();
|
|
48
|
+
for (const snapshot of snapshots) {
|
|
49
|
+
for (const file of snapshot.files) {
|
|
50
|
+
allFiles.add(file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Parse features
|
|
54
|
+
const features = answers.coreFeatures
|
|
55
|
+
? answers.coreFeatures
|
|
56
|
+
.split(/[,;\n]/)
|
|
57
|
+
.map((f) => f.trim())
|
|
58
|
+
.filter((f) => f)
|
|
59
|
+
.map((f) => `- ${f}`)
|
|
60
|
+
.join('\n')
|
|
61
|
+
: 'Not specified';
|
|
62
|
+
let md = `# ${projectName}
|
|
63
|
+
|
|
64
|
+
> Exported from pmpt-cli
|
|
65
|
+
|
|
66
|
+
## Project Overview
|
|
67
|
+
|
|
68
|
+
**Product Idea:**
|
|
69
|
+
${answers.productIdea || 'Not specified'}
|
|
70
|
+
|
|
71
|
+
${answers.additionalContext ? `**Additional Context:**\n${answers.additionalContext}\n` : ''}
|
|
72
|
+
**Key Features:**
|
|
73
|
+
${features}
|
|
74
|
+
|
|
75
|
+
**Tech Stack:**
|
|
76
|
+
${answers.techStack || 'Not specified'}
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Export Information
|
|
81
|
+
|
|
82
|
+
| Item | Value |
|
|
83
|
+
|------|-------|
|
|
84
|
+
| Total Iterations | ${snapshots.length} |
|
|
85
|
+
| First Version | ${startDate} |
|
|
86
|
+
| Last Version | ${lastDate} |
|
|
87
|
+
| Files Tracked | ${allFiles.size} |
|
|
88
|
+
`;
|
|
89
|
+
if (gitInfo) {
|
|
90
|
+
md += `| Git Branch | ${gitInfo.branch} |
|
|
91
|
+
| Last Commit | ${gitInfo.commit} |
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
md += `
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Version Timeline
|
|
98
|
+
|
|
99
|
+
| Version | Date | Files | Git |
|
|
100
|
+
|---------|------|-------|-----|
|
|
101
|
+
`;
|
|
102
|
+
for (const snapshot of snapshots) {
|
|
103
|
+
const gitCol = snapshot.git
|
|
104
|
+
? `${snapshot.git.commit}${snapshot.git.dirty ? ' (dirty)' : ''}`
|
|
105
|
+
: '-';
|
|
106
|
+
md += `| v${snapshot.version} | ${snapshot.timestamp} | ${snapshot.files.length} | ${gitCol} |\n`;
|
|
107
|
+
}
|
|
108
|
+
md += `
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## File List
|
|
112
|
+
|
|
113
|
+
`;
|
|
114
|
+
for (const file of Array.from(allFiles).sort()) {
|
|
115
|
+
md += `- ${file}\n`;
|
|
116
|
+
}
|
|
117
|
+
md += `
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
*Generated by [pmpt-cli](https://pmptwiki.com)*
|
|
121
|
+
`;
|
|
122
|
+
return md;
|
|
123
|
+
}
|
|
124
|
+
export async function cmdExport(path, options) {
|
|
125
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
126
|
+
if (!isInitialized(projectPath)) {
|
|
127
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
p.intro('pmpt export');
|
|
131
|
+
const config = loadConfig(projectPath);
|
|
132
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
133
|
+
const planProgress = getPlanProgress(projectPath);
|
|
134
|
+
if (snapshots.length === 0) {
|
|
135
|
+
p.log.warn('No snapshots found.');
|
|
136
|
+
p.log.info('Run `pmpt save` or `pmpt plan` first to create versions.');
|
|
137
|
+
p.outro('');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
141
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 10);
|
|
142
|
+
const exportName = `pmpt-export-${projectName}-${timestamp}`;
|
|
143
|
+
// Output path
|
|
144
|
+
const outputPath = options?.output
|
|
145
|
+
? resolve(options.output)
|
|
146
|
+
: resolve(projectPath, `${exportName}.zip`);
|
|
147
|
+
const s = p.spinner();
|
|
148
|
+
s.start('Preparing export...');
|
|
149
|
+
// Create temp directory for export
|
|
150
|
+
const tempDir = join(projectPath, '.pmpt', '.export-temp');
|
|
151
|
+
execSync(`rm -rf "${tempDir}" && mkdir -p "${tempDir}"`, { stdio: 'pipe' });
|
|
152
|
+
// Copy history folder
|
|
153
|
+
const historyDir = getHistoryDir(projectPath);
|
|
154
|
+
execSync(`cp -r "${historyDir}" "${tempDir}/history"`, { stdio: 'pipe' });
|
|
155
|
+
// Generate and save metadata
|
|
156
|
+
const metadataContent = generateMetadataMarkdown(projectPath, snapshots, planProgress, config);
|
|
157
|
+
writeFileSync(join(tempDir, 'README.md'), metadataContent, 'utf-8');
|
|
158
|
+
// Copy current docs if exists
|
|
159
|
+
const docsDir = join(getConfigDir(projectPath), 'docs');
|
|
160
|
+
if (existsSync(docsDir)) {
|
|
161
|
+
execSync(`cp -r "${docsDir}" "${tempDir}/docs"`, { stdio: 'pipe' });
|
|
162
|
+
}
|
|
163
|
+
// Copy config (without sensitive data)
|
|
164
|
+
if (config) {
|
|
165
|
+
const exportConfig = {
|
|
166
|
+
projectPath: basename(projectPath),
|
|
167
|
+
docsPath: config.docsPath,
|
|
168
|
+
createdAt: config.createdAt,
|
|
169
|
+
trackGit: config.trackGit,
|
|
170
|
+
};
|
|
171
|
+
writeFileSync(join(tempDir, 'config.json'), JSON.stringify(exportConfig, null, 2), 'utf-8');
|
|
172
|
+
}
|
|
173
|
+
// Copy plan progress if exists
|
|
174
|
+
if (planProgress) {
|
|
175
|
+
writeFileSync(join(tempDir, 'plan.json'), JSON.stringify(planProgress, null, 2), 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
s.message('Creating archive...');
|
|
178
|
+
// Create zip
|
|
179
|
+
const zipSuccess = createZip(tempDir, outputPath);
|
|
180
|
+
// Cleanup temp directory
|
|
181
|
+
execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' });
|
|
182
|
+
if (!zipSuccess) {
|
|
183
|
+
s.stop('Export failed');
|
|
184
|
+
p.log.error('Failed to create zip archive.');
|
|
185
|
+
p.log.info('Make sure `zip` command is available on your system.');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
s.stop('Export complete!');
|
|
189
|
+
// Summary
|
|
190
|
+
const fileSizeBytes = statSync(outputPath).size;
|
|
191
|
+
const fileSize = fileSizeBytes < 1024
|
|
192
|
+
? `${fileSizeBytes} B`
|
|
193
|
+
: fileSizeBytes < 1024 * 1024
|
|
194
|
+
? `${(fileSizeBytes / 1024).toFixed(1)} KB`
|
|
195
|
+
: `${(fileSizeBytes / 1024 / 1024).toFixed(1)} MB`;
|
|
196
|
+
const summary = [
|
|
197
|
+
`Project: ${projectName}`,
|
|
198
|
+
`Versions: ${snapshots.length}`,
|
|
199
|
+
`Size: ${fileSize}`,
|
|
200
|
+
'',
|
|
201
|
+
`Output: ${outputPath}`,
|
|
202
|
+
];
|
|
203
|
+
p.note(summary.join('\n'), 'Export Summary');
|
|
204
|
+
p.log.info('Share this file to let others reproduce your AI development journey!');
|
|
205
|
+
p.outro('');
|
|
206
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } from 'fs';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Extract zip file
|
|
8
|
+
* Uses native unzip command on macOS/Linux, PowerShell on Windows
|
|
9
|
+
*/
|
|
10
|
+
function extractZip(zipPath, destDir) {
|
|
11
|
+
try {
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
mkdirSync(destDir, { recursive: true });
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
// PowerShell Expand-Archive
|
|
16
|
+
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, {
|
|
17
|
+
stdio: 'pipe',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Unix unzip command
|
|
22
|
+
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, {
|
|
23
|
+
stdio: 'pipe',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function cmdImport(zipFile, options) {
|
|
33
|
+
if (!zipFile) {
|
|
34
|
+
p.log.error('Please provide a zip file path.');
|
|
35
|
+
p.log.info('Usage: pmpt import <file.zip>');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const zipPath = resolve(zipFile);
|
|
39
|
+
if (!existsSync(zipPath)) {
|
|
40
|
+
p.log.error(`File not found: ${zipPath}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (!zipPath.endsWith('.zip')) {
|
|
44
|
+
p.log.error('Please provide a .zip file.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
p.intro('pmpt import');
|
|
48
|
+
const projectPath = process.cwd();
|
|
49
|
+
// Check if already initialized
|
|
50
|
+
if (isInitialized(projectPath) && !options?.force) {
|
|
51
|
+
const overwrite = await p.confirm({
|
|
52
|
+
message: 'Project already initialized. Merge imported history?',
|
|
53
|
+
initialValue: true,
|
|
54
|
+
});
|
|
55
|
+
if (p.isCancel(overwrite)) {
|
|
56
|
+
p.cancel('Import cancelled.');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (!overwrite) {
|
|
60
|
+
p.log.info('Use --force to overwrite existing project.');
|
|
61
|
+
p.outro('');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const s = p.spinner();
|
|
66
|
+
s.start('Extracting archive...');
|
|
67
|
+
// Create temp directory for extraction
|
|
68
|
+
const tempDir = join(projectPath, '.pmpt-import-temp');
|
|
69
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
70
|
+
const extracted = extractZip(zipPath, tempDir);
|
|
71
|
+
if (!extracted) {
|
|
72
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
73
|
+
s.stop('Extraction failed');
|
|
74
|
+
p.log.error('Failed to extract zip file.');
|
|
75
|
+
p.log.info('Make sure `unzip` command is available on your system.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
s.message('Importing project...');
|
|
79
|
+
// Read imported data
|
|
80
|
+
let importedPlan = null;
|
|
81
|
+
let importedConfig = null;
|
|
82
|
+
let projectName = 'imported-project';
|
|
83
|
+
// Read plan.json if exists
|
|
84
|
+
const planPath = join(tempDir, 'plan.json');
|
|
85
|
+
if (existsSync(planPath)) {
|
|
86
|
+
try {
|
|
87
|
+
importedPlan = JSON.parse(readFileSync(planPath, 'utf-8'));
|
|
88
|
+
if (importedPlan.answers?.projectName) {
|
|
89
|
+
projectName = importedPlan.answers.projectName;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Ignore parse errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Read config.json if exists
|
|
97
|
+
const configPath = join(tempDir, 'config.json');
|
|
98
|
+
if (existsSync(configPath)) {
|
|
99
|
+
try {
|
|
100
|
+
importedConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Ignore parse errors
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Initialize project if not exists
|
|
107
|
+
if (!isInitialized(projectPath)) {
|
|
108
|
+
initializeProject(projectPath, {
|
|
109
|
+
trackGit: importedConfig?.trackGit ?? true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const pmptDir = getConfigDir(projectPath);
|
|
113
|
+
const historyDir = getHistoryDir(projectPath);
|
|
114
|
+
const docsDir = getDocsDir(projectPath);
|
|
115
|
+
// Import history
|
|
116
|
+
const importedHistoryDir = join(tempDir, 'history');
|
|
117
|
+
if (existsSync(importedHistoryDir)) {
|
|
118
|
+
// Copy all version folders
|
|
119
|
+
cpSync(importedHistoryDir, historyDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
// Import docs
|
|
122
|
+
const importedDocsDir = join(tempDir, 'docs');
|
|
123
|
+
if (existsSync(importedDocsDir)) {
|
|
124
|
+
cpSync(importedDocsDir, docsDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
// Import plan progress
|
|
127
|
+
if (importedPlan) {
|
|
128
|
+
const planProgressPath = join(pmptDir, 'plan-progress.json');
|
|
129
|
+
writeFileSync(planProgressPath, JSON.stringify(importedPlan, null, 2), 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
// Count imported versions
|
|
132
|
+
let versionCount = 0;
|
|
133
|
+
if (existsSync(historyDir)) {
|
|
134
|
+
const { readdirSync } = await import('fs');
|
|
135
|
+
versionCount = readdirSync(historyDir).filter(d => d.startsWith('v')).length;
|
|
136
|
+
}
|
|
137
|
+
// Cleanup temp directory
|
|
138
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
139
|
+
s.stop('Import complete!');
|
|
140
|
+
// Summary
|
|
141
|
+
const summary = [
|
|
142
|
+
`Project: ${projectName}`,
|
|
143
|
+
`Versions imported: ${versionCount}`,
|
|
144
|
+
`Location: ${pmptDir}`,
|
|
145
|
+
];
|
|
146
|
+
p.note(summary.join('\n'), 'Import Summary');
|
|
147
|
+
p.log.info('Next steps:');
|
|
148
|
+
p.log.message(' pmpt history — View imported versions');
|
|
149
|
+
p.log.message(' pmpt plan — View or copy AI prompt');
|
|
150
|
+
p.log.message(' pmpt save — Save a new snapshot');
|
|
151
|
+
p.outro('Ready to continue the journey!');
|
|
152
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { cmdWatch } from './commands/watch.js';
|
|
|
10
10
|
import { cmdPlan } from './commands/plan.js';
|
|
11
11
|
import { cmdSave } from './commands/save.js';
|
|
12
12
|
import { cmdSquash } from './commands/squash.js';
|
|
13
|
+
import { cmdExport } from './commands/export.js';
|
|
14
|
+
import { cmdImport } from './commands/import.js';
|
|
13
15
|
const program = new Command();
|
|
14
16
|
program
|
|
15
17
|
.name('pmpt')
|
|
@@ -18,12 +20,14 @@ program
|
|
|
18
20
|
.addHelpText('after', `
|
|
19
21
|
Examples:
|
|
20
22
|
$ pmpt init Initialize project
|
|
21
|
-
$ pmpt plan Start product planning (
|
|
23
|
+
$ pmpt plan Start product planning (5 questions → AI prompt)
|
|
22
24
|
$ pmpt save Save snapshot of docs folder
|
|
23
25
|
$ pmpt watch Auto-detect file changes
|
|
24
26
|
$ pmpt history View version history
|
|
25
27
|
$ pmpt history --compact Hide minor changes
|
|
26
28
|
$ pmpt squash v2 v5 Merge versions v2-v5 into v2
|
|
29
|
+
$ pmpt export Export history as shareable zip
|
|
30
|
+
$ pmpt import <file.zip> Import project from zip
|
|
27
31
|
|
|
28
32
|
Folder structure:
|
|
29
33
|
.pmpt/
|
|
@@ -60,9 +64,19 @@ program
|
|
|
60
64
|
.command('squash <from> <to> [path]')
|
|
61
65
|
.description('Squash multiple versions into one (e.g., pmpt squash v2 v5)')
|
|
62
66
|
.action(cmdSquash);
|
|
67
|
+
program
|
|
68
|
+
.command('export [path]')
|
|
69
|
+
.description('Export project history as a shareable zip archive')
|
|
70
|
+
.option('-o, --output <file>', 'Output file path')
|
|
71
|
+
.action(cmdExport);
|
|
72
|
+
program
|
|
73
|
+
.command('import <file>')
|
|
74
|
+
.description('Import project from exported zip archive')
|
|
75
|
+
.option('-f, --force', 'Overwrite existing project')
|
|
76
|
+
.action(cmdImport);
|
|
63
77
|
program
|
|
64
78
|
.command('plan [path]')
|
|
65
|
-
.description('Quick product planning with
|
|
79
|
+
.description('Quick product planning with 5 questions — auto-generate AI prompt')
|
|
66
80
|
.option('--reset', 'Restart plan from scratch')
|
|
67
81
|
.action(cmdPlan);
|
|
68
82
|
// Contribution commands
|
package/dist/lib/plan.js
CHANGED
|
@@ -90,7 +90,19 @@ Add a "## Progress" section below and keep it updated:
|
|
|
90
90
|
- [ ] Pending item
|
|
91
91
|
\`\`\`
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Also, add a "## Snapshot Log" section to record what was built at each checkpoint:
|
|
94
|
+
\`\`\`
|
|
95
|
+
## Snapshot Log
|
|
96
|
+
### v1 - Initial Setup
|
|
97
|
+
- Set up project structure
|
|
98
|
+
- Installed dependencies: React, Tailwind
|
|
99
|
+
|
|
100
|
+
### v2 - Auth Feature
|
|
101
|
+
- Implemented login/signup
|
|
102
|
+
- Added JWT authentication
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
This helps others understand what was built at each version when they import this project.
|
|
94
106
|
`;
|
|
95
107
|
}
|
|
96
108
|
// Generate plan document
|