pmpt-cli 1.5.0 → 1.5.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/dist/commands/browse.js +12 -12
- package/dist/commands/clone.js +18 -18
- package/dist/commands/login.js +16 -16
- package/dist/commands/new.js +26 -26
- package/dist/commands/publish.js +26 -26
- package/dist/commands/submit.js +41 -41
- package/dist/commands/validate.js +4 -4
- package/dist/lib/git.js +13 -13
- package/dist/lib/github.js +8 -8
- package/dist/lib/history.js +23 -23
- package/dist/lib/schema.js +11 -11
- package/dist/lib/template.js +1 -1
- package/package.json +1 -1
package/dist/commands/browse.js
CHANGED
|
@@ -3,27 +3,27 @@ import { fetchProjects } from '../lib/api.js';
|
|
|
3
3
|
export async function cmdBrowse() {
|
|
4
4
|
p.intro('pmpt browse');
|
|
5
5
|
const s = p.spinner();
|
|
6
|
-
s.start('
|
|
6
|
+
s.start('Loading projects...');
|
|
7
7
|
let projects;
|
|
8
8
|
try {
|
|
9
9
|
const index = await fetchProjects();
|
|
10
10
|
projects = index.projects;
|
|
11
11
|
}
|
|
12
12
|
catch (err) {
|
|
13
|
-
s.stop('
|
|
14
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
13
|
+
s.stop('Failed to load');
|
|
14
|
+
p.log.error(err instanceof Error ? err.message : 'Could not load project list.');
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
|
-
s.stop(`${projects.length}
|
|
17
|
+
s.stop(`${projects.length} projects`);
|
|
18
18
|
if (projects.length === 0) {
|
|
19
|
-
p.log.info('
|
|
20
|
-
p.log.message(' pmpt publish —
|
|
19
|
+
p.log.info('No published projects yet.');
|
|
20
|
+
p.log.message(' pmpt publish — share your first project!');
|
|
21
21
|
p.outro('');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
// Select project
|
|
25
25
|
const selected = await p.select({
|
|
26
|
-
message: '
|
|
26
|
+
message: 'Select a project:',
|
|
27
27
|
options: projects.map((proj) => ({
|
|
28
28
|
value: proj.slug,
|
|
29
29
|
label: proj.projectName,
|
|
@@ -47,11 +47,11 @@ export async function cmdBrowse() {
|
|
|
47
47
|
].filter(Boolean).join('\n'), 'Project Details');
|
|
48
48
|
// Action
|
|
49
49
|
const action = await p.select({
|
|
50
|
-
message: '
|
|
50
|
+
message: 'What would you like to do?',
|
|
51
51
|
options: [
|
|
52
|
-
{ value: 'clone', label: '
|
|
53
|
-
{ value: 'url', label: 'URL
|
|
54
|
-
{ value: 'back', label: '
|
|
52
|
+
{ value: 'clone', label: 'Clone this project', hint: 'pmpt clone' },
|
|
53
|
+
{ value: 'url', label: 'Show URL', hint: 'View in browser' },
|
|
54
|
+
{ value: 'back', label: 'Go back' },
|
|
55
55
|
],
|
|
56
56
|
});
|
|
57
57
|
if (p.isCancel(action) || action === 'back') {
|
|
@@ -67,7 +67,7 @@ export async function cmdBrowse() {
|
|
|
67
67
|
const url = `https://pmptwiki.com/ko/p/${project.slug}`;
|
|
68
68
|
p.log.info(`URL: ${url}`);
|
|
69
69
|
p.log.message(`Download: ${project.downloadUrl}`);
|
|
70
|
-
p.log.message(`\npmpt clone ${project.slug} —
|
|
70
|
+
p.log.message(`\npmpt clone ${project.slug} — clone via terminal`);
|
|
71
71
|
p.outro('');
|
|
72
72
|
}
|
|
73
73
|
}
|
package/dist/commands/clone.js
CHANGED
|
@@ -46,31 +46,31 @@ export function restoreDocs(docsDir, docs) {
|
|
|
46
46
|
}
|
|
47
47
|
export async function cmdClone(slug) {
|
|
48
48
|
if (!slug) {
|
|
49
|
-
p.log.error('slug
|
|
50
|
-
p.log.info('
|
|
49
|
+
p.log.error('Please provide a slug.');
|
|
50
|
+
p.log.info('Usage: pmpt clone <slug>');
|
|
51
51
|
process.exit(1);
|
|
52
52
|
}
|
|
53
53
|
p.intro(`pmpt clone — ${slug}`);
|
|
54
54
|
const s = p.spinner();
|
|
55
|
-
s.start('
|
|
55
|
+
s.start('Downloading project...');
|
|
56
56
|
let fileContent;
|
|
57
57
|
try {
|
|
58
58
|
fileContent = await fetchPmptFile(slug);
|
|
59
59
|
}
|
|
60
60
|
catch (err) {
|
|
61
|
-
s.stop('
|
|
62
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
61
|
+
s.stop('Download failed');
|
|
62
|
+
p.log.error(err instanceof Error ? err.message : 'Project not found.');
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
|
-
s.message('
|
|
65
|
+
s.message('Validating...');
|
|
66
66
|
const validation = validatePmptFile(fileContent);
|
|
67
67
|
if (!validation.success || !validation.data) {
|
|
68
|
-
s.stop('
|
|
69
|
-
p.log.error(validation.error || '
|
|
68
|
+
s.stop('Validation failed');
|
|
69
|
+
p.log.error(validation.error || 'Invalid .pmpt file.');
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
72
|
const pmptData = validation.data;
|
|
73
|
-
s.stop('
|
|
73
|
+
s.stop('Download complete');
|
|
74
74
|
// Show summary
|
|
75
75
|
p.note([
|
|
76
76
|
`Project: ${pmptData.meta.projectName}`,
|
|
@@ -81,16 +81,16 @@ export async function cmdClone(slug) {
|
|
|
81
81
|
const projectPath = process.cwd();
|
|
82
82
|
if (isInitialized(projectPath)) {
|
|
83
83
|
const overwrite = await p.confirm({
|
|
84
|
-
message: '
|
|
84
|
+
message: 'Project already initialized. Merge history?',
|
|
85
85
|
initialValue: true,
|
|
86
86
|
});
|
|
87
87
|
if (p.isCancel(overwrite) || !overwrite) {
|
|
88
|
-
p.cancel('
|
|
88
|
+
p.cancel('Cancelled');
|
|
89
89
|
process.exit(0);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
const importSpinner = p.spinner();
|
|
93
|
-
importSpinner.start('
|
|
93
|
+
importSpinner.start('Restoring project...');
|
|
94
94
|
if (!isInitialized(projectPath)) {
|
|
95
95
|
initializeProject(projectPath, { trackGit: true });
|
|
96
96
|
}
|
|
@@ -113,15 +113,15 @@ export async function cmdClone(slug) {
|
|
|
113
113
|
if (existsSync(historyDir)) {
|
|
114
114
|
versionCount = readdirSync(historyDir).filter((d) => d.startsWith('v')).length;
|
|
115
115
|
}
|
|
116
|
-
importSpinner.stop('
|
|
116
|
+
importSpinner.stop('Restore complete!');
|
|
117
117
|
p.note([
|
|
118
118
|
`Project: ${pmptData.meta.projectName}`,
|
|
119
119
|
`Versions: ${versionCount}`,
|
|
120
120
|
`Location: ${pmptDir}`,
|
|
121
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('
|
|
122
|
+
p.log.info('Next steps:');
|
|
123
|
+
p.log.message(' pmpt history — view version history');
|
|
124
|
+
p.log.message(' pmpt plan — view AI prompt');
|
|
125
|
+
p.log.message(' pmpt save — save a new snapshot');
|
|
126
|
+
p.outro('Project cloned!');
|
|
127
127
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -18,34 +18,34 @@ export async function cmdLogin() {
|
|
|
18
18
|
}
|
|
19
19
|
// Step 1: Request device code
|
|
20
20
|
const s = p.spinner();
|
|
21
|
-
s.start('GitHub
|
|
21
|
+
s.start('Preparing GitHub authentication...');
|
|
22
22
|
let device;
|
|
23
23
|
try {
|
|
24
24
|
device = await requestDeviceCode();
|
|
25
|
-
s.stop('
|
|
25
|
+
s.stop('Verification code issued.');
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
|
-
s.stop('
|
|
29
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
28
|
+
s.stop('Failed to issue verification code');
|
|
29
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to prepare authentication.');
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
// Step 2: Show code and open browser
|
|
33
|
-
p.log.info(
|
|
34
|
-
`
|
|
35
|
-
`
|
|
33
|
+
p.log.info(`Enter this code on GitHub:\n\n` +
|
|
34
|
+
` Code: ${device.userCode}\n` +
|
|
35
|
+
` URL: ${device.verificationUri}`);
|
|
36
36
|
const shouldOpen = await p.confirm({
|
|
37
|
-
message: '
|
|
37
|
+
message: 'Open browser?',
|
|
38
38
|
initialValue: true,
|
|
39
39
|
});
|
|
40
40
|
if (p.isCancel(shouldOpen)) {
|
|
41
|
-
p.cancel('
|
|
41
|
+
p.cancel('Cancelled');
|
|
42
42
|
process.exit(0);
|
|
43
43
|
}
|
|
44
44
|
if (shouldOpen) {
|
|
45
45
|
await open(device.verificationUri);
|
|
46
46
|
}
|
|
47
47
|
// Step 3: Poll for token
|
|
48
|
-
s.start('
|
|
48
|
+
s.start('Waiting for GitHub authorization... (enter the code in your browser)');
|
|
49
49
|
let interval = device.interval * 1000; // seconds → ms
|
|
50
50
|
const deadline = Date.now() + device.expiresIn * 1000;
|
|
51
51
|
while (Date.now() < deadline) {
|
|
@@ -54,8 +54,8 @@ export async function cmdLogin() {
|
|
|
54
54
|
const result = await pollDeviceToken(device.deviceCode);
|
|
55
55
|
if (result.status === 'complete') {
|
|
56
56
|
saveAuth({ token: result.token, username: result.username });
|
|
57
|
-
s.stop(
|
|
58
|
-
p.outro('
|
|
57
|
+
s.stop(`Authenticated — @${result.username}`);
|
|
58
|
+
p.outro('Login complete! Build projects with AI using pmpt and share your vibe coding journey on pmptwiki.');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
if (result.status === 'slow_down') {
|
|
@@ -64,13 +64,13 @@ export async function cmdLogin() {
|
|
|
64
64
|
// status === 'pending' → keep polling
|
|
65
65
|
}
|
|
66
66
|
catch (err) {
|
|
67
|
-
s.stop('
|
|
68
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
67
|
+
s.stop('Authentication failed');
|
|
68
|
+
p.log.error(err instanceof Error ? err.message : 'Authentication failed.');
|
|
69
69
|
process.exit(1);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
s.stop('
|
|
73
|
-
p.log.error('
|
|
72
|
+
s.stop('Verification code expired.');
|
|
73
|
+
p.log.error('Please run pmpt login again.');
|
|
74
74
|
process.exit(1);
|
|
75
75
|
}
|
|
76
76
|
function sleep(ms) {
|
package/dist/commands/new.js
CHANGED
|
@@ -3,55 +3,55 @@ import { writeFileSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { dirname } from 'path';
|
|
4
4
|
import { generateContent, generateFilePath } from '../lib/template.js';
|
|
5
5
|
export async function cmdNew() {
|
|
6
|
-
p.intro('pmptwiki —
|
|
6
|
+
p.intro('pmptwiki — create new document');
|
|
7
7
|
const answers = await p.group({
|
|
8
8
|
lang: () => p.select({
|
|
9
|
-
message: '
|
|
9
|
+
message: 'Select language',
|
|
10
10
|
options: [
|
|
11
|
-
{ value: 'ko', label: '
|
|
11
|
+
{ value: 'ko', label: 'Korean (ko)' },
|
|
12
12
|
{ value: 'en', label: 'English (en)' },
|
|
13
13
|
],
|
|
14
14
|
}),
|
|
15
15
|
purpose: () => p.select({
|
|
16
|
-
message: '
|
|
16
|
+
message: 'Select document type',
|
|
17
17
|
options: [
|
|
18
|
-
{ value: 'guide', label: '
|
|
19
|
-
{ value: 'rule', label: '
|
|
20
|
-
{ value: 'template', label: '
|
|
21
|
-
{ value: 'example', label: '
|
|
22
|
-
{ value: 'reference', label: '
|
|
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
23
|
],
|
|
24
24
|
}),
|
|
25
25
|
level: () => p.select({
|
|
26
|
-
message: '
|
|
26
|
+
message: 'Select difficulty',
|
|
27
27
|
options: [
|
|
28
|
-
{ value: 'beginner', label: '
|
|
29
|
-
{ value: 'intermediate', label: '
|
|
30
|
-
{ value: 'advanced', label: '
|
|
28
|
+
{ value: 'beginner', label: 'Beginner' },
|
|
29
|
+
{ value: 'intermediate', label: 'Intermediate' },
|
|
30
|
+
{ value: 'advanced', label: 'Advanced' },
|
|
31
31
|
],
|
|
32
32
|
}),
|
|
33
33
|
title: () => p.text({
|
|
34
|
-
message: '
|
|
35
|
-
placeholder: '
|
|
36
|
-
validate: (v) => (v.trim().length < 5 ? '5
|
|
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
37
|
}),
|
|
38
38
|
tags: () => p.text({
|
|
39
|
-
message: '
|
|
39
|
+
message: 'Enter tags (comma-separated, optional)',
|
|
40
40
|
placeholder: 'context, beginner, prompt',
|
|
41
41
|
}),
|
|
42
42
|
persona: () => p.multiselect({
|
|
43
|
-
message: '
|
|
43
|
+
message: 'Select target audience (optional)',
|
|
44
44
|
options: [
|
|
45
|
-
{ value: 'general', label: '
|
|
46
|
-
{ value: 'power-user', label: '
|
|
47
|
-
{ value: 'developer', label: '
|
|
48
|
-
{ value: 'organization', label: '
|
|
45
|
+
{ value: 'general', label: 'General' },
|
|
46
|
+
{ value: 'power-user', label: 'Power User' },
|
|
47
|
+
{ value: 'developer', label: 'Developer' },
|
|
48
|
+
{ value: 'organization', label: 'Organization' },
|
|
49
49
|
],
|
|
50
50
|
required: false,
|
|
51
51
|
}),
|
|
52
52
|
}, {
|
|
53
53
|
onCancel: () => {
|
|
54
|
-
p.cancel('
|
|
54
|
+
p.cancel('Cancelled');
|
|
55
55
|
process.exit(0);
|
|
56
56
|
},
|
|
57
57
|
});
|
|
@@ -69,10 +69,10 @@ export async function cmdNew() {
|
|
|
69
69
|
const content = generateContent(fm);
|
|
70
70
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
71
|
writeFileSync(filePath, content, 'utf-8');
|
|
72
|
-
p.outro(
|
|
72
|
+
p.outro(`File created: ${filePath}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
1.
|
|
74
|
+
Next steps:
|
|
75
|
+
1. Open the file and write the content
|
|
76
76
|
2. pmpt validate ${filePath}
|
|
77
77
|
3. pmpt submit ${filePath}`);
|
|
78
78
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -25,12 +25,12 @@ function readDocsFolder(docsDir) {
|
|
|
25
25
|
export async function cmdPublish(path) {
|
|
26
26
|
const projectPath = path ? resolve(path) : process.cwd();
|
|
27
27
|
if (!isInitialized(projectPath)) {
|
|
28
|
-
p.log.error('
|
|
28
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
31
|
const auth = loadAuth();
|
|
32
32
|
if (!auth?.token || !auth?.username) {
|
|
33
|
-
p.log.error('
|
|
33
|
+
p.log.error('Login required. Run `pmpt login` first.');
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
36
|
p.intro('pmpt publish');
|
|
@@ -38,41 +38,41 @@ export async function cmdPublish(path) {
|
|
|
38
38
|
const snapshots = getAllSnapshots(projectPath);
|
|
39
39
|
const planProgress = getPlanProgress(projectPath);
|
|
40
40
|
if (snapshots.length === 0) {
|
|
41
|
-
p.log.warn('
|
|
41
|
+
p.log.warn('No snapshots found. Run `pmpt save` or `pmpt plan` first.');
|
|
42
42
|
p.outro('');
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
46
46
|
// Collect publish info
|
|
47
47
|
const slug = await p.text({
|
|
48
|
-
message: '
|
|
48
|
+
message: 'Project slug (used in URL):',
|
|
49
49
|
placeholder: projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'),
|
|
50
50
|
validate: (v) => {
|
|
51
51
|
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
52
|
-
return '3
|
|
52
|
+
return '3-50 chars, lowercase letters, numbers, and hyphens only.';
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
if (p.isCancel(slug)) {
|
|
57
|
-
p.cancel('
|
|
57
|
+
p.cancel('Cancelled');
|
|
58
58
|
process.exit(0);
|
|
59
59
|
}
|
|
60
60
|
const description = await p.text({
|
|
61
|
-
message: '
|
|
61
|
+
message: 'Project description (brief):',
|
|
62
62
|
placeholder: planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
63
63
|
defaultValue: planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
64
64
|
});
|
|
65
65
|
if (p.isCancel(description)) {
|
|
66
|
-
p.cancel('
|
|
66
|
+
p.cancel('Cancelled');
|
|
67
67
|
process.exit(0);
|
|
68
68
|
}
|
|
69
69
|
const tagsInput = await p.text({
|
|
70
|
-
message: '
|
|
70
|
+
message: 'Tags (comma-separated):',
|
|
71
71
|
placeholder: 'react, saas, mvp',
|
|
72
72
|
defaultValue: '',
|
|
73
73
|
});
|
|
74
74
|
if (p.isCancel(tagsInput)) {
|
|
75
|
-
p.cancel('
|
|
75
|
+
p.cancel('Cancelled');
|
|
76
76
|
process.exit(0);
|
|
77
77
|
}
|
|
78
78
|
const tags = tagsInput
|
|
@@ -80,20 +80,20 @@ export async function cmdPublish(path) {
|
|
|
80
80
|
.map((t) => t.trim().toLowerCase())
|
|
81
81
|
.filter(Boolean);
|
|
82
82
|
const category = await p.select({
|
|
83
|
-
message: '
|
|
83
|
+
message: 'Project category:',
|
|
84
84
|
options: [
|
|
85
|
-
{ value: 'web-app', label: '
|
|
86
|
-
{ value: 'mobile-app', label: '
|
|
87
|
-
{ value: 'cli-tool', label: 'CLI
|
|
88
|
-
{ value: 'api-backend', label: 'API
|
|
85
|
+
{ value: 'web-app', label: 'Web App' },
|
|
86
|
+
{ value: 'mobile-app', label: 'Mobile App' },
|
|
87
|
+
{ value: 'cli-tool', label: 'CLI Tool' },
|
|
88
|
+
{ value: 'api-backend', label: 'API/Backend' },
|
|
89
89
|
{ value: 'ai-ml', label: 'AI/ML' },
|
|
90
|
-
{ value: 'game', label: '
|
|
91
|
-
{ value: 'library', label: '
|
|
92
|
-
{ value: 'other', label: '
|
|
90
|
+
{ value: 'game', label: 'Game' },
|
|
91
|
+
{ value: 'library', label: 'Library' },
|
|
92
|
+
{ value: 'other', label: 'Other' },
|
|
93
93
|
],
|
|
94
94
|
});
|
|
95
95
|
if (p.isCancel(category)) {
|
|
96
|
-
p.cancel('
|
|
96
|
+
p.cancel('Cancelled');
|
|
97
97
|
process.exit(0);
|
|
98
98
|
}
|
|
99
99
|
// Build .pmpt content (resolve from optimized snapshots)
|
|
@@ -133,16 +133,16 @@ export async function cmdPublish(path) {
|
|
|
133
133
|
tags.length ? `Tags: ${tags.join(', ')}` : '',
|
|
134
134
|
].filter(Boolean).join('\n'), 'Publish Preview');
|
|
135
135
|
const confirm = await p.confirm({
|
|
136
|
-
message: '
|
|
136
|
+
message: 'Publish this project?',
|
|
137
137
|
initialValue: true,
|
|
138
138
|
});
|
|
139
139
|
if (p.isCancel(confirm) || !confirm) {
|
|
140
|
-
p.cancel('
|
|
140
|
+
p.cancel('Cancelled');
|
|
141
141
|
process.exit(0);
|
|
142
142
|
}
|
|
143
143
|
// Upload
|
|
144
144
|
const s = p.spinner();
|
|
145
|
-
s.start('
|
|
145
|
+
s.start('Uploading...');
|
|
146
146
|
try {
|
|
147
147
|
const result = await publishProject(auth.token, {
|
|
148
148
|
slug: slug,
|
|
@@ -151,7 +151,7 @@ export async function cmdPublish(path) {
|
|
|
151
151
|
tags,
|
|
152
152
|
category: category,
|
|
153
153
|
});
|
|
154
|
-
s.stop('
|
|
154
|
+
s.stop('Published!');
|
|
155
155
|
// Update config
|
|
156
156
|
if (config) {
|
|
157
157
|
config.lastPublished = new Date().toISOString();
|
|
@@ -161,12 +161,12 @@ export async function cmdPublish(path) {
|
|
|
161
161
|
`URL: ${result.url}`,
|
|
162
162
|
`Download: ${result.downloadUrl}`,
|
|
163
163
|
'',
|
|
164
|
-
`pmpt clone ${slug} —
|
|
164
|
+
`pmpt clone ${slug} — others can clone this project`,
|
|
165
165
|
].join('\n'), 'Published!');
|
|
166
166
|
}
|
|
167
167
|
catch (err) {
|
|
168
|
-
s.stop('
|
|
169
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
168
|
+
s.stop('Publish failed');
|
|
169
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to publish.');
|
|
170
170
|
process.exit(1);
|
|
171
171
|
}
|
|
172
172
|
p.outro('');
|
package/dist/commands/submit.js
CHANGED
|
@@ -6,98 +6,98 @@ import { createClient, createBranch, createPR, ensureFork, getAuthUser, pushFile
|
|
|
6
6
|
import { validate } from '../lib/schema.js';
|
|
7
7
|
import { today } from '../lib/template.js';
|
|
8
8
|
export async function cmdSubmit(filePath) {
|
|
9
|
-
p.intro(`pmptwiki —
|
|
10
|
-
// 1.
|
|
9
|
+
p.intro(`pmptwiki — submit: ${filePath}`);
|
|
10
|
+
// 1. Validate
|
|
11
11
|
const s1 = p.spinner();
|
|
12
|
-
s1.start('
|
|
12
|
+
s1.start('Validating file...');
|
|
13
13
|
const result = validate(filePath);
|
|
14
14
|
if (!result.valid) {
|
|
15
|
-
s1.stop('
|
|
15
|
+
s1.stop('Validation failed');
|
|
16
16
|
for (const err of result.errors)
|
|
17
17
|
p.log.error(err);
|
|
18
|
-
p.outro('
|
|
18
|
+
p.outro('Fix errors and retry: pmpt validate ' + filePath);
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
|
-
s1.stop(
|
|
21
|
+
s1.stop(`Validation passed${result.warnings.length ? ` (${result.warnings.length} warnings)` : ''}`);
|
|
22
22
|
for (const warn of result.warnings)
|
|
23
23
|
p.log.warn(warn);
|
|
24
|
-
// 2.
|
|
24
|
+
// 2. Auth
|
|
25
25
|
let auth = loadAuth();
|
|
26
26
|
if (!auth) {
|
|
27
|
-
p.log.info('GitHub
|
|
28
|
-
p.log.info('Personal Access Token
|
|
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
29
|
const token = await p.password({
|
|
30
|
-
message: 'GitHub PAT
|
|
31
|
-
validate: (v) => (v.trim().length < 10 ? '
|
|
30
|
+
message: 'Enter your GitHub PAT:',
|
|
31
|
+
validate: (v) => (v.trim().length < 10 ? 'Please enter a valid token' : undefined),
|
|
32
32
|
});
|
|
33
33
|
if (p.isCancel(token)) {
|
|
34
|
-
p.cancel('
|
|
34
|
+
p.cancel('Cancelled');
|
|
35
35
|
process.exit(0);
|
|
36
36
|
}
|
|
37
37
|
const s2 = p.spinner();
|
|
38
|
-
s2.start('
|
|
38
|
+
s2.start('Verifying authentication...');
|
|
39
39
|
try {
|
|
40
40
|
const octokit = createClient(token);
|
|
41
41
|
const username = await getAuthUser(octokit);
|
|
42
42
|
saveAuth({ token: token, username });
|
|
43
43
|
auth = { token: token, username };
|
|
44
|
-
s2.stop(
|
|
44
|
+
s2.stop(`Authenticated — @${username}`);
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
|
-
s2.stop('
|
|
48
|
-
p.outro('
|
|
47
|
+
s2.stop('Authentication failed');
|
|
48
|
+
p.outro('Invalid token. Please try again');
|
|
49
49
|
process.exit(1);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
const octokit = createClient(auth.token);
|
|
53
|
-
// 3.
|
|
53
|
+
// 3. Generate branch name
|
|
54
54
|
const { data: fm } = matter(readFileSync(filePath, 'utf-8'));
|
|
55
55
|
const slug = filePath
|
|
56
56
|
.replace(/^.*?(?=ko\/|en\/)/, '')
|
|
57
57
|
.replace(/\.mdx?$/, '')
|
|
58
58
|
.replace(/\//g, '-');
|
|
59
59
|
const branchName = `content/${slug}-${today()}`;
|
|
60
|
-
// 4.
|
|
60
|
+
// 4. Check / create fork
|
|
61
61
|
const s3 = p.spinner();
|
|
62
|
-
s3.start('
|
|
62
|
+
s3.start('Checking fork...');
|
|
63
63
|
await ensureFork(octokit, auth.username);
|
|
64
|
-
s3.stop('Fork
|
|
65
|
-
// 5.
|
|
64
|
+
s3.stop('Fork ready');
|
|
65
|
+
// 5. Create branch
|
|
66
66
|
const s4 = p.spinner();
|
|
67
|
-
s4.start(
|
|
67
|
+
s4.start(`Creating branch: ${branchName}`);
|
|
68
68
|
await createBranch(octokit, auth.username, branchName);
|
|
69
|
-
s4.stop('
|
|
70
|
-
// 6.
|
|
69
|
+
s4.stop('Branch created');
|
|
70
|
+
// 6. Push file
|
|
71
71
|
const repoPath = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
72
72
|
const s5 = p.spinner();
|
|
73
|
-
s5.start('
|
|
73
|
+
s5.start('Uploading file...');
|
|
74
74
|
await pushFile(octokit, auth.username, branchName, repoPath, filePath, `docs: add ${repoPath}`);
|
|
75
|
-
s5.stop('
|
|
76
|
-
// 7. PR
|
|
75
|
+
s5.stop('File uploaded');
|
|
76
|
+
// 7. Create PR
|
|
77
77
|
const prTitle = fm.purpose
|
|
78
78
|
? `[${fm.purpose}] ${fm.title}`
|
|
79
79
|
: fm.title;
|
|
80
80
|
const prBody = [
|
|
81
|
-
`##
|
|
82
|
-
`-
|
|
83
|
-
`-
|
|
84
|
-
`-
|
|
85
|
-
`-
|
|
86
|
-
fm.tags?.length ? `-
|
|
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
87
|
``,
|
|
88
|
-
`##
|
|
89
|
-
`- [ ]
|
|
90
|
-
`- [ ]
|
|
91
|
-
`- [ ]
|
|
88
|
+
`## Checklist`,
|
|
89
|
+
`- [ ] Is the content clear and practical?`,
|
|
90
|
+
`- [ ] Are examples included?`,
|
|
91
|
+
`- [ ] Does the title match the content?`,
|
|
92
92
|
``,
|
|
93
93
|
`---`,
|
|
94
|
-
`
|
|
94
|
+
`_Submitted via pmpt-cli_`,
|
|
95
95
|
]
|
|
96
96
|
.filter((l) => l !== null)
|
|
97
97
|
.join('\n');
|
|
98
98
|
const s6 = p.spinner();
|
|
99
|
-
s6.start('PR
|
|
99
|
+
s6.start('Creating PR...');
|
|
100
100
|
const prUrl = await createPR(octokit, auth.username, branchName, prTitle, prBody);
|
|
101
|
-
s6.stop('PR
|
|
102
|
-
p.outro(
|
|
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
103
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { validate } from '../lib/schema.js';
|
|
3
3
|
export function cmdValidate(filePath) {
|
|
4
|
-
p.intro(`pmptwiki —
|
|
4
|
+
p.intro(`pmptwiki — validate: ${filePath}`);
|
|
5
5
|
const result = validate(filePath);
|
|
6
6
|
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
7
|
-
p.outro('
|
|
7
|
+
p.outro('All validations passed');
|
|
8
8
|
return true;
|
|
9
9
|
}
|
|
10
10
|
for (const err of result.errors) {
|
|
@@ -14,10 +14,10 @@ export function cmdValidate(filePath) {
|
|
|
14
14
|
p.log.warn(warn);
|
|
15
15
|
}
|
|
16
16
|
if (result.valid) {
|
|
17
|
-
p.outro(
|
|
17
|
+
p.outro(`Validation passed (${result.warnings.length} warnings)`);
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
|
-
p.outro(
|
|
20
|
+
p.outro(`Validation failed (${result.errors.length} errors) — fix and retry`);
|
|
21
21
|
}
|
|
22
22
|
return result.valid;
|
|
23
23
|
}
|
package/dist/lib/git.js
CHANGED
|
@@ -2,13 +2,13 @@ import { execSync } from 'child_process';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Check if directory is a git repository
|
|
6
6
|
*/
|
|
7
7
|
export function isGitRepo(path) {
|
|
8
8
|
return existsSync(join(path, '.git'));
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Git command execution helper
|
|
12
12
|
*/
|
|
13
13
|
function git(path, args) {
|
|
14
14
|
try {
|
|
@@ -23,32 +23,32 @@ function git(path, args) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Collect current git status info
|
|
27
27
|
*/
|
|
28
28
|
export function getGitInfo(path, remoteUrl) {
|
|
29
29
|
if (!isGitRepo(path)) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
//
|
|
32
|
+
// Commit hash
|
|
33
33
|
const commitFull = git(path, 'rev-parse HEAD');
|
|
34
34
|
if (!commitFull)
|
|
35
35
|
return null;
|
|
36
36
|
const commit = git(path, 'rev-parse --short HEAD') || commitFull.slice(0, 7);
|
|
37
|
-
//
|
|
37
|
+
// Branch
|
|
38
38
|
const branch = git(path, 'rev-parse --abbrev-ref HEAD') || 'HEAD';
|
|
39
|
-
// uncommitted
|
|
39
|
+
// Check uncommitted changes
|
|
40
40
|
const status = git(path, 'status --porcelain');
|
|
41
41
|
const dirty = status !== null && status.length > 0;
|
|
42
|
-
//
|
|
42
|
+
// Commit timestamp
|
|
43
43
|
const timestamp = git(path, 'log -1 --format=%cI') || new Date().toISOString();
|
|
44
|
-
//
|
|
44
|
+
// Current commit tag
|
|
45
45
|
const tag = git(path, 'describe --tags --exact-match 2>/dev/null') || undefined;
|
|
46
|
-
//
|
|
46
|
+
// Remote repository URL (fetched from origin if not provided)
|
|
47
47
|
let repo = remoteUrl;
|
|
48
48
|
if (!repo) {
|
|
49
49
|
const origin = git(path, 'remote get-url origin');
|
|
50
50
|
if (origin) {
|
|
51
|
-
// SSH URL
|
|
51
|
+
// Convert SSH URL to HTTPS
|
|
52
52
|
repo = origin
|
|
53
53
|
.replace(/^git@github\.com:/, 'https://github.com/')
|
|
54
54
|
.replace(/\.git$/, '');
|
|
@@ -65,14 +65,14 @@ export function getGitInfo(path, remoteUrl) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* git
|
|
68
|
+
* Check if git status is clean
|
|
69
69
|
*/
|
|
70
70
|
export function isGitClean(path) {
|
|
71
71
|
const status = git(path, 'status --porcelain');
|
|
72
72
|
return status !== null && status.length === 0;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* Check if specific commit matches current commit
|
|
76
76
|
*/
|
|
77
77
|
export function isCommitMatch(path, expectedCommit) {
|
|
78
78
|
const currentFull = git(path, 'rev-parse HEAD');
|
|
@@ -85,7 +85,7 @@ export function isCommitMatch(path, expectedCommit) {
|
|
|
85
85
|
expectedCommit.startsWith(currentShort));
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* git
|
|
88
|
+
* Convert git info to human-readable string
|
|
89
89
|
*/
|
|
90
90
|
export function formatGitInfo(info) {
|
|
91
91
|
const parts = [
|
package/dist/lib/github.js
CHANGED
|
@@ -9,7 +9,7 @@ export async function getAuthUser(octokit) {
|
|
|
9
9
|
const { data } = await octokit.rest.users.getAuthenticated();
|
|
10
10
|
return data.login;
|
|
11
11
|
}
|
|
12
|
-
/** fork
|
|
12
|
+
/** Create fork if not exists, otherwise return existing */
|
|
13
13
|
export async function ensureFork(octokit, username) {
|
|
14
14
|
try {
|
|
15
15
|
await octokit.rest.repos.get({ owner: username, repo: CONTENT_REPO });
|
|
@@ -19,13 +19,13 @@ export async function ensureFork(octokit, username) {
|
|
|
19
19
|
owner: CONTENT_OWNER,
|
|
20
20
|
repo: CONTENT_REPO,
|
|
21
21
|
});
|
|
22
|
-
//
|
|
22
|
+
// Fork creation is async - wait briefly
|
|
23
23
|
await new Promise((r) => setTimeout(r, 3000));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/** Create branch (based on upstream main) */
|
|
27
27
|
export async function createBranch(octokit, username, branchName) {
|
|
28
|
-
// upstream main
|
|
28
|
+
// Get sha of upstream main
|
|
29
29
|
const { data: ref } = await octokit.rest.git.getRef({
|
|
30
30
|
owner: CONTENT_OWNER,
|
|
31
31
|
repo: CONTENT_REPO,
|
|
@@ -39,10 +39,10 @@ export async function createBranch(octokit, username, branchName) {
|
|
|
39
39
|
sha,
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
42
|
+
/** Commit file to fork branch */
|
|
43
43
|
export async function pushFile(octokit, username, branchName, filePath, localFilePath, commitMessage) {
|
|
44
44
|
const content = Buffer.from(readFileSync(localFilePath, 'utf-8')).toString('base64');
|
|
45
|
-
//
|
|
45
|
+
// Check existing file sha (for update)
|
|
46
46
|
let sha;
|
|
47
47
|
try {
|
|
48
48
|
const { data } = await octokit.rest.repos.getContent({
|
|
@@ -55,7 +55,7 @@ export async function pushFile(octokit, username, branchName, filePath, localFil
|
|
|
55
55
|
sha = data.sha;
|
|
56
56
|
}
|
|
57
57
|
catch {
|
|
58
|
-
//
|
|
58
|
+
// New file
|
|
59
59
|
}
|
|
60
60
|
await octokit.rest.repos.createOrUpdateFileContents({
|
|
61
61
|
owner: username,
|
|
@@ -67,7 +67,7 @@ export async function pushFile(octokit, username, branchName, filePath, localFil
|
|
|
67
67
|
...(sha ? { sha } : {}),
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/** Create PR to upstream */
|
|
71
71
|
export async function createPR(octokit, username, branchName, title, body) {
|
|
72
72
|
const { data } = await octokit.rest.pulls.create({
|
|
73
73
|
owner: CONTENT_OWNER,
|
package/dist/lib/history.js
CHANGED
|
@@ -4,21 +4,21 @@ import { getHistoryDir, getDocsDir, loadConfig } from './config.js';
|
|
|
4
4
|
import { getGitInfo, isGitRepo } from './git.js';
|
|
5
5
|
import glob from 'fast-glob';
|
|
6
6
|
/**
|
|
7
|
-
* .pmpt/docs
|
|
8
|
-
*
|
|
7
|
+
* Save .pmpt/docs MD files as snapshot
|
|
8
|
+
* Copy only changed files to optimize storage
|
|
9
9
|
*/
|
|
10
10
|
export function createFullSnapshot(projectPath) {
|
|
11
11
|
const historyDir = getHistoryDir(projectPath);
|
|
12
12
|
const docsDir = getDocsDir(projectPath);
|
|
13
13
|
mkdirSync(historyDir, { recursive: true });
|
|
14
|
-
//
|
|
14
|
+
// Find next version number
|
|
15
15
|
const existing = getAllSnapshots(projectPath);
|
|
16
16
|
const version = existing.length + 1;
|
|
17
17
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
18
18
|
const snapshotName = `v${version}-${timestamp}`;
|
|
19
19
|
const snapshotDir = join(historyDir, snapshotName);
|
|
20
20
|
mkdirSync(snapshotDir, { recursive: true });
|
|
21
|
-
// docs
|
|
21
|
+
// Compare docs folder MD files and copy only changes
|
|
22
22
|
const files = [];
|
|
23
23
|
const changedFiles = [];
|
|
24
24
|
if (existsSync(docsDir)) {
|
|
@@ -27,7 +27,7 @@ export function createFullSnapshot(projectPath) {
|
|
|
27
27
|
const srcPath = join(docsDir, file);
|
|
28
28
|
const newContent = readFileSync(srcPath, 'utf-8');
|
|
29
29
|
files.push(file);
|
|
30
|
-
//
|
|
30
|
+
// Compare with previous version
|
|
31
31
|
let hasChanged = true;
|
|
32
32
|
if (existing.length > 0) {
|
|
33
33
|
const prevContent = resolveFileContent(existing, existing.length - 1, file);
|
|
@@ -37,7 +37,7 @@ export function createFullSnapshot(projectPath) {
|
|
|
37
37
|
}
|
|
38
38
|
if (hasChanged) {
|
|
39
39
|
const destPath = join(snapshotDir, file);
|
|
40
|
-
//
|
|
40
|
+
// Create subdirectory if needed
|
|
41
41
|
const destDir = join(snapshotDir, file.split('/').slice(0, -1).join('/'));
|
|
42
42
|
if (destDir !== snapshotDir) {
|
|
43
43
|
mkdirSync(destDir, { recursive: true });
|
|
@@ -47,7 +47,7 @@ export function createFullSnapshot(projectPath) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
//
|
|
50
|
+
// Collect git info
|
|
51
51
|
const config = loadConfig(projectPath);
|
|
52
52
|
let gitData;
|
|
53
53
|
if (config?.trackGit && isGitRepo(projectPath)) {
|
|
@@ -62,7 +62,7 @@ export function createFullSnapshot(projectPath) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
//
|
|
65
|
+
// Save metadata
|
|
66
66
|
const metaPath = join(snapshotDir, '.meta.json');
|
|
67
67
|
writeFileSync(metaPath, JSON.stringify({
|
|
68
68
|
version,
|
|
@@ -81,16 +81,16 @@ export function createFullSnapshot(projectPath) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
84
|
+
* Single file snapshot (specific file in pmpt folder)
|
|
85
|
+
* Kept for backward compatibility, uses full snapshot internally
|
|
86
86
|
*/
|
|
87
87
|
export function createSnapshot(projectPath, filePath) {
|
|
88
88
|
const historyDir = getHistoryDir(projectPath);
|
|
89
89
|
const docsDir = getDocsDir(projectPath);
|
|
90
90
|
const relPath = relative(docsDir, filePath);
|
|
91
|
-
//
|
|
91
|
+
// If file is outside docs folder
|
|
92
92
|
if (relPath.startsWith('..')) {
|
|
93
|
-
//
|
|
93
|
+
// Use relative path from project root
|
|
94
94
|
const projectRelPath = relative(projectPath, filePath);
|
|
95
95
|
return createSingleFileSnapshot(projectPath, filePath, projectRelPath);
|
|
96
96
|
}
|
|
@@ -99,17 +99,17 @@ export function createSnapshot(projectPath, filePath) {
|
|
|
99
99
|
function createSingleFileSnapshot(projectPath, filePath, relPath) {
|
|
100
100
|
const historyDir = getHistoryDir(projectPath);
|
|
101
101
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
102
|
-
//
|
|
102
|
+
// Check existing version count for this file
|
|
103
103
|
const existing = getFileHistory(projectPath, relPath);
|
|
104
104
|
const version = existing.length + 1;
|
|
105
|
-
//
|
|
105
|
+
// Create version folder
|
|
106
106
|
const snapshotName = `v${version}-${timestamp}`;
|
|
107
107
|
const snapshotDir = join(historyDir, snapshotName);
|
|
108
108
|
mkdirSync(snapshotDir, { recursive: true });
|
|
109
|
-
//
|
|
109
|
+
// Copy file
|
|
110
110
|
const destPath = join(snapshotDir, basename(filePath));
|
|
111
111
|
copyFileSync(filePath, destPath);
|
|
112
|
-
//
|
|
112
|
+
// Collect git info
|
|
113
113
|
const config = loadConfig(projectPath);
|
|
114
114
|
let gitData;
|
|
115
115
|
if (config?.trackGit && isGitRepo(projectPath)) {
|
|
@@ -124,7 +124,7 @@ function createSingleFileSnapshot(projectPath, filePath, relPath) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
//
|
|
127
|
+
// Save metadata
|
|
128
128
|
const metaPath = join(snapshotDir, '.meta.json');
|
|
129
129
|
writeFileSync(metaPath, JSON.stringify({
|
|
130
130
|
version,
|
|
@@ -141,7 +141,7 @@ function createSingleFileSnapshot(projectPath, filePath, relPath) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
144
|
+
* List all snapshots
|
|
145
145
|
*/
|
|
146
146
|
export function getAllSnapshots(projectPath) {
|
|
147
147
|
const historyDir = getHistoryDir(projectPath);
|
|
@@ -163,7 +163,7 @@ export function getAllSnapshots(projectPath) {
|
|
|
163
163
|
meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
164
164
|
}
|
|
165
165
|
catch {
|
|
166
|
-
//
|
|
166
|
+
// Use defaults if meta file parsing fails
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
entries.push({
|
|
@@ -178,7 +178,7 @@ export function getAllSnapshots(projectPath) {
|
|
|
178
178
|
return entries.sort((a, b) => a.version - b.version);
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
|
-
*
|
|
181
|
+
* Get file history (backward compatibility)
|
|
182
182
|
*/
|
|
183
183
|
export function getFileHistory(projectPath, relPath) {
|
|
184
184
|
const historyDir = getHistoryDir(projectPath);
|
|
@@ -205,7 +205,7 @@ export function getFileHistory(projectPath, relPath) {
|
|
|
205
205
|
gitData = meta.git;
|
|
206
206
|
}
|
|
207
207
|
catch {
|
|
208
|
-
//
|
|
208
|
+
// Skip if meta file parsing fails
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
entries.push({
|
|
@@ -219,7 +219,7 @@ export function getFileHistory(projectPath, relPath) {
|
|
|
219
219
|
return entries.sort((a, b) => a.version - b.version);
|
|
220
220
|
}
|
|
221
221
|
/**
|
|
222
|
-
*
|
|
222
|
+
* Get full history (all files across all snapshots)
|
|
223
223
|
*/
|
|
224
224
|
export function getAllHistory(projectPath) {
|
|
225
225
|
const snapshots = getAllSnapshots(projectPath);
|
|
@@ -238,7 +238,7 @@ export function getAllHistory(projectPath) {
|
|
|
238
238
|
return entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
|
-
*
|
|
241
|
+
* List tracked files (from .pmpt/docs)
|
|
242
242
|
*/
|
|
243
243
|
export function getTrackedFiles(projectPath) {
|
|
244
244
|
const docsDir = getDocsDir(projectPath);
|
package/dist/lib/schema.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import matter from 'gray-matter';
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
const frontmatterSchema = z.object({
|
|
5
|
-
title: z.string().min(5, '
|
|
5
|
+
title: z.string().min(5, 'Title must be at least 5 characters'),
|
|
6
6
|
purpose: z.enum(['guide', 'rule', 'template', 'example', 'reference']),
|
|
7
7
|
level: z.enum(['beginner', 'intermediate', 'advanced']),
|
|
8
8
|
lang: z.enum(['ko', 'en']),
|
|
@@ -18,21 +18,21 @@ const FILE_PATH_RE = /^(ko|en)\/(guide|rule|template|example|reference)\/(beginn
|
|
|
18
18
|
export function validate(filePath) {
|
|
19
19
|
const errors = [];
|
|
20
20
|
const warnings = [];
|
|
21
|
-
// 1.
|
|
21
|
+
// 1. File path rules
|
|
22
22
|
const relative = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
23
23
|
if (!FILE_PATH_RE.test(relative)) {
|
|
24
|
-
errors.push(
|
|
24
|
+
errors.push(`File path does not match: {lang}/{purpose}/{level}/filename.md`);
|
|
25
25
|
}
|
|
26
|
-
// 2.
|
|
26
|
+
// 2. Read file
|
|
27
27
|
let raw;
|
|
28
28
|
try {
|
|
29
29
|
raw = readFileSync(filePath, 'utf-8');
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
|
-
errors.push('
|
|
32
|
+
errors.push('Cannot read file');
|
|
33
33
|
return { valid: false, errors, warnings };
|
|
34
34
|
}
|
|
35
|
-
// 3. frontmatter
|
|
35
|
+
// 3. Parse frontmatter
|
|
36
36
|
const { data, content } = matter(raw);
|
|
37
37
|
const result = frontmatterSchema.safeParse(data);
|
|
38
38
|
if (!result.success) {
|
|
@@ -41,17 +41,17 @@ export function validate(filePath) {
|
|
|
41
41
|
errors.push(`[${field}] ${issue.message}`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
// 4.
|
|
44
|
+
// 4. Body length
|
|
45
45
|
const bodyLength = content.trim().length;
|
|
46
46
|
if (bodyLength < 200) {
|
|
47
|
-
errors.push(
|
|
47
|
+
errors.push(`Content too short (${bodyLength} chars, minimum 200)`);
|
|
48
48
|
}
|
|
49
|
-
// 5.
|
|
49
|
+
// 5. Warnings
|
|
50
50
|
if (!data.tags || data.tags.length === 0) {
|
|
51
|
-
warnings.push('tags
|
|
51
|
+
warnings.push('Adding tags helps with search and related document links');
|
|
52
52
|
}
|
|
53
53
|
if (!data.persona) {
|
|
54
|
-
warnings.push('persona
|
|
54
|
+
warnings.push('Specifying a persona clarifies the target audience');
|
|
55
55
|
}
|
|
56
56
|
return {
|
|
57
57
|
valid: errors.length === 0,
|
package/dist/lib/template.js
CHANGED
|
@@ -31,7 +31,7 @@ export function generateContent(fm) {
|
|
|
31
31
|
.filter(Boolean)
|
|
32
32
|
.join('\n');
|
|
33
33
|
const body = fm.lang === 'ko'
|
|
34
|
-
? `\n##
|
|
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
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
36
|
return frontmatter + body;
|
|
37
37
|
}
|