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
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Vibe Orchestrator
|
|
2
|
+
|
|
3
|
+
**누가 어떤 AI를 써도, 항상 한 팀처럼 작동하는 바이브 코딩 협업 도구**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vibe-orchestrator)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://modelcontextprotocol.io/)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 사전 요구사항
|
|
12
|
+
|
|
13
|
+
- **Node.js 18+**
|
|
14
|
+
- 환경변수 설정 (`.env` 파일 또는 쉘 환경):
|
|
15
|
+
|
|
16
|
+
```env
|
|
17
|
+
ANTHROPIC_API_KEY=sk-ant-... # Claude API 키
|
|
18
|
+
GITHUB_TOKEN=ghp_... # GitHub Personal Access Token (repo 권한 필요)
|
|
19
|
+
GITHUB_OWNER=your-username # GitHub 유저명 (폴백용)
|
|
20
|
+
GITHUB_REPO=test-repo-name # 레포 이름 (폴백용)
|
|
21
|
+
GITHUB_DEFAULT_BRANCH=main # 기본 브랜치 (폴백용)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 빠른 시작 (3단계)
|
|
27
|
+
|
|
28
|
+
### 1단계 — 초기화
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 레포 디렉토리로 이동
|
|
32
|
+
cd /path/to/your/project
|
|
33
|
+
|
|
34
|
+
# Vibe Orchestrator 초기화
|
|
35
|
+
npx vibe-orchestrator init
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
대화형 질문에 답하면 다음 파일이 생성됩니다:
|
|
39
|
+
- `CHARTER.md` — AI가 분석한 레포 컨벤션 (직접 수정해 정확하게 만드세요)
|
|
40
|
+
- `.vibe/state.json` — 팀 협업 상태
|
|
41
|
+
- `.vibe/config.json` — 설정
|
|
42
|
+
- `.vibe/mcp.json` — AI 에이전트 연결 설정
|
|
43
|
+
|
|
44
|
+
### 2단계 — AI 에이전트 연결
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Claude Code
|
|
48
|
+
claude --mcp-config .vibe/mcp.json
|
|
49
|
+
|
|
50
|
+
# Gemini CLI
|
|
51
|
+
gemini --mcp-config .vibe/mcp.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3단계 — 작업 시작
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# CLI로 직접 작업 시작 (컨텍스트 블록 생성)
|
|
58
|
+
npx vibe-orchestrator start
|
|
59
|
+
|
|
60
|
+
# 또는 AI 에이전트 연결 후 vibe_start_session 도구 호출
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 에이전트별 연결 방법
|
|
66
|
+
|
|
67
|
+
### Claude Code
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude --mcp-config .vibe/mcp.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Claude Code 내에서 `vibe_start_session`을 호출하면 자동으로 컨텍스트가 주입됩니다.
|
|
74
|
+
|
|
75
|
+
### Gemini CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gemini --mcp-config .vibe/mcp.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
동일한 `.vibe/state.json`을 공유하므로 팀 현황이 실시간으로 동기화됩니다.
|
|
82
|
+
|
|
83
|
+
### Cursor / 기타 MCP 지원 에디터
|
|
84
|
+
|
|
85
|
+
Cursor의 MCP 설정에 `.vibe/mcp.json` 내용을 추가하세요.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## CLI 명령어 레퍼런스
|
|
90
|
+
|
|
91
|
+
| 명령어 | 설명 |
|
|
92
|
+
|--------|------|
|
|
93
|
+
| `vibe init` | 프로젝트를 Vibe Orchestrator에 연결하고 CHARTER, state.json 생성 |
|
|
94
|
+
| `vibe start` | 작업을 시작하거나 이어서 진행 (컨텍스트 블록 출력) |
|
|
95
|
+
| `vibe status` | 팀 현황 출력 (진행 중 작업, 시작 가능한 작업, 최근 완료) |
|
|
96
|
+
| `vibe serve` | MCP 서버 시작 (AI 에이전트 연결용, mcp.json에서 자동 실행됨) |
|
|
97
|
+
|
|
98
|
+
### 옵션
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
vibe start --user "김지혁" --github-id "jihyuk-kim"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 생성 파일 설명
|
|
107
|
+
|
|
108
|
+
### CHARTER.md
|
|
109
|
+
|
|
110
|
+
AI가 레포를 분석해 자동 생성하는 팀 헌법입니다.
|
|
111
|
+
|
|
112
|
+
- **이 프로젝트는 무엇인가** — 한 문단 요약
|
|
113
|
+
- **기술 스택** — 감지된 언어/프레임워크
|
|
114
|
+
- **폴더 구조와 역할** — 각 폴더의 역할
|
|
115
|
+
- **현재 구현된 기능 맵** — 완료된 기능 테이블 (merge 시 자동 갱신)
|
|
116
|
+
- **핵심 설계 결정** — 중요한 아키텍처 결정
|
|
117
|
+
- **절대 하면 안 되는 것** — QA Guardian이 자동으로 검사하는 금지 패턴
|
|
118
|
+
|
|
119
|
+
> 생성 후 직접 수정해 프로젝트에 맞게 정확히 채워주세요.
|
|
120
|
+
|
|
121
|
+
### .vibe/state.json
|
|
122
|
+
|
|
123
|
+
모든 에이전트가 공유하는 협업 상태 파일입니다.
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"project": "my-project",
|
|
128
|
+
"lastUpdated": "2024-01-01T00:00:00.000Z",
|
|
129
|
+
"collaborators": { "jihyuk-kim": { "name": "김지혁", ... } },
|
|
130
|
+
"activeWork": { "issueNumber": 7, "actor": "jihyuk-kim", ... },
|
|
131
|
+
"issues": [ { "number": 7, "title": "환불 기능", "stage": "work_started", ... } ],
|
|
132
|
+
"workLog": [ { "id": "log-...", "stage": "work_started", ... } ]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### .vibe/intents/
|
|
137
|
+
|
|
138
|
+
각 이슈별 의도 로그가 저장됩니다 (`{issueNumber}.json`).
|
|
139
|
+
|
|
140
|
+
작업 중 결정한 사항, 거절한 방안, 경고, 다음 단계를 기록합니다.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## MCP 도구 목록 (8개)
|
|
145
|
+
|
|
146
|
+
| 도구 | 설명 |
|
|
147
|
+
|------|------|
|
|
148
|
+
| `vibe_start_session` | 세션 시작 시 가장 먼저 호출 — CHARTER, 팀 현황, 내 상태 반환 |
|
|
149
|
+
| `vibe_analyze_request` | 사용자 요청을 분석해 관련 기존 작업 항목을 찾거나 새 항목 제안 |
|
|
150
|
+
| `vibe_start_work` | 작업 항목 선택 후 작업 공간(브랜치) 준비 |
|
|
151
|
+
| `vibe_record_checkpoint` | 각 단계 완료 시 기록 — 다음 단계 안내 메시지 반환 |
|
|
152
|
+
| `vibe_request_qa` | 코드 검토 요청 — 정적 분석 + AI 의미 검사 2단계 수행 |
|
|
153
|
+
| `vibe_create_pr` | 검토 통과 후 팀에 공유 (PR 자동 생성) |
|
|
154
|
+
| `vibe_request_merge_review` | 최종 반영 전 충돌 검사 — 다른 팀원 작업과 파일 겹침 확인 |
|
|
155
|
+
| `vibe_execute_merge` | 최종 반영 실행 — squash merge + CHARTER 기능 맵 자동 갱신 |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 두 에이전트 동시 사용 예시
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 터미널 A — 김지혁 (Claude Code)
|
|
163
|
+
claude --mcp-config .vibe/mcp.json
|
|
164
|
+
# → vibe_start_session { userName: "김지혁", githubId: "jihyuk-kim", agentName: "claude-code" }
|
|
165
|
+
|
|
166
|
+
# 터미널 B — 이민수 (Gemini CLI)
|
|
167
|
+
gemini --mcp-config .vibe/mcp.json
|
|
168
|
+
# → vibe_start_session { userName: "이민수", githubId: "minsoo-lee", agentName: "gemini-cli" }
|
|
169
|
+
|
|
170
|
+
# 두 세션이 동일한 state.json을 공유 → 팀 현황 실시간 동기화
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 라이선스
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 통합 AI 클라이언트
|
|
3
|
+
* 우선순위: proxy (로그인 토큰) > ANTHROPIC_API_KEY > OPENAI_API_KEY
|
|
4
|
+
* 모두 없으면 AI 기능 비활성화 (graceful degradation)
|
|
5
|
+
*/
|
|
6
|
+
export type AIProvider = 'proxy' | 'anthropic' | 'openai' | null;
|
|
7
|
+
export type AITier = 'smart' | 'fast';
|
|
8
|
+
export declare function getStoredToken(): string | null;
|
|
9
|
+
export declare function getAIProvider(): AIProvider;
|
|
10
|
+
export declare function callAI(params: {
|
|
11
|
+
system: string;
|
|
12
|
+
user: string;
|
|
13
|
+
tier?: AITier;
|
|
14
|
+
maxTokens?: number;
|
|
15
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 통합 AI 클라이언트
|
|
3
|
+
* 우선순위: proxy (로그인 토큰) > ANTHROPIC_API_KEY > OPENAI_API_KEY
|
|
4
|
+
* 모두 없으면 AI 기능 비활성화 (graceful degradation)
|
|
5
|
+
*/
|
|
6
|
+
import { readAuthData } from '../cli/commands/auth.js';
|
|
7
|
+
const VIBE_PROXY_URL = process.env.VIBE_API_URL ?? 'https://vibeorchestratorserver.vercel.app';
|
|
8
|
+
const MODELS = {
|
|
9
|
+
anthropic: {
|
|
10
|
+
smart: 'claude-sonnet-4-6',
|
|
11
|
+
fast: 'claude-haiku-4-5-20251001',
|
|
12
|
+
},
|
|
13
|
+
openai: {
|
|
14
|
+
smart: 'gpt-4o',
|
|
15
|
+
fast: 'gpt-4o-mini',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export function getStoredToken() {
|
|
19
|
+
try {
|
|
20
|
+
const data = readAuthData();
|
|
21
|
+
return data?.token ?? null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getAIProvider() {
|
|
28
|
+
if (getStoredToken())
|
|
29
|
+
return 'proxy';
|
|
30
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
31
|
+
return 'anthropic';
|
|
32
|
+
if (process.env.OPENAI_API_KEY)
|
|
33
|
+
return 'openai';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
export async function callAI(params) {
|
|
37
|
+
const { system, user, tier = 'fast', maxTokens = 1024 } = params;
|
|
38
|
+
const provider = getAIProvider();
|
|
39
|
+
if (!provider) {
|
|
40
|
+
throw new Error('AI를 사용하려면 로그인이 필요합니다.\n vibe auth login');
|
|
41
|
+
}
|
|
42
|
+
// 1. Proxy (operator server — user logged in)
|
|
43
|
+
if (provider === 'proxy') {
|
|
44
|
+
const token = getStoredToken();
|
|
45
|
+
const res = await fetch(`${VIBE_PROXY_URL}/api/ai/complete`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ system, user, tier, maxTokens }),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const err = (await res.json().catch(() => ({ error: res.statusText })));
|
|
55
|
+
if (res.status === 401) {
|
|
56
|
+
throw new Error('토큰이 만료됐습니다. 다시 로그인하세요:\n vibe auth login');
|
|
57
|
+
}
|
|
58
|
+
throw new Error(err.error ?? `프록시 서버 오류 (${res.status})`);
|
|
59
|
+
}
|
|
60
|
+
const data = (await res.json());
|
|
61
|
+
return data.text;
|
|
62
|
+
}
|
|
63
|
+
// 2. Direct Anthropic (operator running locally)
|
|
64
|
+
if (provider === 'anthropic') {
|
|
65
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
66
|
+
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
67
|
+
const response = await client.messages.create({
|
|
68
|
+
model: MODELS.anthropic[tier],
|
|
69
|
+
max_tokens: maxTokens,
|
|
70
|
+
system,
|
|
71
|
+
messages: [{ role: 'user', content: user }],
|
|
72
|
+
});
|
|
73
|
+
const content = response.content[0];
|
|
74
|
+
return content.type === 'text' ? content.text : '';
|
|
75
|
+
}
|
|
76
|
+
// 3. Direct OpenAI (operator running locally)
|
|
77
|
+
const { default: OpenAI } = await import('openai');
|
|
78
|
+
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
79
|
+
const response = await client.chat.completions.create({
|
|
80
|
+
model: MODELS.openai[tier],
|
|
81
|
+
max_tokens: maxTokens,
|
|
82
|
+
messages: [
|
|
83
|
+
{ role: 'system', content: system },
|
|
84
|
+
{ role: 'user', content: user },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
return response.choices[0]?.message?.content ?? '';
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IssueState } from '../state/types.js';
|
|
2
|
+
export declare function generateCharter(params: {
|
|
3
|
+
projectName: string;
|
|
4
|
+
owner: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
readme: string | null;
|
|
7
|
+
packageJson: string | null;
|
|
8
|
+
rootStructure: string[];
|
|
9
|
+
openIssues: IssueState[];
|
|
10
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { callAI } from '../ai/client.js';
|
|
2
|
+
export async function generateCharter(params) {
|
|
3
|
+
const { projectName, owner, repo, readme, packageJson, rootStructure, openIssues } = params;
|
|
4
|
+
const userContent = `
|
|
5
|
+
프로젝트 이름: ${projectName}
|
|
6
|
+
레포: ${owner}/${repo}
|
|
7
|
+
|
|
8
|
+
루트 구조:
|
|
9
|
+
${rootStructure.join('\n')}
|
|
10
|
+
|
|
11
|
+
README:
|
|
12
|
+
${readme ?? '(없음)'}
|
|
13
|
+
|
|
14
|
+
package.json:
|
|
15
|
+
${packageJson ?? '(없음)'}
|
|
16
|
+
|
|
17
|
+
열린 이슈 목록:
|
|
18
|
+
${openIssues.length > 0 ? openIssues.map((i) => `- #${i.number} ${i.title}`).join('\n') : '(없음)'}
|
|
19
|
+
`.trim();
|
|
20
|
+
const text = await callAI({
|
|
21
|
+
system: `당신은 소프트웨어 팀의 기술 리드입니다.
|
|
22
|
+
GitHub 레포 정보를 분석하여 팀 협업을 위한 CHARTER.md 마크다운을 작성하세요.
|
|
23
|
+
|
|
24
|
+
반드시 다음 섹션을 포함하세요:
|
|
25
|
+
1. 이 프로젝트는 무엇인가 (한 문단)
|
|
26
|
+
2. 기술 스택 (감지된 언어/프레임워크 기반)
|
|
27
|
+
3. 폴더 구조와 역할 (루트 구조 기반 추론)
|
|
28
|
+
4. 현재 구현된 기능 맵 (빈 마크다운 테이블로 시작, 헤더: | 기능 | 위치 | 담당 | 이슈 | 상태 |)
|
|
29
|
+
5. 핵심 설계 결정 (감지된 패턴 기반, 없으면 placeholder)
|
|
30
|
+
6. 절대 하면 안 되는 것 (구체적인 금지 패턴, [Guardian이 자동 검사] 태그 포함)
|
|
31
|
+
|
|
32
|
+
추상적 원칙보다 "이 패턴은 금지"처럼 구체적이고 검사 가능한 규칙을 작성하세요.
|
|
33
|
+
정보가 부족한 섹션은 <!-- TODO: 채워주세요 --> 플레이스홀더를 사용하세요.
|
|
34
|
+
마크다운만 반환하세요 (코드블록 감싸지 말 것).`,
|
|
35
|
+
user: userContent,
|
|
36
|
+
tier: 'smart',
|
|
37
|
+
maxTokens: 4096,
|
|
38
|
+
});
|
|
39
|
+
return text || '# CHARTER\n\n<!-- TODO: 채워주세요 -->';
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function updateFeatureMap(repoPath: string, feature: {
|
|
2
|
+
title: string;
|
|
3
|
+
path: string;
|
|
4
|
+
assignee: string;
|
|
5
|
+
issueNumber: number;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function extractForbiddenPatterns(charter: string): Array<{
|
|
8
|
+
description: string;
|
|
9
|
+
patternString?: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readCharter, } from '../state/reader.js';
|
|
2
|
+
import { writeCharter } from '../state/writer.js';
|
|
3
|
+
export async function updateFeatureMap(repoPath, feature) {
|
|
4
|
+
let charter = await readCharter(repoPath);
|
|
5
|
+
const date = new Date().toLocaleDateString('ko-KR');
|
|
6
|
+
const newRow = `| ${feature.title} | ${feature.path} | ${feature.assignee} | #${feature.issueNumber} | ${date} |`;
|
|
7
|
+
// 기능 맵 테이블 찾기
|
|
8
|
+
const tableHeaderPattern = /\|\s*기능\s*\|\s*위치\s*\|\s*담당\s*\|\s*이슈\s*\|\s*상태\s*\|/;
|
|
9
|
+
if (tableHeaderPattern.test(charter)) {
|
|
10
|
+
// 테이블 구분선 다음 줄 찾아서 행 추가
|
|
11
|
+
charter = charter.replace(/((\|\s*[-:]+\s*\|)+\s*\n)/, `$1${newRow}\n`);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
// 섹션 없으면 전체 추가
|
|
15
|
+
const featureSection = `\n## 현재 구현된 기능 맵\n\n| 기능 | 위치 | 담당 | 이슈 | 상태 |\n|------|------|------|------|------|\n${newRow}\n`;
|
|
16
|
+
charter += featureSection;
|
|
17
|
+
}
|
|
18
|
+
await writeCharter(repoPath, charter);
|
|
19
|
+
}
|
|
20
|
+
export function extractForbiddenPatterns(charter) {
|
|
21
|
+
const results = [];
|
|
22
|
+
// "절대 하면 안 되는 것" 섹션 찾기
|
|
23
|
+
const sectionMatch = charter.match(/##\s*절대\s*하면\s*안\s*되는\s*것[\s\S]*?(?=\n##\s|\n#\s|$)/);
|
|
24
|
+
if (!sectionMatch)
|
|
25
|
+
return results;
|
|
26
|
+
const section = sectionMatch[0];
|
|
27
|
+
const lines = section.split('\n').filter((line) => line.trim().startsWith('-'));
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const text = line.replace(/^[\s-]+/, '').trim();
|
|
30
|
+
if (!text)
|
|
31
|
+
continue;
|
|
32
|
+
// 코드 패턴 추출 시도 (backtick으로 감싸진 패턴)
|
|
33
|
+
const codeMatch = text.match(/`([^`]+)`/);
|
|
34
|
+
results.push({
|
|
35
|
+
description: text,
|
|
36
|
+
patternString: codeMatch ? codeMatch[1] : undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=updater.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface AuthData {
|
|
2
|
+
token: string;
|
|
3
|
+
user: {
|
|
4
|
+
login: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
githubToken?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function readAuthData(): AuthData | null;
|
|
11
|
+
export declare function authCommand(action: string): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
const AUTH_FILE = path.join(os.homedir(), '.vibe', 'auth.json');
|
|
8
|
+
const PROXY_URL = process.env.VIBE_API_URL ?? 'https://vibeorchestratorserver.vercel.app';
|
|
9
|
+
const CALLBACK_PORT = 7777;
|
|
10
|
+
/* ── token helpers ─────────────────────────────────────────────── */
|
|
11
|
+
export function readAuthData() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = fs.readFileSync(AUTH_FILE, 'utf-8');
|
|
14
|
+
const data = JSON.parse(raw);
|
|
15
|
+
if (new Date(data.expiresAt) < new Date())
|
|
16
|
+
return null; // expired
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function saveAuthData(data) {
|
|
24
|
+
const dir = path.dirname(AUTH_FILE);
|
|
25
|
+
if (!fs.existsSync(dir))
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
function deleteAuthData() {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(AUTH_FILE);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// already gone
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Decode JWT payload without verifying signature (CLI only needs expiry/user info) */
|
|
38
|
+
function decodeJwtPayload(token) {
|
|
39
|
+
try {
|
|
40
|
+
const parts = token.split('.');
|
|
41
|
+
if (parts.length !== 3)
|
|
42
|
+
return null;
|
|
43
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
44
|
+
return JSON.parse(payload);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/* ── open browser cross-platform ───────────────────────────────── */
|
|
51
|
+
function openBrowser(url) {
|
|
52
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"` :
|
|
53
|
+
process.platform === 'darwin' ? `open "${url}"` :
|
|
54
|
+
`xdg-open "${url}"`;
|
|
55
|
+
try {
|
|
56
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.log(chalk.yellow(`브라우저를 열 수 없습니다. 직접 아래 URL을 여세요:\n${url}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/* ── login flow ─────────────────────────────────────────────────── */
|
|
63
|
+
async function loginFlow() {
|
|
64
|
+
const state = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
65
|
+
console.log(chalk.cyan('\n🔐 Vibe Orchestrator 로그인\n'));
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
let resolved = false;
|
|
68
|
+
// 1. Start local callback server
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`);
|
|
71
|
+
if (url.pathname !== '/callback') {
|
|
72
|
+
res.writeHead(404);
|
|
73
|
+
res.end();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const receivedToken = url.searchParams.get('token');
|
|
77
|
+
const receivedState = url.searchParams.get('state');
|
|
78
|
+
// Validate state to prevent CSRF
|
|
79
|
+
if (receivedState !== state || !receivedToken) {
|
|
80
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
81
|
+
res.end('<h1>인증 실패</h1><p>잘못된 요청입니다. 다시 시도해주세요.</p>');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Decode payload
|
|
85
|
+
const payload = decodeJwtPayload(receivedToken);
|
|
86
|
+
if (!payload) {
|
|
87
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
88
|
+
res.end('<h1>인증 실패</h1><p>잘못된 토큰입니다.</p>');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Save token
|
|
92
|
+
const authData = {
|
|
93
|
+
token: receivedToken,
|
|
94
|
+
user: { login: payload.login ?? payload.sub, name: payload.name },
|
|
95
|
+
expiresAt: new Date(payload.exp * 1000).toISOString(),
|
|
96
|
+
githubToken: payload.githubToken ?? null,
|
|
97
|
+
};
|
|
98
|
+
saveAuthData(authData);
|
|
99
|
+
// Respond to browser — redirect to /callback page
|
|
100
|
+
res.writeHead(302, { Location: `${PROXY_URL}/callback` });
|
|
101
|
+
res.end();
|
|
102
|
+
// Shutdown server and resolve
|
|
103
|
+
if (!resolved) {
|
|
104
|
+
resolved = true;
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
server.close();
|
|
107
|
+
console.log(chalk.green(`\n✅ 로그인 완료! 환영합니다, ${authData.user.name} (@${authData.user.login})\n`));
|
|
108
|
+
resolve();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.listen(CALLBACK_PORT, () => {
|
|
112
|
+
// 2. Open browser
|
|
113
|
+
const loginUrl = `${PROXY_URL}/login?port=${CALLBACK_PORT}&state=${encodeURIComponent(state)}`;
|
|
114
|
+
console.log(chalk.gray(`브라우저가 열립니다... (${loginUrl})\n`));
|
|
115
|
+
openBrowser(loginUrl);
|
|
116
|
+
console.log(chalk.gray('GitHub 로그인을 완료해주세요. 대기 중...\n'));
|
|
117
|
+
});
|
|
118
|
+
server.on('error', (err) => {
|
|
119
|
+
if (err.code === 'EADDRINUSE') {
|
|
120
|
+
reject(new Error(`포트 ${CALLBACK_PORT}이 이미 사용 중입니다. 잠시 후 다시 시도해주세요.`));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
reject(err);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// Timeout after 5 minutes
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
if (!resolved) {
|
|
129
|
+
resolved = true;
|
|
130
|
+
server.close();
|
|
131
|
+
reject(new Error('로그인 시간이 초과됐습니다 (5분). 다시 시도해주세요.'));
|
|
132
|
+
}
|
|
133
|
+
}, 5 * 60 * 1000);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/* ── public command ─────────────────────────────────────────────── */
|
|
137
|
+
export async function authCommand(action) {
|
|
138
|
+
switch (action) {
|
|
139
|
+
case 'login': {
|
|
140
|
+
const existing = readAuthData();
|
|
141
|
+
if (existing) {
|
|
142
|
+
console.log(chalk.yellow(`이미 로그인되어 있습니다: @${existing.user.login}\n` +
|
|
143
|
+
`만료: ${new Date(existing.expiresAt).toLocaleString('ko-KR')}\n\n` +
|
|
144
|
+
`재로그인하려면: vibe auth logout 후 vibe auth login`));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await loginFlow();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
case 'logout': {
|
|
151
|
+
const data = readAuthData();
|
|
152
|
+
deleteAuthData();
|
|
153
|
+
if (data) {
|
|
154
|
+
console.log(chalk.green(`👋 @${data.user.login} 로그아웃 완료.`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log(chalk.yellow('로그인 상태가 아닙니다.'));
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'status': {
|
|
162
|
+
const data = readAuthData();
|
|
163
|
+
if (!data) {
|
|
164
|
+
console.log(chalk.yellow('\n로그인되어 있지 않습니다.\n vibe auth login\n'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const expiresAt = new Date(data.expiresAt);
|
|
168
|
+
const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / 86400000);
|
|
169
|
+
console.log(chalk.green(`
|
|
170
|
+
✅ 로그인 상태
|
|
171
|
+
사용자: ${data.user.name} (@${data.user.login})
|
|
172
|
+
만료: ${expiresAt.toLocaleString('ko-KR')} (${daysLeft}일 남음)
|
|
173
|
+
`));
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
default:
|
|
177
|
+
console.log(chalk.red(`알 수 없는 액션: ${action}\n사용법: vibe auth <login|logout|status>`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function connectCommand(cwd: string): Promise<void>;
|