pmpt-cli 1.5.0 → 1.5.2
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/publish.js +26 -26
- package/dist/index.js +1 -1
- package/dist/lib/git.js +13 -13
- package/dist/lib/history.js +23 -23
- 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/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/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/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const program = new Command();
|
|
|
17
17
|
program
|
|
18
18
|
.name('pmpt')
|
|
19
19
|
.description('pmpt — Record and share your AI-driven product development journey')
|
|
20
|
-
.version('1.
|
|
20
|
+
.version('1.5.2')
|
|
21
21
|
.addHelpText('after', `
|
|
22
22
|
Examples:
|
|
23
23
|
$ pmpt init Initialize project
|
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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmpt-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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 — 새 문서 만들기');
|
|
7
|
-
const answers = await p.group({
|
|
8
|
-
lang: () => p.select({
|
|
9
|
-
message: '언어를 선택하세요',
|
|
10
|
-
options: [
|
|
11
|
-
{ value: 'ko', label: '한국어 (ko)' },
|
|
12
|
-
{ value: 'en', label: 'English (en)' },
|
|
13
|
-
],
|
|
14
|
-
}),
|
|
15
|
-
purpose: () => p.select({
|
|
16
|
-
message: '문서 유형을 선택하세요',
|
|
17
|
-
options: [
|
|
18
|
-
{ value: 'guide', label: '가이드', hint: '개념 설명 + 방법' },
|
|
19
|
-
{ value: 'rule', label: '규칙', hint: '해야 할 것 / 하지 말 것' },
|
|
20
|
-
{ value: 'template', label: '템플릿', hint: '복사해서 쓰는 프롬프트' },
|
|
21
|
-
{ value: 'example', label: '사례', hint: '실제 사용 사례' },
|
|
22
|
-
{ value: 'reference', label: '레퍼런스', hint: '참고 자료 모음' },
|
|
23
|
-
],
|
|
24
|
-
}),
|
|
25
|
-
level: () => p.select({
|
|
26
|
-
message: '난이도를 선택하세요',
|
|
27
|
-
options: [
|
|
28
|
-
{ value: 'beginner', label: '입문' },
|
|
29
|
-
{ value: 'intermediate', label: '중급' },
|
|
30
|
-
{ value: 'advanced', label: '고급' },
|
|
31
|
-
],
|
|
32
|
-
}),
|
|
33
|
-
title: () => p.text({
|
|
34
|
-
message: '제목을 입력하세요',
|
|
35
|
-
placeholder: 'AI에게 충분한 배경을 주면 답변이 달라진다',
|
|
36
|
-
validate: (v) => (v.trim().length < 5 ? '5자 이상 입력해주세요' : undefined),
|
|
37
|
-
}),
|
|
38
|
-
tags: () => p.text({
|
|
39
|
-
message: '태그를 입력하세요 (쉼표 구분, 선택)',
|
|
40
|
-
placeholder: 'context, beginner, prompt',
|
|
41
|
-
}),
|
|
42
|
-
persona: () => p.multiselect({
|
|
43
|
-
message: '대상 독자를 선택하세요 (선택)',
|
|
44
|
-
options: [
|
|
45
|
-
{ value: 'general', label: '일반' },
|
|
46
|
-
{ value: 'power-user', label: '파워유저' },
|
|
47
|
-
{ value: 'developer', label: '개발자' },
|
|
48
|
-
{ value: 'organization', label: '조직' },
|
|
49
|
-
],
|
|
50
|
-
required: false,
|
|
51
|
-
}),
|
|
52
|
-
}, {
|
|
53
|
-
onCancel: () => {
|
|
54
|
-
p.cancel('취소되었습니다');
|
|
55
|
-
process.exit(0);
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
const fm = {
|
|
59
|
-
title: answers.title,
|
|
60
|
-
purpose: answers.purpose,
|
|
61
|
-
level: answers.level,
|
|
62
|
-
lang: answers.lang,
|
|
63
|
-
tags: answers.tags
|
|
64
|
-
? answers.tags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
65
|
-
: [],
|
|
66
|
-
persona: answers.persona.length ? answers.persona : undefined,
|
|
67
|
-
};
|
|
68
|
-
const filePath = generateFilePath(fm);
|
|
69
|
-
const content = generateContent(fm);
|
|
70
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
72
|
-
p.outro(`파일이 생성되었습니다: ${filePath}
|
|
73
|
-
|
|
74
|
-
다음 단계:
|
|
75
|
-
1. 파일을 열어 본문을 작성하세요
|
|
76
|
-
2. 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 — 제출: ${filePath}`);
|
|
10
|
-
// 1. 검증
|
|
11
|
-
const s1 = p.spinner();
|
|
12
|
-
s1.start('파일 검증 중...');
|
|
13
|
-
const result = validate(filePath);
|
|
14
|
-
if (!result.valid) {
|
|
15
|
-
s1.stop('검증 실패');
|
|
16
|
-
for (const err of result.errors)
|
|
17
|
-
p.log.error(err);
|
|
18
|
-
p.outro('오류를 수정한 후 다시 시도하세요: pmpt validate ' + filePath);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
s1.stop(`검증 통과${result.warnings.length ? ` (경고 ${result.warnings.length}개)` : ''}`);
|
|
22
|
-
for (const warn of result.warnings)
|
|
23
|
-
p.log.warn(warn);
|
|
24
|
-
// 2. 인증
|
|
25
|
-
let auth = loadAuth();
|
|
26
|
-
if (!auth) {
|
|
27
|
-
p.log.info('GitHub 인증이 필요합니다.');
|
|
28
|
-
p.log.info('Personal Access Token을 발급하세요:\n https://github.com/settings/tokens/new\n 필요 권한: repo (전체)');
|
|
29
|
-
const token = await p.password({
|
|
30
|
-
message: 'GitHub PAT를 입력하세요:',
|
|
31
|
-
validate: (v) => (v.trim().length < 10 ? '올바른 토큰을 입력하세요' : undefined),
|
|
32
|
-
});
|
|
33
|
-
if (p.isCancel(token)) {
|
|
34
|
-
p.cancel('취소됨');
|
|
35
|
-
process.exit(0);
|
|
36
|
-
}
|
|
37
|
-
const s2 = p.spinner();
|
|
38
|
-
s2.start('인증 확인 중...');
|
|
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(`인증 완료 — @${username}`);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
s2.stop('인증 실패');
|
|
48
|
-
p.outro('토큰이 올바르지 않습니다. 다시 시도하세요');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const octokit = createClient(auth.token);
|
|
53
|
-
// 3. 브랜치명 생성
|
|
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. fork 확인 / 생성
|
|
61
|
-
const s3 = p.spinner();
|
|
62
|
-
s3.start('Fork 확인 중...');
|
|
63
|
-
await ensureFork(octokit, auth.username);
|
|
64
|
-
s3.stop('Fork 준비 완료');
|
|
65
|
-
// 5. 브랜치 생성
|
|
66
|
-
const s4 = p.spinner();
|
|
67
|
-
s4.start(`브랜치 생성 중: ${branchName}`);
|
|
68
|
-
await createBranch(octokit, auth.username, branchName);
|
|
69
|
-
s4.stop('브랜치 생성 완료');
|
|
70
|
-
// 6. 파일 push
|
|
71
|
-
const repoPath = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
72
|
-
const s5 = p.spinner();
|
|
73
|
-
s5.start('파일 업로드 중...');
|
|
74
|
-
await pushFile(octokit, auth.username, branchName, repoPath, filePath, `docs: add ${repoPath}`);
|
|
75
|
-
s5.stop('파일 업로드 완료');
|
|
76
|
-
// 7. PR 생성
|
|
77
|
-
const prTitle = fm.purpose
|
|
78
|
-
? `[${fm.purpose}] ${fm.title}`
|
|
79
|
-
: fm.title;
|
|
80
|
-
const prBody = [
|
|
81
|
-
`## 문서 정보`,
|
|
82
|
-
`- **제목**: ${fm.title}`,
|
|
83
|
-
`- **유형**: ${fm.purpose ?? '-'}`,
|
|
84
|
-
`- **난이도**: ${fm.level ?? '-'}`,
|
|
85
|
-
`- **언어**: ${fm.lang ?? '-'}`,
|
|
86
|
-
fm.tags?.length ? `- **태그**: ${fm.tags.map((t) => `\`${t}\``).join(' ')}` : null,
|
|
87
|
-
``,
|
|
88
|
-
`## 체크리스트`,
|
|
89
|
-
`- [ ] 본문이 명확하고 실용적인가?`,
|
|
90
|
-
`- [ ] 예시가 포함되어 있는가?`,
|
|
91
|
-
`- [ ] 제목과 내용이 일치하는가?`,
|
|
92
|
-
``,
|
|
93
|
-
`---`,
|
|
94
|
-
`_pmpt-cli로 제출됨_`,
|
|
95
|
-
]
|
|
96
|
-
.filter((l) => l !== null)
|
|
97
|
-
.join('\n');
|
|
98
|
-
const s6 = p.spinner();
|
|
99
|
-
s6.start('PR 생성 중...');
|
|
100
|
-
const prUrl = await createPR(octokit, auth.username, branchName, prTitle, prBody);
|
|
101
|
-
s6.stop('PR 생성 완료');
|
|
102
|
-
p.outro(`제출 완료!\n\n PR: ${prUrl}\n\n리뷰 후 머지되면 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 — 검증: ${filePath}`);
|
|
5
|
-
const result = validate(filePath);
|
|
6
|
-
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
7
|
-
p.outro('모든 검증 통과');
|
|
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(`검증 통과 (경고 ${result.warnings.length}개)`);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
p.outro(`검증 실패 (오류 ${result.errors.length}개) — 수정 후 다시 시도하세요`);
|
|
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
|
-
/** fork가 없으면 생성, 있으면 그대로 반환 */
|
|
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 생성은 비동기 - 잠시 대기
|
|
23
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** 브랜치 생성 (upstream main 기준) */
|
|
27
|
-
export async function createBranch(octokit, username, branchName) {
|
|
28
|
-
// upstream main의 sha 가져오기
|
|
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
|
-
/** 파일을 fork의 브랜치에 커밋 */
|
|
43
|
-
export async function pushFile(octokit, username, branchName, filePath, localFilePath, commitMessage) {
|
|
44
|
-
const content = Buffer.from(readFileSync(localFilePath, 'utf-8')).toString('base64');
|
|
45
|
-
// 기존 파일 sha 확인 (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
|
-
// 신규 파일
|
|
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
|
-
/** upstream으로 PR 생성 */
|
|
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, '제목은 5자 이상이어야 합니다'),
|
|
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. 파일 경로 규칙
|
|
22
|
-
const relative = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
|
|
23
|
-
if (!FILE_PATH_RE.test(relative)) {
|
|
24
|
-
errors.push(`파일 경로가 규칙에 맞지 않습니다: {lang}/{purpose}/{level}/파일명.md`);
|
|
25
|
-
}
|
|
26
|
-
// 2. 파일 읽기
|
|
27
|
-
let raw;
|
|
28
|
-
try {
|
|
29
|
-
raw = readFileSync(filePath, 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
errors.push('파일을 읽을 수 없습니다');
|
|
33
|
-
return { valid: false, errors, warnings };
|
|
34
|
-
}
|
|
35
|
-
// 3. 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. 본문 길이
|
|
45
|
-
const bodyLength = content.trim().length;
|
|
46
|
-
if (bodyLength < 200) {
|
|
47
|
-
errors.push(`본문이 너무 짧습니다 (현재 ${bodyLength}자, 최소 200자)`);
|
|
48
|
-
}
|
|
49
|
-
// 5. 경고
|
|
50
|
-
if (!data.tags || data.tags.length === 0) {
|
|
51
|
-
warnings.push('tags를 추가하면 검색과 관련 문서 연결에 도움이 됩니다');
|
|
52
|
-
}
|
|
53
|
-
if (!data.persona) {
|
|
54
|
-
warnings.push('persona를 지정하면 대상 독자가 명확해집니다');
|
|
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## 왜 중요한가\n\n<!-- 이 문서가 필요한 이유를 설명하세요 -->\n\n## 방법\n\n<!-- 단계별로 설명하세요 -->\n\n## 예시\n\n<!-- 실제 예시를 추가하세요 -->\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
|
-
}
|