pmpt-cli 1.3.1 → 1.4.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.
- package/README.md +40 -26
- package/dist/commands/browse.js +73 -0
- package/dist/commands/clone.js +127 -0
- package/dist/commands/login.js +46 -0
- package/dist/commands/publish.js +167 -0
- package/dist/index.js +26 -8
- package/dist/lib/api.js +46 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,9 @@ pmpt plan
|
|
|
32
32
|
# 4. Save your progress
|
|
33
33
|
pmpt save
|
|
34
34
|
|
|
35
|
-
# 5.
|
|
36
|
-
pmpt
|
|
35
|
+
# 5. Publish to pmptwiki
|
|
36
|
+
pmpt login
|
|
37
|
+
pmpt publish
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
---
|
|
@@ -42,12 +43,15 @@ pmpt export
|
|
|
42
43
|
|
|
43
44
|
- **5 questions** — Quick product planning with AI-ready prompts
|
|
44
45
|
- **Version history** — Track every step of your AI-assisted development
|
|
45
|
-
- **Share & reproduce** —
|
|
46
|
+
- **Share & reproduce** — Publish projects for others to learn from and clone
|
|
47
|
+
- **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/en/explore)
|
|
46
48
|
|
|
47
49
|
---
|
|
48
50
|
|
|
49
51
|
## Commands
|
|
50
52
|
|
|
53
|
+
### Local
|
|
54
|
+
|
|
51
55
|
| Command | Description |
|
|
52
56
|
|---------|-------------|
|
|
53
57
|
| `pmpt init` | Initialize project |
|
|
@@ -61,6 +65,33 @@ pmpt export
|
|
|
61
65
|
| `pmpt import <file>` | Import from `.pmpt` file |
|
|
62
66
|
| `pmpt status` | Check project status |
|
|
63
67
|
|
|
68
|
+
### Platform
|
|
69
|
+
|
|
70
|
+
| Command | Description |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| `pmpt login` | Authenticate with GitHub (one-time setup) |
|
|
73
|
+
| `pmpt publish` | Publish project to pmptwiki |
|
|
74
|
+
| `pmpt clone <slug>` | Clone a published project |
|
|
75
|
+
| `pmpt browse` | Browse and discover projects |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Workflow
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
[You]
|
|
83
|
+
│
|
|
84
|
+
├─ pmpt plan ─────→ 5 questions → AI prompt (clipboard)
|
|
85
|
+
│
|
|
86
|
+
├─ Build with AI ──→ Create files, iterate
|
|
87
|
+
│
|
|
88
|
+
├─ pmpt save ─────→ Save to .pmpt/.history
|
|
89
|
+
│
|
|
90
|
+
├─ pmpt publish ──→ Share on pmptwiki.com
|
|
91
|
+
│
|
|
92
|
+
└─ pmpt clone ────→ Reproduce someone's project
|
|
93
|
+
```
|
|
94
|
+
|
|
64
95
|
---
|
|
65
96
|
|
|
66
97
|
## Folder Structure
|
|
@@ -79,24 +110,6 @@ pmpt export
|
|
|
79
110
|
|
|
80
111
|
---
|
|
81
112
|
|
|
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
113
|
## .pmpt File Format
|
|
101
114
|
|
|
102
115
|
Single JSON file containing your entire development journey:
|
|
@@ -104,12 +117,12 @@ Single JSON file containing your entire development journey:
|
|
|
104
117
|
```json
|
|
105
118
|
{
|
|
106
119
|
"schemaVersion": "1.0",
|
|
107
|
-
"meta": { "projectName", "description", "createdAt" },
|
|
108
|
-
"plan": { "productIdea", "coreFeatures", "techStack" },
|
|
120
|
+
"meta": { "projectName": "", "description": "", "createdAt": "" },
|
|
121
|
+
"plan": { "productIdea": "", "coreFeatures": "", "techStack": "" },
|
|
109
122
|
"docs": { "plan.md": "...", "pmpt.md": "..." },
|
|
110
123
|
"history": [
|
|
111
|
-
{ "version": 1, "timestamp": "...", "files": {
|
|
112
|
-
{ "version": 2, "timestamp": "...", "files": {
|
|
124
|
+
{ "version": 1, "timestamp": "...", "files": {} },
|
|
125
|
+
{ "version": 2, "timestamp": "...", "files": {} }
|
|
113
126
|
]
|
|
114
127
|
}
|
|
115
128
|
```
|
|
@@ -121,13 +134,14 @@ Single JSON file containing your entire development journey:
|
|
|
121
134
|
- **Side project builders** — Track your AI-assisted development
|
|
122
135
|
- **Startup founders** — Document MVP creation process
|
|
123
136
|
- **Content creators** — Share your coding journey
|
|
124
|
-
- **Learners** —
|
|
137
|
+
- **Learners** — Browse and clone projects to study how others build with AI
|
|
125
138
|
|
|
126
139
|
---
|
|
127
140
|
|
|
128
141
|
## Links
|
|
129
142
|
|
|
130
143
|
- [Website](https://pmptwiki.com)
|
|
144
|
+
- [Explore Projects](https://pmptwiki.com/en/explore)
|
|
131
145
|
- [GitHub](https://github.com/pmptwiki/pmpt-cli)
|
|
132
146
|
- [npm](https://www.npmjs.com/package/pmpt-cli)
|
|
133
147
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { fetchProjects } from '../lib/api.js';
|
|
3
|
+
export async function cmdBrowse() {
|
|
4
|
+
p.intro('pmpt browse');
|
|
5
|
+
const s = p.spinner();
|
|
6
|
+
s.start('프로젝트 목록 불러오는 중...');
|
|
7
|
+
let projects;
|
|
8
|
+
try {
|
|
9
|
+
const index = await fetchProjects();
|
|
10
|
+
projects = index.projects;
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
s.stop('불러오기 실패');
|
|
14
|
+
p.log.error(err instanceof Error ? err.message : '프로젝트 목록을 불러올 수 없습니다.');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
s.stop(`${projects.length}개 프로젝트`);
|
|
18
|
+
if (projects.length === 0) {
|
|
19
|
+
p.log.info('아직 공개된 프로젝트가 없습니다.');
|
|
20
|
+
p.log.message(' pmpt publish — 첫 번째 프로젝트를 공유해보세요!');
|
|
21
|
+
p.outro('');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Select project
|
|
25
|
+
const selected = await p.select({
|
|
26
|
+
message: '프로젝트를 선택하세요:',
|
|
27
|
+
options: projects.map((proj) => ({
|
|
28
|
+
value: proj.slug,
|
|
29
|
+
label: proj.projectName,
|
|
30
|
+
hint: `v${proj.versionCount} · @${proj.author}${proj.description ? ` — ${proj.description.slice(0, 40)}` : ''}`,
|
|
31
|
+
})),
|
|
32
|
+
});
|
|
33
|
+
if (p.isCancel(selected)) {
|
|
34
|
+
p.cancel('');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
const project = projects.find((p) => p.slug === selected);
|
|
38
|
+
// Show details
|
|
39
|
+
p.note([
|
|
40
|
+
`Project: ${project.projectName}`,
|
|
41
|
+
`Author: @${project.author}`,
|
|
42
|
+
`Versions: ${project.versionCount}`,
|
|
43
|
+
project.description ? `Description: ${project.description}` : '',
|
|
44
|
+
project.tags.length ? `Tags: ${project.tags.join(', ')}` : '',
|
|
45
|
+
`Published: ${project.publishedAt.slice(0, 10)}`,
|
|
46
|
+
`Size: ${(project.fileSize / 1024).toFixed(1)} KB`,
|
|
47
|
+
].filter(Boolean).join('\n'), 'Project Details');
|
|
48
|
+
// Action
|
|
49
|
+
const action = await p.select({
|
|
50
|
+
message: '어떻게 할까요?',
|
|
51
|
+
options: [
|
|
52
|
+
{ value: 'clone', label: '이 프로젝트 복제', hint: 'pmpt clone' },
|
|
53
|
+
{ value: 'url', label: 'URL 표시', hint: '브라우저에서 보기' },
|
|
54
|
+
{ value: 'back', label: '돌아가기' },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(action) || action === 'back') {
|
|
58
|
+
p.outro('');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (action === 'clone') {
|
|
62
|
+
const { cmdClone } = await import('./clone.js');
|
|
63
|
+
await cmdClone(project.slug);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (action === 'url') {
|
|
67
|
+
const url = `https://pmptwiki.com/ko/p/${project.slug}`;
|
|
68
|
+
p.log.info(`URL: ${url}`);
|
|
69
|
+
p.log.message(`Download: ${project.downloadUrl}`);
|
|
70
|
+
p.log.message(`\npmpt clone ${project.slug} — 터미널에서 복제`);
|
|
71
|
+
p.outro('');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
|
|
5
|
+
import { validatePmptFile } from '../lib/pmptFile.js';
|
|
6
|
+
import { fetchPmptFile } from '../lib/api.js';
|
|
7
|
+
/**
|
|
8
|
+
* Restore history from .pmpt data (shared with import command)
|
|
9
|
+
*/
|
|
10
|
+
export function restoreHistory(historyDir, history) {
|
|
11
|
+
mkdirSync(historyDir, { recursive: true });
|
|
12
|
+
for (const version of history) {
|
|
13
|
+
const timestamp = version.timestamp.replace(/[:.]/g, '-').slice(0, 19);
|
|
14
|
+
const snapshotName = `v${version.version}-${timestamp}`;
|
|
15
|
+
const snapshotDir = join(historyDir, snapshotName);
|
|
16
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
17
|
+
for (const [filename, content] of Object.entries(version.files)) {
|
|
18
|
+
const filePath = join(snapshotDir, filename);
|
|
19
|
+
const fileDir = dirname(filePath);
|
|
20
|
+
if (fileDir !== snapshotDir) {
|
|
21
|
+
mkdirSync(fileDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
writeFileSync(join(snapshotDir, '.meta.json'), JSON.stringify({
|
|
26
|
+
version: version.version,
|
|
27
|
+
timestamp: version.timestamp,
|
|
28
|
+
files: Object.keys(version.files),
|
|
29
|
+
git: version.git,
|
|
30
|
+
}, null, 2), 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Restore docs from .pmpt data (shared with import command)
|
|
35
|
+
*/
|
|
36
|
+
export function restoreDocs(docsDir, docs) {
|
|
37
|
+
mkdirSync(docsDir, { recursive: true });
|
|
38
|
+
for (const [filename, content] of Object.entries(docs)) {
|
|
39
|
+
const filePath = join(docsDir, filename);
|
|
40
|
+
const fileDir = dirname(filePath);
|
|
41
|
+
if (fileDir !== docsDir) {
|
|
42
|
+
mkdirSync(fileDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function cmdClone(slug) {
|
|
48
|
+
if (!slug) {
|
|
49
|
+
p.log.error('slug를 입력하세요.');
|
|
50
|
+
p.log.info('사용법: pmpt clone <slug>');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
p.intro(`pmpt clone — ${slug}`);
|
|
54
|
+
const s = p.spinner();
|
|
55
|
+
s.start('프로젝트 다운로드 중...');
|
|
56
|
+
let fileContent;
|
|
57
|
+
try {
|
|
58
|
+
fileContent = await fetchPmptFile(slug);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
s.stop('다운로드 실패');
|
|
62
|
+
p.log.error(err instanceof Error ? err.message : '프로젝트를 찾을 수 없습니다.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
s.message('검증 중...');
|
|
66
|
+
const validation = validatePmptFile(fileContent);
|
|
67
|
+
if (!validation.success || !validation.data) {
|
|
68
|
+
s.stop('검증 실패');
|
|
69
|
+
p.log.error(validation.error || '잘못된 .pmpt 파일입니다.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const pmptData = validation.data;
|
|
73
|
+
s.stop('다운로드 완료');
|
|
74
|
+
// Show summary
|
|
75
|
+
p.note([
|
|
76
|
+
`Project: ${pmptData.meta.projectName}`,
|
|
77
|
+
`Versions: ${pmptData.history.length}`,
|
|
78
|
+
pmptData.meta.author ? `Author: @${pmptData.meta.author}` : '',
|
|
79
|
+
pmptData.meta.description ? `Description: ${pmptData.meta.description.slice(0, 80)}` : '',
|
|
80
|
+
].filter(Boolean).join('\n'), 'Project Info');
|
|
81
|
+
const projectPath = process.cwd();
|
|
82
|
+
if (isInitialized(projectPath)) {
|
|
83
|
+
const overwrite = await p.confirm({
|
|
84
|
+
message: '이미 초기화된 프로젝트입니다. 히스토리를 병합하시겠습니까?',
|
|
85
|
+
initialValue: true,
|
|
86
|
+
});
|
|
87
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
88
|
+
p.cancel('취소됨');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const importSpinner = p.spinner();
|
|
93
|
+
importSpinner.start('프로젝트 복원 중...');
|
|
94
|
+
if (!isInitialized(projectPath)) {
|
|
95
|
+
initializeProject(projectPath, { trackGit: true });
|
|
96
|
+
}
|
|
97
|
+
const pmptDir = getConfigDir(projectPath);
|
|
98
|
+
const historyDir = getHistoryDir(projectPath);
|
|
99
|
+
const docsDir = getDocsDir(projectPath);
|
|
100
|
+
restoreHistory(historyDir, pmptData.history);
|
|
101
|
+
if (pmptData.docs) {
|
|
102
|
+
restoreDocs(docsDir, pmptData.docs);
|
|
103
|
+
}
|
|
104
|
+
if (pmptData.plan) {
|
|
105
|
+
writeFileSync(join(pmptDir, 'plan-progress.json'), JSON.stringify({
|
|
106
|
+
completed: true,
|
|
107
|
+
startedAt: pmptData.meta.createdAt,
|
|
108
|
+
updatedAt: pmptData.meta.exportedAt,
|
|
109
|
+
answers: pmptData.plan,
|
|
110
|
+
}, null, 2), 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
let versionCount = 0;
|
|
113
|
+
if (existsSync(historyDir)) {
|
|
114
|
+
versionCount = readdirSync(historyDir).filter((d) => d.startsWith('v')).length;
|
|
115
|
+
}
|
|
116
|
+
importSpinner.stop('복원 완료!');
|
|
117
|
+
p.note([
|
|
118
|
+
`Project: ${pmptData.meta.projectName}`,
|
|
119
|
+
`Versions: ${versionCount}`,
|
|
120
|
+
`Location: ${pmptDir}`,
|
|
121
|
+
].join('\n'), 'Clone Summary');
|
|
122
|
+
p.log.info('다음 단계:');
|
|
123
|
+
p.log.message(' pmpt history — 버전 히스토리 보기');
|
|
124
|
+
p.log.message(' pmpt plan — AI 프롬프트 보기');
|
|
125
|
+
p.log.message(' pmpt save — 새 스냅샷 저장');
|
|
126
|
+
p.outro('프로젝트가 복제되었습니다!');
|
|
127
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { loadAuth, saveAuth } from '../lib/auth.js';
|
|
3
|
+
import { registerAuth } from '../lib/api.js';
|
|
4
|
+
export async function cmdLogin() {
|
|
5
|
+
p.intro('pmpt login');
|
|
6
|
+
const existing = loadAuth();
|
|
7
|
+
if (existing?.token && existing?.username) {
|
|
8
|
+
p.log.info(`Currently logged in as @${existing.username}`);
|
|
9
|
+
const reauth = await p.confirm({
|
|
10
|
+
message: 'Re-authenticate?',
|
|
11
|
+
initialValue: false,
|
|
12
|
+
});
|
|
13
|
+
if (p.isCancel(reauth) || !reauth) {
|
|
14
|
+
p.outro('');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
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
|
+
}
|
|
29
|
+
const s = p.spinner();
|
|
30
|
+
s.start('인증 중...');
|
|
31
|
+
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}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
s.stop('인증 실패');
|
|
42
|
+
p.log.error(err instanceof Error ? err.message : '인증에 실패했습니다.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
p.outro('로그인 완료! pmpt publish로 프로젝트를 공유하세요.');
|
|
46
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, basename } from 'path';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { isInitialized, loadConfig, saveConfig, getDocsDir } from '../lib/config.js';
|
|
5
|
+
import { getAllSnapshots } from '../lib/history.js';
|
|
6
|
+
import { getPlanProgress } from '../lib/plan.js';
|
|
7
|
+
import { createPmptFile } from '../lib/pmptFile.js';
|
|
8
|
+
import { loadAuth } from '../lib/auth.js';
|
|
9
|
+
import { publishProject } from '../lib/api.js';
|
|
10
|
+
import glob from 'fast-glob';
|
|
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
|
+
function readDocsFolder(docsDir) {
|
|
26
|
+
const files = {};
|
|
27
|
+
if (!existsSync(docsDir))
|
|
28
|
+
return files;
|
|
29
|
+
const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
|
|
30
|
+
for (const file of mdFiles) {
|
|
31
|
+
try {
|
|
32
|
+
files[file] = readFileSync(join(docsDir, file), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
catch { /* skip */ }
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
export async function cmdPublish(path) {
|
|
39
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
40
|
+
if (!isInitialized(projectPath)) {
|
|
41
|
+
p.log.error('프로젝트가 초기화되지 않았습니다. `pmpt init`을 먼저 실행하세요.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const auth = loadAuth();
|
|
45
|
+
if (!auth?.token || !auth?.username) {
|
|
46
|
+
p.log.error('로그인이 필요합니다. `pmpt login`을 먼저 실행하세요.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
p.intro('pmpt publish');
|
|
50
|
+
const config = loadConfig(projectPath);
|
|
51
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
52
|
+
const planProgress = getPlanProgress(projectPath);
|
|
53
|
+
if (snapshots.length === 0) {
|
|
54
|
+
p.log.warn('스냅샷이 없습니다. `pmpt save` 또는 `pmpt plan`을 먼저 실행하세요.');
|
|
55
|
+
p.outro('');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
59
|
+
// Collect publish info
|
|
60
|
+
const slug = await p.text({
|
|
61
|
+
message: '프로젝트 slug (URL에 사용될 이름):',
|
|
62
|
+
placeholder: projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'),
|
|
63
|
+
validate: (v) => {
|
|
64
|
+
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
65
|
+
return '3~50자, 소문자/숫자/하이픈만 사용 가능합니다.';
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (p.isCancel(slug)) {
|
|
70
|
+
p.cancel('취소됨');
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
const description = await p.text({
|
|
74
|
+
message: '프로젝트 설명 (짧게):',
|
|
75
|
+
placeholder: planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
76
|
+
defaultValue: planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
77
|
+
});
|
|
78
|
+
if (p.isCancel(description)) {
|
|
79
|
+
p.cancel('취소됨');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
const tagsInput = await p.text({
|
|
83
|
+
message: '태그 (쉼표로 구분):',
|
|
84
|
+
placeholder: 'react, saas, mvp',
|
|
85
|
+
defaultValue: '',
|
|
86
|
+
});
|
|
87
|
+
if (p.isCancel(tagsInput)) {
|
|
88
|
+
p.cancel('취소됨');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const tags = tagsInput
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((t) => t.trim().toLowerCase())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
// Build .pmpt content (reuse export logic)
|
|
96
|
+
const history = snapshots.map((snapshot) => ({
|
|
97
|
+
version: snapshot.version,
|
|
98
|
+
timestamp: snapshot.timestamp,
|
|
99
|
+
files: readSnapshotFiles(snapshot.snapshotDir),
|
|
100
|
+
git: snapshot.git,
|
|
101
|
+
}));
|
|
102
|
+
const docsDir = getDocsDir(projectPath);
|
|
103
|
+
const docs = readDocsFolder(docsDir);
|
|
104
|
+
const meta = {
|
|
105
|
+
projectName,
|
|
106
|
+
author: auth.username,
|
|
107
|
+
description: description,
|
|
108
|
+
createdAt: config?.createdAt || new Date().toISOString(),
|
|
109
|
+
exportedAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
const planAnswers = planProgress?.answers
|
|
112
|
+
? {
|
|
113
|
+
projectName: planProgress.answers.projectName,
|
|
114
|
+
productIdea: planProgress.answers.productIdea,
|
|
115
|
+
additionalContext: planProgress.answers.additionalContext,
|
|
116
|
+
coreFeatures: planProgress.answers.coreFeatures,
|
|
117
|
+
techStack: planProgress.answers.techStack,
|
|
118
|
+
}
|
|
119
|
+
: undefined;
|
|
120
|
+
const pmptContent = createPmptFile(meta, planAnswers, docs, history);
|
|
121
|
+
// Confirm
|
|
122
|
+
p.note([
|
|
123
|
+
`Project: ${projectName}`,
|
|
124
|
+
`Slug: ${slug}`,
|
|
125
|
+
`Versions: ${snapshots.length}`,
|
|
126
|
+
`Size: ${(pmptContent.length / 1024).toFixed(1)} KB`,
|
|
127
|
+
`Author: @${auth.username}`,
|
|
128
|
+
tags.length ? `Tags: ${tags.join(', ')}` : '',
|
|
129
|
+
].filter(Boolean).join('\n'), 'Publish Preview');
|
|
130
|
+
const confirm = await p.confirm({
|
|
131
|
+
message: '게시하시겠습니까?',
|
|
132
|
+
initialValue: true,
|
|
133
|
+
});
|
|
134
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
135
|
+
p.cancel('취소됨');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
// Upload
|
|
139
|
+
const s = p.spinner();
|
|
140
|
+
s.start('업로드 중...');
|
|
141
|
+
try {
|
|
142
|
+
const result = await publishProject(auth.token, {
|
|
143
|
+
slug: slug,
|
|
144
|
+
pmptContent,
|
|
145
|
+
description: description,
|
|
146
|
+
tags,
|
|
147
|
+
});
|
|
148
|
+
s.stop('게시 완료!');
|
|
149
|
+
// Update config
|
|
150
|
+
if (config) {
|
|
151
|
+
config.lastPublished = new Date().toISOString();
|
|
152
|
+
saveConfig(projectPath, config);
|
|
153
|
+
}
|
|
154
|
+
p.note([
|
|
155
|
+
`URL: ${result.url}`,
|
|
156
|
+
`Download: ${result.downloadUrl}`,
|
|
157
|
+
'',
|
|
158
|
+
`pmpt clone ${slug} — 다른 사람이 이 프로젝트를 복제할 수 있습니다`,
|
|
159
|
+
].join('\n'), 'Published!');
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
s.stop('게시 실패');
|
|
163
|
+
p.log.error(err instanceof Error ? err.message : '게시에 실패했습니다.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
p.outro('');
|
|
167
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,15 @@ import { cmdSave } from './commands/save.js';
|
|
|
12
12
|
import { cmdSquash } from './commands/squash.js';
|
|
13
13
|
import { cmdExport } from './commands/export.js';
|
|
14
14
|
import { cmdImport } from './commands/import.js';
|
|
15
|
+
import { cmdLogin } from './commands/login.js';
|
|
16
|
+
import { cmdPublish } from './commands/publish.js';
|
|
17
|
+
import { cmdClone } from './commands/clone.js';
|
|
18
|
+
import { cmdBrowse } from './commands/browse.js';
|
|
15
19
|
const program = new Command();
|
|
16
20
|
program
|
|
17
21
|
.name('pmpt')
|
|
18
22
|
.description('pmpt — Record and share your AI-driven product development journey')
|
|
19
|
-
.version('1.
|
|
23
|
+
.version('1.4.1')
|
|
20
24
|
.addHelpText('after', `
|
|
21
25
|
Examples:
|
|
22
26
|
$ pmpt init Initialize project
|
|
@@ -24,16 +28,13 @@ Examples:
|
|
|
24
28
|
$ pmpt save Save snapshot of docs folder
|
|
25
29
|
$ pmpt watch Auto-detect file changes
|
|
26
30
|
$ pmpt history View version history
|
|
27
|
-
$ pmpt history --compact Hide minor changes
|
|
28
31
|
$ pmpt squash v2 v5 Merge versions v2-v5 into v2
|
|
29
32
|
$ pmpt export Export as .pmpt file (single JSON)
|
|
30
33
|
$ pmpt import <file.pmpt> Import from .pmpt file
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
├── docs/ Working folder (MD files)
|
|
36
|
-
└── .history/ Version history
|
|
34
|
+
$ pmpt login Authenticate with pmptwiki
|
|
35
|
+
$ pmpt publish Publish project to pmptwiki
|
|
36
|
+
$ pmpt clone <slug> Clone a project from pmptwiki
|
|
37
|
+
$ pmpt browse Browse published projects
|
|
37
38
|
|
|
38
39
|
Documentation: https://pmptwiki.com
|
|
39
40
|
`);
|
|
@@ -104,4 +105,21 @@ program
|
|
|
104
105
|
clearAuth();
|
|
105
106
|
console.log('Logged out successfully');
|
|
106
107
|
});
|
|
108
|
+
// Platform commands
|
|
109
|
+
program
|
|
110
|
+
.command('login')
|
|
111
|
+
.description('Authenticate with pmptwiki platform')
|
|
112
|
+
.action(cmdLogin);
|
|
113
|
+
program
|
|
114
|
+
.command('publish [path]')
|
|
115
|
+
.description('Publish project to pmptwiki platform')
|
|
116
|
+
.action(cmdPublish);
|
|
117
|
+
program
|
|
118
|
+
.command('clone <slug>')
|
|
119
|
+
.description('Clone a project from pmptwiki platform')
|
|
120
|
+
.action(cmdClone);
|
|
121
|
+
program
|
|
122
|
+
.command('browse')
|
|
123
|
+
.description('Browse and search published projects')
|
|
124
|
+
.action(cmdBrowse);
|
|
107
125
|
program.parse();
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pmptwiki API client
|
|
3
|
+
*/
|
|
4
|
+
const API_BASE = 'https://pmptwiki-api.sin2da.workers.dev';
|
|
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`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
body: JSON.stringify({ githubToken }),
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
const err = await res.json().catch(() => ({ error: 'Auth failed' }));
|
|
14
|
+
throw new Error(err.error);
|
|
15
|
+
}
|
|
16
|
+
return res.json();
|
|
17
|
+
}
|
|
18
|
+
export async function publishProject(token, data) {
|
|
19
|
+
const res = await fetch(`${API_BASE}/publish`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${token}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(data),
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const err = await res.json().catch(() => ({ error: 'Publish failed' }));
|
|
29
|
+
throw new Error(err.error);
|
|
30
|
+
}
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
export async function fetchProjects() {
|
|
34
|
+
const res = await fetch(`${API_BASE}/projects`);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error('Failed to fetch projects');
|
|
37
|
+
}
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
export async function fetchPmptFile(slug) {
|
|
41
|
+
const res = await fetch(`${R2_PUBLIC_URL}/projects/${slug}.pmpt`);
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`Project not found: ${slug}`);
|
|
44
|
+
}
|
|
45
|
+
return res.text();
|
|
46
|
+
}
|