vibe-collab 0.1.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 +177 -0
- package/dist/ai/client.d.ts +15 -0
- package/dist/ai/client.js +89 -0
- package/dist/charter/generator.d.ts +10 -0
- package/dist/charter/generator.js +41 -0
- package/dist/charter/updater.d.ts +10 -0
- package/dist/charter/updater.js +41 -0
- package/dist/cli/commands/auth.d.ts +12 -0
- package/dist/cli/commands/auth.js +180 -0
- package/dist/cli/commands/connect.d.ts +1 -0
- package/dist/cli/commands/connect.js +171 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +149 -0
- package/dist/cli/commands/serve.d.ts +2 -0
- package/dist/cli/commands/serve.js +26 -0
- package/dist/cli/commands/start.d.ts +4 -0
- package/dist/cli/commands/start.js +307 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +89 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +55 -0
- package/dist/github/auth.d.ts +22 -0
- package/dist/github/auth.js +77 -0
- package/dist/github/branches.d.ts +5 -0
- package/dist/github/branches.js +70 -0
- package/dist/github/client.d.ts +4 -0
- package/dist/github/client.js +38 -0
- package/dist/github/files.d.ts +6 -0
- package/dist/github/files.js +52 -0
- package/dist/github/issues.d.ts +7 -0
- package/dist/github/issues.js +51 -0
- package/dist/github/merges.d.ts +1 -0
- package/dist/github/merges.js +30 -0
- package/dist/github/pulls.d.ts +10 -0
- package/dist/github/pulls.js +27 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +264 -0
- package/dist/mcp/tools/analyzeRequest.d.ts +5 -0
- package/dist/mcp/tools/analyzeRequest.js +60 -0
- package/dist/mcp/tools/createPR.d.ts +5 -0
- package/dist/mcp/tools/createPR.js +71 -0
- package/dist/mcp/tools/executeMerge.d.ts +6 -0
- package/dist/mcp/tools/executeMerge.js +61 -0
- package/dist/mcp/tools/recordCheckpoint.d.ts +15 -0
- package/dist/mcp/tools/recordCheckpoint.js +76 -0
- package/dist/mcp/tools/requestMergeReview.d.ts +5 -0
- package/dist/mcp/tools/requestMergeReview.js +85 -0
- package/dist/mcp/tools/requestQA.d.ts +6 -0
- package/dist/mcp/tools/requestQA.js +147 -0
- package/dist/mcp/tools/startSession.d.ts +5 -0
- package/dist/mcp/tools/startSession.js +97 -0
- package/dist/mcp/tools/startWork.d.ts +7 -0
- package/dist/mcp/tools/startWork.js +97 -0
- package/dist/state/reader.d.ts +5 -0
- package/dist/state/reader.js +50 -0
- package/dist/state/types.d.ts +83 -0
- package/dist/state/types.js +2 -0
- package/dist/state/writer.d.ts +10 -0
- package/dist/state/writer.js +82 -0
- package/package.json +42 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
const STANDARD_MCP_ENTRY = {
|
|
6
|
+
command: 'npx',
|
|
7
|
+
args: ['vibe-orchestrator', 'serve'],
|
|
8
|
+
env: {},
|
|
9
|
+
};
|
|
10
|
+
const VSCODE_MCP_ENTRY = {
|
|
11
|
+
type: 'stdio',
|
|
12
|
+
command: 'npx',
|
|
13
|
+
args: ['vibe-orchestrator', 'serve'],
|
|
14
|
+
};
|
|
15
|
+
function hasBinary(name) {
|
|
16
|
+
try {
|
|
17
|
+
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
|
|
18
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function hasDir(dir) {
|
|
26
|
+
return fs.existsSync(dir);
|
|
27
|
+
}
|
|
28
|
+
function readJsonSafe(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeJson(filePath, data) {
|
|
38
|
+
const dir = path.dirname(filePath);
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
function isAlreadyConfigured(existing, serverKey) {
|
|
45
|
+
const mcpServers = existing['mcpServers'];
|
|
46
|
+
const servers = existing['servers'];
|
|
47
|
+
return !!(mcpServers?.[serverKey] || servers?.[serverKey]);
|
|
48
|
+
}
|
|
49
|
+
function addStandardEntry(existing, serverKey) {
|
|
50
|
+
const mcpServers = existing['mcpServers'] ?? {};
|
|
51
|
+
return {
|
|
52
|
+
...existing,
|
|
53
|
+
mcpServers: {
|
|
54
|
+
...mcpServers,
|
|
55
|
+
[serverKey]: STANDARD_MCP_ENTRY,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function addVSCodeEntry(existing, serverKey) {
|
|
60
|
+
const servers = existing['servers'] ?? {};
|
|
61
|
+
return {
|
|
62
|
+
...existing,
|
|
63
|
+
servers: {
|
|
64
|
+
...servers,
|
|
65
|
+
[serverKey]: VSCODE_MCP_ENTRY,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function configureClaudeCode(cwd) {
|
|
70
|
+
const tool = 'Claude Code';
|
|
71
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
72
|
+
const detected = hasDir(claudeDir) || hasBinary('claude');
|
|
73
|
+
if (!detected) {
|
|
74
|
+
return { tool, configPath: '', status: 'skipped' };
|
|
75
|
+
}
|
|
76
|
+
const configPath = path.join(claudeDir, 'settings.json');
|
|
77
|
+
const existing = readJsonSafe(configPath);
|
|
78
|
+
if (isAlreadyConfigured(existing, 'vibe-orchestrator')) {
|
|
79
|
+
return { tool, configPath, status: 'already' };
|
|
80
|
+
}
|
|
81
|
+
writeJson(configPath, addStandardEntry(existing, 'vibe-orchestrator'));
|
|
82
|
+
return { tool, configPath, status: 'added' };
|
|
83
|
+
}
|
|
84
|
+
async function configureCursor(cwd) {
|
|
85
|
+
const tool = 'Cursor';
|
|
86
|
+
const cursorDir = path.join(cwd, '.cursor');
|
|
87
|
+
const detected = hasDir(cursorDir) || hasBinary('cursor');
|
|
88
|
+
if (!detected) {
|
|
89
|
+
return { tool, configPath: '', status: 'skipped' };
|
|
90
|
+
}
|
|
91
|
+
const configPath = path.join(cursorDir, 'mcp.json');
|
|
92
|
+
const existing = readJsonSafe(configPath);
|
|
93
|
+
if (isAlreadyConfigured(existing, 'vibe-orchestrator')) {
|
|
94
|
+
return { tool, configPath, status: 'already' };
|
|
95
|
+
}
|
|
96
|
+
writeJson(configPath, addStandardEntry(existing, 'vibe-orchestrator'));
|
|
97
|
+
return { tool, configPath, status: 'added' };
|
|
98
|
+
}
|
|
99
|
+
async function configureVSCode(cwd) {
|
|
100
|
+
const tool = 'VS Code';
|
|
101
|
+
const vscodeDir = path.join(cwd, '.vscode');
|
|
102
|
+
const detected = hasDir(vscodeDir) || hasBinary('code');
|
|
103
|
+
if (!detected) {
|
|
104
|
+
return { tool, configPath: '', status: 'skipped' };
|
|
105
|
+
}
|
|
106
|
+
const configPath = path.join(vscodeDir, 'mcp.json');
|
|
107
|
+
const existing = readJsonSafe(configPath);
|
|
108
|
+
if (isAlreadyConfigured(existing, 'vibe-orchestrator')) {
|
|
109
|
+
return { tool, configPath, status: 'already' };
|
|
110
|
+
}
|
|
111
|
+
writeJson(configPath, addVSCodeEntry(existing, 'vibe-orchestrator'));
|
|
112
|
+
return { tool, configPath, status: 'added' };
|
|
113
|
+
}
|
|
114
|
+
// Windsurf도 글로벌 설정(~/.codeium/)이므로 자동 설정 제외
|
|
115
|
+
async function configureWindsurf() {
|
|
116
|
+
return { tool: 'Windsurf', configPath: '', status: 'skipped' };
|
|
117
|
+
}
|
|
118
|
+
// Claude Desktop과 Gemini CLI는 글로벌 설정 파일을 수정하므로
|
|
119
|
+
// vibe init 시 자동 설정하지 않습니다.
|
|
120
|
+
// 원하면 vibe connect --global 로 별도 실행하세요. (미구현)
|
|
121
|
+
async function configureClaudeDesktop() {
|
|
122
|
+
return { tool: 'Claude Desktop', configPath: '', status: 'skipped' };
|
|
123
|
+
}
|
|
124
|
+
async function configureGeminiCLI() {
|
|
125
|
+
return { tool: 'Gemini CLI', configPath: '', status: 'skipped' };
|
|
126
|
+
}
|
|
127
|
+
export async function connectCommand(cwd) {
|
|
128
|
+
console.log(chalk.cyan('\n🔌 AI 도구 자동 설정 중...\n'));
|
|
129
|
+
const results = await Promise.all([
|
|
130
|
+
configureClaudeCode(cwd),
|
|
131
|
+
configureCursor(cwd),
|
|
132
|
+
configureVSCode(cwd),
|
|
133
|
+
configureWindsurf(),
|
|
134
|
+
configureClaudeDesktop(),
|
|
135
|
+
configureGeminiCLI(),
|
|
136
|
+
]);
|
|
137
|
+
let addedCount = 0;
|
|
138
|
+
let alreadyCount = 0;
|
|
139
|
+
let skippedCount = 0;
|
|
140
|
+
for (const result of results) {
|
|
141
|
+
if (result.status === 'added') {
|
|
142
|
+
console.log(chalk.green(` ✅ ${result.tool} — 설정 완료`));
|
|
143
|
+
console.log(chalk.gray(` ${result.configPath}`));
|
|
144
|
+
addedCount++;
|
|
145
|
+
}
|
|
146
|
+
else if (result.status === 'already') {
|
|
147
|
+
console.log(chalk.yellow(` ☑️ ${result.tool} — 이미 설정됨`));
|
|
148
|
+
alreadyCount++;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(chalk.gray(` ⬜ ${result.tool} — 미감지`));
|
|
152
|
+
skippedCount++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log('');
|
|
156
|
+
if (addedCount === 0 && alreadyCount === 0) {
|
|
157
|
+
console.log(chalk.yellow('감지된 AI 도구가 없습니다.\nCursor, VS Code, Claude Code 중 하나를 설치하거나\n해당 도구의 프로젝트 디렉터리(.cursor, .vscode, .claude)를 먼저 만들어주세요.'));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (addedCount > 0)
|
|
162
|
+
parts.push(`${addedCount}개 도구 설정 완료`);
|
|
163
|
+
if (alreadyCount > 0)
|
|
164
|
+
parts.push(`${alreadyCount}개 이미 설정됨`);
|
|
165
|
+
if (skippedCount > 0)
|
|
166
|
+
parts.push(`${skippedCount}개 미감지`);
|
|
167
|
+
console.log(chalk.green(`✨ ${parts.join(', ')}`));
|
|
168
|
+
console.log(chalk.gray(' AI 도구를 재시작하면 vibe-orchestrator MCP가 활성화됩니다.\n'));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { writeFile } from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { listOpenIssues } from '../../github/issues.js';
|
|
7
|
+
import { getFileContent, getRootStructure } from '../../github/files.js';
|
|
8
|
+
import { generateCharter } from '../../charter/generator.js';
|
|
9
|
+
import { ensureVibeDir, writeState, writeConfig, writeCharter } from '../../state/writer.js';
|
|
10
|
+
import { detectGitHubToken, parseGitHubUrl, getDefaultBranch } from '../../github/auth.js';
|
|
11
|
+
import { getAIProvider } from '../../ai/client.js';
|
|
12
|
+
import { connectCommand } from './connect.js';
|
|
13
|
+
export async function initCommand() {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
console.log(chalk.bold('\n🚀 Vibe Orchestrator 초기화\n'));
|
|
17
|
+
// AI 제공자 확인
|
|
18
|
+
const aiProvider = getAIProvider();
|
|
19
|
+
if (!aiProvider) {
|
|
20
|
+
console.log(chalk.yellow('⚠️ AI API 키가 없습니다. CHARTER 자동 생성을 건너뜁니다.\n' +
|
|
21
|
+
' (ANTHROPIC_API_KEY 또는 OPENAI_API_KEY를 설정하면 CHARTER가 자동 생성됩니다)\n'));
|
|
22
|
+
}
|
|
23
|
+
// GitHub 토큰 자동 감지
|
|
24
|
+
const detectedToken = await detectGitHubToken();
|
|
25
|
+
if (detectedToken) {
|
|
26
|
+
console.log(chalk.green('✓ GitHub 인증 감지됨\n'));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(chalk.yellow('⚠️ GitHub 인증이 필요합니다.\n' +
|
|
30
|
+
' 아래 중 하나를 실행하세요:\n' +
|
|
31
|
+
' • vibe auth login — 브라우저로 GitHub 로그인 (권장)\n' +
|
|
32
|
+
' • gh auth login — GitHub CLI 로그인\n'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const answers = await inquirer.prompt([
|
|
36
|
+
{
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'projectName',
|
|
39
|
+
message: '이 프로젝트 이름이 뭔가요?',
|
|
40
|
+
validate: (v) => v.trim() ? true : '프로젝트 이름을 입력해주세요.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'repoUrl',
|
|
45
|
+
message: 'GitHub 레포 주소를 입력하세요 (예: https://github.com/owner/repo 또는 owner/repo):',
|
|
46
|
+
validate: (v) => parseGitHubUrl(v) ? true : '올바른 GitHub 레포 주소를 입력해주세요.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'input',
|
|
50
|
+
name: 'protectedPaths',
|
|
51
|
+
message: '건드리면 안 되는 중요한 폴더가 있나요? (쉼표로 구분, 없으면 엔터)',
|
|
52
|
+
default: '',
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
// URL 파싱
|
|
56
|
+
const parsed = parseGitHubUrl(answers.repoUrl);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
console.error(chalk.red('❌ GitHub 레포 주소 파싱 실패'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const { owner, repo } = parsed;
|
|
62
|
+
const token = detectedToken;
|
|
63
|
+
// 기본 브랜치 자동 감지
|
|
64
|
+
const branchSpinner = ora(`${owner}/${repo} 정보 확인 중...`).start();
|
|
65
|
+
const defaultBranch = await getDefaultBranch(owner, repo, token);
|
|
66
|
+
branchSpinner.succeed(`기본 브랜치: ${chalk.cyan(defaultBranch)}`);
|
|
67
|
+
const protectedPaths = answers.protectedPaths
|
|
68
|
+
.split(',')
|
|
69
|
+
.map((p) => p.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
const config = {
|
|
72
|
+
owner,
|
|
73
|
+
repo,
|
|
74
|
+
defaultBranch,
|
|
75
|
+
protectedPaths,
|
|
76
|
+
};
|
|
77
|
+
// 1. 디렉토리 생성 + config 저장
|
|
78
|
+
await ensureVibeDir(cwd);
|
|
79
|
+
await writeConfig(cwd, config);
|
|
80
|
+
// 2. 이슈 목록 가져오기
|
|
81
|
+
const issueSpinner = ora('GitHub 이슈 목록 가져오는 중...').start();
|
|
82
|
+
const openIssues = await listOpenIssues(owner, repo);
|
|
83
|
+
issueSpinner.succeed(`이슈 ${openIssues.length}개 로드됨`);
|
|
84
|
+
// 3. state.json 초기화
|
|
85
|
+
const initialState = {
|
|
86
|
+
project: answers.projectName,
|
|
87
|
+
lastUpdated: new Date().toISOString(),
|
|
88
|
+
collaborators: {},
|
|
89
|
+
activeWork: null,
|
|
90
|
+
issues: openIssues,
|
|
91
|
+
workLog: [],
|
|
92
|
+
};
|
|
93
|
+
await writeState(cwd, initialState);
|
|
94
|
+
// 4. 레포 분석 + CHARTER 생성
|
|
95
|
+
const analysisSpinner = ora('레포 분석 중...').start();
|
|
96
|
+
const [readme, packageJson, rootStructure] = await Promise.all([
|
|
97
|
+
getFileContent(owner, repo, 'README.md'),
|
|
98
|
+
getFileContent(owner, repo, 'package.json'),
|
|
99
|
+
getRootStructure(owner, repo),
|
|
100
|
+
]);
|
|
101
|
+
if (aiProvider) {
|
|
102
|
+
analysisSpinner.text = 'CHARTER 초안 생성 중 (약 20초)...';
|
|
103
|
+
try {
|
|
104
|
+
const charter = await generateCharter({
|
|
105
|
+
projectName: answers.projectName,
|
|
106
|
+
owner,
|
|
107
|
+
repo,
|
|
108
|
+
readme,
|
|
109
|
+
packageJson,
|
|
110
|
+
rootStructure,
|
|
111
|
+
openIssues,
|
|
112
|
+
});
|
|
113
|
+
await writeCharter(cwd, charter);
|
|
114
|
+
analysisSpinner.succeed('CHARTER.md 생성됨');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
analysisSpinner.warn('CHARTER 생성 실패 — 나중에 수동으로 작성해주세요');
|
|
118
|
+
await writeCharter(cwd, `# CHARTER — ${answers.projectName}\n\n<!-- TODO: 채워주세요 -->\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
analysisSpinner.stop();
|
|
123
|
+
await writeCharter(cwd, `# CHARTER — ${answers.projectName}\n\n<!-- TODO: 채워주세요 -->\n`);
|
|
124
|
+
}
|
|
125
|
+
// 5. mcp.json 생성 (운영자가 서버에서 API 키 관리 — 사용자 측 env 불필요)
|
|
126
|
+
const mcpConfig = {
|
|
127
|
+
mcpServers: {
|
|
128
|
+
'vibe-orchestrator': {
|
|
129
|
+
command: 'npx',
|
|
130
|
+
args: ['vibe-orchestrator', 'serve'],
|
|
131
|
+
env: {},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
await writeFile(path.join(cwd, '.vibe', 'mcp.json'), JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
136
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
137
|
+
console.log(chalk.green(`
|
|
138
|
+
✅ Vibe Orchestrator 초기화 완료! (소요: ${elapsed}초)
|
|
139
|
+
|
|
140
|
+
생성된 파일:
|
|
141
|
+
CHARTER.md — 레포 컨벤션 (직접 수정해서 정확하게 만드세요)
|
|
142
|
+
.vibe/state.json — 협업 로그
|
|
143
|
+
.vibe/config.json — 설정 (owner: ${owner}, repo: ${repo}, branch: ${defaultBranch})
|
|
144
|
+
.vibe/mcp.json — 에이전트 연결 설정
|
|
145
|
+
`));
|
|
146
|
+
// AI 도구 자동 설정
|
|
147
|
+
await connectCommand(cwd);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { startMCPServer } from '../../mcp/server.js';
|
|
3
|
+
import { detectGitHubToken } from '../../github/auth.js';
|
|
4
|
+
import { getAIProvider } from '../../ai/client.js';
|
|
5
|
+
export async function serveCommand() {
|
|
6
|
+
// AI 제공자 확인 (경고만, 종료 안 함)
|
|
7
|
+
const aiProvider = getAIProvider();
|
|
8
|
+
if (!aiProvider) {
|
|
9
|
+
process.stderr.write('[Vibe Orchestrator] ⚠️ AI API 키 없음 — AI 기능(CHARTER 생성, QA, PR 설명)이 비활성화됩니다.\n' +
|
|
10
|
+
'[Vibe Orchestrator] ANTHROPIC_API_KEY 또는 OPENAI_API_KEY를 설정하면 활성화됩니다.\n');
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
process.stderr.write(`[Vibe Orchestrator] ✓ AI 제공자: ${aiProvider}\n`);
|
|
14
|
+
}
|
|
15
|
+
// GitHub 토큰 확인 (gh CLI 포함)
|
|
16
|
+
const githubToken = await detectGitHubToken();
|
|
17
|
+
if (!githubToken) {
|
|
18
|
+
process.stderr.write('[Vibe Orchestrator] ⚠️ GitHub 토큰 없음 — GitHub 연동 기능이 제한됩니다.\n' +
|
|
19
|
+
'[Vibe Orchestrator] gh auth login 또는 GITHUB_TOKEN 환경변수를 설정해주세요.\n');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
process.stderr.write('[Vibe Orchestrator] ✓ GitHub 인증 확인됨\n');
|
|
23
|
+
}
|
|
24
|
+
await startMCPServer();
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { callAI, getAIProvider } from '../../ai/client.js';
|
|
4
|
+
import { readState, readCharter, readIntentLog, readConfig } from '../../state/reader.js';
|
|
5
|
+
import { writeState, registerCollaborator, setActiveWork, updateIssueStage, appendWorkLog, } from '../../state/writer.js';
|
|
6
|
+
import { branchExists, createBranch } from '../../github/branches.js';
|
|
7
|
+
import { createIssue } from '../../github/issues.js';
|
|
8
|
+
function stageToKorean(stage) {
|
|
9
|
+
const map = {
|
|
10
|
+
not_started: '시작 전',
|
|
11
|
+
context_loaded: '맥락 로드됨',
|
|
12
|
+
work_started: '작업 중',
|
|
13
|
+
code_complete: '코드 완료',
|
|
14
|
+
qa_passed: '검토 통과, 팀 공유 대기',
|
|
15
|
+
qa_failed: '검토 실패, 재작업 중',
|
|
16
|
+
pr_created: '팀 공유됨, 최종 반영 대기',
|
|
17
|
+
merge_approved: '최종 반영 승인됨',
|
|
18
|
+
merge_rejected: '최종 반영 거절됨',
|
|
19
|
+
merge_completed: '완료',
|
|
20
|
+
};
|
|
21
|
+
return map[stage] ?? stage;
|
|
22
|
+
}
|
|
23
|
+
function slugify(title) {
|
|
24
|
+
return (title
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[가-힣]/g, '')
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-|-$/g, '')
|
|
29
|
+
.substring(0, 30) || 'task');
|
|
30
|
+
}
|
|
31
|
+
function formatDateTime(isoString) {
|
|
32
|
+
const d = new Date(isoString);
|
|
33
|
+
return d.toLocaleString('ko-KR');
|
|
34
|
+
}
|
|
35
|
+
export async function startCommand(options) {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const state = await readState(cwd);
|
|
38
|
+
const config = await readConfig(cwd);
|
|
39
|
+
// 1. 사용자 확인/등록
|
|
40
|
+
let name = options.user;
|
|
41
|
+
let githubId = options.githubId;
|
|
42
|
+
if (!name || !githubId) {
|
|
43
|
+
const userAnswers = await inquirer.prompt([
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'name',
|
|
47
|
+
message: '이름이 뭔가요?',
|
|
48
|
+
when: !name,
|
|
49
|
+
default: name,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'githubId',
|
|
54
|
+
message: 'GitHub 아이디가 뭔가요?',
|
|
55
|
+
when: !githubId,
|
|
56
|
+
default: githubId,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
name = (name ?? userAnswers.name);
|
|
60
|
+
githubId = (githubId ?? userAnswers.githubId);
|
|
61
|
+
}
|
|
62
|
+
// Collaborator 등록
|
|
63
|
+
await registerCollaborator(cwd, githubId, {
|
|
64
|
+
name,
|
|
65
|
+
githubId,
|
|
66
|
+
lastActive: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
// 2. 재개 감지
|
|
69
|
+
const freshState = await readState(cwd);
|
|
70
|
+
if (freshState.activeWork?.paused && freshState.activeWork.actor === githubId) {
|
|
71
|
+
const aw = freshState.activeWork;
|
|
72
|
+
const pausedIssue = freshState.issues.find((i) => i.number === aw.issueNumber);
|
|
73
|
+
console.log(chalk.yellow(`
|
|
74
|
+
👋 다시 오셨군요, ${name}님!
|
|
75
|
+
|
|
76
|
+
아까 하던 작업이 있습니다:
|
|
77
|
+
· "${pausedIssue?.title ?? `이슈 #${aw.issueNumber}`}"
|
|
78
|
+
· 멈춘 시각: ${aw.pausedAt ? formatDateTime(aw.pausedAt) : '알 수 없음'}
|
|
79
|
+
· 현재 단계: ${stageToKorean(aw.stage)}
|
|
80
|
+
`));
|
|
81
|
+
const resumeAnswer = await inquirer.prompt([
|
|
82
|
+
{
|
|
83
|
+
type: 'list',
|
|
84
|
+
name: 'choice',
|
|
85
|
+
message: '어떻게 하시겠어요?',
|
|
86
|
+
choices: ['이어서 진행', '다른 작업 선택'],
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
if (resumeAnswer.choice === '이어서 진행') {
|
|
90
|
+
// activeWork 재개
|
|
91
|
+
await setActiveWork(cwd, {
|
|
92
|
+
...aw,
|
|
93
|
+
paused: false,
|
|
94
|
+
pausedAt: null,
|
|
95
|
+
});
|
|
96
|
+
// 컨텍스트 블록 출력
|
|
97
|
+
await printContextBlock({
|
|
98
|
+
cwd,
|
|
99
|
+
name,
|
|
100
|
+
githubId,
|
|
101
|
+
issueNumber: aw.issueNumber,
|
|
102
|
+
issueTitle: pausedIssue?.title ?? `이슈 #${aw.issueNumber}`,
|
|
103
|
+
branchName: pausedIssue?.branch ?? '',
|
|
104
|
+
userRequest: '이전 작업 재개',
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 3. 새 작업 시작
|
|
110
|
+
const requestAnswer = await inquirer.prompt([
|
|
111
|
+
{
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'request',
|
|
114
|
+
message: '어떤 작업을 하고 싶으신가요?',
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
const userRequest = requestAnswer.request;
|
|
118
|
+
// Claude API로 이슈 매칭
|
|
119
|
+
const freshState2 = await readState(cwd);
|
|
120
|
+
const openIssues = freshState2.issues.filter((i) => i.status === 'open' || i.status === 'in_progress');
|
|
121
|
+
let matchedIssueNumber = null;
|
|
122
|
+
let matchedIssueTitle = '';
|
|
123
|
+
let isNewIssue = false;
|
|
124
|
+
let suggestedTitle = userRequest;
|
|
125
|
+
if (openIssues.length > 0 && getAIProvider()) {
|
|
126
|
+
try {
|
|
127
|
+
const issueList = openIssues.map((i) => `#${i.number}: ${i.title}`).join('\n');
|
|
128
|
+
const raw = await callAI({
|
|
129
|
+
system: '사용자 요청과 이슈 목록을 비교해서 가장 관련 있는 이슈를 찾아라. 반드시 순수 JSON만 반환 (마크다운 코드블록 없이): { "matched": true|false, "issueNumber": number|null, "confidence": 0~1, "reason": string }',
|
|
130
|
+
user: `요청: ${userRequest}\n\n이슈 목록:\n${issueList}`,
|
|
131
|
+
tier: 'fast',
|
|
132
|
+
});
|
|
133
|
+
const clean = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
134
|
+
const parsed = JSON.parse(clean);
|
|
135
|
+
if (parsed.matched && parsed.confidence >= 0.7 && parsed.issueNumber) {
|
|
136
|
+
matchedIssueNumber = parsed.issueNumber;
|
|
137
|
+
matchedIssueTitle =
|
|
138
|
+
openIssues.find((i) => i.number === parsed.issueNumber)?.title ?? '';
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
isNewIssue = true;
|
|
142
|
+
suggestedTitle = userRequest.substring(0, 50);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
isNewIssue = true;
|
|
147
|
+
suggestedTitle = userRequest.substring(0, 50);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
isNewIssue = true;
|
|
152
|
+
suggestedTitle = userRequest.substring(0, 50);
|
|
153
|
+
}
|
|
154
|
+
let finalIssueNumber;
|
|
155
|
+
let finalIssueTitle;
|
|
156
|
+
if (matchedIssueNumber && !isNewIssue) {
|
|
157
|
+
// 체크포인트 A — 기존 이슈 매칭
|
|
158
|
+
console.log(chalk.cyan(`
|
|
159
|
+
관련된 작업 항목이 있습니다:
|
|
160
|
+
|
|
161
|
+
#${matchedIssueNumber} "${matchedIssueTitle}"
|
|
162
|
+
|
|
163
|
+
이 작업에 이어서 작업할까요, 아니면 새로 시작할까요?`));
|
|
164
|
+
const workChoice = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'list',
|
|
167
|
+
name: 'choice',
|
|
168
|
+
message: '선택하세요:',
|
|
169
|
+
choices: ['이어서 작업', '새로 시작'],
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
if (workChoice.choice === '이어서 작업') {
|
|
173
|
+
finalIssueNumber = matchedIssueNumber;
|
|
174
|
+
finalIssueTitle = matchedIssueTitle;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
isNewIssue = true;
|
|
178
|
+
finalIssueNumber = 0;
|
|
179
|
+
finalIssueTitle = suggestedTitle;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
finalIssueNumber = 0;
|
|
184
|
+
finalIssueTitle = suggestedTitle;
|
|
185
|
+
}
|
|
186
|
+
if (isNewIssue || finalIssueNumber === 0) {
|
|
187
|
+
console.log(chalk.cyan(`
|
|
188
|
+
관련된 기존 작업이 없습니다. 새 작업 항목을 만들까요?
|
|
189
|
+
|
|
190
|
+
제목: "${suggestedTitle}"`));
|
|
191
|
+
const createAnswer = await inquirer.prompt([
|
|
192
|
+
{
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'create',
|
|
195
|
+
message: '새 작업 항목을 만들까요?',
|
|
196
|
+
default: true,
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
if (!createAnswer.create) {
|
|
200
|
+
console.log(chalk.yellow('작업을 취소했습니다.'));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (config) {
|
|
204
|
+
const newIssueNumber = await createIssue(config.owner, config.repo, suggestedTitle, `Vibe Orchestrator로 생성된 작업\n\n요청: ${userRequest}`);
|
|
205
|
+
finalIssueNumber = newIssueNumber;
|
|
206
|
+
finalIssueTitle = suggestedTitle;
|
|
207
|
+
// state에 추가
|
|
208
|
+
const state3 = await readState(cwd);
|
|
209
|
+
state3.issues.push({
|
|
210
|
+
number: finalIssueNumber,
|
|
211
|
+
title: finalIssueTitle,
|
|
212
|
+
status: 'open',
|
|
213
|
+
branch: null,
|
|
214
|
+
assignee: null,
|
|
215
|
+
stage: 'not_started',
|
|
216
|
+
prNumber: null,
|
|
217
|
+
intentLogPath: null,
|
|
218
|
+
});
|
|
219
|
+
await writeState(cwd, state3);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.log(chalk.red('config.json이 없어 GitHub 이슈를 생성할 수 없습니다.'));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 브랜치 생성
|
|
227
|
+
const branchName = `vibe/${githubId}/${finalIssueNumber}/${slugify(finalIssueTitle)}`;
|
|
228
|
+
if (config) {
|
|
229
|
+
const exists = await branchExists(config.owner, config.repo, branchName);
|
|
230
|
+
if (!exists) {
|
|
231
|
+
await createBranch(config.owner, config.repo, branchName, config.defaultBranch);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// state 업데이트
|
|
235
|
+
await setActiveWork(cwd, {
|
|
236
|
+
issueNumber: finalIssueNumber,
|
|
237
|
+
actor: githubId,
|
|
238
|
+
stage: 'work_started',
|
|
239
|
+
startedAt: new Date().toISOString(),
|
|
240
|
+
paused: false,
|
|
241
|
+
pausedAt: null,
|
|
242
|
+
retryCount: 0,
|
|
243
|
+
});
|
|
244
|
+
await updateIssueStage(cwd, finalIssueNumber, {
|
|
245
|
+
status: 'in_progress',
|
|
246
|
+
branch: branchName,
|
|
247
|
+
assignee: githubId,
|
|
248
|
+
stage: 'work_started',
|
|
249
|
+
});
|
|
250
|
+
await appendWorkLog(cwd, {
|
|
251
|
+
actor: { name, githubId, agent: 'cli' },
|
|
252
|
+
stage: 'work_started',
|
|
253
|
+
issueNumber: finalIssueNumber,
|
|
254
|
+
action: 'start_work',
|
|
255
|
+
message: `작업 시작: #${finalIssueNumber} "${finalIssueTitle}"`,
|
|
256
|
+
});
|
|
257
|
+
// 컨텍스트 블록 출력
|
|
258
|
+
await printContextBlock({
|
|
259
|
+
cwd,
|
|
260
|
+
name,
|
|
261
|
+
githubId,
|
|
262
|
+
issueNumber: finalIssueNumber,
|
|
263
|
+
issueTitle: finalIssueTitle,
|
|
264
|
+
branchName,
|
|
265
|
+
userRequest,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function printContextBlock(params) {
|
|
269
|
+
const { cwd, name, githubId, issueNumber, issueTitle, branchName, userRequest } = params;
|
|
270
|
+
const charter = await readCharter(cwd);
|
|
271
|
+
const state = await readState(cwd);
|
|
272
|
+
const intentLog = await readIntentLog(cwd, issueNumber);
|
|
273
|
+
const inProgress = state.issues.filter((i) => i.status === 'in_progress' || i.status === 'pr_created');
|
|
274
|
+
const stateSummary = inProgress.length > 0
|
|
275
|
+
? inProgress.map((i) => ` #${i.number} ${i.title} (${i.assignee ?? '미배정'})`).join('\n')
|
|
276
|
+
: ' 진행 중인 작업 없음';
|
|
277
|
+
const intentContent = intentLog
|
|
278
|
+
? `결정 사항: ${intentLog.decided.join(', ')}\n거절된 방안: ${intentLog.rejected.join(', ')}`
|
|
279
|
+
: '없음 (새 작업)';
|
|
280
|
+
console.log(chalk.green(`
|
|
281
|
+
═══════════════════════════════════════════════════════
|
|
282
|
+
✅ 준비 완료! 아래 내용을 AI에게 복사해서 넣어주세요:
|
|
283
|
+
(MCP 연결 시에는 자동으로 주입됩니다)
|
|
284
|
+
═══════════════════════════════════════════════════════
|
|
285
|
+
|
|
286
|
+
[Vibe Orchestrator 컨텍스트]
|
|
287
|
+
작업자: ${name} (${githubId})
|
|
288
|
+
작업: 이슈 #${issueNumber} — ${issueTitle}
|
|
289
|
+
브랜치: ${branchName}
|
|
290
|
+
|
|
291
|
+
=== CHARTER ===
|
|
292
|
+
${charter || '(CHARTER 없음)'}
|
|
293
|
+
|
|
294
|
+
=== 팀 현황 ===
|
|
295
|
+
${stateSummary}
|
|
296
|
+
|
|
297
|
+
=== 의도 로그 ===
|
|
298
|
+
${intentContent}
|
|
299
|
+
|
|
300
|
+
요청사항: ${userRequest}
|
|
301
|
+
|
|
302
|
+
───────────────────────────────────────────────────────
|
|
303
|
+
MCP 연결 방법: claude --mcp-config .vibe/mcp.json
|
|
304
|
+
═══════════════════════════════════════════════════════
|
|
305
|
+
`));
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function statusCommand(): Promise<void>;
|