pmpt-cli 1.0.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 +58 -0
- package/dist/commands/hist.js +90 -0
- package/dist/commands/init.js +130 -0
- package/dist/commands/new.js +78 -0
- package/dist/commands/plan.js +158 -0
- package/dist/commands/save.js +48 -0
- package/dist/commands/squash.js +75 -0
- package/dist/commands/status.js +35 -0
- package/dist/commands/submit.js +103 -0
- package/dist/commands/validate.js +23 -0
- package/dist/commands/watch.js +33 -0
- package/dist/index.js +93 -0
- package/dist/lib/auth.js +24 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/git.js +102 -0
- package/dist/lib/github.js +81 -0
- package/dist/lib/history.js +233 -0
- package/dist/lib/plan.js +173 -0
- package/dist/lib/schema.js +61 -0
- package/dist/lib/template.js +37 -0
- package/dist/lib/watcher.js +63 -0
- package/dist/types.js +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# promptwiki-cli
|
|
2
|
+
|
|
3
|
+
CLI tool for contributing to [PromptWiki](https://pmptwiki.com) — a community-driven guide to working with AI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g promptwiki-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Create a new document
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
promptwiki new
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Interactive prompts will guide you through creating a properly structured markdown file.
|
|
20
|
+
|
|
21
|
+
### Validate a document
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
promptwiki validate ko/guide/beginner/my-guide.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Checks frontmatter schema, required fields, and minimum content length before submission.
|
|
28
|
+
|
|
29
|
+
### Submit via Pull Request
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
promptwiki submit ko/guide/beginner/my-guide.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Automatically forks `promptwiki/content`, creates a branch, and opens a Pull Request. Requires a GitHub Personal Access Token with `repo` scope (you'll be prompted on first use).
|
|
36
|
+
|
|
37
|
+
### Logout
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
promptwiki logout
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Removes stored GitHub credentials from `~/.config/promptwiki/auth.json`.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
promptwiki new # create file interactively
|
|
49
|
+
# edit the file in your editor
|
|
50
|
+
promptwiki validate # check before submitting
|
|
51
|
+
promptwiki submit # fork → branch → PR
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Once your PR is merged into `promptwiki/content`, the site at pmptwiki.com updates automatically.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { isInitialized } from '../lib/config.js';
|
|
5
|
+
import { getAllSnapshots } from '../lib/history.js';
|
|
6
|
+
// Simple diff calculation: count changed lines
|
|
7
|
+
function calculateDiffSize(oldContent, newContent) {
|
|
8
|
+
const oldLines = oldContent.split('\n');
|
|
9
|
+
const newLines = newContent.split('\n');
|
|
10
|
+
let changes = 0;
|
|
11
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
12
|
+
for (let i = 0; i < maxLen; i++) {
|
|
13
|
+
if (oldLines[i] !== newLines[i]) {
|
|
14
|
+
changes++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return changes;
|
|
18
|
+
}
|
|
19
|
+
// Get total diff between two snapshots
|
|
20
|
+
function getSnapshotDiff(prev, curr) {
|
|
21
|
+
let totalChanges = 0;
|
|
22
|
+
const allFiles = new Set([...prev.files, ...curr.files]);
|
|
23
|
+
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') : '';
|
|
28
|
+
totalChanges += calculateDiffSize(prevContent, currContent);
|
|
29
|
+
}
|
|
30
|
+
return totalChanges;
|
|
31
|
+
}
|
|
32
|
+
export function cmdHistory(path, options) {
|
|
33
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
34
|
+
if (!isInitialized(projectPath)) {
|
|
35
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
39
|
+
if (snapshots.length === 0) {
|
|
40
|
+
p.intro('PromptWiki — Version History');
|
|
41
|
+
p.log.warn('No snapshots saved yet.');
|
|
42
|
+
p.log.info('Save snapshots with pmpt save or pmpt watch.');
|
|
43
|
+
p.outro('');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// In compact mode, filter out versions with minimal changes
|
|
47
|
+
let displaySnapshots = snapshots;
|
|
48
|
+
const hiddenVersions = [];
|
|
49
|
+
if (options?.compact && snapshots.length > 1) {
|
|
50
|
+
displaySnapshots = [snapshots[0]]; // Always show first
|
|
51
|
+
for (let i = 1; i < snapshots.length; i++) {
|
|
52
|
+
const diffSize = getSnapshotDiff(snapshots[i - 1], snapshots[i]);
|
|
53
|
+
// Threshold: hide if less than 5 lines changed
|
|
54
|
+
if (diffSize < 5) {
|
|
55
|
+
hiddenVersions.push(snapshots[i].version);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
displaySnapshots.push(snapshots[i]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const title = options?.compact
|
|
63
|
+
? `PromptWiki — Version History (${displaySnapshots.length} shown, ${hiddenVersions.length} hidden)`
|
|
64
|
+
: `PromptWiki — Version History (${snapshots.length} total)`;
|
|
65
|
+
p.intro(title);
|
|
66
|
+
for (const snapshot of displaySnapshots) {
|
|
67
|
+
const dateStr = new Date(snapshot.timestamp).toLocaleString(undefined, {
|
|
68
|
+
month: '2-digit',
|
|
69
|
+
day: '2-digit',
|
|
70
|
+
hour: '2-digit',
|
|
71
|
+
minute: '2-digit',
|
|
72
|
+
});
|
|
73
|
+
let header = `v${snapshot.version} — ${dateStr}`;
|
|
74
|
+
if (snapshot.git) {
|
|
75
|
+
header += ` · ${snapshot.git.commit}`;
|
|
76
|
+
if (snapshot.git.dirty)
|
|
77
|
+
header += ' (dirty)';
|
|
78
|
+
}
|
|
79
|
+
const files = snapshot.files.map((f) => ` - ${f}`).join('\n');
|
|
80
|
+
p.note(files || ' (no files)', header);
|
|
81
|
+
}
|
|
82
|
+
if (options?.compact && hiddenVersions.length > 0) {
|
|
83
|
+
p.log.info(`Hidden versions (minor changes): ${hiddenVersions.map(v => `v${v}`).join(', ')}`);
|
|
84
|
+
p.log.info('Run without --compact to see all versions.');
|
|
85
|
+
}
|
|
86
|
+
if (!options?.compact && snapshots.length > 3) {
|
|
87
|
+
p.log.info('Tip: Use --compact to hide minor changes, or "pmpt squash v1 v3" to merge.');
|
|
88
|
+
}
|
|
89
|
+
p.outro('');
|
|
90
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { initializeProject, isInitialized } from '../lib/config.js';
|
|
5
|
+
import { isGitRepo, getGitInfo, formatGitInfo } from '../lib/git.js';
|
|
6
|
+
import { cmdPlan } from './plan.js';
|
|
7
|
+
export async function cmdInit(path, options) {
|
|
8
|
+
p.intro('PromptWiki — Project Initialization');
|
|
9
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
10
|
+
if (!existsSync(projectPath)) {
|
|
11
|
+
p.outro(`Path does not exist: ${projectPath}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
if (isInitialized(projectPath)) {
|
|
15
|
+
p.outro(`Project already initialized: ${projectPath}`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
// Detect Git repository
|
|
19
|
+
const isGit = isGitRepo(projectPath);
|
|
20
|
+
let repoUrl = options?.repo;
|
|
21
|
+
let gitInfo = null;
|
|
22
|
+
if (isGit) {
|
|
23
|
+
gitInfo = getGitInfo(projectPath, repoUrl);
|
|
24
|
+
if (gitInfo?.repo && !repoUrl) {
|
|
25
|
+
repoUrl = gitInfo.repo;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Build confirmation message
|
|
29
|
+
const confirmMessage = [
|
|
30
|
+
`Track AI conversation history in this folder?`,
|
|
31
|
+
` Path: ${projectPath}`,
|
|
32
|
+
];
|
|
33
|
+
if (isGit && gitInfo) {
|
|
34
|
+
confirmMessage.push(` Git: ${formatGitInfo(gitInfo)}`);
|
|
35
|
+
if (repoUrl) {
|
|
36
|
+
confirmMessage.push(` Repository: ${repoUrl}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const confirm = await p.confirm({
|
|
40
|
+
message: confirmMessage.join('\n'),
|
|
41
|
+
initialValue: true,
|
|
42
|
+
});
|
|
43
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
44
|
+
p.cancel('Cancelled');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
// If Git repo but no repoUrl, suggest connecting
|
|
48
|
+
if (isGit && !repoUrl) {
|
|
49
|
+
p.log.info(`Tip: Connect a GitHub repo with --repo for more features!`);
|
|
50
|
+
p.log.message(` • Auto-record commit hash for each version`);
|
|
51
|
+
p.log.message(` • Create PRs directly with pmpt submit`);
|
|
52
|
+
p.log.message(` • Others can reproduce exact code states`);
|
|
53
|
+
p.log.message('');
|
|
54
|
+
const repoChoice = await p.select({
|
|
55
|
+
message: 'Connect GitHub repository?',
|
|
56
|
+
options: [
|
|
57
|
+
{ value: 'now', label: 'Connect now', hint: 'Enter repository URL' },
|
|
58
|
+
{ value: 'later', label: 'Connect later', hint: 'Re-run with pmpt init --repo <url>' },
|
|
59
|
+
{ value: 'skip', label: 'Skip', hint: 'Use Git tracking only' },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
if (p.isCancel(repoChoice)) {
|
|
63
|
+
p.cancel('Cancelled');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (repoChoice === 'now') {
|
|
67
|
+
const inputRepo = await p.text({
|
|
68
|
+
message: 'Enter GitHub repository URL',
|
|
69
|
+
placeholder: 'https://github.com/username/repo',
|
|
70
|
+
validate: (value) => {
|
|
71
|
+
if (!value)
|
|
72
|
+
return 'Please enter repository URL';
|
|
73
|
+
if (!value.includes('github.com'))
|
|
74
|
+
return 'Please enter a GitHub URL';
|
|
75
|
+
return undefined;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
if (!p.isCancel(inputRepo) && inputRepo) {
|
|
79
|
+
repoUrl = inputRepo;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const s = p.spinner();
|
|
84
|
+
s.start('Initializing project...');
|
|
85
|
+
try {
|
|
86
|
+
const config = initializeProject(projectPath, {
|
|
87
|
+
repo: repoUrl,
|
|
88
|
+
trackGit: isGit,
|
|
89
|
+
});
|
|
90
|
+
s.stop('Initialized');
|
|
91
|
+
const notes = [
|
|
92
|
+
`Path: ${config.projectPath}`,
|
|
93
|
+
'',
|
|
94
|
+
'Folder structure:',
|
|
95
|
+
' .promptwiki/',
|
|
96
|
+
' ├── config.json Config file',
|
|
97
|
+
' ├── pmpt/ Working folder (MD files)',
|
|
98
|
+
' └── .history/ Version history',
|
|
99
|
+
];
|
|
100
|
+
if (config.repo) {
|
|
101
|
+
notes.push('', `Git repository: ${config.repo}`);
|
|
102
|
+
}
|
|
103
|
+
if (config.trackGit) {
|
|
104
|
+
notes.push(`Git tracking: Enabled`);
|
|
105
|
+
}
|
|
106
|
+
notes.push('', 'Get started with:');
|
|
107
|
+
notes.push(' pmpt plan # Start product planning');
|
|
108
|
+
notes.push(' pmpt save # Save current state snapshot');
|
|
109
|
+
notes.push(' pmpt watch # Auto-detect file changes');
|
|
110
|
+
notes.push(' pmpt history # View version history');
|
|
111
|
+
p.note(notes.join('\n'), 'Project Info');
|
|
112
|
+
// Ask to start plan mode
|
|
113
|
+
const startPlan = await p.confirm({
|
|
114
|
+
message: 'Start plan mode? (Recommended for first-timers!)',
|
|
115
|
+
initialValue: true,
|
|
116
|
+
});
|
|
117
|
+
if (!p.isCancel(startPlan) && startPlan) {
|
|
118
|
+
p.log.message('');
|
|
119
|
+
await cmdPlan(projectPath);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
p.outro('PromptWiki project initialized');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
s.stop('Initialization failed');
|
|
127
|
+
p.log.error(error.message);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import { generateContent, generateFilePath } from '../lib/template.js';
|
|
5
|
+
export async function cmdNew() {
|
|
6
|
+
p.intro('PromptWiki — 새 문서 만들기');
|
|
7
|
+
const answers = await p.group({
|
|
8
|
+
lang: () => p.select({
|
|
9
|
+
message: '언어를 선택하세요',
|
|
10
|
+
options: [
|
|
11
|
+
{ value: 'ko', label: '한국어 (ko)' },
|
|
12
|
+
{ value: 'en', label: 'English (en)' },
|
|
13
|
+
],
|
|
14
|
+
}),
|
|
15
|
+
purpose: () => p.select({
|
|
16
|
+
message: '문서 유형을 선택하세요',
|
|
17
|
+
options: [
|
|
18
|
+
{ value: 'guide', label: '가이드', hint: '개념 설명 + 방법' },
|
|
19
|
+
{ value: 'rule', label: '규칙', hint: '해야 할 것 / 하지 말 것' },
|
|
20
|
+
{ value: 'template', label: '템플릿', hint: '복사해서 쓰는 프롬프트' },
|
|
21
|
+
{ value: 'example', label: '사례', hint: '실제 사용 사례' },
|
|
22
|
+
{ value: 'reference', label: '레퍼런스', hint: '참고 자료 모음' },
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
level: () => p.select({
|
|
26
|
+
message: '난이도를 선택하세요',
|
|
27
|
+
options: [
|
|
28
|
+
{ value: 'beginner', label: '입문' },
|
|
29
|
+
{ value: 'intermediate', label: '중급' },
|
|
30
|
+
{ value: 'advanced', label: '고급' },
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
title: () => p.text({
|
|
34
|
+
message: '제목을 입력하세요',
|
|
35
|
+
placeholder: 'AI에게 충분한 배경을 주면 답변이 달라진다',
|
|
36
|
+
validate: (v) => (v.trim().length < 5 ? '5자 이상 입력해주세요' : undefined),
|
|
37
|
+
}),
|
|
38
|
+
tags: () => p.text({
|
|
39
|
+
message: '태그를 입력하세요 (쉼표 구분, 선택)',
|
|
40
|
+
placeholder: 'context, beginner, prompt',
|
|
41
|
+
}),
|
|
42
|
+
persona: () => p.multiselect({
|
|
43
|
+
message: '대상 독자를 선택하세요 (선택)',
|
|
44
|
+
options: [
|
|
45
|
+
{ value: 'general', label: '일반' },
|
|
46
|
+
{ value: 'power-user', label: '파워유저' },
|
|
47
|
+
{ value: 'developer', label: '개발자' },
|
|
48
|
+
{ value: 'organization', label: '조직' },
|
|
49
|
+
],
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
52
|
+
}, {
|
|
53
|
+
onCancel: () => {
|
|
54
|
+
p.cancel('취소되었습니다');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const fm = {
|
|
59
|
+
title: answers.title,
|
|
60
|
+
purpose: answers.purpose,
|
|
61
|
+
level: answers.level,
|
|
62
|
+
lang: answers.lang,
|
|
63
|
+
tags: answers.tags
|
|
64
|
+
? answers.tags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
65
|
+
: [],
|
|
66
|
+
persona: answers.persona.length ? answers.persona : undefined,
|
|
67
|
+
};
|
|
68
|
+
const filePath = generateFilePath(fm);
|
|
69
|
+
const content = generateContent(fm);
|
|
70
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
72
|
+
p.outro(`파일이 생성되었습니다: ${filePath}
|
|
73
|
+
|
|
74
|
+
다음 단계:
|
|
75
|
+
1. 파일을 열어 본문을 작성하세요
|
|
76
|
+
2. promptwiki validate ${filePath}
|
|
77
|
+
3. promptwiki submit ${filePath}`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { isInitialized } from '../lib/config.js';
|
|
5
|
+
import { cmdWatch } from './watch.js';
|
|
6
|
+
import { PLAN_QUESTIONS, getPlanProgress, initPlanProgress, savePlanProgress, savePlanDocuments, } from '../lib/plan.js';
|
|
7
|
+
export async function cmdPlan(path, options) {
|
|
8
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
9
|
+
// Check initialization
|
|
10
|
+
if (!isInitialized(projectPath)) {
|
|
11
|
+
p.intro('PromptWiki Plan');
|
|
12
|
+
p.log.error('Project not initialized.');
|
|
13
|
+
p.log.info('Run `pmpt init` first to initialize the project.');
|
|
14
|
+
p.outro('');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
// Reset option
|
|
18
|
+
if (options?.reset) {
|
|
19
|
+
const confirm = await p.confirm({
|
|
20
|
+
message: 'Restart plan from scratch?',
|
|
21
|
+
initialValue: false,
|
|
22
|
+
});
|
|
23
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
24
|
+
p.cancel('Cancelled');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
initPlanProgress(projectPath);
|
|
28
|
+
}
|
|
29
|
+
// Check progress
|
|
30
|
+
let progress = getPlanProgress(projectPath);
|
|
31
|
+
// If already completed
|
|
32
|
+
if (progress?.completed) {
|
|
33
|
+
p.intro('PromptWiki Plan');
|
|
34
|
+
p.log.success('Plan already completed.');
|
|
35
|
+
const action = await p.select({
|
|
36
|
+
message: 'What would you like to do?',
|
|
37
|
+
options: [
|
|
38
|
+
{ value: 'view', label: 'View AI prompt', hint: 'Copy and paste to AI' },
|
|
39
|
+
{ value: 'restart', label: 'Restart plan', hint: 'Start fresh' },
|
|
40
|
+
{ value: 'watch', label: 'Start file watching', hint: 'pmpt watch' },
|
|
41
|
+
{ value: 'exit', label: 'Exit' },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(action) || action === 'exit') {
|
|
45
|
+
p.outro('See you next time!');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
if (action === 'view') {
|
|
49
|
+
// Read pmpt.md from pmpt folder
|
|
50
|
+
const { getPmptDir } = await import('../lib/config.js');
|
|
51
|
+
const { existsSync } = await import('fs');
|
|
52
|
+
const { join } = await import('path');
|
|
53
|
+
const pmptDir = getPmptDir(projectPath);
|
|
54
|
+
const promptPath = join(pmptDir, 'pmpt.md');
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(promptPath)) {
|
|
57
|
+
const content = readFileSync(promptPath, 'utf-8');
|
|
58
|
+
p.log.message('');
|
|
59
|
+
p.log.info('=== AI Prompt (copy the content below) ===');
|
|
60
|
+
p.log.message('');
|
|
61
|
+
console.log(content);
|
|
62
|
+
p.log.message('');
|
|
63
|
+
p.log.info(`File location: ${promptPath}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
p.log.error('AI prompt file not found.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
p.log.error('Failed to read AI prompt file.');
|
|
71
|
+
}
|
|
72
|
+
p.outro('');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (action === 'restart') {
|
|
76
|
+
initPlanProgress(projectPath);
|
|
77
|
+
progress = getPlanProgress(projectPath);
|
|
78
|
+
}
|
|
79
|
+
if (action === 'watch') {
|
|
80
|
+
await cmdWatch(projectPath);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Starting fresh
|
|
85
|
+
if (!progress) {
|
|
86
|
+
progress = initPlanProgress(projectPath);
|
|
87
|
+
}
|
|
88
|
+
p.intro('PromptWiki Plan — Quick Product Planning');
|
|
89
|
+
p.log.info('Answer 6 questions to generate an AI-ready prompt.');
|
|
90
|
+
p.log.message('You can answer in any language you prefer.');
|
|
91
|
+
p.log.message('');
|
|
92
|
+
const answers = {};
|
|
93
|
+
// Ask questions
|
|
94
|
+
for (let i = 0; i < PLAN_QUESTIONS.length; i++) {
|
|
95
|
+
const question = PLAN_QUESTIONS[i];
|
|
96
|
+
const questionNum = `[${i + 1}/${PLAN_QUESTIONS.length}]`;
|
|
97
|
+
const answer = await p.text({
|
|
98
|
+
message: `${questionNum} ${question.question}`,
|
|
99
|
+
placeholder: question.placeholder,
|
|
100
|
+
validate: question.required
|
|
101
|
+
? (value) => (!value ? 'This field is required' : undefined)
|
|
102
|
+
: undefined,
|
|
103
|
+
});
|
|
104
|
+
if (p.isCancel(answer)) {
|
|
105
|
+
p.cancel('Continue later with `pmpt plan`');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
answers[question.key] = answer;
|
|
109
|
+
}
|
|
110
|
+
// Generate documents
|
|
111
|
+
const s = p.spinner();
|
|
112
|
+
s.start('Generating documents...');
|
|
113
|
+
const { planPath, promptPath } = savePlanDocuments(projectPath, answers);
|
|
114
|
+
// Update progress
|
|
115
|
+
progress.completed = true;
|
|
116
|
+
progress.answers = answers;
|
|
117
|
+
savePlanProgress(projectPath, progress);
|
|
118
|
+
s.stop('Done!');
|
|
119
|
+
// Show results
|
|
120
|
+
p.log.message('');
|
|
121
|
+
p.log.success('Plan completed!');
|
|
122
|
+
p.log.message('');
|
|
123
|
+
p.log.info(`Plan document: ${planPath}`);
|
|
124
|
+
p.log.info(`AI prompt: ${promptPath}`);
|
|
125
|
+
p.log.message('');
|
|
126
|
+
// Preview AI prompt
|
|
127
|
+
const showPrompt = await p.confirm({
|
|
128
|
+
message: 'View AI prompt now?',
|
|
129
|
+
initialValue: true,
|
|
130
|
+
});
|
|
131
|
+
if (!p.isCancel(showPrompt) && showPrompt) {
|
|
132
|
+
const content = readFileSync(promptPath, 'utf-8');
|
|
133
|
+
p.log.message('');
|
|
134
|
+
p.log.info('=== AI Prompt (copy the content below) ===');
|
|
135
|
+
p.log.message('');
|
|
136
|
+
console.log(content);
|
|
137
|
+
p.log.message('');
|
|
138
|
+
}
|
|
139
|
+
// Next steps
|
|
140
|
+
p.log.message('');
|
|
141
|
+
p.log.step('Next steps:');
|
|
142
|
+
p.log.message('1. Copy the AI prompt above and paste to Claude/ChatGPT');
|
|
143
|
+
p.log.message('2. Build your product with AI');
|
|
144
|
+
p.log.message('3. Save snapshots with pmpt save (or pmpt watch for auto-save)');
|
|
145
|
+
p.log.message('');
|
|
146
|
+
const startWatch = await p.confirm({
|
|
147
|
+
message: 'Start file watching? (pmpt watch)',
|
|
148
|
+
initialValue: false,
|
|
149
|
+
});
|
|
150
|
+
if (!p.isCancel(startWatch) && startWatch) {
|
|
151
|
+
p.log.message('');
|
|
152
|
+
await cmdWatch(projectPath);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
p.log.info('Start watching later with `pmpt watch`');
|
|
156
|
+
p.outro('Good luck!');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync, statSync } from 'fs';
|
|
4
|
+
import { isInitialized, getPmptDir } from '../lib/config.js';
|
|
5
|
+
import { createFullSnapshot, getTrackedFiles } from '../lib/history.js';
|
|
6
|
+
export async function cmdSave(fileOrPath) {
|
|
7
|
+
const projectPath = fileOrPath && existsSync(fileOrPath) && statSync(fileOrPath).isDirectory()
|
|
8
|
+
? resolve(fileOrPath)
|
|
9
|
+
: process.cwd();
|
|
10
|
+
if (!isInitialized(projectPath)) {
|
|
11
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
p.intro('PromptWiki — Save Snapshot');
|
|
15
|
+
const pmptDir = getPmptDir(projectPath);
|
|
16
|
+
const files = getTrackedFiles(projectPath);
|
|
17
|
+
if (files.length === 0) {
|
|
18
|
+
p.log.warn('No files to save.');
|
|
19
|
+
p.log.info(`pmpt folder: ${pmptDir}`);
|
|
20
|
+
p.log.info('Start with `pmpt plan` or add MD files to the pmpt folder.');
|
|
21
|
+
p.outro('');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const s = p.spinner();
|
|
25
|
+
s.start(`Creating snapshot of ${files.length} file(s)...`);
|
|
26
|
+
try {
|
|
27
|
+
const entry = createFullSnapshot(projectPath);
|
|
28
|
+
s.stop('Snapshot saved');
|
|
29
|
+
let msg = `v${entry.version} saved`;
|
|
30
|
+
if (entry.git) {
|
|
31
|
+
msg += ` · ${entry.git.commit}`;
|
|
32
|
+
if (entry.git.dirty)
|
|
33
|
+
msg += ' (uncommitted)';
|
|
34
|
+
}
|
|
35
|
+
p.log.success(msg);
|
|
36
|
+
p.log.message('');
|
|
37
|
+
p.log.info('Files included:');
|
|
38
|
+
for (const file of entry.files) {
|
|
39
|
+
p.log.message(` - ${file}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
s.stop('Save failed');
|
|
44
|
+
p.log.error(error.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
p.outro('View history: pmpt history');
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { existsSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
|
4
|
+
import { isInitialized, getHistoryDir } from '../lib/config.js';
|
|
5
|
+
import { getAllSnapshots } from '../lib/history.js';
|
|
6
|
+
export async function cmdSquash(from, to, path) {
|
|
7
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
8
|
+
if (!isInitialized(projectPath)) {
|
|
9
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
// Parse version numbers
|
|
13
|
+
const fromVersion = parseInt(from.replace(/^v/, ''), 10);
|
|
14
|
+
const toVersion = parseInt(to.replace(/^v/, ''), 10);
|
|
15
|
+
if (isNaN(fromVersion) || isNaN(toVersion)) {
|
|
16
|
+
p.log.error('Invalid version format. Use: pmpt squash v2 v3');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
if (fromVersion >= toVersion) {
|
|
20
|
+
p.log.error('First version must be less than second version.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
24
|
+
if (snapshots.length === 0) {
|
|
25
|
+
p.log.error('No snapshots found.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Find snapshots to squash
|
|
29
|
+
const toSquash = snapshots.filter(s => s.version >= fromVersion && s.version <= toVersion);
|
|
30
|
+
if (toSquash.length < 2) {
|
|
31
|
+
p.log.error(`Need at least 2 versions to squash. Found ${toSquash.length} in range v${fromVersion}-v${toVersion}.`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
p.intro('PromptWiki — Squash Versions');
|
|
35
|
+
p.log.info(`Squashing v${fromVersion} through v${toVersion} (${toSquash.length} versions)`);
|
|
36
|
+
const confirm = await p.confirm({
|
|
37
|
+
message: `This will keep v${fromVersion} and delete v${fromVersion + 1} through v${toVersion}. Continue?`,
|
|
38
|
+
initialValue: false,
|
|
39
|
+
});
|
|
40
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
41
|
+
p.cancel('Cancelled');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
const s = p.spinner();
|
|
45
|
+
s.start('Squashing versions...');
|
|
46
|
+
try {
|
|
47
|
+
const historyDir = getHistoryDir(projectPath);
|
|
48
|
+
const keepSnapshot = toSquash[0]; // Keep the first one
|
|
49
|
+
const deleteSnapshots = toSquash.slice(1); // Delete the rest
|
|
50
|
+
// Delete the snapshots we're squashing
|
|
51
|
+
for (const snapshot of deleteSnapshots) {
|
|
52
|
+
const snapshotDir = snapshot.snapshotDir;
|
|
53
|
+
if (existsSync(snapshotDir)) {
|
|
54
|
+
rmSync(snapshotDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Update metadata of kept snapshot to note squash
|
|
58
|
+
const metaPath = join(keepSnapshot.snapshotDir, '.meta.json');
|
|
59
|
+
if (existsSync(metaPath)) {
|
|
60
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
61
|
+
meta.squashedFrom = toSquash.map(s => s.version);
|
|
62
|
+
meta.squashedAt = new Date().toISOString();
|
|
63
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
s.stop('Squashed');
|
|
66
|
+
p.log.success(`Squashed v${fromVersion}-v${toVersion} into v${fromVersion}`);
|
|
67
|
+
p.log.info(`Deleted ${deleteSnapshots.length} version(s)`);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
s.stop('Squash failed');
|
|
71
|
+
p.log.error(error.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
p.outro('View history: pmpt history');
|
|
75
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { isInitialized, loadConfig } from '../lib/config.js';
|
|
4
|
+
import { getTrackedFiles, getAllSnapshots } from '../lib/history.js';
|
|
5
|
+
export function cmdStatus(path) {
|
|
6
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
7
|
+
if (!isInitialized(projectPath)) {
|
|
8
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const config = loadConfig(projectPath);
|
|
12
|
+
const tracked = getTrackedFiles(projectPath);
|
|
13
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
14
|
+
p.intro('PromptWiki — Project Status');
|
|
15
|
+
const notes = [
|
|
16
|
+
`Path: ${projectPath}`,
|
|
17
|
+
`Created: ${new Date(config.createdAt).toLocaleString()}`,
|
|
18
|
+
];
|
|
19
|
+
if (config.lastPublished) {
|
|
20
|
+
notes.push(`Last published: ${new Date(config.lastPublished).toLocaleString()}`);
|
|
21
|
+
}
|
|
22
|
+
notes.push('');
|
|
23
|
+
notes.push(`pmpt folder: .promptwiki/pmpt/`);
|
|
24
|
+
notes.push(`Snapshots: ${snapshots.length}`);
|
|
25
|
+
notes.push('');
|
|
26
|
+
notes.push(`Tracked files: ${tracked.length}`);
|
|
27
|
+
for (const f of tracked) {
|
|
28
|
+
notes.push(` - ${f}`);
|
|
29
|
+
}
|
|
30
|
+
if (tracked.length === 0) {
|
|
31
|
+
notes.push(' (none - start with pmpt plan)');
|
|
32
|
+
}
|
|
33
|
+
p.note(notes.join('\n'), 'Project Info');
|
|
34
|
+
p.outro('');
|
|
35
|
+
}
|