pmpt-cli 1.5.1 → 1.6.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/LICENSE +21 -0
- package/README.md +182 -59
- package/dist/commands/edit.js +99 -0
- package/dist/commands/publish.js +20 -5
- package/dist/commands/unpublish.js +62 -0
- package/dist/index.js +11 -1
- package/dist/lib/api.js +26 -0
- package/package.json +1 -3
- package/dist/commands/new.js +0 -78
- package/dist/commands/submit.js +0 -103
- package/dist/commands/validate.js +0 -23
- package/dist/lib/github.js +0 -81
- package/dist/lib/schema.js +0 -61
- package/dist/lib/template.js +0 -37
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pmptwiki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# pmpt
|
|
2
4
|
|
|
3
|
-
**
|
|
5
|
+
**Answer 5 questions. Start building with AI.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/pmpt-cli)
|
|
8
|
+
[](https://github.com/pmptwiki/pmpt-cli/blob/main/LICENSE)
|
|
9
|
+
[](https://github.com/pmptwiki/pmpt-cli)
|
|
10
|
+
|
|
11
|
+
The CLI that turns your idea into an AI-ready prompt in 30 seconds.
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
No coding required. No complex setup. Just answer 5 questions.
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
[Quick Start](#quick-start) · [Commands](#commands) · [How It Works](#how-it-works) · [Explore Projects](#explore-projects)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Demo
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ pmpt plan
|
|
25
|
+
|
|
26
|
+
┌ pmpt — Let's plan your product!
|
|
27
|
+
│
|
|
28
|
+
◆ What should we call your project?
|
|
29
|
+
│ my-budget-app
|
|
30
|
+
│
|
|
31
|
+
◆ What would you like to build with AI?
|
|
32
|
+
│ A personal budget tracking app for freelancers
|
|
33
|
+
│
|
|
34
|
+
◆ Any additional context AI should know? (optional)
|
|
35
|
+
│ Simple UI, mobile-friendly, works offline
|
|
36
|
+
│
|
|
37
|
+
◆ Key features to include?
|
|
38
|
+
│ Expense tracking; Income categories; Monthly reports; Export to CSV
|
|
39
|
+
│
|
|
40
|
+
◆ Preferred tech stack? (optional)
|
|
41
|
+
│ React, Node.js
|
|
42
|
+
│
|
|
43
|
+
└ Done! AI prompt copied to clipboard.
|
|
44
|
+
|
|
45
|
+
→ Open Claude / ChatGPT / Cursor → Ctrl+V → Start building!
|
|
46
|
+
```
|
|
8
47
|
|
|
9
48
|
---
|
|
10
49
|
|
|
@@ -14,35 +53,59 @@
|
|
|
14
53
|
npm install -g pmpt-cli
|
|
15
54
|
```
|
|
16
55
|
|
|
56
|
+
> Requires Node.js 18+
|
|
57
|
+
|
|
17
58
|
---
|
|
18
59
|
|
|
19
60
|
## Quick Start
|
|
20
61
|
|
|
21
62
|
```bash
|
|
22
|
-
# 1.
|
|
63
|
+
# 1. Install
|
|
64
|
+
npm i -g pmpt-cli
|
|
65
|
+
|
|
66
|
+
# 2. Initialize your project
|
|
23
67
|
pmpt init
|
|
24
68
|
|
|
25
|
-
#
|
|
69
|
+
# 3. Answer 5 questions → AI prompt auto-generated & copied
|
|
26
70
|
pmpt plan
|
|
27
71
|
|
|
28
|
-
#
|
|
72
|
+
# 4. Paste into Claude / ChatGPT / Cursor → Build your product!
|
|
29
73
|
|
|
30
|
-
#
|
|
74
|
+
# 5. Save your progress anytime
|
|
31
75
|
pmpt save
|
|
32
76
|
|
|
33
|
-
#
|
|
34
|
-
pmpt login
|
|
35
|
-
|
|
77
|
+
# 6. Share with the community
|
|
78
|
+
pmpt login && pmpt publish
|
|
79
|
+
|
|
80
|
+
# Bonus: Explore what others are building
|
|
81
|
+
pmpt browse
|
|
36
82
|
```
|
|
37
83
|
|
|
38
84
|
---
|
|
39
85
|
|
|
40
86
|
## Why pmpt?
|
|
41
87
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
88
|
+
| | Without pmpt | With pmpt |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| **Planning** | Stare at blank screen, write vague prompts | Answer 5 guided questions, get structured AI prompt |
|
|
91
|
+
| **Tracking** | Lose track of what you built and how | Every version auto-saved with full history |
|
|
92
|
+
| **Sharing** | Share finished code only | Share the entire journey — others can reproduce it |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## The 5 Questions
|
|
97
|
+
|
|
98
|
+
`pmpt plan` asks just 5 questions to generate a complete AI prompt:
|
|
99
|
+
|
|
100
|
+
| # | Question | Example |
|
|
101
|
+
|---|----------|---------|
|
|
102
|
+
| 1 | **Project name** | `my-budget-app` |
|
|
103
|
+
| 2 | **What to build** | `A budget tracking app for freelancers` |
|
|
104
|
+
| 3 | **Additional context** *(optional)* | `Simple UI, mobile-friendly` |
|
|
105
|
+
| 4 | **Key features** | `Expense tracking; Monthly reports; CSV export` |
|
|
106
|
+
| 5 | **Tech stack** *(optional)* | `React, Node.js` — or let AI decide |
|
|
107
|
+
|
|
108
|
+
The generated prompt is **automatically copied to your clipboard**. Just paste it into your favorite AI tool.
|
|
46
109
|
|
|
47
110
|
---
|
|
48
111
|
|
|
@@ -52,72 +115,95 @@ pmpt publish
|
|
|
52
115
|
|
|
53
116
|
| Command | Description |
|
|
54
117
|
|---------|-------------|
|
|
55
|
-
| `pmpt init` | Initialize project |
|
|
56
|
-
| `pmpt plan` | 5 questions → AI prompt (copied to clipboard) |
|
|
57
|
-
| `pmpt save` | Save current state as snapshot |
|
|
58
|
-
| `pmpt watch` | Auto-detect file changes |
|
|
118
|
+
| `pmpt init` | Initialize project and start tracking |
|
|
119
|
+
| `pmpt plan` | 5 questions → AI prompt (auto-copied to clipboard) |
|
|
120
|
+
| `pmpt save` | Save current state as a snapshot |
|
|
121
|
+
| `pmpt watch` | Auto-detect file changes and save versions |
|
|
122
|
+
| `pmpt status` | Check project status and tracked files |
|
|
59
123
|
| `pmpt history` | View version history |
|
|
60
|
-
| `pmpt
|
|
61
|
-
| `pmpt
|
|
62
|
-
| `pmpt export` | Export as `.pmpt` file (single JSON) |
|
|
124
|
+
| `pmpt squash v2 v5` | Merge versions v2–v5 into one |
|
|
125
|
+
| `pmpt export` | Export project as `.pmpt` file |
|
|
63
126
|
| `pmpt import <file>` | Import from `.pmpt` file |
|
|
64
|
-
| `pmpt status` | Check project status |
|
|
65
127
|
|
|
66
128
|
### Platform
|
|
67
129
|
|
|
68
130
|
| Command | Description |
|
|
69
131
|
|---------|-------------|
|
|
70
|
-
| `pmpt login` | Authenticate
|
|
71
|
-
| `pmpt publish` | Publish project to
|
|
72
|
-
| `pmpt
|
|
73
|
-
| `pmpt
|
|
132
|
+
| `pmpt login` | Authenticate via GitHub (one-time) |
|
|
133
|
+
| `pmpt publish` | Publish your project for others to discover |
|
|
134
|
+
| `pmpt edit` | Edit published project metadata (description, tags, category) |
|
|
135
|
+
| `pmpt unpublish` | Remove a published project from pmptwiki |
|
|
136
|
+
| `pmpt clone <slug>` | Clone and reproduce someone's project |
|
|
137
|
+
| `pmpt browse` | Browse and search published projects |
|
|
138
|
+
|
|
139
|
+
> See the full documentation at [pmptwiki.com/docs](https://pmptwiki.com/docs)
|
|
74
140
|
|
|
75
141
|
---
|
|
76
142
|
|
|
77
|
-
##
|
|
143
|
+
## How It Works
|
|
78
144
|
|
|
79
145
|
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
│
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
│
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
146
|
+
You pmpt AI Tool
|
|
147
|
+
│ │ │
|
|
148
|
+
├── pmpt init ──────────────→│ │
|
|
149
|
+
│ │ Creates .pmpt/ project │
|
|
150
|
+
│ │ │
|
|
151
|
+
├── pmpt plan ──────────────→│ │
|
|
152
|
+
│ (answer 5 questions) │ Generates AI prompt │
|
|
153
|
+
│ │ → Copied to clipboard │
|
|
154
|
+
│ │ │
|
|
155
|
+
├── Ctrl+V ─────────────────────────────────────────────────→│
|
|
156
|
+
│ │ Builds your product
|
|
157
|
+
│ │ │
|
|
158
|
+
├── pmpt save ──────────────→│ │
|
|
159
|
+
│ │ Snapshots version history │
|
|
160
|
+
│ │ │
|
|
161
|
+
├── pmpt publish ───────────→│ │
|
|
162
|
+
│ │ Shares with community │
|
|
163
|
+
│ │ │
|
|
164
|
+
└── Others: pmpt clone ─────→│ Reproduces your journey │
|
|
91
165
|
```
|
|
92
166
|
|
|
93
167
|
---
|
|
94
168
|
|
|
95
|
-
##
|
|
169
|
+
## Project Structure
|
|
96
170
|
|
|
97
171
|
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
├──
|
|
101
|
-
|
|
102
|
-
│
|
|
103
|
-
└── .
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
172
|
+
your-project/
|
|
173
|
+
└── .pmpt/
|
|
174
|
+
├── config.json # Project configuration
|
|
175
|
+
├── docs/ # Generated documents
|
|
176
|
+
│ ├── plan.md # Product plan (features checklist)
|
|
177
|
+
│ └── pmpt.md # AI-ready prompt
|
|
178
|
+
└── .history/ # Auto-saved version history
|
|
179
|
+
├── v1-2024-02-20/
|
|
180
|
+
├── v2-2024-02-21/
|
|
181
|
+
└── ...
|
|
107
182
|
```
|
|
108
183
|
|
|
109
184
|
---
|
|
110
185
|
|
|
111
186
|
## .pmpt File Format
|
|
112
187
|
|
|
113
|
-
|
|
188
|
+
A single portable file containing your entire development journey:
|
|
114
189
|
|
|
115
190
|
```json
|
|
116
191
|
{
|
|
117
192
|
"schemaVersion": "1.0",
|
|
118
|
-
"meta": {
|
|
119
|
-
|
|
120
|
-
|
|
193
|
+
"meta": {
|
|
194
|
+
"projectName": "my-budget-app",
|
|
195
|
+
"description": "Budget tracking app for freelancers",
|
|
196
|
+
"createdAt": "2024-02-20T10:00:00Z"
|
|
197
|
+
},
|
|
198
|
+
"plan": {
|
|
199
|
+
"productIdea": "A budget tracking app...",
|
|
200
|
+
"coreFeatures": "Expense tracking; Monthly reports",
|
|
201
|
+
"techStack": "React, Node.js"
|
|
202
|
+
},
|
|
203
|
+
"docs": {
|
|
204
|
+
"plan.md": "...",
|
|
205
|
+
"pmpt.md": "..."
|
|
206
|
+
},
|
|
121
207
|
"history": [
|
|
122
208
|
{ "version": 1, "timestamp": "...", "files": {} },
|
|
123
209
|
{ "version": 2, "timestamp": "...", "files": {} }
|
|
@@ -125,26 +211,63 @@ Single JSON file containing your entire development journey:
|
|
|
125
211
|
}
|
|
126
212
|
```
|
|
127
213
|
|
|
214
|
+
Share it. Clone it. Reproduce it.
|
|
215
|
+
|
|
128
216
|
---
|
|
129
217
|
|
|
130
218
|
## Use Cases
|
|
131
219
|
|
|
132
|
-
- **
|
|
133
|
-
- **Startup founders** — Document MVP creation process
|
|
134
|
-
- **Content creators** — Share your
|
|
135
|
-
- **Learners** —
|
|
220
|
+
- **Have an idea but no coding skills?** — Answer 5 questions, paste the prompt into AI, and start building
|
|
221
|
+
- **Startup founders** — Document your MVP creation process from day one
|
|
222
|
+
- **Content creators** — Share your AI-assisted building journey as reproducible content
|
|
223
|
+
- **Learners** — Clone published projects to study how others build with AI
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Explore Projects
|
|
228
|
+
|
|
229
|
+
Don't know what to build? Browse what others have created with AI.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Discover projects from the community
|
|
233
|
+
pmpt browse
|
|
234
|
+
|
|
235
|
+
# Found something interesting? Clone it and make it yours
|
|
236
|
+
pmpt clone budget-tracker-app
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**[Explore Projects on pmptwiki.com →](https://pmptwiki.com/explore)**
|
|
240
|
+
|
|
241
|
+
See how others planned their products, what prompts they used, and how their projects evolved step by step. Clone any project and use it as a starting point for your own.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Contributing
|
|
246
|
+
|
|
247
|
+
Contributions are welcome! Feel free to open an [issue](https://github.com/pmptwiki/pmpt-cli/issues) or submit a [pull request](https://github.com/pmptwiki/pmpt-cli/pulls).
|
|
136
248
|
|
|
137
249
|
---
|
|
138
250
|
|
|
139
251
|
## Links
|
|
140
252
|
|
|
141
|
-
- [Website](https://pmptwiki.com)
|
|
142
|
-
- [Explore Projects](https://pmptwiki.com/explore)
|
|
143
253
|
- [GitHub](https://github.com/pmptwiki/pmpt-cli)
|
|
144
254
|
- [npm](https://www.npmjs.com/package/pmpt-cli)
|
|
255
|
+
- [Documentation](https://pmptwiki.com/docs)
|
|
145
256
|
|
|
146
257
|
---
|
|
147
258
|
|
|
148
259
|
## License
|
|
149
260
|
|
|
150
|
-
MIT
|
|
261
|
+
[MIT](https://github.com/pmptwiki/pmpt-cli/blob/main/LICENSE)
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
<div align="center">
|
|
266
|
+
|
|
267
|
+
**If pmpt helps you build something, give it a ⭐**
|
|
268
|
+
|
|
269
|
+
[Explore what others are building →](https://pmptwiki.com/explore)
|
|
270
|
+
|
|
271
|
+
[](https://buymeacoffee.com/pmpt_cafe)
|
|
272
|
+
|
|
273
|
+
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { loadAuth } from '../lib/auth.js';
|
|
3
|
+
import { fetchProjects, editProject } from '../lib/api.js';
|
|
4
|
+
export async function cmdEdit() {
|
|
5
|
+
const auth = loadAuth();
|
|
6
|
+
if (!auth?.token || !auth?.username) {
|
|
7
|
+
p.log.error('Login required. Run `pmpt login` first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
p.intro('pmpt edit');
|
|
11
|
+
const s = p.spinner();
|
|
12
|
+
s.start('Loading your projects...');
|
|
13
|
+
let myProjects;
|
|
14
|
+
try {
|
|
15
|
+
const index = await fetchProjects();
|
|
16
|
+
myProjects = index.projects.filter((proj) => proj.author === auth.username);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
s.stop('Failed to load projects');
|
|
20
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to fetch projects.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
s.stop('Projects loaded');
|
|
24
|
+
if (myProjects.length === 0) {
|
|
25
|
+
p.log.warn('No published projects found. Run `pmpt publish` first.');
|
|
26
|
+
p.outro('');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const slug = await p.select({
|
|
30
|
+
message: 'Select a project to edit:',
|
|
31
|
+
options: myProjects.map((proj) => ({
|
|
32
|
+
value: proj.slug,
|
|
33
|
+
label: proj.slug,
|
|
34
|
+
hint: proj.description?.slice(0, 50) || '',
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
37
|
+
if (p.isCancel(slug)) {
|
|
38
|
+
p.cancel('Cancelled');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
const project = myProjects.find((proj) => proj.slug === slug);
|
|
42
|
+
const description = await p.text({
|
|
43
|
+
message: 'Description:',
|
|
44
|
+
defaultValue: project.description,
|
|
45
|
+
placeholder: project.description,
|
|
46
|
+
});
|
|
47
|
+
if (p.isCancel(description)) {
|
|
48
|
+
p.cancel('Cancelled');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
const tagsInput = await p.text({
|
|
52
|
+
message: 'Tags (comma-separated):',
|
|
53
|
+
defaultValue: project.tags.join(', '),
|
|
54
|
+
placeholder: project.tags.join(', '),
|
|
55
|
+
});
|
|
56
|
+
if (p.isCancel(tagsInput)) {
|
|
57
|
+
p.cancel('Cancelled');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
const tags = tagsInput
|
|
61
|
+
.split(',')
|
|
62
|
+
.map((t) => t.trim().toLowerCase())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
const category = await p.select({
|
|
65
|
+
message: 'Category:',
|
|
66
|
+
initialValue: project.category || 'other',
|
|
67
|
+
options: [
|
|
68
|
+
{ value: 'web-app', label: 'Web App' },
|
|
69
|
+
{ value: 'mobile-app', label: 'Mobile App' },
|
|
70
|
+
{ value: 'cli-tool', label: 'CLI Tool' },
|
|
71
|
+
{ value: 'api-backend', label: 'API/Backend' },
|
|
72
|
+
{ value: 'ai-ml', label: 'AI/ML' },
|
|
73
|
+
{ value: 'game', label: 'Game' },
|
|
74
|
+
{ value: 'library', label: 'Library' },
|
|
75
|
+
{ value: 'other', label: 'Other' },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
if (p.isCancel(category)) {
|
|
79
|
+
p.cancel('Cancelled');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
const s2 = p.spinner();
|
|
83
|
+
s2.start('Updating...');
|
|
84
|
+
try {
|
|
85
|
+
await editProject(auth.token, slug, {
|
|
86
|
+
description: description,
|
|
87
|
+
tags,
|
|
88
|
+
category: category,
|
|
89
|
+
});
|
|
90
|
+
s2.stop('Updated!');
|
|
91
|
+
p.log.success(`Project "${slug}" has been updated.`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
s2.stop('Update failed');
|
|
95
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to update project.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
p.outro('');
|
|
99
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
-
import { publishProject } from '../lib/api.js';
|
|
9
|
+
import { publishProject, fetchProjects } from '../lib/api.js';
|
|
10
10
|
import glob from 'fast-glob';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
function readDocsFolder(docsDir) {
|
|
@@ -43,10 +43,23 @@ export async function cmdPublish(path) {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
46
|
+
// Try to load existing published data for prefill
|
|
47
|
+
let existing;
|
|
48
|
+
const savedSlug = config?.lastPublishedSlug;
|
|
49
|
+
try {
|
|
50
|
+
const index = await fetchProjects();
|
|
51
|
+
if (savedSlug) {
|
|
52
|
+
existing = index.projects.find((p) => p.slug === savedSlug && p.author === auth.username);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore — first publish or offline */ }
|
|
56
|
+
const defaultSlug = savedSlug
|
|
57
|
+
|| projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
46
58
|
// Collect publish info
|
|
47
59
|
const slug = await p.text({
|
|
48
60
|
message: 'Project slug (used in URL):',
|
|
49
|
-
placeholder:
|
|
61
|
+
placeholder: defaultSlug,
|
|
62
|
+
defaultValue: savedSlug || '',
|
|
50
63
|
validate: (v) => {
|
|
51
64
|
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
52
65
|
return '3-50 chars, lowercase letters, numbers, and hyphens only.';
|
|
@@ -59,8 +72,8 @@ export async function cmdPublish(path) {
|
|
|
59
72
|
}
|
|
60
73
|
const description = await p.text({
|
|
61
74
|
message: 'Project description (brief):',
|
|
62
|
-
placeholder: planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
63
|
-
defaultValue: planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
75
|
+
placeholder: existing?.description || planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
76
|
+
defaultValue: existing?.description || planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
64
77
|
});
|
|
65
78
|
if (p.isCancel(description)) {
|
|
66
79
|
p.cancel('Cancelled');
|
|
@@ -69,7 +82,7 @@ export async function cmdPublish(path) {
|
|
|
69
82
|
const tagsInput = await p.text({
|
|
70
83
|
message: 'Tags (comma-separated):',
|
|
71
84
|
placeholder: 'react, saas, mvp',
|
|
72
|
-
defaultValue: '',
|
|
85
|
+
defaultValue: existing?.tags?.join(', ') || '',
|
|
73
86
|
});
|
|
74
87
|
if (p.isCancel(tagsInput)) {
|
|
75
88
|
p.cancel('Cancelled');
|
|
@@ -81,6 +94,7 @@ export async function cmdPublish(path) {
|
|
|
81
94
|
.filter(Boolean);
|
|
82
95
|
const category = await p.select({
|
|
83
96
|
message: 'Project category:',
|
|
97
|
+
initialValue: existing?.category || 'other',
|
|
84
98
|
options: [
|
|
85
99
|
{ value: 'web-app', label: 'Web App' },
|
|
86
100
|
{ value: 'mobile-app', label: 'Mobile App' },
|
|
@@ -155,6 +169,7 @@ export async function cmdPublish(path) {
|
|
|
155
169
|
// Update config
|
|
156
170
|
if (config) {
|
|
157
171
|
config.lastPublished = new Date().toISOString();
|
|
172
|
+
config.lastPublishedSlug = slug;
|
|
158
173
|
saveConfig(projectPath, config);
|
|
159
174
|
}
|
|
160
175
|
p.note([
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { loadAuth } from '../lib/auth.js';
|
|
3
|
+
import { fetchProjects, unpublishProject } from '../lib/api.js';
|
|
4
|
+
export async function cmdUnpublish() {
|
|
5
|
+
const auth = loadAuth();
|
|
6
|
+
if (!auth?.token || !auth?.username) {
|
|
7
|
+
p.log.error('Login required. Run `pmpt login` first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
p.intro('pmpt unpublish');
|
|
11
|
+
const s = p.spinner();
|
|
12
|
+
s.start('Loading your projects...');
|
|
13
|
+
let myProjects;
|
|
14
|
+
try {
|
|
15
|
+
const index = await fetchProjects();
|
|
16
|
+
myProjects = index.projects.filter((proj) => proj.author === auth.username);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
s.stop('Failed to load projects');
|
|
20
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to fetch projects.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
s.stop('Projects loaded');
|
|
24
|
+
if (myProjects.length === 0) {
|
|
25
|
+
p.log.warn('No published projects found.');
|
|
26
|
+
p.outro('');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const slug = await p.select({
|
|
30
|
+
message: 'Select a project to remove:',
|
|
31
|
+
options: myProjects.map((proj) => ({
|
|
32
|
+
value: proj.slug,
|
|
33
|
+
label: proj.slug,
|
|
34
|
+
hint: proj.description?.slice(0, 50) || '',
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
37
|
+
if (p.isCancel(slug)) {
|
|
38
|
+
p.cancel('Cancelled');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
const confirm = await p.confirm({
|
|
42
|
+
message: `Delete "${slug}" from pmptwiki? This cannot be undone.`,
|
|
43
|
+
initialValue: false,
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
46
|
+
p.cancel('Cancelled');
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
const s2 = p.spinner();
|
|
50
|
+
s2.start('Removing...');
|
|
51
|
+
try {
|
|
52
|
+
await unpublishProject(auth.token, slug);
|
|
53
|
+
s2.stop('Removed!');
|
|
54
|
+
p.log.success(`Project "${slug}" has been removed from pmptwiki.`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
s2.stop('Remove failed');
|
|
58
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to remove project.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
p.outro('');
|
|
62
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,13 +11,15 @@ import { cmdExport } from './commands/export.js';
|
|
|
11
11
|
import { cmdImport } from './commands/import.js';
|
|
12
12
|
import { cmdLogin } from './commands/login.js';
|
|
13
13
|
import { cmdPublish } from './commands/publish.js';
|
|
14
|
+
import { cmdEdit } from './commands/edit.js';
|
|
15
|
+
import { cmdUnpublish } from './commands/unpublish.js';
|
|
14
16
|
import { cmdClone } from './commands/clone.js';
|
|
15
17
|
import { cmdBrowse } from './commands/browse.js';
|
|
16
18
|
const program = new Command();
|
|
17
19
|
program
|
|
18
20
|
.name('pmpt')
|
|
19
21
|
.description('pmpt — Record and share your AI-driven product development journey')
|
|
20
|
-
.version('1.
|
|
22
|
+
.version('1.5.2')
|
|
21
23
|
.addHelpText('after', `
|
|
22
24
|
Examples:
|
|
23
25
|
$ pmpt init Initialize project
|
|
@@ -94,6 +96,14 @@ program
|
|
|
94
96
|
.command('publish [path]')
|
|
95
97
|
.description('Publish project to pmptwiki platform')
|
|
96
98
|
.action(cmdPublish);
|
|
99
|
+
program
|
|
100
|
+
.command('edit')
|
|
101
|
+
.description('Edit published project metadata (description, tags, category)')
|
|
102
|
+
.action(cmdEdit);
|
|
103
|
+
program
|
|
104
|
+
.command('unpublish')
|
|
105
|
+
.description('Remove a published project from pmptwiki')
|
|
106
|
+
.action(cmdUnpublish);
|
|
97
107
|
program
|
|
98
108
|
.command('clone <slug>')
|
|
99
109
|
.description('Clone a project from pmptwiki platform')
|
package/dist/lib/api.js
CHANGED
|
@@ -48,6 +48,32 @@ export async function fetchProjects() {
|
|
|
48
48
|
}
|
|
49
49
|
return res.json();
|
|
50
50
|
}
|
|
51
|
+
export async function editProject(token, slug, data) {
|
|
52
|
+
const res = await fetch(`${API_BASE}/publish/${slug}`, {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${token}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(data),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const err = await res.json().catch(() => ({ error: 'Edit failed' }));
|
|
62
|
+
throw new Error(err.error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function unpublishProject(token, slug) {
|
|
66
|
+
const res = await fetch(`${API_BASE}/publish/${slug}`, {
|
|
67
|
+
method: 'DELETE',
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${token}`,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const err = await res.json().catch(() => ({ error: 'Unpublish failed' }));
|
|
74
|
+
throw new Error(err.error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
51
77
|
export async function fetchPmptFile(slug) {
|
|
52
78
|
const res = await fetch(`${R2_PUBLIC_URL}/projects/${slug}.pmpt`);
|
|
53
79
|
if (!res.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmpt-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Record and share your AI-driven product development journey",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,11 +35,9 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@clack/prompts": "^0.7.0",
|
|
38
|
-
"@octokit/rest": "^21.0.0",
|
|
39
38
|
"chokidar": "^3.6.0",
|
|
40
39
|
"commander": "^12.0.0",
|
|
41
40
|
"fast-glob": "^3.3.0",
|
|
42
|
-
"gray-matter": "^4.0.3",
|
|
43
41
|
"open": "^11.0.0",
|
|
44
42
|
"zod": "^3.22.0"
|
|
45
43
|
},
|
package/dist/commands/new.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
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('pmptwiki — create new document');
|
|
7
|
-
const answers = await p.group({
|
|
8
|
-
lang: () => p.select({
|
|
9
|
-
message: 'Select language',
|
|
10
|
-
options: [
|
|
11
|
-
{ value: 'ko', label: 'Korean (ko)' },
|
|
12
|
-
{ value: 'en', label: 'English (en)' },
|
|
13
|
-
],
|
|
14
|
-
}),
|
|
15
|
-
purpose: () => p.select({
|
|
16
|
-
message: 'Select document type',
|
|
17
|
-
options: [
|
|
18
|
-
{ value: 'guide', label: 'Guide', hint: 'Concept explanation + how-to' },
|
|
19
|
-
{ value: 'rule', label: 'Rule', hint: 'Do / Don\'t' },
|
|
20
|
-
{ value: 'template', label: 'Template', hint: 'Copy-paste prompt' },
|
|
21
|
-
{ value: 'example', label: 'Example', hint: 'Real-world use case' },
|
|
22
|
-
{ value: 'reference', label: 'Reference', hint: 'Resource collection' },
|
|
23
|
-
],
|
|
24
|
-
}),
|
|
25
|
-
level: () => p.select({
|
|
26
|
-
message: 'Select difficulty',
|
|
27
|
-
options: [
|
|
28
|
-
{ value: 'beginner', label: 'Beginner' },
|
|
29
|
-
{ value: 'intermediate', label: 'Intermediate' },
|
|
30
|
-
{ value: 'advanced', label: 'Advanced' },
|
|
31
|
-
],
|
|
32
|
-
}),
|
|
33
|
-
title: () => p.text({
|
|
34
|
-
message: 'Enter a title',
|
|
35
|
-
placeholder: 'Providing enough context to AI changes the response',
|
|
36
|
-
validate: (v) => (v.trim().length < 5 ? 'At least 5 characters required' : undefined),
|
|
37
|
-
}),
|
|
38
|
-
tags: () => p.text({
|
|
39
|
-
message: 'Enter tags (comma-separated, optional)',
|
|
40
|
-
placeholder: 'context, beginner, prompt',
|
|
41
|
-
}),
|
|
42
|
-
persona: () => p.multiselect({
|
|
43
|
-
message: 'Select target audience (optional)',
|
|
44
|
-
options: [
|
|
45
|
-
{ value: 'general', label: 'General' },
|
|
46
|
-
{ value: 'power-user', label: 'Power User' },
|
|
47
|
-
{ value: 'developer', label: 'Developer' },
|
|
48
|
-
{ value: 'organization', label: 'Organization' },
|
|
49
|
-
],
|
|
50
|
-
required: false,
|
|
51
|
-
}),
|
|
52
|
-
}, {
|
|
53
|
-
onCancel: () => {
|
|
54
|
-
p.cancel('Cancelled');
|
|
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(`File created: ${filePath}
|
|
73
|
-
|
|
74
|
-
Next steps:
|
|
75
|
-
1. Open the file and write the content
|
|
76
|
-
2. pmpt validate ${filePath}
|
|
77
|
-
3. pmpt submit ${filePath}`);
|
|
78
|
-
}
|
package/dist/commands/submit.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import * as p from '@clack/prompts';
|
|
2
|
-
import matter from 'gray-matter';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
import { loadAuth, saveAuth } from '../lib/auth.js';
|
|
5
|
-
import { createClient, createBranch, createPR, ensureFork, getAuthUser, pushFile, } from '../lib/github.js';
|
|
6
|
-
import { validate } from '../lib/schema.js';
|
|
7
|
-
import { today } from '../lib/template.js';
|
|
8
|
-
export async function cmdSubmit(filePath) {
|
|
9
|
-
p.intro(`pmptwiki — submit: ${filePath}`);
|
|
10
|
-
// 1. Validate
|
|
11
|
-
const s1 = p.spinner();
|
|
12
|
-
s1.start('Validating file...');
|
|
13
|
-
const result = validate(filePath);
|
|
14
|
-
if (!result.valid) {
|
|
15
|
-
s1.stop('Validation failed');
|
|
16
|
-
for (const err of result.errors)
|
|
17
|
-
p.log.error(err);
|
|
18
|
-
p.outro('Fix errors and retry: pmpt validate ' + filePath);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
s1.stop(`Validation passed${result.warnings.length ? ` (${result.warnings.length} warnings)` : ''}`);
|
|
22
|
-
for (const warn of result.warnings)
|
|
23
|
-
p.log.warn(warn);
|
|
24
|
-
// 2. Auth
|
|
25
|
-
let auth = loadAuth();
|
|
26
|
-
if (!auth) {
|
|
27
|
-
p.log.info('GitHub authentication required.');
|
|
28
|
-
p.log.info('Create a Personal Access Token:\n https://github.com/settings/tokens/new\n Required scope: repo (full)');
|
|
29
|
-
const token = await p.password({
|
|
30
|
-
message: 'Enter your GitHub PAT:',
|
|
31
|
-
validate: (v) => (v.trim().length < 10 ? 'Please enter a valid token' : undefined),
|
|
32
|
-
});
|
|
33
|
-
if (p.isCancel(token)) {
|
|
34
|
-
p.cancel('Cancelled');
|
|
35
|
-
process.exit(0);
|
|
36
|
-
}
|
|
37
|
-
const s2 = p.spinner();
|
|
38
|
-
s2.start('Verifying authentication...');
|
|
39
|
-
try {
|
|
40
|
-
const octokit = createClient(token);
|
|
41
|
-
const username = await getAuthUser(octokit);
|
|
42
|
-
saveAuth({ token: token, username });
|
|
43
|
-
auth = { token: token, username };
|
|
44
|
-
s2.stop(`Authenticated — @${username}`);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
s2.stop('Authentication failed');
|
|
48
|
-
p.outro('Invalid token. Please try again');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const octokit = createClient(auth.token);
|
|
53
|
-
// 3. Generate branch name
|
|
54
|
-
const { data: fm } = matter(readFileSync(filePath, 'utf-8'));
|
|
55
|
-
const slug = filePath
|
|
56
|
-
.replace(/^.*?(?=ko\/|en\/)/, '')
|
|
57
|
-
.replace(/\.mdx?$/, '')
|
|
58
|
-
.replace(/\//g, '-');
|
|
59
|
-
const branchName = `content/${slug}-${today()}`;
|
|
60
|
-
// 4. Check / create fork
|
|
61
|
-
const s3 = p.spinner();
|
|
62
|
-
s3.start('Checking fork...');
|
|
63
|
-
await ensureFork(octokit, auth.username);
|
|
64
|
-
s3.stop('Fork ready');
|
|
65
|
-
// 5. Create branch
|
|
66
|
-
const s4 = p.spinner();
|
|
67
|
-
s4.start(`Creating branch: ${branchName}`);
|
|
68
|
-
await createBranch(octokit, auth.username, branchName);
|
|
69
|
-
s4.stop('Branch created');
|
|
70
|
-
// 6. Push file
|
|
71
|
-
const repoPath = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
72
|
-
const s5 = p.spinner();
|
|
73
|
-
s5.start('Uploading file...');
|
|
74
|
-
await pushFile(octokit, auth.username, branchName, repoPath, filePath, `docs: add ${repoPath}`);
|
|
75
|
-
s5.stop('File uploaded');
|
|
76
|
-
// 7. Create PR
|
|
77
|
-
const prTitle = fm.purpose
|
|
78
|
-
? `[${fm.purpose}] ${fm.title}`
|
|
79
|
-
: fm.title;
|
|
80
|
-
const prBody = [
|
|
81
|
-
`## Document Info`,
|
|
82
|
-
`- **Title**: ${fm.title}`,
|
|
83
|
-
`- **Type**: ${fm.purpose ?? '-'}`,
|
|
84
|
-
`- **Level**: ${fm.level ?? '-'}`,
|
|
85
|
-
`- **Language**: ${fm.lang ?? '-'}`,
|
|
86
|
-
fm.tags?.length ? `- **Tags**: ${fm.tags.map((t) => `\`${t}\``).join(' ')}` : null,
|
|
87
|
-
``,
|
|
88
|
-
`## Checklist`,
|
|
89
|
-
`- [ ] Is the content clear and practical?`,
|
|
90
|
-
`- [ ] Are examples included?`,
|
|
91
|
-
`- [ ] Does the title match the content?`,
|
|
92
|
-
``,
|
|
93
|
-
`---`,
|
|
94
|
-
`_Submitted via pmpt-cli_`,
|
|
95
|
-
]
|
|
96
|
-
.filter((l) => l !== null)
|
|
97
|
-
.join('\n');
|
|
98
|
-
const s6 = p.spinner();
|
|
99
|
-
s6.start('Creating PR...');
|
|
100
|
-
const prUrl = await createPR(octokit, auth.username, branchName, prTitle, prBody);
|
|
101
|
-
s6.stop('PR created');
|
|
102
|
-
p.outro(`Submitted!\n\n PR: ${prUrl}\n\nOnce reviewed and merged, it will be published on pmptwiki.com.`);
|
|
103
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as p from '@clack/prompts';
|
|
2
|
-
import { validate } from '../lib/schema.js';
|
|
3
|
-
export function cmdValidate(filePath) {
|
|
4
|
-
p.intro(`pmptwiki — validate: ${filePath}`);
|
|
5
|
-
const result = validate(filePath);
|
|
6
|
-
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
7
|
-
p.outro('All validations passed');
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
for (const err of result.errors) {
|
|
11
|
-
p.log.error(err);
|
|
12
|
-
}
|
|
13
|
-
for (const warn of result.warnings) {
|
|
14
|
-
p.log.warn(warn);
|
|
15
|
-
}
|
|
16
|
-
if (result.valid) {
|
|
17
|
-
p.outro(`Validation passed (${result.warnings.length} warnings)`);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
p.outro(`Validation failed (${result.errors.length} errors) — fix and retry`);
|
|
21
|
-
}
|
|
22
|
-
return result.valid;
|
|
23
|
-
}
|
package/dist/lib/github.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { Octokit } from '@octokit/rest';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
|
-
const CONTENT_OWNER = 'pmptwiki';
|
|
4
|
-
const CONTENT_REPO = 'content';
|
|
5
|
-
export function createClient(token) {
|
|
6
|
-
return new Octokit({ auth: token });
|
|
7
|
-
}
|
|
8
|
-
export async function getAuthUser(octokit) {
|
|
9
|
-
const { data } = await octokit.rest.users.getAuthenticated();
|
|
10
|
-
return data.login;
|
|
11
|
-
}
|
|
12
|
-
/** Create fork if not exists, otherwise return existing */
|
|
13
|
-
export async function ensureFork(octokit, username) {
|
|
14
|
-
try {
|
|
15
|
-
await octokit.rest.repos.get({ owner: username, repo: CONTENT_REPO });
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
await octokit.rest.repos.createFork({
|
|
19
|
-
owner: CONTENT_OWNER,
|
|
20
|
-
repo: CONTENT_REPO,
|
|
21
|
-
});
|
|
22
|
-
// Fork creation is async - wait briefly
|
|
23
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** Create branch (based on upstream main) */
|
|
27
|
-
export async function createBranch(octokit, username, branchName) {
|
|
28
|
-
// Get sha of upstream main
|
|
29
|
-
const { data: ref } = await octokit.rest.git.getRef({
|
|
30
|
-
owner: CONTENT_OWNER,
|
|
31
|
-
repo: CONTENT_REPO,
|
|
32
|
-
ref: 'heads/main',
|
|
33
|
-
});
|
|
34
|
-
const sha = ref.object.sha;
|
|
35
|
-
await octokit.rest.git.createRef({
|
|
36
|
-
owner: username,
|
|
37
|
-
repo: CONTENT_REPO,
|
|
38
|
-
ref: `refs/heads/${branchName}`,
|
|
39
|
-
sha,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** Commit file to fork branch */
|
|
43
|
-
export async function pushFile(octokit, username, branchName, filePath, localFilePath, commitMessage) {
|
|
44
|
-
const content = Buffer.from(readFileSync(localFilePath, 'utf-8')).toString('base64');
|
|
45
|
-
// Check existing file sha (for update)
|
|
46
|
-
let sha;
|
|
47
|
-
try {
|
|
48
|
-
const { data } = await octokit.rest.repos.getContent({
|
|
49
|
-
owner: username,
|
|
50
|
-
repo: CONTENT_REPO,
|
|
51
|
-
path: filePath,
|
|
52
|
-
ref: branchName,
|
|
53
|
-
});
|
|
54
|
-
if (!Array.isArray(data) && 'sha' in data)
|
|
55
|
-
sha = data.sha;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// New file
|
|
59
|
-
}
|
|
60
|
-
await octokit.rest.repos.createOrUpdateFileContents({
|
|
61
|
-
owner: username,
|
|
62
|
-
repo: CONTENT_REPO,
|
|
63
|
-
path: filePath,
|
|
64
|
-
message: commitMessage,
|
|
65
|
-
content,
|
|
66
|
-
branch: branchName,
|
|
67
|
-
...(sha ? { sha } : {}),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
/** Create PR to upstream */
|
|
71
|
-
export async function createPR(octokit, username, branchName, title, body) {
|
|
72
|
-
const { data } = await octokit.rest.pulls.create({
|
|
73
|
-
owner: CONTENT_OWNER,
|
|
74
|
-
repo: CONTENT_REPO,
|
|
75
|
-
title,
|
|
76
|
-
body,
|
|
77
|
-
head: `${username}:${branchName}`,
|
|
78
|
-
base: 'main',
|
|
79
|
-
});
|
|
80
|
-
return data.html_url;
|
|
81
|
-
}
|
package/dist/lib/schema.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import matter from 'gray-matter';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
const frontmatterSchema = z.object({
|
|
5
|
-
title: z.string().min(5, 'Title must be at least 5 characters'),
|
|
6
|
-
purpose: z.enum(['guide', 'rule', 'template', 'example', 'reference']),
|
|
7
|
-
level: z.enum(['beginner', 'intermediate', 'advanced']),
|
|
8
|
-
lang: z.enum(['ko', 'en']),
|
|
9
|
-
persona: z.array(z.enum(['general', 'power-user', 'developer', 'organization'])).optional(),
|
|
10
|
-
status: z.enum(['draft', 'review', 'stable', 'recommended', 'deprecated']).optional(),
|
|
11
|
-
translationKey: z.string().optional(),
|
|
12
|
-
tags: z.array(z.string()).optional(),
|
|
13
|
-
created: z.string().optional(),
|
|
14
|
-
updated: z.string().optional(),
|
|
15
|
-
contributors: z.array(z.string()).optional(),
|
|
16
|
-
});
|
|
17
|
-
const FILE_PATH_RE = /^(ko|en)\/(guide|rule|template|example|reference)\/(beginner|intermediate|advanced)\/.+\.mdx?$/;
|
|
18
|
-
export function validate(filePath) {
|
|
19
|
-
const errors = [];
|
|
20
|
-
const warnings = [];
|
|
21
|
-
// 1. File path rules
|
|
22
|
-
const relative = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
23
|
-
if (!FILE_PATH_RE.test(relative)) {
|
|
24
|
-
errors.push(`File path does not match: {lang}/{purpose}/{level}/filename.md`);
|
|
25
|
-
}
|
|
26
|
-
// 2. Read file
|
|
27
|
-
let raw;
|
|
28
|
-
try {
|
|
29
|
-
raw = readFileSync(filePath, 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
errors.push('Cannot read file');
|
|
33
|
-
return { valid: false, errors, warnings };
|
|
34
|
-
}
|
|
35
|
-
// 3. Parse frontmatter
|
|
36
|
-
const { data, content } = matter(raw);
|
|
37
|
-
const result = frontmatterSchema.safeParse(data);
|
|
38
|
-
if (!result.success) {
|
|
39
|
-
for (const issue of result.error.issues) {
|
|
40
|
-
const field = issue.path.join('.');
|
|
41
|
-
errors.push(`[${field}] ${issue.message}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// 4. Body length
|
|
45
|
-
const bodyLength = content.trim().length;
|
|
46
|
-
if (bodyLength < 200) {
|
|
47
|
-
errors.push(`Content too short (${bodyLength} chars, minimum 200)`);
|
|
48
|
-
}
|
|
49
|
-
// 5. Warnings
|
|
50
|
-
if (!data.tags || data.tags.length === 0) {
|
|
51
|
-
warnings.push('Adding tags helps with search and related document links');
|
|
52
|
-
}
|
|
53
|
-
if (!data.persona) {
|
|
54
|
-
warnings.push('Specifying a persona clarifies the target audience');
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
valid: errors.length === 0,
|
|
58
|
-
errors,
|
|
59
|
-
warnings,
|
|
60
|
-
};
|
|
61
|
-
}
|
package/dist/lib/template.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export function toSlug(title) {
|
|
2
|
-
return title
|
|
3
|
-
.toLowerCase()
|
|
4
|
-
.replace(/[^\w\s-]/g, '')
|
|
5
|
-
.trim()
|
|
6
|
-
.replace(/\s+/g, '-')
|
|
7
|
-
.replace(/-+/g, '-')
|
|
8
|
-
.slice(0, 60);
|
|
9
|
-
}
|
|
10
|
-
export function today() {
|
|
11
|
-
return new Date().toISOString().slice(0, 10);
|
|
12
|
-
}
|
|
13
|
-
export function generateFilePath(fm) {
|
|
14
|
-
const slug = toSlug(fm.title);
|
|
15
|
-
return `${fm.lang}/${fm.purpose}/${fm.level}/${slug}.md`;
|
|
16
|
-
}
|
|
17
|
-
export function generateContent(fm) {
|
|
18
|
-
const frontmatter = [
|
|
19
|
-
'---',
|
|
20
|
-
`title: "${fm.title}"`,
|
|
21
|
-
`purpose: ${fm.purpose}`,
|
|
22
|
-
`level: ${fm.level}`,
|
|
23
|
-
`lang: ${fm.lang}`,
|
|
24
|
-
fm.persona?.length ? `persona: [${fm.persona.map((p) => `"${p}"`).join(', ')}]` : null,
|
|
25
|
-
`status: draft`,
|
|
26
|
-
fm.tags?.length ? `tags: [${fm.tags.map((t) => `"${t}"`).join(', ')}]` : null,
|
|
27
|
-
`created: "${today()}"`,
|
|
28
|
-
`updated: "${today()}"`,
|
|
29
|
-
'---',
|
|
30
|
-
]
|
|
31
|
-
.filter(Boolean)
|
|
32
|
-
.join('\n');
|
|
33
|
-
const body = fm.lang === 'ko'
|
|
34
|
-
? `\n## Why It Matters\n\n<!-- Explain why this document is needed -->\n\n## How To\n\n<!-- Explain step by step -->\n\n## Examples\n\n<!-- Add real examples -->\n`
|
|
35
|
-
: `\n## Why it matters\n\n<!-- Explain why this document is needed -->\n\n## How to\n\n<!-- Explain step by step -->\n\n## Example\n\n<!-- Add a real example -->\n`;
|
|
36
|
-
return frontmatter + body;
|
|
37
|
-
}
|