pmpt-cli 1.10.0 → 1.11.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.
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from
4
4
  import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
5
5
  import { validatePmptFile, isSafeFilename } from '../lib/pmptFile.js';
6
6
  import { fetchPmptFile, trackClone } from '../lib/api.js';
7
+ import { copyToClipboard } from '../lib/clipboard.js';
7
8
  /**
8
9
  * Restore history from .pmpt data (shared with import command)
9
10
  */
@@ -147,6 +148,19 @@ export async function cmdClone(slug) {
147
148
  '',
148
149
  '---',
149
150
  '',
151
+ `## Documentation Rule`,
152
+ '',
153
+ `**Important:** When you make progress, update \`.pmpt/docs/pmpt.md\` (the human-facing project document) at these moments:`,
154
+ `- When architecture or tech decisions are finalized`,
155
+ `- When a feature is implemented (mark as done)`,
156
+ `- When a development phase is completed`,
157
+ `- When requirements change or new decisions are made`,
158
+ '',
159
+ `Keep the Progress and Snapshot Log sections in pmpt.md up to date.`,
160
+ `After significant milestones, run \`pmpt save\` to create a snapshot.`,
161
+ '',
162
+ '---',
163
+ '',
150
164
  originalAiMd,
151
165
  ].join('\n');
152
166
  writeFileSync(aiMdPath, cloneGuide, 'utf-8');
@@ -170,9 +184,38 @@ export async function cmdClone(slug) {
170
184
  `Versions: ${versionCount}`,
171
185
  `Location: ${pmptDir}`,
172
186
  ].join('\n'), 'Clone Summary');
173
- p.log.info('Next steps:');
187
+ // Copy AI prompt to clipboard
188
+ const aiContent = readFileSync(aiMdPath, 'utf-8');
189
+ const copied = copyToClipboard(aiContent);
190
+ if (copied) {
191
+ p.log.message('');
192
+ p.log.success('AI prompt copied to clipboard!');
193
+ p.log.message('');
194
+ const banner = [
195
+ '',
196
+ '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓',
197
+ '┃ ┃',
198
+ '┃ 📋 NEXT STEP ┃',
199
+ '┃ ┃',
200
+ '┃ Open your AI coding tool and press: ┃',
201
+ '┃ ┃',
202
+ '┃ ⌘ + V (Mac) ┃',
203
+ '┃ Ctrl + V (Windows/Linux) ┃',
204
+ '┃ ┃',
205
+ '┃ Your cloned project context is ready! 🚀 ┃',
206
+ '┃ ┃',
207
+ '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
208
+ '',
209
+ ];
210
+ console.log(banner.join('\n'));
211
+ }
212
+ else {
213
+ p.log.warn('Could not copy to clipboard.');
214
+ p.log.info(`Read it at: ${aiMdPath}`);
215
+ }
216
+ p.log.info('Tips:');
174
217
  p.log.message(' pmpt history — view version history');
175
- p.log.message(' pmpt plan — view AI prompt');
218
+ p.log.message(' pmpt plan — view or edit AI prompt');
176
219
  p.log.message(' pmpt save — save a new snapshot');
177
220
  p.outro('Project cloned!');
178
221
  }
@@ -79,6 +79,47 @@ export async function cmdEdit() {
79
79
  p.cancel('Cancelled');
80
80
  process.exit(0);
81
81
  }
82
+ // Product link (optional)
83
+ const linkTypeInput = await p.select({
84
+ message: 'Product link (optional):',
85
+ initialValue: project.productUrlType || 'none',
86
+ options: [
87
+ { value: 'none', label: 'No link' },
88
+ { value: 'git', label: 'Git Repository' },
89
+ { value: 'url', label: 'Website / URL' },
90
+ ],
91
+ });
92
+ if (p.isCancel(linkTypeInput)) {
93
+ p.cancel('Cancelled');
94
+ process.exit(0);
95
+ }
96
+ let productUrl = '';
97
+ let productUrlType = '';
98
+ if (linkTypeInput !== 'none') {
99
+ productUrlType = linkTypeInput;
100
+ const productUrlInput = await p.text({
101
+ message: 'Product URL:',
102
+ placeholder: linkTypeInput === 'git'
103
+ ? `https://github.com/${auth.username}/${slug}`
104
+ : 'https://...',
105
+ defaultValue: project.productUrl || '',
106
+ validate: (v) => {
107
+ if (!v.trim())
108
+ return 'URL is required when link type is selected.';
109
+ try {
110
+ new URL(v);
111
+ }
112
+ catch {
113
+ return 'Invalid URL format.';
114
+ }
115
+ },
116
+ });
117
+ if (p.isCancel(productUrlInput)) {
118
+ p.cancel('Cancelled');
119
+ process.exit(0);
120
+ }
121
+ productUrl = productUrlInput;
122
+ }
82
123
  const s2 = p.spinner();
83
124
  s2.start('Updating...');
84
125
  try {
@@ -86,6 +127,8 @@ export async function cmdEdit() {
86
127
  description: description,
87
128
  tags,
88
129
  category: category,
130
+ productUrl,
131
+ productUrlType,
89
132
  });
90
133
  s2.stop('Updated!');
91
134
  p.log.success(`Project "${slug}" has been updated.`);
@@ -75,8 +75,12 @@ export function cmdHistory(path, options) {
75
75
  if (snapshot.git.dirty)
76
76
  header += ' (dirty)';
77
77
  }
78
- const files = snapshot.files.map((f) => ` - ${f}`).join('\n');
79
- p.note(files || ' (no files)', header);
78
+ const lines = [];
79
+ if (snapshot.note) {
80
+ lines.push(` ${snapshot.note}`, '');
81
+ }
82
+ lines.push(...snapshot.files.map((f) => ` - ${f}`));
83
+ p.note(lines.join('\n') || ' (no files)', header);
80
84
  }
81
85
  if (options?.compact && hiddenVersions.length > 0) {
82
86
  p.log.info(`Hidden versions (minor changes): ${hiddenVersions.map(v => `v${v}`).join(', ')}`);
@@ -84,9 +84,9 @@ export async function cmdInternalSeed(options) {
84
84
  const content = readFileSync(resolve(specDir, fromPath), 'utf-8');
85
85
  writeDocFile(docsDir, fileName, content);
86
86
  }
87
- const entry = createFullSnapshot(projectPath);
88
- const note = step.saveNote ? ` — ${step.saveNote}` : '';
89
- p.log.success(`v${entry.version} saved${note}`);
87
+ const entry = createFullSnapshot(projectPath, { note: step.saveNote });
88
+ const noteStr = entry.note ? ` — ${entry.note}` : '';
89
+ p.log.success(`v${entry.version} saved${noteStr}`);
90
90
  }
91
91
  if (spec.publish?.enabled) {
92
92
  await cmdPublish(projectPath, {
@@ -148,6 +148,8 @@ export async function cmdPublish(path, options) {
148
148
  let description;
149
149
  let tags;
150
150
  let category;
151
+ let productUrl = '';
152
+ let productUrlType = '';
151
153
  if (options?.nonInteractive) {
152
154
  let metaFromFile = {};
153
155
  if (options.metaFile) {
@@ -170,6 +172,8 @@ export async function cmdPublish(path, options) {
170
172
  ?? '').trim();
171
173
  tags = normalizeTags(options.tags ?? metaFromFile.tags ?? existing?.tags ?? []);
172
174
  category = String(options.category ?? metaFromFile.category ?? existing?.category ?? 'other').trim();
175
+ productUrl = String(options.productUrl ?? metaFromFile.productUrl ?? existing?.productUrl ?? '').trim();
176
+ productUrlType = String(options.productUrlType ?? metaFromFile.productUrlType ?? existing?.productUrlType ?? '').trim();
173
177
  if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(slug)) {
174
178
  p.log.error('Invalid slug. Use 3-50 chars, lowercase letters, numbers, and hyphens only.');
175
179
  process.exit(1);
@@ -230,6 +234,45 @@ export async function cmdPublish(path, options) {
230
234
  process.exit(0);
231
235
  }
232
236
  category = categoryInput;
237
+ // Product link (optional)
238
+ const linkTypeInput = await p.select({
239
+ message: 'Product link (optional):',
240
+ initialValue: existing?.productUrlType || 'none',
241
+ options: [
242
+ { value: 'none', label: 'No link' },
243
+ { value: 'git', label: 'Git Repository' },
244
+ { value: 'url', label: 'Website / URL' },
245
+ ],
246
+ });
247
+ if (p.isCancel(linkTypeInput)) {
248
+ p.cancel('Cancelled');
249
+ process.exit(0);
250
+ }
251
+ if (linkTypeInput !== 'none') {
252
+ productUrlType = linkTypeInput;
253
+ const productUrlInput = await p.text({
254
+ message: 'Product URL:',
255
+ placeholder: linkTypeInput === 'git'
256
+ ? `https://github.com/${auth.username}/${slug}`
257
+ : 'https://...',
258
+ defaultValue: existing?.productUrl || '',
259
+ validate: (v) => {
260
+ if (!v.trim())
261
+ return 'URL is required when link type is selected.';
262
+ try {
263
+ new URL(v);
264
+ }
265
+ catch {
266
+ return 'Invalid URL format.';
267
+ }
268
+ },
269
+ });
270
+ if (p.isCancel(productUrlInput)) {
271
+ p.cancel('Cancelled');
272
+ process.exit(0);
273
+ }
274
+ productUrl = productUrlInput;
275
+ }
233
276
  }
234
277
  // Build .pmpt content (resolve from optimized snapshots)
235
278
  const history = snapshots.map((snapshot, i) => ({
@@ -265,6 +308,7 @@ export async function cmdPublish(path, options) {
265
308
  `Author: @${auth.username}`,
266
309
  `Category: ${category}`,
267
310
  tags.length ? `Tags: ${tags.join(', ')}` : '',
311
+ productUrl ? `Product: ${productUrl} (${productUrlType})` : '',
268
312
  ].filter(Boolean).join('\n'), 'Publish Preview');
269
313
  if (options?.nonInteractive) {
270
314
  if (!options.yes) {
@@ -292,6 +336,7 @@ export async function cmdPublish(path, options) {
292
336
  description,
293
337
  tags,
294
338
  category,
339
+ ...(productUrl && { productUrl, productUrlType }),
295
340
  });
296
341
  s.stop('Published!');
297
342
  // Update config
@@ -38,6 +38,12 @@ export async function cmdSave(fileOrPath) {
38
38
  msg += ` (${changedCount} changed, ${unchangedCount} skipped)`;
39
39
  }
40
40
  p.log.success(msg);
41
+ // Warn if pmpt.md was not updated since last save
42
+ if (entry.version > 1 && entry.changedFiles && !entry.changedFiles.includes('pmpt.md')) {
43
+ p.log.message('');
44
+ p.log.warn('pmpt.md has not been updated since the last save.');
45
+ p.log.message(' Tip: Mark completed features and update the Snapshot Log before saving.');
46
+ }
41
47
  p.log.message('');
42
48
  p.log.info('Files included:');
43
49
  for (const file of entry.files) {
package/dist/index.js CHANGED
@@ -140,6 +140,8 @@ program
140
140
  .option('--description <text>', 'Project description')
141
141
  .option('--tags <csv>', 'Comma-separated tags')
142
142
  .option('--category <id>', 'Project category')
143
+ .option('--product-url <url>', 'Product link URL')
144
+ .option('--product-url-type <type>', 'Product link type: git or url')
143
145
  .option('--yes', 'Skip confirmation prompt')
144
146
  .action(cmdPublish);
145
147
  program
@@ -37,6 +37,11 @@ export function initializeProject(projectPath, options) {
37
37
  trackGit: options?.trackGit ?? true,
38
38
  };
39
39
  saveConfig(projectPath, config);
40
+ // Create README.md if it doesn't exist
41
+ const readmePath = join(configDir, 'README.md');
42
+ if (!existsSync(readmePath)) {
43
+ writeFileSync(readmePath, PMPT_README, 'utf-8');
44
+ }
40
45
  return config;
41
46
  }
42
47
  export function loadConfig(projectPath) {
@@ -54,3 +59,55 @@ export function saveConfig(projectPath, config) {
54
59
  const configPath = join(getConfigDir(projectPath), CONFIG_FILE);
55
60
  writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
56
61
  }
62
+ const PMPT_README = `# .pmpt — Your Project's Development Journal
63
+
64
+ This folder is managed by [pmpt](https://pmptwiki.com). It records your product development journey with AI.
65
+
66
+ ## What's Inside
67
+
68
+ \`\`\`
69
+ .pmpt/
70
+ ├── config.json ← Project settings (auto-generated)
71
+ ├── docs/
72
+ │ ├── pmpt.md ← Human-facing project document (YOU update this)
73
+ │ ├── pmpt.ai.md ← AI-facing prompt (paste into your AI tool)
74
+ │ └── plan.md ← Original plan from pmpt plan
75
+ └── .history/ ← Version snapshots (auto-managed)
76
+ \`\`\`
77
+
78
+ ## Quick Reference
79
+
80
+ | Command | What it does |
81
+ |---------|-------------|
82
+ | \`pmpt plan\` | Create or view your AI prompt |
83
+ | \`pmpt save\` | Save a snapshot of current docs |
84
+ | \`pmpt history\` | View version history |
85
+ | \`pmpt diff\` | Compare versions side by side |
86
+ | \`pmpt publish\` | Share your journey on pmptwiki.com |
87
+
88
+ ## How to Get the Most Out of pmpt
89
+
90
+ 1. **Paste \`pmpt.ai.md\` into your AI tool** to start building
91
+ 2. **Update \`pmpt.md\` as you go** — mark features done, log decisions
92
+ 3. **Run \`pmpt save\` at milestones** — after setup, after each feature, after big changes
93
+ 4. **Publish when ready** — others can clone your journey and learn from it
94
+
95
+ ## When Things Go Wrong
96
+
97
+ | Problem | Solution |
98
+ |---------|----------|
99
+ | Lost your AI prompt | \`pmpt plan\` to regenerate or view it |
100
+ | Messed up docs | \`pmpt history\` → \`pmpt diff\` to find the good version |
101
+ | Need to start over | \`pmpt recover\` rebuilds context from history |
102
+ | Accidentally deleted .pmpt | Re-clone from pmptwiki.com if published |
103
+
104
+ ## One Request
105
+
106
+ Please keep \`pmpt.md\` updated as you build. It's the human-readable record of your journey — what you tried, what worked, what you decided. When you publish, this is what others will learn from.
107
+
108
+ Your snapshots tell a story. Make it a good one.
109
+
110
+ ---
111
+
112
+ *Learn more at [pmptwiki.com](https://pmptwiki.com)*
113
+ `;
@@ -3,18 +3,31 @@ import { basename, join, relative } from 'path';
3
3
  import { getHistoryDir, getDocsDir, loadConfig } from './config.js';
4
4
  import { getGitInfo, isGitRepo } from './git.js';
5
5
  import glob from 'fast-glob';
6
+ /** Generate compact timestamp for snapshot dir names: 20260225T163000 */
7
+ function compactTimestamp() {
8
+ return new Date().toISOString().replace(/[-:\.]/g, '').slice(0, 15);
9
+ }
10
+ /** Parse snapshot dir timestamp (compact or legacy) to ISO string */
11
+ function parseTimestamp(raw) {
12
+ // Compact: 20260225T163000
13
+ if (/^\d{8}T\d{6}$/.test(raw)) {
14
+ return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}T${raw.slice(9, 11)}:${raw.slice(11, 13)}:${raw.slice(13, 15)}`;
15
+ }
16
+ // Legacy: 2026-02-25T16-30-00
17
+ return raw.replace(/T(.+)$/, (_, time) => 'T' + time.replace(/-/g, ':'));
18
+ }
6
19
  /**
7
20
  * Save .pmpt/docs MD files as snapshot
8
21
  * Copy only changed files to optimize storage
9
22
  */
10
- export function createFullSnapshot(projectPath) {
23
+ export function createFullSnapshot(projectPath, options) {
11
24
  const historyDir = getHistoryDir(projectPath);
12
25
  const docsDir = getDocsDir(projectPath);
13
26
  mkdirSync(historyDir, { recursive: true });
14
27
  // Find next version number
15
28
  const existing = getAllSnapshots(projectPath);
16
29
  const version = existing.length + 1;
17
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
30
+ const timestamp = compactTimestamp();
18
31
  const snapshotName = `v${version}-${timestamp}`;
19
32
  const snapshotDir = join(historyDir, snapshotName);
20
33
  mkdirSync(snapshotDir, { recursive: true });
@@ -63,12 +76,14 @@ export function createFullSnapshot(projectPath) {
63
76
  }
64
77
  }
65
78
  // Save metadata
79
+ const note = options?.note;
66
80
  const metaPath = join(snapshotDir, '.meta.json');
67
81
  writeFileSync(metaPath, JSON.stringify({
68
82
  version,
69
83
  timestamp,
70
84
  files,
71
85
  changedFiles,
86
+ ...(note ? { note } : {}),
72
87
  git: gitData,
73
88
  }, null, 2), 'utf-8');
74
89
  return {
@@ -77,6 +92,7 @@ export function createFullSnapshot(projectPath) {
77
92
  snapshotDir,
78
93
  files,
79
94
  changedFiles,
95
+ note,
80
96
  git: gitData,
81
97
  };
82
98
  }
@@ -98,7 +114,7 @@ export function createSnapshot(projectPath, filePath) {
98
114
  }
99
115
  function createSingleFileSnapshot(projectPath, filePath, relPath) {
100
116
  const historyDir = getHistoryDir(projectPath);
101
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
117
+ const timestamp = compactTimestamp();
102
118
  // Check existing version count for this file
103
119
  const existing = getFileHistory(projectPath, relPath);
104
120
  const version = existing.length + 1;
@@ -168,10 +184,11 @@ export function getAllSnapshots(projectPath) {
168
184
  }
169
185
  entries.push({
170
186
  version: parseInt(match[1], 10),
171
- timestamp: match[2].replace(/-/g, ':'),
187
+ timestamp: parseTimestamp(match[2]),
172
188
  snapshotDir,
173
189
  files: meta.files || [],
174
190
  changedFiles: meta.changedFiles,
191
+ note: meta.note,
175
192
  git: meta.git,
176
193
  });
177
194
  }
@@ -210,7 +227,7 @@ export function getFileHistory(projectPath, relPath) {
210
227
  }
211
228
  entries.push({
212
229
  version: parseInt(match[1], 10),
213
- timestamp: match[2].replace(/-/g, ':'),
230
+ timestamp: parseTimestamp(match[2]),
214
231
  filePath: relPath,
215
232
  historyPath: filePath,
216
233
  git: gitData,
package/dist/lib/plan.js CHANGED
@@ -88,6 +88,12 @@ I'll confirm progress at each step before moving to the next.
88
88
 
89
89
  Keep the Progress and Snapshot Log sections in pmpt.md up to date.
90
90
  After significant milestones, run \`pmpt save\` to create a snapshot.
91
+
92
+ ### Per-Feature Checklist
93
+ After completing each feature above:
94
+ 1. Mark the feature done in \`.pmpt/docs/pmpt.md\` (change \`- [ ]\` to \`- [x]\`)
95
+ 2. Add a brief note to the Snapshot Log section
96
+ 3. Run \`pmpt save\` in terminal
91
97
  `;
92
98
  }
93
99
  // Generate human-facing project document (pmpt.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {