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.
@@ -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: ' 프로젝트 복제', hint: 'pmpt clone' },
53
- { value: 'url', label: 'URL 표시', hint: '브라우저에서 보기' },
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
  }
@@ -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('사용법: pmpt clone <slug>');
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 || '잘못된 .pmpt 파일입니다.');
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
  }
@@ -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(`아래 코드를 GitHub에 입력하세요:\n\n` +
34
- ` 코드: ${device.userCode}\n` +
35
- ` 주소: ${device.verificationUri}`);
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('GitHub 인증 대기 중... (브라우저에서 코드를 입력하세요)');
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(`인증 완료 — @${result.username}`);
58
- p.outro('로그인 완료! pmpt publish로 프로젝트를 공유하세요.');
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('다시 pmpt login 실행해 주세요.');
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) {
@@ -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('프로젝트가 초기화되지 않았습니다. `pmpt init`을 먼저 실행하세요.');
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('로그인이 필요합니다. `pmpt login`을 먼저 실행하세요.');
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('스냅샷이 없습니다. `pmpt save` 또는 `pmpt plan`을 먼저 실행하세요.');
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: '프로젝트 slug (URL에 사용될 이름):',
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~50자, 소문자/숫자/하이픈만 사용 가능합니다.';
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: '웹 앱 (Web App)' },
86
- { value: 'mobile-app', label: '모바일 앱 (Mobile App)' },
87
- { value: 'cli-tool', label: 'CLI 도구 (CLI Tool)' },
88
- { value: 'api-backend', label: 'API/백엔드 (API/Backend)' },
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: '게임 (Game)' },
91
- { value: 'library', label: '라이브러리 (Library)' },
92
- { value: 'other', label: '기타 (Other)' },
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.4.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
- * 디렉토리가 git 저장소인지 확인
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
- * git 명령어 실행 헬퍼
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
- * 현재 git 상태 정보 수집
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
- // 원격 저장소 URL (제공되지 않으면 origin에서 가져옴)
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 HTTPS로 변환
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 상태가 clean한지 확인
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 = [
@@ -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 폴더의 MD 파일을 스냅샷으로 저장
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 폴더의 MD 파일 비교 변경분만 복사
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
- // Git 정보 수집
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
- * 단일 파일 스냅샷 (pmpt 폴더 특정 파일만)
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
- // 파일이 docs 폴더 외부에 있는 경우
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
- // Git 정보 수집
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
- * 추적 중인 파일 목록 (.pmpt/docs 기준)
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.0",
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
  },
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }