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.
- package/README.md +58 -0
- package/dist/commands/hist.js +90 -0
- package/dist/commands/init.js +130 -0
- package/dist/commands/new.js +78 -0
- package/dist/commands/plan.js +158 -0
- package/dist/commands/save.js +48 -0
- package/dist/commands/squash.js +75 -0
- package/dist/commands/status.js +35 -0
- package/dist/commands/submit.js +103 -0
- package/dist/commands/validate.js +23 -0
- package/dist/commands/watch.js +33 -0
- package/dist/index.js +93 -0
- package/dist/lib/auth.js +24 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/git.js +102 -0
- package/dist/lib/github.js +81 -0
- package/dist/lib/history.js +233 -0
- package/dist/lib/plan.js +173 -0
- package/dist/lib/schema.js +61 -0
- package/dist/lib/template.js +37 -0
- package/dist/lib/watcher.js +63 -0
- package/dist/types.js +1 -0
- package/package.json +49 -0
|
@@ -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();
|
package/dist/lib/auth.js
ADDED
|
@@ -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
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -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
|
+
}
|