pmpt-cli 1.4.1 → 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 +2 -4
- package/dist/commands/export.js +7 -26
- package/dist/commands/hist.js +9 -10
- package/dist/commands/login.js +55 -23
- package/dist/commands/publish.js +23 -17
- package/dist/commands/save.js +7 -1
- package/dist/index.js +0 -20
- package/dist/lib/api.js +14 -3
- package/dist/lib/auth.js +4 -1
- package/dist/lib/history.js +53 -9
- package/dist/lib/pmptFile.js +73 -1
- package/package.json +2 -1
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
|
[](https://www.npmjs.com/package/pmpt-cli)
|
|
8
6
|
|
|
9
7
|
**Website**: [pmptwiki.com](https://pmptwiki.com)
|
|
@@ -44,7 +42,7 @@ pmpt publish
|
|
|
44
42
|
- **5 questions** — Quick product planning with AI-ready prompts
|
|
45
43
|
- **Version history** — Track every step of your AI-assisted development
|
|
46
44
|
- **Share & reproduce** — Publish projects for others to learn from and clone
|
|
47
|
-
- **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/
|
|
45
|
+
- **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/explore)
|
|
48
46
|
|
|
49
47
|
---
|
|
50
48
|
|
|
@@ -141,7 +139,7 @@ Single JSON file containing your entire development journey:
|
|
|
141
139
|
## Links
|
|
142
140
|
|
|
143
141
|
- [Website](https://pmptwiki.com)
|
|
144
|
-
- [Explore Projects](https://pmptwiki.com/
|
|
142
|
+
- [Explore Projects](https://pmptwiki.com/explore)
|
|
145
143
|
- [GitHub](https://github.com/pmptwiki/pmpt-cli)
|
|
146
144
|
- [npm](https://www.npmjs.com/package/pmpt-cli)
|
|
147
145
|
|
package/dist/commands/export.js
CHANGED
|
@@ -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 (
|
|
75
|
-
const files =
|
|
55
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
56
|
+
const files = resolveFullSnapshot(snapshots, i);
|
|
76
57
|
history.push({
|
|
77
|
-
version:
|
|
78
|
-
timestamp:
|
|
58
|
+
version: snapshots[i].version,
|
|
59
|
+
timestamp: snapshots[i].timestamp,
|
|
79
60
|
files,
|
|
80
|
-
git:
|
|
61
|
+
git: snapshots[i].git,
|
|
81
62
|
});
|
|
82
63
|
}
|
|
83
64
|
// Read current docs
|
package/dist/commands/hist.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import { resolve
|
|
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(
|
|
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
|
|
25
|
-
const
|
|
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
|
|
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);
|
package/dist/commands/login.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
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:
|
|
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
|
package/dist/commands/save.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
|
@@ -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
|
|
7
|
-
const res = await fetch(`${API_BASE}/auth/
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/lib/history.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
+
}
|
package/dist/lib/pmptFile.js
CHANGED
|
@@ -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.
|
|
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": {
|