pmpt-cli 1.2.0 → 1.3.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.
@@ -1,125 +1,48 @@
1
1
  import * as p from '@clack/prompts';
2
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';
3
+ import { existsSync, readFileSync, writeFileSync, statSync } from 'fs';
4
+ import { isInitialized, getDocsDir, loadConfig } from '../lib/config.js';
6
5
  import { getAllSnapshots } from '../lib/history.js';
7
6
  import { getPlanProgress } from '../lib/plan.js';
7
+ import { createPmptFile, SCHEMA_VERSION } from '../lib/pmptFile.js';
8
+ import glob from 'fast-glob';
8
9
  /**
9
- * Create a zip file without external dependencies
10
- * Uses native zip command on macOS/Linux, PowerShell on Windows
10
+ * Read all files from a snapshot directory
11
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
- });
12
+ function readSnapshotFiles(snapshotDir) {
13
+ const files = {};
14
+ if (!existsSync(snapshotDir))
15
+ return files;
16
+ const mdFiles = glob.sync('**/*.md', { cwd: snapshotDir });
17
+ for (const file of mdFiles) {
18
+ const filePath = join(snapshotDir, file);
19
+ try {
20
+ files[file] = readFileSync(filePath, 'utf-8');
20
21
  }
21
- else {
22
- // Unix zip command
23
- execSync(`cd "${sourceDir}" && zip -r "${outputPath}" .`, {
24
- stdio: 'pipe',
25
- });
22
+ catch {
23
+ // Skip files that can't be read
26
24
  }
27
- return true;
28
- }
29
- catch {
30
- return false;
31
25
  }
26
+ return files;
32
27
  }
33
28
  /**
34
- * Generate metadata markdown file
29
+ * Read current docs folder
35
30
  */
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);
31
+ function readDocsFolder(docsDir) {
32
+ const files = {};
33
+ if (!existsSync(docsDir))
34
+ return files;
35
+ const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
36
+ for (const file of mdFiles) {
37
+ const filePath = join(docsDir, file);
38
+ try {
39
+ files[file] = readFileSync(filePath, 'utf-8');
40
+ }
41
+ catch {
42
+ // Skip files that can't be read
51
43
  }
52
44
  }
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;
45
+ return files;
123
46
  }
124
47
  export async function cmdExport(path, options) {
125
48
  const projectPath = path ? resolve(path) : process.cwd();
@@ -138,69 +61,75 @@ export async function cmdExport(path, options) {
138
61
  return;
139
62
  }
140
63
  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
64
+ const timestamp = new Date().toISOString().slice(0, 10);
65
+ const exportName = `${projectName}-${timestamp}`;
66
+ // Output path - .pmpt extension
144
67
  const outputPath = options?.output
145
68
  ? resolve(options.output)
146
- : resolve(projectPath, `${exportName}.zip`);
69
+ : resolve(projectPath, `${exportName}.pmpt`);
147
70
  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');
71
+ s.start('Creating .pmpt file...');
72
+ // Build history array with file contents
73
+ const history = [];
74
+ for (const snapshot of snapshots) {
75
+ const files = readSnapshotFiles(snapshot.snapshotDir);
76
+ history.push({
77
+ version: snapshot.version,
78
+ timestamp: snapshot.timestamp,
79
+ files,
80
+ git: snapshot.git,
81
+ });
172
82
  }
173
- // Copy plan progress if exists
174
- if (planProgress) {
175
- writeFileSync(join(tempDir, 'plan.json'), JSON.stringify(planProgress, null, 2), 'utf-8');
83
+ // Read current docs
84
+ const docsDir = getDocsDir(projectPath);
85
+ const docs = readDocsFolder(docsDir);
86
+ // Build metadata
87
+ const meta = {
88
+ projectName,
89
+ description: planProgress?.answers?.productIdea,
90
+ createdAt: config?.createdAt || new Date().toISOString(),
91
+ exportedAt: new Date().toISOString(),
92
+ };
93
+ // Convert plan answers to typed format
94
+ const planAnswers = planProgress?.answers
95
+ ? {
96
+ projectName: planProgress.answers.projectName,
97
+ productIdea: planProgress.answers.productIdea,
98
+ additionalContext: planProgress.answers.additionalContext,
99
+ coreFeatures: planProgress.answers.coreFeatures,
100
+ techStack: planProgress.answers.techStack,
101
+ }
102
+ : undefined;
103
+ // Create .pmpt file content
104
+ const pmptContent = createPmptFile(meta, planAnswers, docs, history);
105
+ // Write file
106
+ try {
107
+ writeFileSync(outputPath, pmptContent, 'utf-8');
176
108
  }
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) {
109
+ catch (err) {
183
110
  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.');
111
+ p.log.error('Failed to write .pmpt file.');
186
112
  process.exit(1);
187
113
  }
188
114
  s.stop('Export complete!');
189
- // Summary
115
+ // File size
190
116
  const fileSizeBytes = statSync(outputPath).size;
191
117
  const fileSize = fileSizeBytes < 1024
192
118
  ? `${fileSizeBytes} B`
193
119
  : fileSizeBytes < 1024 * 1024
194
120
  ? `${(fileSizeBytes / 1024).toFixed(1)} KB`
195
121
  : `${(fileSizeBytes / 1024 / 1024).toFixed(1)} MB`;
122
+ // Summary
196
123
  const summary = [
197
124
  `Project: ${projectName}`,
198
125
  `Versions: ${snapshots.length}`,
126
+ `Schema: v${SCHEMA_VERSION}`,
199
127
  `Size: ${fileSize}`,
200
128
  '',
201
129
  `Output: ${outputPath}`,
202
130
  ];
203
131
  p.note(summary.join('\n'), 'Export Summary');
204
- p.log.info('Share this file to let others reproduce your AI development journey!');
132
+ p.log.info('Share this .pmpt file to let others reproduce your AI development journey!');
133
+ p.log.message(' pmpt import <file.pmpt> — Import on another machine');
205
134
  p.outro('');
206
135
  }
@@ -0,0 +1,156 @@
1
+ import * as p from '@clack/prompts';
2
+ import { resolve, join, dirname } from 'path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
4
+ import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
5
+ import { validatePmptFile } from '../lib/pmptFile.js';
6
+ /**
7
+ * Restore history from .pmpt file
8
+ */
9
+ function restoreHistory(historyDir, history) {
10
+ mkdirSync(historyDir, { recursive: true });
11
+ for (const version of history) {
12
+ const timestamp = version.timestamp.replace(/[:.]/g, '-').slice(0, 19);
13
+ const snapshotName = `v${version.version}-${timestamp}`;
14
+ const snapshotDir = join(historyDir, snapshotName);
15
+ mkdirSync(snapshotDir, { recursive: true });
16
+ // Write files
17
+ for (const [filename, content] of Object.entries(version.files)) {
18
+ const filePath = join(snapshotDir, filename);
19
+ const fileDir = dirname(filePath);
20
+ if (fileDir !== snapshotDir) {
21
+ mkdirSync(fileDir, { recursive: true });
22
+ }
23
+ writeFileSync(filePath, content, 'utf-8');
24
+ }
25
+ // Write metadata
26
+ const metaPath = join(snapshotDir, '.meta.json');
27
+ writeFileSync(metaPath, JSON.stringify({
28
+ version: version.version,
29
+ timestamp: version.timestamp,
30
+ files: Object.keys(version.files),
31
+ git: version.git,
32
+ }, null, 2), 'utf-8');
33
+ }
34
+ }
35
+ /**
36
+ * Restore docs from .pmpt file
37
+ */
38
+ function restoreDocs(docsDir, docs) {
39
+ mkdirSync(docsDir, { recursive: true });
40
+ for (const [filename, content] of Object.entries(docs)) {
41
+ const filePath = join(docsDir, filename);
42
+ const fileDir = dirname(filePath);
43
+ if (fileDir !== docsDir) {
44
+ mkdirSync(fileDir, { recursive: true });
45
+ }
46
+ writeFileSync(filePath, content, 'utf-8');
47
+ }
48
+ }
49
+ export async function cmdImport(pmptFile, options) {
50
+ if (!pmptFile) {
51
+ p.log.error('Please provide a .pmpt file path.');
52
+ p.log.info('Usage: pmpt import <file.pmpt>');
53
+ process.exit(1);
54
+ }
55
+ const filePath = resolve(pmptFile);
56
+ if (!existsSync(filePath)) {
57
+ p.log.error(`File not found: ${filePath}`);
58
+ process.exit(1);
59
+ }
60
+ if (!filePath.endsWith('.pmpt')) {
61
+ p.log.error('Please provide a .pmpt file.');
62
+ p.log.info('Use `pmpt export` to create a .pmpt file.');
63
+ process.exit(1);
64
+ }
65
+ p.intro('pmpt import');
66
+ const s = p.spinner();
67
+ s.start('Reading .pmpt file...');
68
+ // Read file
69
+ let fileContent;
70
+ try {
71
+ fileContent = readFileSync(filePath, 'utf-8');
72
+ }
73
+ catch {
74
+ s.stop('Read failed');
75
+ p.log.error('Failed to read file.');
76
+ process.exit(1);
77
+ }
78
+ // Validate
79
+ s.message('Validating...');
80
+ const validation = validatePmptFile(fileContent);
81
+ if (!validation.success || !validation.data) {
82
+ s.stop('Validation failed');
83
+ p.log.error(validation.error || 'Invalid .pmpt file.');
84
+ process.exit(1);
85
+ }
86
+ const pmptData = validation.data;
87
+ s.stop('Validation passed');
88
+ // Show summary and confirm
89
+ const summaryLines = [
90
+ `Project: ${pmptData.meta.projectName}`,
91
+ `Versions: ${pmptData.history.length}`,
92
+ `Schema: v${pmptData.schemaVersion}`,
93
+ pmptData.meta.description ? `Description: ${pmptData.meta.description.slice(0, 50)}...` : '',
94
+ ].filter(Boolean);
95
+ p.note(summaryLines.join('\n'), 'Import Preview');
96
+ const projectPath = process.cwd();
97
+ // Check if already initialized
98
+ if (isInitialized(projectPath) && !options?.force) {
99
+ const overwrite = await p.confirm({
100
+ message: 'Project already initialized. Merge imported history?',
101
+ initialValue: true,
102
+ });
103
+ if (p.isCancel(overwrite)) {
104
+ p.cancel('Import cancelled.');
105
+ process.exit(0);
106
+ }
107
+ if (!overwrite) {
108
+ p.log.info('Use --force to overwrite existing project.');
109
+ p.outro('');
110
+ return;
111
+ }
112
+ }
113
+ const importSpinner = p.spinner();
114
+ importSpinner.start('Importing project...');
115
+ // Initialize project if not exists
116
+ if (!isInitialized(projectPath)) {
117
+ initializeProject(projectPath, { trackGit: true });
118
+ }
119
+ const pmptDir = getConfigDir(projectPath);
120
+ const historyDir = getHistoryDir(projectPath);
121
+ const docsDir = getDocsDir(projectPath);
122
+ // Restore history
123
+ restoreHistory(historyDir, pmptData.history);
124
+ // Restore docs
125
+ if (pmptData.docs) {
126
+ restoreDocs(docsDir, pmptData.docs);
127
+ }
128
+ // Restore plan progress
129
+ if (pmptData.plan) {
130
+ const planProgressPath = join(pmptDir, 'plan-progress.json');
131
+ writeFileSync(planProgressPath, JSON.stringify({
132
+ completed: true,
133
+ startedAt: pmptData.meta.createdAt,
134
+ updatedAt: pmptData.meta.exportedAt,
135
+ answers: pmptData.plan,
136
+ }, null, 2), 'utf-8');
137
+ }
138
+ // Count imported versions
139
+ let versionCount = 0;
140
+ if (existsSync(historyDir)) {
141
+ versionCount = readdirSync(historyDir).filter(d => d.startsWith('v')).length;
142
+ }
143
+ importSpinner.stop('Import complete!');
144
+ // Summary
145
+ const summary = [
146
+ `Project: ${pmptData.meta.projectName}`,
147
+ `Versions imported: ${versionCount}`,
148
+ `Location: ${pmptDir}`,
149
+ ];
150
+ p.note(summary.join('\n'), 'Import Summary');
151
+ p.log.info('Next steps:');
152
+ p.log.message(' pmpt history — View imported versions');
153
+ p.log.message(' pmpt plan — View or copy AI prompt');
154
+ p.log.message(' pmpt save — Save a new snapshot');
155
+ p.outro('Ready to continue the journey!');
156
+ }
package/dist/index.js CHANGED
@@ -11,11 +11,12 @@ import { cmdPlan } from './commands/plan.js';
11
11
  import { cmdSave } from './commands/save.js';
12
12
  import { cmdSquash } from './commands/squash.js';
13
13
  import { cmdExport } from './commands/export.js';
14
+ import { cmdImport } from './commands/import.js';
14
15
  const program = new Command();
15
16
  program
16
17
  .name('pmpt')
17
18
  .description('pmpt — Record and share your AI-driven product development journey')
18
- .version('1.0.0')
19
+ .version('1.3.0')
19
20
  .addHelpText('after', `
20
21
  Examples:
21
22
  $ pmpt init Initialize project
@@ -25,7 +26,8 @@ Examples:
25
26
  $ pmpt history View version history
26
27
  $ pmpt history --compact Hide minor changes
27
28
  $ pmpt squash v2 v5 Merge versions v2-v5 into v2
28
- $ pmpt export Export history as shareable zip
29
+ $ pmpt export Export as .pmpt file (single JSON)
30
+ $ pmpt import <file.pmpt> Import from .pmpt file
29
31
 
30
32
  Folder structure:
31
33
  .pmpt/
@@ -67,6 +69,11 @@ program
67
69
  .description('Export project history as a shareable zip archive')
68
70
  .option('-o, --output <file>', 'Output file path')
69
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);
70
77
  program
71
78
  .command('plan [path]')
72
79
  .description('Quick product planning with 5 questions — auto-generate AI prompt')
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
- This keeps a living record of our development journey.
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
@@ -0,0 +1,106 @@
1
+ /**
2
+ * .pmpt File Format Schema (v1.0)
3
+ *
4
+ * Single JSON file format for sharing pmpt projects.
5
+ */
6
+ import { z } from 'zod';
7
+ // Schema version
8
+ export const SCHEMA_VERSION = '1.0';
9
+ export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
10
+ // Git info schema
11
+ const GitInfoSchema = z.object({
12
+ commit: z.string(),
13
+ commitFull: z.string().optional(),
14
+ branch: z.string().optional(),
15
+ dirty: z.boolean().optional(),
16
+ tag: z.string().optional(),
17
+ }).optional();
18
+ // Single version/snapshot schema
19
+ const VersionSchema = z.object({
20
+ version: z.number().min(1),
21
+ timestamp: z.string(),
22
+ files: z.record(z.string(), z.string()), // filename -> content
23
+ git: GitInfoSchema,
24
+ });
25
+ // Plan answers schema
26
+ const PlanSchema = z.object({
27
+ projectName: z.string(),
28
+ productIdea: z.string().optional(),
29
+ additionalContext: z.string().optional(),
30
+ coreFeatures: z.string().optional(),
31
+ techStack: z.string().optional(),
32
+ }).optional();
33
+ // Project metadata schema
34
+ const MetaSchema = z.object({
35
+ projectName: z.string(),
36
+ author: z.string().optional(),
37
+ description: z.string().optional(),
38
+ createdAt: z.string(),
39
+ exportedAt: z.string(),
40
+ });
41
+ // Full .pmpt file schema
42
+ export const PmptFileSchema = z.object({
43
+ schemaVersion: z.string(),
44
+ cliMinVersion: z.string().optional(),
45
+ meta: MetaSchema,
46
+ plan: PlanSchema,
47
+ docs: z.record(z.string(), z.string()).optional(), // current docs
48
+ history: z.array(VersionSchema),
49
+ });
50
+ /**
51
+ * Validate .pmpt file content
52
+ */
53
+ export function validatePmptFile(content) {
54
+ // Check size
55
+ if (content.length > MAX_FILE_SIZE) {
56
+ return {
57
+ success: false,
58
+ error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
59
+ };
60
+ }
61
+ // Parse JSON
62
+ let json;
63
+ try {
64
+ json = JSON.parse(content);
65
+ }
66
+ catch {
67
+ return {
68
+ success: false,
69
+ error: 'Invalid JSON format',
70
+ };
71
+ }
72
+ // Validate schema
73
+ const result = PmptFileSchema.safeParse(json);
74
+ if (!result.success) {
75
+ const firstError = result.error.errors[0];
76
+ return {
77
+ success: false,
78
+ error: `Validation error: ${firstError.path.join('.')} - ${firstError.message}`,
79
+ };
80
+ }
81
+ // Check schema version
82
+ if (result.data.schemaVersion !== SCHEMA_VERSION) {
83
+ return {
84
+ success: false,
85
+ error: `Unsupported schema version: ${result.data.schemaVersion}. Current: ${SCHEMA_VERSION}`,
86
+ };
87
+ }
88
+ return {
89
+ success: true,
90
+ data: result.data,
91
+ };
92
+ }
93
+ /**
94
+ * Create .pmpt file content from project data
95
+ */
96
+ export function createPmptFile(meta, plan, docs, history) {
97
+ const pmptFile = {
98
+ schemaVersion: SCHEMA_VERSION,
99
+ cliMinVersion: '1.3.0',
100
+ meta,
101
+ plan,
102
+ docs,
103
+ history,
104
+ };
105
+ return JSON.stringify(pmptFile, null, 2);
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {