pmpt-cli 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  **Record and share your AI-driven product development journey.**
4
4
 
5
- AI와 대화하며 제품을 만드는 여정을 기록하고 공유하세요.
6
-
7
5
  [![npm version](https://img.shields.io/npm/v/pmpt-cli.svg)](https://www.npmjs.com/package/pmpt-cli)
8
6
 
9
7
  **Website**: [pmptwiki.com](https://pmptwiki.com)
@@ -32,8 +30,9 @@ pmpt plan
32
30
  # 4. Save your progress
33
31
  pmpt save
34
32
 
35
- # 5. Export & share
36
- pmpt export
33
+ # 5. Publish to pmptwiki
34
+ pmpt login
35
+ pmpt publish
37
36
  ```
38
37
 
39
38
  ---
@@ -42,12 +41,15 @@ pmpt export
42
41
 
43
42
  - **5 questions** — Quick product planning with AI-ready prompts
44
43
  - **Version history** — Track every step of your AI-assisted development
45
- - **Share & reproduce** — Export `.pmpt` files for others to learn from
44
+ - **Share & reproduce** — Publish projects for others to learn from and clone
45
+ - **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/explore)
46
46
 
47
47
  ---
48
48
 
49
49
  ## Commands
50
50
 
51
+ ### Local
52
+
51
53
  | Command | Description |
52
54
  |---------|-------------|
53
55
  | `pmpt init` | Initialize project |
@@ -61,6 +63,33 @@ pmpt export
61
63
  | `pmpt import <file>` | Import from `.pmpt` file |
62
64
  | `pmpt status` | Check project status |
63
65
 
66
+ ### Platform
67
+
68
+ | Command | Description |
69
+ |---------|-------------|
70
+ | `pmpt login` | Authenticate with GitHub (one-time setup) |
71
+ | `pmpt publish` | Publish project to pmptwiki |
72
+ | `pmpt clone <slug>` | Clone a published project |
73
+ | `pmpt browse` | Browse and discover projects |
74
+
75
+ ---
76
+
77
+ ## Workflow
78
+
79
+ ```
80
+ [You]
81
+
82
+ ├─ pmpt plan ─────→ 5 questions → AI prompt (clipboard)
83
+
84
+ ├─ Build with AI ──→ Create files, iterate
85
+
86
+ ├─ pmpt save ─────→ Save to .pmpt/.history
87
+
88
+ ├─ pmpt publish ──→ Share on pmptwiki.com
89
+
90
+ └─ pmpt clone ────→ Reproduce someone's project
91
+ ```
92
+
64
93
  ---
65
94
 
66
95
  ## Folder Structure
@@ -79,24 +108,6 @@ pmpt export
79
108
 
80
109
  ---
81
110
 
82
- ## Workflow
83
-
84
- ```
85
- [You]
86
-
87
- ├─ pmpt plan ────→ 5 questions → AI prompt (clipboard)
88
-
89
- ├─ Build with AI ─→ Create files, iterate
90
-
91
- ├─ pmpt save ────→ Save to .pmpt/.history
92
-
93
- ├─ pmpt export ──→ Create .pmpt file (shareable)
94
-
95
- └─ pmpt import ──→ Reproduce someone's project
96
- ```
97
-
98
- ---
99
-
100
111
  ## .pmpt File Format
101
112
 
102
113
  Single JSON file containing your entire development journey:
@@ -104,12 +115,12 @@ Single JSON file containing your entire development journey:
104
115
  ```json
105
116
  {
106
117
  "schemaVersion": "1.0",
107
- "meta": { "projectName", "description", "createdAt" },
108
- "plan": { "productIdea", "coreFeatures", "techStack" },
118
+ "meta": { "projectName": "", "description": "", "createdAt": "" },
119
+ "plan": { "productIdea": "", "coreFeatures": "", "techStack": "" },
109
120
  "docs": { "plan.md": "...", "pmpt.md": "..." },
110
121
  "history": [
111
- { "version": 1, "timestamp": "...", "files": {...} },
112
- { "version": 2, "timestamp": "...", "files": {...} }
122
+ { "version": 1, "timestamp": "...", "files": {} },
123
+ { "version": 2, "timestamp": "...", "files": {} }
113
124
  ]
114
125
  }
115
126
  ```
@@ -121,13 +132,14 @@ Single JSON file containing your entire development journey:
121
132
  - **Side project builders** — Track your AI-assisted development
122
133
  - **Startup founders** — Document MVP creation process
123
134
  - **Content creators** — Share your coding journey
124
- - **Learners** — Study how others build with AI
135
+ - **Learners** — Browse and clone projects to study how others build with AI
125
136
 
126
137
  ---
127
138
 
128
139
  ## Links
129
140
 
130
141
  - [Website](https://pmptwiki.com)
142
+ - [Explore Projects](https://pmptwiki.com/explore)
131
143
  - [GitHub](https://github.com/pmptwiki/pmpt-cli)
132
144
  - [npm](https://www.npmjs.com/package/pmpt-cli)
133
145
 
@@ -2,29 +2,10 @@ import * as p from '@clack/prompts';
2
2
  import { resolve, join, basename } from 'path';
3
3
  import { existsSync, readFileSync, writeFileSync, statSync } from 'fs';
4
4
  import { isInitialized, getDocsDir, loadConfig } from '../lib/config.js';
5
- import { getAllSnapshots } from '../lib/history.js';
5
+ import { getAllSnapshots, resolveFullSnapshot } from '../lib/history.js';
6
6
  import { getPlanProgress } from '../lib/plan.js';
7
7
  import { createPmptFile, SCHEMA_VERSION } from '../lib/pmptFile.js';
8
8
  import glob from 'fast-glob';
9
- /**
10
- * Read all files from a snapshot directory
11
- */
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');
21
- }
22
- catch {
23
- // Skip files that can't be read
24
- }
25
- }
26
- return files;
27
- }
28
9
  /**
29
10
  * Read current docs folder
30
11
  */
@@ -69,15 +50,15 @@ export async function cmdExport(path, options) {
69
50
  : resolve(projectPath, `${exportName}.pmpt`);
70
51
  const s = p.spinner();
71
52
  s.start('Creating .pmpt file...');
72
- // Build history array with file contents
53
+ // Build history array with file contents (resolve from optimized snapshots)
73
54
  const history = [];
74
- for (const snapshot of snapshots) {
75
- const files = readSnapshotFiles(snapshot.snapshotDir);
55
+ for (let i = 0; i < snapshots.length; i++) {
56
+ const files = resolveFullSnapshot(snapshots, i);
76
57
  history.push({
77
- version: snapshot.version,
78
- timestamp: snapshot.timestamp,
58
+ version: snapshots[i].version,
59
+ timestamp: snapshots[i].timestamp,
79
60
  files,
80
- git: snapshot.git,
61
+ git: snapshots[i].git,
81
62
  });
82
63
  }
83
64
  // Read current docs
@@ -1,8 +1,7 @@
1
1
  import * as p from '@clack/prompts';
2
- import { resolve, join } from 'path';
3
- import { readFileSync, existsSync } from 'fs';
2
+ import { resolve } from 'path';
4
3
  import { isInitialized } from '../lib/config.js';
5
- import { getAllSnapshots } from '../lib/history.js';
4
+ import { getAllSnapshots, resolveFileContent } from '../lib/history.js';
6
5
  // Simple diff calculation: count changed lines
7
6
  function calculateDiffSize(oldContent, newContent) {
8
7
  const oldLines = oldContent.split('\n');
@@ -16,15 +15,15 @@ function calculateDiffSize(oldContent, newContent) {
16
15
  }
17
16
  return changes;
18
17
  }
19
- // Get total diff between two snapshots
20
- function getSnapshotDiff(prev, curr) {
18
+ // Get total diff between two snapshots (supports optimized snapshots)
19
+ function getSnapshotDiff(snapshots, prevIndex, currIndex) {
20
+ const prev = snapshots[prevIndex];
21
+ const curr = snapshots[currIndex];
21
22
  let totalChanges = 0;
22
23
  const allFiles = new Set([...prev.files, ...curr.files]);
23
24
  for (const file of allFiles) {
24
- const prevPath = join(prev.snapshotDir, file);
25
- const currPath = join(curr.snapshotDir, file);
26
- const prevContent = existsSync(prevPath) ? readFileSync(prevPath, 'utf-8') : '';
27
- const currContent = existsSync(currPath) ? readFileSync(currPath, 'utf-8') : '';
25
+ const prevContent = resolveFileContent(snapshots, prevIndex, file) || '';
26
+ const currContent = resolveFileContent(snapshots, currIndex, file) || '';
28
27
  totalChanges += calculateDiffSize(prevContent, currContent);
29
28
  }
30
29
  return totalChanges;
@@ -49,7 +48,7 @@ export function cmdHistory(path, options) {
49
48
  if (options?.compact && snapshots.length > 1) {
50
49
  displaySnapshots = [snapshots[0]]; // Always show first
51
50
  for (let i = 1; i < snapshots.length; i++) {
52
- const diffSize = getSnapshotDiff(snapshots[i - 1], snapshots[i]);
51
+ const diffSize = getSnapshotDiff(snapshots, i - 1, i);
53
52
  // Threshold: hide if less than 5 lines changed
54
53
  if (diffSize < 5) {
55
54
  hiddenVersions.push(snapshots[i].version);
@@ -1,6 +1,7 @@
1
1
  import * as p from '@clack/prompts';
2
+ import open from 'open';
2
3
  import { loadAuth, saveAuth } from '../lib/auth.js';
3
- import { registerAuth } from '../lib/api.js';
4
+ import { requestDeviceCode, pollDeviceToken } from '../lib/api.js';
4
5
  export async function cmdLogin() {
5
6
  p.intro('pmpt login');
6
7
  const existing = loadAuth();
@@ -15,32 +16,63 @@ export async function cmdLogin() {
15
16
  return;
16
17
  }
17
18
  }
18
- p.log.info('GitHub Personal Access Token이 필요합니다.\n' +
19
- ' https://github.com/settings/tokens/new\n' +
20
- ' 필요 권한: read:user');
21
- const pat = await p.password({
22
- message: 'GitHub PAT를 입력하세요:',
23
- validate: (v) => (v.trim().length < 10 ? '올바른 토큰을 입력하세요' : undefined),
24
- });
25
- if (p.isCancel(pat)) {
26
- p.cancel('취소됨');
27
- process.exit(0);
28
- }
19
+ // Step 1: Request device code
29
20
  const s = p.spinner();
30
- s.start('인증 중...');
21
+ s.start('GitHub 인증 준비 중...');
22
+ let device;
31
23
  try {
32
- const result = await registerAuth(pat);
33
- saveAuth({
34
- token: result.token,
35
- githubToken: pat,
36
- username: result.username,
37
- });
38
- s.stop(`인증 완료 — @${result.username}`);
24
+ device = await requestDeviceCode();
25
+ s.stop('인증 코드가 발급되었습니다.');
39
26
  }
40
27
  catch (err) {
41
- s.stop('인증 실패');
42
- p.log.error(err instanceof Error ? err.message : '인증에 실패했습니다.');
28
+ s.stop('인증 코드 발급 실패');
29
+ p.log.error(err instanceof Error ? err.message : '인증 준비에 실패했습니다.');
43
30
  process.exit(1);
44
31
  }
45
- p.outro('로그인 완료! pmpt publish로 프로젝트를 공유하세요.');
32
+ // Step 2: Show code and open browser
33
+ p.log.info(`아래 코드를 GitHub에 입력하세요:\n\n` +
34
+ ` 코드: ${device.userCode}\n` +
35
+ ` 주소: ${device.verificationUri}`);
36
+ const shouldOpen = await p.confirm({
37
+ message: '브라우저를 열까요?',
38
+ initialValue: true,
39
+ });
40
+ if (p.isCancel(shouldOpen)) {
41
+ p.cancel('취소됨');
42
+ process.exit(0);
43
+ }
44
+ if (shouldOpen) {
45
+ await open(device.verificationUri);
46
+ }
47
+ // Step 3: Poll for token
48
+ s.start('GitHub 인증 대기 중... (브라우저에서 코드를 입력하세요)');
49
+ let interval = device.interval * 1000; // seconds → ms
50
+ const deadline = Date.now() + device.expiresIn * 1000;
51
+ while (Date.now() < deadline) {
52
+ await sleep(interval);
53
+ try {
54
+ const result = await pollDeviceToken(device.deviceCode);
55
+ if (result.status === 'complete') {
56
+ saveAuth({ token: result.token, username: result.username });
57
+ s.stop(`인증 완료 — @${result.username}`);
58
+ p.outro('로그인 완료! pmpt publish로 프로젝트를 공유하세요.');
59
+ return;
60
+ }
61
+ if (result.status === 'slow_down') {
62
+ interval = (result.interval ?? 10) * 1000;
63
+ }
64
+ // status === 'pending' → keep polling
65
+ }
66
+ catch (err) {
67
+ s.stop('인증 실패');
68
+ p.log.error(err instanceof Error ? err.message : '인증에 실패했습니다.');
69
+ process.exit(1);
70
+ }
71
+ }
72
+ s.stop('인증 코드가 만료되었습니다.');
73
+ p.log.error('다시 pmpt login을 실행해 주세요.');
74
+ process.exit(1);
75
+ }
76
+ function sleep(ms) {
77
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
78
  }
@@ -2,26 +2,13 @@ import * as p from '@clack/prompts';
2
2
  import { resolve, basename } from 'path';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { isInitialized, loadConfig, saveConfig, getDocsDir } from '../lib/config.js';
5
- import { getAllSnapshots } from '../lib/history.js';
5
+ import { getAllSnapshots, resolveFullSnapshot } from '../lib/history.js';
6
6
  import { getPlanProgress } from '../lib/plan.js';
7
7
  import { createPmptFile } from '../lib/pmptFile.js';
8
8
  import { loadAuth } from '../lib/auth.js';
9
9
  import { publishProject } from '../lib/api.js';
10
10
  import glob from 'fast-glob';
11
11
  import { join } from 'path';
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
- try {
19
- files[file] = readFileSync(join(snapshotDir, file), 'utf-8');
20
- }
21
- catch { /* skip */ }
22
- }
23
- return files;
24
- }
25
12
  function readDocsFolder(docsDir) {
26
13
  const files = {};
27
14
  if (!existsSync(docsDir))
@@ -92,11 +79,28 @@ export async function cmdPublish(path) {
92
79
  .split(',')
93
80
  .map((t) => t.trim().toLowerCase())
94
81
  .filter(Boolean);
95
- // Build .pmpt content (reuse export logic)
96
- const history = snapshots.map((snapshot) => ({
82
+ const category = await p.select({
83
+ message: '프로젝트 카테고리:',
84
+ options: [
85
+ { value: 'web-app', label: '웹 앱 (Web App)' },
86
+ { value: 'mobile-app', label: '모바일 앱 (Mobile App)' },
87
+ { value: 'cli-tool', label: 'CLI 도구 (CLI Tool)' },
88
+ { value: 'api-backend', label: 'API/백엔드 (API/Backend)' },
89
+ { value: 'ai-ml', label: 'AI/ML' },
90
+ { value: 'game', label: '게임 (Game)' },
91
+ { value: 'library', label: '라이브러리 (Library)' },
92
+ { value: 'other', label: '기타 (Other)' },
93
+ ],
94
+ });
95
+ if (p.isCancel(category)) {
96
+ p.cancel('취소됨');
97
+ process.exit(0);
98
+ }
99
+ // Build .pmpt content (resolve from optimized snapshots)
100
+ const history = snapshots.map((snapshot, i) => ({
97
101
  version: snapshot.version,
98
102
  timestamp: snapshot.timestamp,
99
- files: readSnapshotFiles(snapshot.snapshotDir),
103
+ files: resolveFullSnapshot(snapshots, i),
100
104
  git: snapshot.git,
101
105
  }));
102
106
  const docsDir = getDocsDir(projectPath);
@@ -125,6 +129,7 @@ export async function cmdPublish(path) {
125
129
  `Versions: ${snapshots.length}`,
126
130
  `Size: ${(pmptContent.length / 1024).toFixed(1)} KB`,
127
131
  `Author: @${auth.username}`,
132
+ `Category: ${category}`,
128
133
  tags.length ? `Tags: ${tags.join(', ')}` : '',
129
134
  ].filter(Boolean).join('\n'), 'Publish Preview');
130
135
  const confirm = await p.confirm({
@@ -144,6 +149,7 @@ export async function cmdPublish(path) {
144
149
  pmptContent,
145
150
  description: description,
146
151
  tags,
152
+ category: category,
147
153
  });
148
154
  s.stop('게시 완료!');
149
155
  // Update config
@@ -32,11 +32,17 @@ export async function cmdSave(fileOrPath) {
32
32
  if (entry.git.dirty)
33
33
  msg += ' (uncommitted)';
34
34
  }
35
+ const changedCount = entry.changedFiles?.length ?? entry.files.length;
36
+ const unchangedCount = entry.files.length - changedCount;
37
+ if (unchangedCount > 0) {
38
+ msg += ` (${changedCount} changed, ${unchangedCount} skipped)`;
39
+ }
35
40
  p.log.success(msg);
36
41
  p.log.message('');
37
42
  p.log.info('Files included:');
38
43
  for (const file of entry.files) {
39
- p.log.message(` - ${file}`);
44
+ const isChanged = entry.changedFiles ? entry.changedFiles.includes(file) : true;
45
+ p.log.message(` - ${file}${isChanged ? '' : ' (unchanged)'}`);
40
46
  }
41
47
  }
42
48
  catch (error) {
package/dist/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { cmdNew } from './commands/new.js';
4
- import { cmdValidate } from './commands/validate.js';
5
- import { cmdSubmit } from './commands/submit.js';
6
3
  import { cmdInit } from './commands/init.js';
7
4
  import { cmdStatus } from './commands/status.js';
8
5
  import { cmdHistory } from './commands/hist.js';
@@ -20,7 +17,7 @@ const program = new Command();
20
17
  program
21
18
  .name('pmpt')
22
19
  .description('pmpt — Record and share your AI-driven product development journey')
23
- .version('1.4.0')
20
+ .version('1.4.1')
24
21
  .addHelpText('after', `
25
22
  Examples:
26
23
  $ pmpt init Initialize project
@@ -80,23 +77,6 @@ program
80
77
  .description('Quick product planning with 5 questions — auto-generate AI prompt')
81
78
  .option('--reset', 'Restart plan from scratch')
82
79
  .action(cmdPlan);
83
- // Contribution commands
84
- program
85
- .command('new')
86
- .description('Create new document interactively')
87
- .action(cmdNew);
88
- program
89
- .command('validate <file>')
90
- .description('Validate document frontmatter and content')
91
- .action((file) => {
92
- const ok = cmdValidate(file);
93
- if (!ok)
94
- process.exit(1);
95
- });
96
- program
97
- .command('submit <file>')
98
- .description('Submit document via Fork → Branch → PR')
99
- .action(cmdSubmit);
100
80
  program
101
81
  .command('logout')
102
82
  .description('Clear saved GitHub authentication')
package/dist/lib/api.js CHANGED
@@ -3,11 +3,22 @@
3
3
  */
4
4
  const API_BASE = 'https://pmptwiki-api.sin2da.workers.dev';
5
5
  const R2_PUBLIC_URL = 'https://pub-ce73b2410943490d80b60ddad9243d31.r2.dev';
6
- export async function registerAuth(githubToken) {
7
- const res = await fetch(`${API_BASE}/auth/register`, {
6
+ export async function requestDeviceCode() {
7
+ const res = await fetch(`${API_BASE}/auth/device`, {
8
8
  method: 'POST',
9
9
  headers: { 'Content-Type': 'application/json' },
10
- body: JSON.stringify({ githubToken }),
10
+ });
11
+ if (!res.ok) {
12
+ const err = await res.json().catch(() => ({ error: 'Device code request failed' }));
13
+ throw new Error(err.error);
14
+ }
15
+ return res.json();
16
+ }
17
+ export async function pollDeviceToken(deviceCode) {
18
+ const res = await fetch(`${API_BASE}/auth/device/token`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ deviceCode }),
11
22
  });
12
23
  if (!res.ok) {
13
24
  const err = await res.json().catch(() => ({ error: 'Auth failed' }));
package/dist/lib/auth.js CHANGED
@@ -7,7 +7,10 @@ export function loadAuth() {
7
7
  try {
8
8
  if (!existsSync(TOKEN_FILE))
9
9
  return null;
10
- return JSON.parse(readFileSync(TOKEN_FILE, 'utf-8'));
10
+ const data = JSON.parse(readFileSync(TOKEN_FILE, 'utf-8'));
11
+ if (!data.token || !data.username)
12
+ return null;
13
+ return { token: data.token, username: data.username };
11
14
  }
12
15
  catch {
13
16
  return null;
@@ -5,7 +5,7 @@ import { getGitInfo, isGitRepo } from './git.js';
5
5
  import glob from 'fast-glob';
6
6
  /**
7
7
  * .pmpt/docs 폴더의 MD 파일을 스냅샷으로 저장
8
- * .history/v{N}-{timestamp}/ 폴더에 모든 파일 복사
8
+ * 변경된 파일만 복사하여 저장 공간 최적화
9
9
  */
10
10
  export function createFullSnapshot(projectPath) {
11
11
  const historyDir = getHistoryDir(projectPath);
@@ -18,20 +18,33 @@ export function createFullSnapshot(projectPath) {
18
18
  const snapshotName = `v${version}-${timestamp}`;
19
19
  const snapshotDir = join(historyDir, snapshotName);
20
20
  mkdirSync(snapshotDir, { recursive: true });
21
- // docs 폴더의 MD 파일 복사
21
+ // docs 폴더의 MD 파일 비교 후 변경분만 복사
22
22
  const files = [];
23
+ const changedFiles = [];
23
24
  if (existsSync(docsDir)) {
24
25
  const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
25
26
  for (const file of mdFiles) {
26
27
  const srcPath = join(docsDir, file);
27
- const destPath = join(snapshotDir, file);
28
- // 하위 디렉토리가 있으면 생성
29
- const destDir = join(snapshotDir, file.split('/').slice(0, -1).join('/'));
30
- if (destDir !== snapshotDir) {
31
- mkdirSync(destDir, { recursive: true });
32
- }
33
- copyFileSync(srcPath, destPath);
28
+ const newContent = readFileSync(srcPath, 'utf-8');
34
29
  files.push(file);
30
+ // 이전 버전과 비교
31
+ let hasChanged = true;
32
+ if (existing.length > 0) {
33
+ const prevContent = resolveFileContent(existing, existing.length - 1, file);
34
+ if (prevContent !== null && prevContent === newContent) {
35
+ hasChanged = false;
36
+ }
37
+ }
38
+ if (hasChanged) {
39
+ const destPath = join(snapshotDir, file);
40
+ // 하위 디렉토리가 있으면 생성
41
+ const destDir = join(snapshotDir, file.split('/').slice(0, -1).join('/'));
42
+ if (destDir !== snapshotDir) {
43
+ mkdirSync(destDir, { recursive: true });
44
+ }
45
+ copyFileSync(srcPath, destPath);
46
+ changedFiles.push(file);
47
+ }
35
48
  }
36
49
  }
37
50
  // Git 정보 수집
@@ -55,6 +68,7 @@ export function createFullSnapshot(projectPath) {
55
68
  version,
56
69
  timestamp,
57
70
  files,
71
+ changedFiles,
58
72
  git: gitData,
59
73
  }, null, 2), 'utf-8');
60
74
  return {
@@ -62,6 +76,7 @@ export function createFullSnapshot(projectPath) {
62
76
  timestamp,
63
77
  snapshotDir,
64
78
  files,
79
+ changedFiles,
65
80
  git: gitData,
66
81
  };
67
82
  }
@@ -156,6 +171,7 @@ export function getAllSnapshots(projectPath) {
156
171
  timestamp: match[2].replace(/-/g, ':'),
157
172
  snapshotDir,
158
173
  files: meta.files || [],
174
+ changedFiles: meta.changedFiles,
159
175
  git: meta.git,
160
176
  });
161
177
  }
@@ -230,3 +246,31 @@ export function getTrackedFiles(projectPath) {
230
246
  return [];
231
247
  return glob.sync('**/*.md', { cwd: docsDir });
232
248
  }
249
+ /**
250
+ * Resolve file content by walking backwards through snapshots.
251
+ * Handles optimized snapshots where unchanged files are not stored.
252
+ */
253
+ export function resolveFileContent(snapshots, fromIndex, fileName) {
254
+ for (let i = fromIndex; i >= 0; i--) {
255
+ const filePath = join(snapshots[i].snapshotDir, fileName);
256
+ if (existsSync(filePath)) {
257
+ return readFileSync(filePath, 'utf-8');
258
+ }
259
+ }
260
+ return null;
261
+ }
262
+ /**
263
+ * Resolve all file contents for a specific snapshot version.
264
+ * Reconstructs the full file set by walking backwards through history.
265
+ */
266
+ export function resolveFullSnapshot(snapshots, targetIndex) {
267
+ const target = snapshots[targetIndex];
268
+ const files = {};
269
+ for (const fileName of target.files) {
270
+ const content = resolveFileContent(snapshots, targetIndex, fileName);
271
+ if (content !== null) {
272
+ files[fileName] = content;
273
+ }
274
+ }
275
+ return files;
276
+ }
@@ -19,6 +19,7 @@ const GitInfoSchema = z.object({
19
19
  const VersionSchema = z.object({
20
20
  version: z.number().min(1),
21
21
  timestamp: z.string(),
22
+ summary: z.string().optional(),
22
23
  files: z.record(z.string(), z.string()), // filename -> content
23
24
  git: GitInfoSchema,
24
25
  });
@@ -42,6 +43,7 @@ const MetaSchema = z.object({
42
43
  export const PmptFileSchema = z.object({
43
44
  schemaVersion: z.string(),
44
45
  cliMinVersion: z.string().optional(),
46
+ guide: z.string().optional(),
45
47
  meta: MetaSchema,
46
48
  plan: PlanSchema,
47
49
  docs: z.record(z.string(), z.string()).optional(), // current docs
@@ -90,17 +92,87 @@ export function validatePmptFile(content) {
90
92
  data: result.data,
91
93
  };
92
94
  }
95
+ /**
96
+ * AI guide text embedded in every .pmpt file.
97
+ * Helps AI models understand the project's development journey.
98
+ */
99
+ const AI_GUIDE = [
100
+ 'This is a .pmpt file — a complete record of a product built with AI.',
101
+ '',
102
+ 'How to read this file:',
103
+ '- "plan" contains the original intent: what the creator wanted to build, key features, and tech preferences.',
104
+ '- "history" is an ordered array of snapshots (v1, v2, v3...). Each snapshot captures every tracked file at that point in time.',
105
+ '- To understand the evolution, compare files across versions sequentially. Look for what was added, removed, or rewritten between each version.',
106
+ '- "docs" contains the latest working documents (plan.md for the product plan, pmpt.md for the AI prompt used).',
107
+ '- "git" fields in each version link snapshots to source code commits, showing when code changes happened alongside document changes.',
108
+ '',
109
+ 'Key insight: The value of this file is not just the final result — it is the journey. The sequence of iterations reveals how decisions were made, what was tried, and how the product evolved through AI-assisted development.',
110
+ ].join('\n');
111
+ /**
112
+ * Generate a short summary describing what changed between two versions.
113
+ */
114
+ function generateVersionSummary(version, prevVersion) {
115
+ if (!prevVersion) {
116
+ const fileCount = Object.keys(version.files).length;
117
+ const fileNames = Object.keys(version.files).join(', ');
118
+ return `Initial version with ${fileCount} file(s): ${fileNames}`;
119
+ }
120
+ const prevFiles = new Set(Object.keys(prevVersion.files));
121
+ const currFiles = new Set(Object.keys(version.files));
122
+ const added = [...currFiles].filter(f => !prevFiles.has(f));
123
+ const removed = [...prevFiles].filter(f => !currFiles.has(f));
124
+ const shared = [...currFiles].filter(f => prevFiles.has(f));
125
+ const modified = shared.filter(f => version.files[f] !== prevVersion.files[f]);
126
+ const parts = [];
127
+ if (added.length > 0) {
128
+ parts.push(`Added ${added.join(', ')}`);
129
+ }
130
+ if (removed.length > 0) {
131
+ parts.push(`Removed ${removed.join(', ')}`);
132
+ }
133
+ if (modified.length > 0) {
134
+ // Calculate total line changes for modified files
135
+ let totalAdded = 0;
136
+ let totalRemoved = 0;
137
+ for (const f of modified) {
138
+ const oldLines = prevVersion.files[f].split('\n');
139
+ const newLines = version.files[f].split('\n');
140
+ totalAdded += Math.max(0, newLines.length - oldLines.length);
141
+ totalRemoved += Math.max(0, oldLines.length - newLines.length);
142
+ }
143
+ let detail = `Modified ${modified.join(', ')}`;
144
+ if (totalAdded > 0 || totalRemoved > 0) {
145
+ const changes = [];
146
+ if (totalAdded > 0)
147
+ changes.push(`+${totalAdded} lines`);
148
+ if (totalRemoved > 0)
149
+ changes.push(`-${totalRemoved} lines`);
150
+ detail += ` (${changes.join(', ')})`;
151
+ }
152
+ parts.push(detail);
153
+ }
154
+ if (parts.length === 0) {
155
+ return 'No changes detected';
156
+ }
157
+ return parts.join('. ');
158
+ }
93
159
  /**
94
160
  * Create .pmpt file content from project data
95
161
  */
96
162
  export function createPmptFile(meta, plan, docs, history) {
163
+ // Auto-generate summaries for each version
164
+ const historyWithSummary = history.map((version, i) => ({
165
+ ...version,
166
+ summary: version.summary || generateVersionSummary(version, i > 0 ? history[i - 1] : null),
167
+ }));
97
168
  const pmptFile = {
98
169
  schemaVersion: SCHEMA_VERSION,
99
170
  cliMinVersion: '1.3.0',
171
+ guide: AI_GUIDE,
100
172
  meta,
101
173
  plan,
102
174
  docs,
103
- history,
175
+ history: historyWithSummary,
104
176
  };
105
177
  return JSON.stringify(pmptFile, null, 2);
106
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "commander": "^12.0.0",
41
41
  "fast-glob": "^3.3.0",
42
42
  "gray-matter": "^4.0.3",
43
+ "open": "^11.0.0",
43
44
  "zod": "^3.22.0"
44
45
  },
45
46
  "devDependencies": {