pmpt-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
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(`PromptWiki — 제출: ${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('오류를 수정한 후 다시 시도하세요: promptwiki 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
+ `_promptwiki-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
+ }
@@ -0,0 +1,23 @@
1
+ import * as p from '@clack/prompts';
2
+ import { validate } from '../lib/schema.js';
3
+ export function cmdValidate(filePath) {
4
+ p.intro(`PromptWiki — 검증: ${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
+ }
@@ -0,0 +1,33 @@
1
+ import * as p from '@clack/prompts';
2
+ import { resolve } from 'path';
3
+ import { isInitialized, getPmptDir } from '../lib/config.js';
4
+ import { startWatching } from '../lib/watcher.js';
5
+ export function cmdWatch(path) {
6
+ const projectPath = path ? resolve(path) : process.cwd();
7
+ if (!isInitialized(projectPath)) {
8
+ p.log.error('Project not initialized. Run `pmpt init` first.');
9
+ process.exit(1);
10
+ }
11
+ const pmptDir = getPmptDir(projectPath);
12
+ p.intro('PromptWiki — File Watcher');
13
+ p.log.info(`Watching: ${pmptDir}`);
14
+ p.log.info('Auto-saving snapshots on MD file changes.');
15
+ p.log.info('Press Ctrl+C to stop.');
16
+ p.log.message('');
17
+ const watcher = startWatching(projectPath, (version, files, git) => {
18
+ let msg = `v${version} saved (${files.length} file(s))`;
19
+ if (git) {
20
+ msg += ` · ${git.commit}`;
21
+ if (git.dirty)
22
+ msg += ' (uncommitted)';
23
+ }
24
+ p.log.success(msg);
25
+ });
26
+ process.on('SIGINT', () => {
27
+ p.log.message('');
28
+ p.log.info('Stopping watcher...');
29
+ watcher.close();
30
+ p.outro('PromptWiki watcher stopped');
31
+ process.exit(0);
32
+ });
33
+ }
package/dist/index.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { cmdNew } from './commands/new.js';
4
+ import { cmdValidate } from './commands/validate.js';
5
+ import { cmdSubmit } from './commands/submit.js';
6
+ import { cmdInit } from './commands/init.js';
7
+ import { cmdStatus } from './commands/status.js';
8
+ import { cmdHistory } from './commands/hist.js';
9
+ import { cmdWatch } from './commands/watch.js';
10
+ import { cmdPlan } from './commands/plan.js';
11
+ import { cmdSave } from './commands/save.js';
12
+ import { cmdSquash } from './commands/squash.js';
13
+ const program = new Command();
14
+ program
15
+ .name('pmpt')
16
+ .description('pmpt — Record and share your AI-driven product development journey')
17
+ .version('1.0.0')
18
+ .addHelpText('after', `
19
+ Examples:
20
+ $ pmpt init Initialize project
21
+ $ pmpt plan Start product planning (6 questions → AI prompt)
22
+ $ pmpt save Save snapshot of pmpt folder
23
+ $ pmpt watch Auto-detect file changes
24
+ $ pmpt history View version history
25
+ $ pmpt history --compact Hide minor changes
26
+ $ pmpt squash v2 v5 Merge versions v2-v5 into v2
27
+
28
+ Folder structure:
29
+ .promptwiki/
30
+ ├── config.json Config file
31
+ ├── pmpt/ Working folder (MD files)
32
+ └── .history/ Version history
33
+
34
+ Documentation: https://pmptwiki.com
35
+ `);
36
+ // Project tracking commands
37
+ program
38
+ .command('init [path]')
39
+ .description('Initialize project folder and start history tracking')
40
+ .option('-r, --repo <url>', 'GitHub repository URL')
41
+ .action(cmdInit);
42
+ program
43
+ .command('watch [path]')
44
+ .description('Watch for file changes and auto-save versions')
45
+ .action(cmdWatch);
46
+ program
47
+ .command('save [path]')
48
+ .description('Save current state of pmpt folder as snapshot')
49
+ .action(cmdSave);
50
+ program
51
+ .command('status [path]')
52
+ .description('Check project status and tracked files')
53
+ .action(cmdStatus);
54
+ program
55
+ .command('history [path]')
56
+ .description('View saved version history')
57
+ .option('-c, --compact', 'Show compact history (hide small changes)')
58
+ .action(cmdHistory);
59
+ program
60
+ .command('squash <from> <to> [path]')
61
+ .description('Squash multiple versions into one (e.g., pmpt squash v2 v5)')
62
+ .action(cmdSquash);
63
+ program
64
+ .command('plan [path]')
65
+ .description('Quick product planning with 6 questions — auto-generate AI prompt')
66
+ .option('--reset', 'Restart plan from scratch')
67
+ .action(cmdPlan);
68
+ // Contribution commands
69
+ program
70
+ .command('new')
71
+ .description('Create new document interactively')
72
+ .action(cmdNew);
73
+ program
74
+ .command('validate <file>')
75
+ .description('Validate document frontmatter and content')
76
+ .action((file) => {
77
+ const ok = cmdValidate(file);
78
+ if (!ok)
79
+ process.exit(1);
80
+ });
81
+ program
82
+ .command('submit <file>')
83
+ .description('Submit document via Fork → Branch → PR')
84
+ .action(cmdSubmit);
85
+ program
86
+ .command('logout')
87
+ .description('Clear saved GitHub authentication')
88
+ .action(async () => {
89
+ const { clearAuth } = await import('./lib/auth.js');
90
+ clearAuth();
91
+ console.log('Logged out successfully');
92
+ });
93
+ program.parse();
@@ -0,0 +1,24 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ const CONFIG_DIR = join(homedir(), '.config', 'promptwiki');
5
+ const TOKEN_FILE = join(CONFIG_DIR, 'auth.json');
6
+ export function loadAuth() {
7
+ try {
8
+ if (!existsSync(TOKEN_FILE))
9
+ return null;
10
+ return JSON.parse(readFileSync(TOKEN_FILE, 'utf-8'));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function saveAuth(config) {
17
+ mkdirSync(CONFIG_DIR, { recursive: true });
18
+ writeFileSync(TOKEN_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
19
+ }
20
+ export function clearAuth() {
21
+ if (existsSync(TOKEN_FILE)) {
22
+ writeFileSync(TOKEN_FILE, '{}');
23
+ }
24
+ }
@@ -0,0 +1,51 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ const CONFIG_DIR = '.promptwiki';
4
+ const CONFIG_FILE = 'config.json';
5
+ const PMPT_DIR = 'pmpt'; // 작업 폴더 (추적 대상)
6
+ const HISTORY_DIR = '.history'; // 히스토리 (숨김)
7
+ export function getConfigDir(projectPath) {
8
+ return join(projectPath, CONFIG_DIR);
9
+ }
10
+ export function getPmptDir(projectPath) {
11
+ return join(getConfigDir(projectPath), PMPT_DIR);
12
+ }
13
+ export function getHistoryDir(projectPath) {
14
+ return join(getConfigDir(projectPath), HISTORY_DIR);
15
+ }
16
+ export function isInitialized(projectPath) {
17
+ return existsSync(join(getConfigDir(projectPath), CONFIG_FILE));
18
+ }
19
+ export function initializeProject(projectPath, options) {
20
+ const configDir = getConfigDir(projectPath);
21
+ const pmptDir = getPmptDir(projectPath);
22
+ const historyDir = getHistoryDir(projectPath);
23
+ mkdirSync(configDir, { recursive: true });
24
+ mkdirSync(pmptDir, { recursive: true });
25
+ mkdirSync(historyDir, { recursive: true });
26
+ const config = {
27
+ projectPath,
28
+ watchPatterns: ['.promptwiki/pmpt/**/*.md'], // pmpt 폴더만 추적
29
+ ignorePatterns: ['node_modules/**', '.promptwiki/.history/**', 'dist/**'],
30
+ createdAt: new Date().toISOString(),
31
+ repo: options?.repo,
32
+ trackGit: options?.trackGit ?? true,
33
+ };
34
+ saveConfig(projectPath, config);
35
+ return config;
36
+ }
37
+ export function loadConfig(projectPath) {
38
+ const configPath = join(getConfigDir(projectPath), CONFIG_FILE);
39
+ if (!existsSync(configPath))
40
+ return null;
41
+ try {
42
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function saveConfig(projectPath, config) {
49
+ const configPath = join(getConfigDir(projectPath), CONFIG_FILE);
50
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
51
+ }
@@ -0,0 +1,102 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ /**
5
+ * 디렉토리가 git 저장소인지 확인
6
+ */
7
+ export function isGitRepo(path) {
8
+ return existsSync(join(path, '.git'));
9
+ }
10
+ /**
11
+ * git 명령어 실행 헬퍼
12
+ */
13
+ function git(path, args) {
14
+ try {
15
+ return execSync(`git ${args}`, {
16
+ cwd: path,
17
+ encoding: 'utf-8',
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ }).trim();
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * 현재 git 상태 정보 수집
27
+ */
28
+ export function getGitInfo(path, remoteUrl) {
29
+ if (!isGitRepo(path)) {
30
+ return null;
31
+ }
32
+ // 커밋 해시
33
+ const commitFull = git(path, 'rev-parse HEAD');
34
+ if (!commitFull)
35
+ return null;
36
+ const commit = git(path, 'rev-parse --short HEAD') || commitFull.slice(0, 7);
37
+ // 브랜치
38
+ const branch = git(path, 'rev-parse --abbrev-ref HEAD') || 'HEAD';
39
+ // uncommitted 변경 확인
40
+ const status = git(path, 'status --porcelain');
41
+ const dirty = status !== null && status.length > 0;
42
+ // 커밋 타임스탬프
43
+ const timestamp = git(path, 'log -1 --format=%cI') || new Date().toISOString();
44
+ // 현재 커밋의 태그
45
+ const tag = git(path, 'describe --tags --exact-match 2>/dev/null') || undefined;
46
+ // 원격 저장소 URL (제공되지 않으면 origin에서 가져옴)
47
+ let repo = remoteUrl;
48
+ if (!repo) {
49
+ const origin = git(path, 'remote get-url origin');
50
+ if (origin) {
51
+ // SSH URL을 HTTPS로 변환
52
+ repo = origin
53
+ .replace(/^git@github\.com:/, 'https://github.com/')
54
+ .replace(/\.git$/, '');
55
+ }
56
+ }
57
+ return {
58
+ repo,
59
+ commit,
60
+ commitFull,
61
+ branch,
62
+ dirty,
63
+ timestamp,
64
+ tag: tag || undefined,
65
+ };
66
+ }
67
+ /**
68
+ * git 상태가 clean한지 확인
69
+ */
70
+ export function isGitClean(path) {
71
+ const status = git(path, 'status --porcelain');
72
+ return status !== null && status.length === 0;
73
+ }
74
+ /**
75
+ * 특정 커밋이 현재 커밋과 일치하는지 확인
76
+ */
77
+ export function isCommitMatch(path, expectedCommit) {
78
+ const currentFull = git(path, 'rev-parse HEAD');
79
+ const currentShort = git(path, 'rev-parse --short HEAD');
80
+ if (!currentFull || !currentShort)
81
+ return false;
82
+ return (currentFull === expectedCommit ||
83
+ currentShort === expectedCommit ||
84
+ currentFull.startsWith(expectedCommit) ||
85
+ expectedCommit.startsWith(currentShort));
86
+ }
87
+ /**
88
+ * git 정보를 사람이 읽기 쉬운 문자열로 변환
89
+ */
90
+ export function formatGitInfo(info) {
91
+ const parts = [
92
+ `commit: ${info.commit}`,
93
+ `branch: ${info.branch}`,
94
+ ];
95
+ if (info.tag) {
96
+ parts.push(`tag: ${info.tag}`);
97
+ }
98
+ if (info.dirty) {
99
+ parts.push('(uncommitted changes)');
100
+ }
101
+ return parts.join(' · ');
102
+ }
@@ -0,0 +1,81 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { readFileSync } from 'fs';
3
+ const CONTENT_OWNER = 'promptwiki';
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
+ }