@su-record/vibe 2.4.6 → 2.4.7

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.
Files changed (109) hide show
  1. package/.claude/settings.local.json +28 -24
  2. package/README.md +32 -15
  3. package/dist/cli/auth.d.ts +13 -0
  4. package/dist/cli/auth.d.ts.map +1 -0
  5. package/dist/cli/auth.js +118 -0
  6. package/dist/cli/auth.js.map +1 -0
  7. package/dist/cli/collaborator.d.ts +8 -0
  8. package/dist/cli/collaborator.d.ts.map +1 -0
  9. package/dist/cli/collaborator.js +136 -0
  10. package/dist/cli/collaborator.js.map +1 -0
  11. package/dist/cli/detect.d.ts +35 -0
  12. package/dist/cli/detect.d.ts.map +1 -0
  13. package/dist/cli/detect.js +376 -0
  14. package/dist/cli/detect.js.map +1 -0
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +201 -2001
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/llm.d.ts +49 -0
  19. package/dist/cli/llm.d.ts.map +1 -0
  20. package/dist/cli/llm.js +464 -0
  21. package/dist/cli/llm.js.map +1 -0
  22. package/dist/cli/mcp.d.ts +49 -0
  23. package/dist/cli/mcp.d.ts.map +1 -0
  24. package/dist/cli/mcp.js +169 -0
  25. package/dist/cli/mcp.js.map +1 -0
  26. package/dist/cli/setup.d.ts +53 -0
  27. package/dist/cli/setup.d.ts.map +1 -0
  28. package/dist/cli/setup.js +455 -0
  29. package/dist/cli/setup.js.map +1 -0
  30. package/dist/cli/types.d.ts +83 -0
  31. package/dist/cli/types.d.ts.map +1 -0
  32. package/dist/cli/types.js +5 -0
  33. package/dist/cli/types.js.map +1 -0
  34. package/dist/cli/utils.d.ts +40 -0
  35. package/dist/cli/utils.d.ts.map +1 -0
  36. package/dist/cli/utils.js +112 -0
  37. package/dist/cli/utils.js.map +1 -0
  38. package/dist/lib/MemoryManager.js +93 -93
  39. package/dist/lib/MemoryManager.js.map +1 -1
  40. package/dist/lib/ProjectCache.d.ts.map +1 -1
  41. package/dist/lib/ProjectCache.js +2 -1
  42. package/dist/lib/ProjectCache.js.map +1 -1
  43. package/dist/lib/PythonParser.js +109 -109
  44. package/dist/lib/PythonParser.js.map +1 -1
  45. package/dist/lib/constants.d.ts +31 -0
  46. package/dist/lib/constants.d.ts.map +1 -0
  47. package/dist/lib/constants.js +36 -0
  48. package/dist/lib/constants.js.map +1 -0
  49. package/dist/lib/gemini-api.d.ts.map +1 -1
  50. package/dist/lib/gemini-api.js +1 -6
  51. package/dist/lib/gemini-api.js.map +1 -1
  52. package/dist/lib/gemini-mcp.js +15 -15
  53. package/dist/lib/gemini-oauth.js +36 -36
  54. package/dist/lib/gemini-oauth.js.map +1 -1
  55. package/dist/lib/gpt-api.d.ts.map +1 -1
  56. package/dist/lib/gpt-api.js +6 -11
  57. package/dist/lib/gpt-api.js.map +1 -1
  58. package/dist/lib/gpt-mcp.js +17 -17
  59. package/dist/lib/gpt-oauth.js +45 -45
  60. package/dist/lib/gpt-oauth.js.map +1 -1
  61. package/dist/lib/utils.d.ts +21 -0
  62. package/dist/lib/utils.d.ts.map +1 -0
  63. package/dist/lib/utils.js +51 -0
  64. package/dist/lib/utils.js.map +1 -0
  65. package/dist/orchestrator/agentDiscovery.d.ts.map +1 -1
  66. package/dist/orchestrator/agentDiscovery.js +3 -2
  67. package/dist/orchestrator/agentDiscovery.js.map +1 -1
  68. package/dist/orchestrator/backgroundAgent.d.ts.map +1 -1
  69. package/dist/orchestrator/backgroundAgent.js +4 -16
  70. package/dist/orchestrator/backgroundAgent.js.map +1 -1
  71. package/dist/orchestrator/orchestrator.d.ts.map +1 -1
  72. package/dist/orchestrator/orchestrator.js +17 -15
  73. package/dist/orchestrator/orchestrator.js.map +1 -1
  74. package/dist/orchestrator/parallelResearch.d.ts.map +1 -1
  75. package/dist/orchestrator/parallelResearch.js +30 -44
  76. package/dist/orchestrator/parallelResearch.js.map +1 -1
  77. package/dist/orchestrator/types.d.ts +3 -2
  78. package/dist/orchestrator/types.d.ts.map +1 -1
  79. package/dist/tools/analytics/getUsageAnalytics.js +13 -13
  80. package/dist/tools/analytics/getUsageAnalytics.js.map +1 -1
  81. package/dist/tools/convention/getCodingGuide.js +2 -2
  82. package/dist/tools/convention/getCodingGuide.js.map +1 -1
  83. package/dist/tools/memory/createMemoryTimeline.js +11 -11
  84. package/dist/tools/memory/createMemoryTimeline.js.map +1 -1
  85. package/dist/tools/memory/getMemoryGraph.js +12 -12
  86. package/dist/tools/memory/getSessionContext.js +10 -10
  87. package/dist/tools/memory/getSessionContext.js.map +1 -1
  88. package/dist/tools/memory/linkMemories.js +14 -14
  89. package/dist/tools/memory/listMemories.js +4 -4
  90. package/dist/tools/memory/recallMemory.js +4 -4
  91. package/dist/tools/memory/restoreSessionContext.js +2 -2
  92. package/dist/tools/memory/restoreSessionContext.js.map +1 -1
  93. package/dist/tools/memory/saveMemory.js +4 -4
  94. package/dist/tools/memory/searchMemoriesAdvanced.js +23 -23
  95. package/dist/tools/memory/searchMemoriesAdvanced.js.map +1 -1
  96. package/dist/tools/memory/startSession.js +3 -3
  97. package/dist/tools/memory/startSession.js.map +1 -1
  98. package/dist/tools/planning/generatePrd.js +46 -46
  99. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  100. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  101. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  102. package/dist/tools/semantic/findReferences.d.ts.map +1 -1
  103. package/dist/tools/semantic/findReferences.js +2 -1
  104. package/dist/tools/semantic/findReferences.js.map +1 -1
  105. package/dist/tools/semantic/findSymbol.d.ts.map +1 -1
  106. package/dist/tools/semantic/findSymbol.js +2 -1
  107. package/dist/tools/semantic/findSymbol.js.map +1 -1
  108. package/hooks/hooks.json +10 -8
  109. package/package.json +4 -2
package/dist/cli/index.js CHANGED
@@ -5,10 +5,16 @@
5
5
  */
6
6
  import path from 'path';
7
7
  import fs from 'fs';
8
- import os from 'os';
9
8
  import { execSync } from 'child_process';
10
9
  import { fileURLToPath } from 'url';
11
10
  import { createRequire } from 'module';
11
+ import { log, setSilentMode, ensureDir, removeDirRecursive, getPackageJson, compareVersions, } from './utils.js';
12
+ import { unregisterMcp } from './mcp.js';
13
+ import { detectTechStacks } from './detect.js';
14
+ import { formatLLMStatus } from './auth.js';
15
+ import { setupCollaboratorAutoInstall } from './collaborator.js';
16
+ import { setupExternalLLM, removeExternalLLM, gptAuth, gptStatus, gptLogout, geminiAuth, geminiStatus, geminiLogout, showAuthHelp, showLogoutHelp, } from './llm.js';
17
+ import { registerMcpServers, updateConstitution, updateClaudeMd, updateRules, installGlobalAssets, migrateLegacyVibe, updateGitignore, updateConfig, cleanupLegacy, removeLocalAssets, cleanupClaudeConfig, cleanupLegacyMcp, } from './setup.js';
12
18
  const require = createRequire(import.meta.url);
13
19
  const __filename = fileURLToPath(import.meta.url);
14
20
  const __dirname = path.dirname(__filename);
@@ -21,843 +27,8 @@ const options = {
21
27
  silent: args.includes('--silent') || args.includes('-s')
22
28
  };
23
29
  const positionalArgs = args.filter(arg => !arg.startsWith('-'));
24
- /**
25
- * 버전 비교 (semver)
26
- * @returns 1 if a > b, -1 if a < b, 0 if equal
27
- */
28
- function compareVersions(a, b) {
29
- const partsA = a.replace(/^v/, '').split('.').map(Number);
30
- const partsB = b.replace(/^v/, '').split('.').map(Number);
31
- for (let i = 0; i < 3; i++) {
32
- const numA = partsA[i] || 0;
33
- const numB = partsB[i] || 0;
34
- if (numA > numB)
35
- return 1;
36
- if (numA < numB)
37
- return -1;
38
- }
39
- return 0;
40
- }
41
- const DEFAULT_MCPS = [
42
- { name: 'vibe', type: 'node', local: true },
43
- { name: 'context7', type: 'npx', package: '@upstash/context7-mcp@latest' }
44
- ];
45
- const EXTERNAL_LLMS = {
46
- gpt: {
47
- name: 'vibe-gpt',
48
- role: 'architecture',
49
- description: '아키텍처/디버깅 (GPT 5.2)',
50
- package: '@anthropics/openai-mcp',
51
- envKey: 'OPENAI_API_KEY'
52
- },
53
- gemini: {
54
- name: 'vibe-gemini',
55
- role: 'ui-ux',
56
- description: 'UI/UX 설계 (Gemini 3)',
57
- package: '@anthropics/gemini-mcp',
58
- envKey: 'GOOGLE_API_KEY'
59
- }
60
- };
61
- // ============================================================================
62
- // Utility Functions
63
- // ============================================================================
64
- /**
65
- * Claude CLI 경로 찾기 (Windows/macOS/Linux 지원)
66
- * 네이티브 설치와 npm 설치 모두 지원
67
- */
68
- function getClaudePath() {
69
- // 1. PATH에서 'claude' 찾기 (npm 설치 또는 PATH에 추가된 네이티브)
70
- try {
71
- execSync('claude --version', { stdio: 'pipe' });
72
- return 'claude';
73
- }
74
- catch {
75
- // PATH에 없으면 플랫폼별 기본 경로 확인
76
- }
77
- // 2. 플랫폼별 네이티브 설치 경로 확인
78
- if (process.platform === 'win32') {
79
- // Windows 네이티브 설치 경로
80
- const possiblePaths = [
81
- // 네이티브 설치 (claude install)
82
- path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
83
- // npm 전역 설치
84
- path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
85
- // 기타 가능한 경로
86
- path.join(os.homedir(), 'AppData', 'Local', 'Programs', '@anthropic', 'claude-code', 'claude.exe'),
87
- path.join(os.homedir(), 'AppData', 'Local', 'AnthropicClaude', 'claude.exe'),
88
- path.join(os.homedir(), '.claude', 'local', 'claude.exe'),
89
- 'C:\\Program Files\\Anthropic\\Claude\\claude.exe',
90
- 'C:\\Program Files (x86)\\Anthropic\\Claude\\claude.exe',
91
- ];
92
- for (const p of possiblePaths) {
93
- if (p && fs.existsSync(p)) {
94
- return `"${p}"`;
95
- }
96
- }
97
- }
98
- else {
99
- // macOS/Linux 네이티브 설치 경로
100
- const possiblePaths = [
101
- // 네이티브 설치 (claude install)
102
- path.join(os.homedir(), '.local', 'bin', 'claude'),
103
- // npm 전역 설치 (일반적인 경로)
104
- '/usr/local/bin/claude',
105
- '/usr/bin/claude',
106
- // macOS Homebrew
107
- '/opt/homebrew/bin/claude',
108
- ];
109
- for (const p of possiblePaths) {
110
- if (fs.existsSync(p)) {
111
- return p;
112
- }
113
- }
114
- }
115
- // 찾지 못하면 기본값 반환 (에러는 호출 측에서 처리)
116
- return 'claude';
117
- }
118
- // Claude CLI 경로 캐시
119
- let _claudePath = null;
120
- function claudeCmd() {
121
- if (_claudePath === null) {
122
- _claudePath = getClaudePath();
123
- }
124
- return _claudePath;
125
- }
126
- /**
127
- * Claude CLI가 사용 가능한지 확인
128
- */
129
- function isClaudeCliAvailable() {
130
- const cmd = claudeCmd();
131
- try {
132
- execSync(`${cmd} --version`, { stdio: 'pipe' });
133
- return true;
134
- }
135
- catch {
136
- return false;
137
- }
138
- }
139
- /**
140
- * Claude 설정 파일 경로 가져오기
141
- */
142
- function getClaudeSettingsPath() {
143
- return path.join(os.homedir(), '.claude', 'settings.json');
144
- }
145
- function readClaudeSettings() {
146
- const settingsPath = getClaudeSettingsPath();
147
- if (fs.existsSync(settingsPath)) {
148
- try {
149
- return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
150
- }
151
- catch {
152
- return {};
153
- }
154
- }
155
- return {};
156
- }
157
- /**
158
- * Claude 설정 파일 쓰기
159
- */
160
- function writeClaudeSettings(settings) {
161
- const settingsPath = getClaudeSettingsPath();
162
- const dir = path.dirname(settingsPath);
163
- if (!fs.existsSync(dir)) {
164
- fs.mkdirSync(dir, { recursive: true });
165
- }
166
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
167
- }
168
- /**
169
- * MCP 서버 직접 추가 (CLI 없이 설정 파일 수정)
170
- */
171
- function addMcpServer(name, config) {
172
- const settings = readClaudeSettings();
173
- if (!settings.mcpServers) {
174
- settings.mcpServers = {};
175
- }
176
- settings.mcpServers[name] = config;
177
- writeClaudeSettings(settings);
178
- }
179
- /**
180
- * MCP 서버 직접 제거 (CLI 없이 설정 파일 수정)
181
- */
182
- function removeMcpServer(name) {
183
- const settings = readClaudeSettings();
184
- if (settings.mcpServers && settings.mcpServers[name]) {
185
- delete settings.mcpServers[name];
186
- writeClaudeSettings(settings);
187
- }
188
- }
189
- /**
190
- * MCP 등록 (CLI 또는 직접 설정 파일 수정)
191
- */
192
- function registerMcp(name, config) {
193
- if (isClaudeCliAvailable()) {
194
- // CLI 사용
195
- const argsStr = config.args ? config.args.map(a => `"${a}"`).join(' ') : '';
196
- const envStr = config.env ? Object.entries(config.env).map(([k, v]) => `-e ${k}=${v}`).join(' ') : '';
197
- const cmd = `${claudeCmd()} mcp add ${name} -s user ${envStr} -- ${config.command} ${argsStr}`.trim();
198
- try {
199
- execSync(cmd, { stdio: 'pipe' });
200
- }
201
- catch {
202
- // CLI 실패시 직접 설정 파일 수정
203
- addMcpServer(name, config);
204
- }
205
- }
206
- else {
207
- // CLI 없으면 직접 설정 파일 수정
208
- addMcpServer(name, config);
209
- }
210
- }
211
- /**
212
- * MCP 제거 (CLI 또는 직접 설정 파일 수정)
213
- */
214
- function unregisterMcp(name) {
215
- if (isClaudeCliAvailable()) {
216
- try {
217
- execSync(`${claudeCmd()} mcp remove ${name}`, { stdio: 'pipe' });
218
- }
219
- catch { /* ignore */ }
220
- try {
221
- execSync(`${claudeCmd()} mcp remove ${name} -s user`, { stdio: 'pipe' });
222
- }
223
- catch { /* ignore */ }
224
- }
225
- // 항상 설정 파일에서도 제거 (중복 가능)
226
- removeMcpServer(name);
227
- }
228
- function log(message) {
229
- if (!options.silent) {
230
- console.log(message);
231
- }
232
- }
233
- function ensureDir(dir) {
234
- if (!fs.existsSync(dir)) {
235
- fs.mkdirSync(dir, { recursive: true });
236
- }
237
- }
238
- function copyDirContents(sourceDir, targetDir) {
239
- if (fs.existsSync(sourceDir)) {
240
- fs.readdirSync(sourceDir).forEach(file => {
241
- fs.copyFileSync(path.join(sourceDir, file), path.join(targetDir, file));
242
- });
243
- }
244
- }
245
- function copyDirRecursive(sourceDir, targetDir) {
246
- if (!fs.existsSync(sourceDir))
247
- return;
248
- ensureDir(targetDir);
249
- fs.readdirSync(sourceDir).forEach(item => {
250
- const sourcePath = path.join(sourceDir, item);
251
- const targetPath = path.join(targetDir, item);
252
- if (fs.statSync(sourcePath).isDirectory()) {
253
- copyDirRecursive(sourcePath, targetPath);
254
- }
255
- else {
256
- fs.copyFileSync(sourcePath, targetPath);
257
- }
258
- });
259
- }
260
- function removeDirRecursive(dirPath) {
261
- if (!fs.existsSync(dirPath))
262
- return;
263
- fs.readdirSync(dirPath).forEach(item => {
264
- const itemPath = path.join(dirPath, item);
265
- if (fs.statSync(itemPath).isDirectory()) {
266
- removeDirRecursive(itemPath);
267
- }
268
- else {
269
- fs.unlinkSync(itemPath);
270
- }
271
- });
272
- fs.rmdirSync(dirPath);
273
- }
274
- function getPackageJson() {
275
- const pkgPath = path.join(__dirname, '../../package.json');
276
- return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
277
- }
278
- // ============================================================================
279
- // LLM Auth Status
280
- // ============================================================================
281
- function getLLMAuthStatus() {
282
- const status = { gpt: null, gemini: null };
283
- // GPT 상태 확인
284
- try {
285
- const gptStoragePath = path.join(__dirname, '../lib/gpt-storage.js');
286
- if (fs.existsSync(gptStoragePath)) {
287
- const gptStorage = require(gptStoragePath);
288
- const account = gptStorage.getActiveAccount();
289
- if (account) {
290
- const isExpired = gptStorage.isTokenExpired(account);
291
- status.gpt = {
292
- type: 'oauth',
293
- email: account.email,
294
- valid: !isExpired
295
- };
296
- }
297
- }
298
- }
299
- catch (e) { }
300
- // GPT API 키 확인 (프로젝트 config)
301
- if (!status.gpt) {
302
- try {
303
- const configPath = path.join(process.cwd(), '.claude', 'vibe', 'config.json');
304
- if (fs.existsSync(configPath)) {
305
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
306
- if (config.models?.gpt?.enabled) {
307
- status.gpt = { type: 'apikey', valid: true };
308
- }
309
- }
310
- }
311
- catch (e) { }
312
- }
313
- // Gemini 상태 확인
314
- try {
315
- // Windows: %APPDATA%/vibe, macOS/Linux: ~/.config/vibe
316
- const geminiConfigDir = process.platform === 'win32'
317
- ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'vibe')
318
- : path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'vibe');
319
- const tokenPath = path.join(geminiConfigDir, 'gemini-auth.json');
320
- if (fs.existsSync(tokenPath)) {
321
- const tokenData = JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
322
- if (tokenData.accounts && tokenData.accounts.length > 0) {
323
- const activeAccount = tokenData.accounts.find((a) => a.active) || tokenData.accounts[0];
324
- const isExpired = activeAccount.expires && Date.now() > activeAccount.expires;
325
- status.gemini = {
326
- type: 'oauth',
327
- email: activeAccount.email || 'default',
328
- valid: !isExpired || !!activeAccount.refreshToken
329
- };
330
- }
331
- }
332
- }
333
- catch (e) { }
334
- // Gemini API 키 확인 (프로젝트 config)
335
- if (!status.gemini) {
336
- try {
337
- const configPath = path.join(process.cwd(), '.claude', 'vibe', 'config.json');
338
- if (fs.existsSync(configPath)) {
339
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
340
- if (config.models?.gemini?.enabled) {
341
- status.gemini = { type: 'apikey', valid: true };
342
- }
343
- }
344
- }
345
- catch (e) { }
346
- }
347
- return status;
348
- }
349
- function formatLLMStatus() {
350
- const status = getLLMAuthStatus();
351
- const lines = [];
352
- lines.push('외부 LLM:');
353
- // GPT 상태
354
- if (status.gpt) {
355
- if (status.gpt.type === 'oauth') {
356
- const icon = status.gpt.valid ? '✓' : '⚠';
357
- lines.push(` GPT: ${icon} OAuth 인증됨 (${status.gpt.email})`);
358
- }
359
- else {
360
- lines.push(' GPT: ✓ API 키 설정됨');
361
- }
362
- }
363
- else {
364
- lines.push(' GPT: ✗ 미설정 (vibe gpt --auth 또는 vibe gpt <api-key>)');
365
- }
366
- // Gemini 상태
367
- if (status.gemini) {
368
- if (status.gemini.type === 'oauth') {
369
- const icon = status.gemini.valid ? '✓' : '⚠';
370
- lines.push(` Gemini: ${icon} OAuth 인증됨 (${status.gemini.email})`);
371
- }
372
- else {
373
- lines.push(' Gemini: ✓ API 키 설정됨');
374
- }
375
- }
376
- else {
377
- lines.push(' Gemini: ✗ 미설정 (vibe gemini --auth 또는 vibe gemini <api-key>)');
378
- }
379
- return lines.join('\n');
380
- }
381
- // ============================================================================
382
- // Tech Stack Detection
383
- // ============================================================================
384
- function detectTechStacks(projectRoot) {
385
- const stacks = [];
386
- const details = { databases: [], stateManagement: [], hosting: [], cicd: [] };
387
- const detectInDir = (dir, prefix = '') => {
388
- const detected = [];
389
- // Node.js / TypeScript
390
- if (fs.existsSync(path.join(dir, 'package.json'))) {
391
- try {
392
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
393
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
394
- // 프레임워크 감지
395
- if (deps['next'])
396
- detected.push({ type: 'typescript-nextjs', path: prefix });
397
- else if (deps['react-native'])
398
- detected.push({ type: 'typescript-react-native', path: prefix });
399
- else if (deps['react'])
400
- detected.push({ type: 'typescript-react', path: prefix });
401
- else if (deps['nuxt'] || deps['nuxt3'])
402
- detected.push({ type: 'typescript-nuxt', path: prefix });
403
- else if (deps['vue'])
404
- detected.push({ type: 'typescript-vue', path: prefix });
405
- else if (deps['express'] || deps['fastify'] || deps['koa'] || deps['nest'] || deps['@nestjs/core'])
406
- detected.push({ type: 'typescript-node', path: prefix });
407
- else if (pkg.name)
408
- detected.push({ type: 'typescript-node', path: prefix });
409
- // DB 감지
410
- if (deps['pg'] || deps['postgres'] || deps['@prisma/client'])
411
- details.databases.push('PostgreSQL');
412
- if (deps['mysql'] || deps['mysql2'])
413
- details.databases.push('MySQL');
414
- if (deps['mongodb'] || deps['mongoose'])
415
- details.databases.push('MongoDB');
416
- if (deps['redis'] || deps['ioredis'])
417
- details.databases.push('Redis');
418
- if (deps['sqlite3'] || deps['better-sqlite3'])
419
- details.databases.push('SQLite');
420
- if (deps['typeorm'])
421
- details.databases.push('TypeORM');
422
- if (deps['prisma'] || deps['@prisma/client'])
423
- details.databases.push('Prisma');
424
- if (deps['drizzle-orm'])
425
- details.databases.push('Drizzle');
426
- if (deps['sequelize'])
427
- details.databases.push('Sequelize');
428
- // 상태관리 감지
429
- if (deps['redux'] || deps['@reduxjs/toolkit'])
430
- details.stateManagement.push('Redux');
431
- if (deps['zustand'])
432
- details.stateManagement.push('Zustand');
433
- if (deps['jotai'])
434
- details.stateManagement.push('Jotai');
435
- if (deps['recoil'])
436
- details.stateManagement.push('Recoil');
437
- if (deps['mobx'])
438
- details.stateManagement.push('MobX');
439
- if (deps['@tanstack/react-query'] || deps['react-query'])
440
- details.stateManagement.push('React Query');
441
- if (deps['swr'])
442
- details.stateManagement.push('SWR');
443
- if (deps['pinia'])
444
- details.stateManagement.push('Pinia');
445
- if (deps['vuex'])
446
- details.stateManagement.push('Vuex');
447
- }
448
- catch (e) { }
449
- }
450
- // Python
451
- if (fs.existsSync(path.join(dir, 'pyproject.toml'))) {
452
- try {
453
- const content = fs.readFileSync(path.join(dir, 'pyproject.toml'), 'utf-8');
454
- if (content.includes('fastapi'))
455
- detected.push({ type: 'python-fastapi', path: prefix });
456
- else if (content.includes('django'))
457
- detected.push({ type: 'python-django', path: prefix });
458
- else
459
- detected.push({ type: 'python', path: prefix });
460
- if (content.includes('psycopg') || content.includes('asyncpg'))
461
- details.databases.push('PostgreSQL');
462
- if (content.includes('pymongo'))
463
- details.databases.push('MongoDB');
464
- if (content.includes('sqlalchemy'))
465
- details.databases.push('SQLAlchemy');
466
- if (content.includes('prisma'))
467
- details.databases.push('Prisma');
468
- }
469
- catch (e) { }
470
- }
471
- else if (fs.existsSync(path.join(dir, 'requirements.txt'))) {
472
- try {
473
- const content = fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf-8');
474
- if (content.includes('fastapi'))
475
- detected.push({ type: 'python-fastapi', path: prefix });
476
- else if (content.includes('django'))
477
- detected.push({ type: 'python-django', path: prefix });
478
- else
479
- detected.push({ type: 'python', path: prefix });
480
- if (content.includes('psycopg') || content.includes('asyncpg'))
481
- details.databases.push('PostgreSQL');
482
- if (content.includes('pymongo'))
483
- details.databases.push('MongoDB');
484
- if (content.includes('sqlalchemy'))
485
- details.databases.push('SQLAlchemy');
486
- }
487
- catch (e) { }
488
- }
489
- // Flutter / Dart
490
- if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
491
- detected.push({ type: 'dart-flutter', path: prefix });
492
- try {
493
- const content = fs.readFileSync(path.join(dir, 'pubspec.yaml'), 'utf-8');
494
- if (content.includes('flutter_riverpod') || content.includes('riverpod'))
495
- details.stateManagement.push('Riverpod');
496
- else if (content.includes('provider'))
497
- details.stateManagement.push('Provider');
498
- if (content.includes('bloc'))
499
- details.stateManagement.push('BLoC');
500
- if (content.includes('getx') || content.includes('get:'))
501
- details.stateManagement.push('GetX');
502
- }
503
- catch (e) { }
504
- }
505
- // Go
506
- if (fs.existsSync(path.join(dir, 'go.mod'))) {
507
- detected.push({ type: 'go', path: prefix });
508
- try {
509
- const content = fs.readFileSync(path.join(dir, 'go.mod'), 'utf-8');
510
- if (content.includes('pgx') || content.includes('pq'))
511
- details.databases.push('PostgreSQL');
512
- if (content.includes('go-redis'))
513
- details.databases.push('Redis');
514
- if (content.includes('mongo-driver'))
515
- details.databases.push('MongoDB');
516
- }
517
- catch (e) { }
518
- }
519
- // Rust
520
- if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
521
- detected.push({ type: 'rust', path: prefix });
522
- try {
523
- const content = fs.readFileSync(path.join(dir, 'Cargo.toml'), 'utf-8');
524
- if (content.includes('sqlx') || content.includes('diesel'))
525
- details.databases.push('PostgreSQL');
526
- if (content.includes('mongodb'))
527
- details.databases.push('MongoDB');
528
- }
529
- catch (e) { }
530
- }
531
- // Java / Kotlin
532
- if (fs.existsSync(path.join(dir, 'build.gradle')) || fs.existsSync(path.join(dir, 'build.gradle.kts'))) {
533
- try {
534
- const gradleFile = fs.existsSync(path.join(dir, 'build.gradle.kts'))
535
- ? path.join(dir, 'build.gradle.kts')
536
- : path.join(dir, 'build.gradle');
537
- const content = fs.readFileSync(gradleFile, 'utf-8');
538
- if (content.includes('com.android'))
539
- detected.push({ type: 'kotlin-android', path: prefix });
540
- else if (content.includes('kotlin'))
541
- detected.push({ type: 'kotlin', path: prefix });
542
- else if (content.includes('spring'))
543
- detected.push({ type: 'java-spring', path: prefix });
544
- else
545
- detected.push({ type: 'java', path: prefix });
546
- if (content.includes('postgresql'))
547
- details.databases.push('PostgreSQL');
548
- if (content.includes('mysql'))
549
- details.databases.push('MySQL');
550
- if (content.includes('jpa') || content.includes('hibernate'))
551
- details.databases.push('JPA/Hibernate');
552
- }
553
- catch (e) { }
554
- }
555
- else if (fs.existsSync(path.join(dir, 'pom.xml'))) {
556
- try {
557
- const content = fs.readFileSync(path.join(dir, 'pom.xml'), 'utf-8');
558
- if (content.includes('spring'))
559
- detected.push({ type: 'java-spring', path: prefix });
560
- else
561
- detected.push({ type: 'java', path: prefix });
562
- if (content.includes('postgresql'))
563
- details.databases.push('PostgreSQL');
564
- if (content.includes('mysql'))
565
- details.databases.push('MySQL');
566
- }
567
- catch (e) { }
568
- }
569
- // Swift / iOS
570
- if (fs.existsSync(path.join(dir, 'Package.swift')) ||
571
- fs.readdirSync(dir).some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
572
- detected.push({ type: 'swift-ios', path: prefix });
573
- }
574
- return detected;
575
- };
576
- // CI/CD 감지
577
- if (fs.existsSync(path.join(projectRoot, '.github', 'workflows'))) {
578
- details.cicd.push('GitHub Actions');
579
- }
580
- if (fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml'))) {
581
- details.cicd.push('GitLab CI');
582
- }
583
- if (fs.existsSync(path.join(projectRoot, 'Jenkinsfile'))) {
584
- details.cicd.push('Jenkins');
585
- }
586
- if (fs.existsSync(path.join(projectRoot, '.circleci'))) {
587
- details.cicd.push('CircleCI');
588
- }
589
- // Hosting 감지
590
- if (fs.existsSync(path.join(projectRoot, 'vercel.json')) ||
591
- fs.existsSync(path.join(projectRoot, '.vercel'))) {
592
- details.hosting.push('Vercel');
593
- }
594
- if (fs.existsSync(path.join(projectRoot, 'netlify.toml'))) {
595
- details.hosting.push('Netlify');
596
- }
597
- if (fs.existsSync(path.join(projectRoot, 'app.yaml')) ||
598
- fs.existsSync(path.join(projectRoot, 'cloudbuild.yaml'))) {
599
- details.hosting.push('Google Cloud');
600
- }
601
- if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
602
- fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
603
- details.hosting.push('Docker');
604
- }
605
- if (fs.existsSync(path.join(projectRoot, 'fly.toml'))) {
606
- details.hosting.push('Fly.io');
607
- }
608
- if (fs.existsSync(path.join(projectRoot, 'railway.json'))) {
609
- details.hosting.push('Railway');
610
- }
611
- // 루트 디렉토리 검사
612
- stacks.push(...detectInDir(projectRoot));
613
- // 1레벨 하위 폴더 검사
614
- const subDirs = ['backend', 'frontend', 'server', 'client', 'api', 'web', 'mobile', 'app', 'packages', 'apps'];
615
- for (const subDir of subDirs) {
616
- const subPath = path.join(projectRoot, subDir);
617
- if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
618
- stacks.push(...detectInDir(subPath, subDir));
619
- }
620
- }
621
- // packages/* 또는 apps/* 내부 검사 (monorepo)
622
- for (const monoDir of ['packages', 'apps']) {
623
- const monoPath = path.join(projectRoot, monoDir);
624
- if (fs.existsSync(monoPath) && fs.statSync(monoPath).isDirectory()) {
625
- const subPackages = fs.readdirSync(monoPath).filter(f => {
626
- const fullPath = path.join(monoPath, f);
627
- return fs.statSync(fullPath).isDirectory() && !f.startsWith('.');
628
- });
629
- for (const pkg of subPackages) {
630
- stacks.push(...detectInDir(path.join(monoPath, pkg), `${monoDir}/${pkg}`));
631
- }
632
- }
633
- }
634
- // 중복 제거
635
- details.databases = [...new Set(details.databases)];
636
- details.stateManagement = [...new Set(details.stateManagement)];
637
- details.hosting = [...new Set(details.hosting)];
638
- details.cicd = [...new Set(details.cicd)];
639
- return { stacks, details };
640
- }
641
- // ============================================================================
642
- // Collaborator Setup
643
- // ============================================================================
644
- function setupCollaboratorAutoInstall(projectRoot) {
645
- const packageJsonPath = path.join(projectRoot, 'package.json');
646
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
647
- const vibeVersion = getPackageJson().version;
648
- // 1. Node.js 프로젝트: package.json 정리
649
- if (fs.existsSync(packageJsonPath)) {
650
- try {
651
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
652
- let modified = false;
653
- // 기존 devDependencies에서 @su-record/vibe 제거
654
- if (pkg.devDependencies?.['@su-record/vibe']) {
655
- delete pkg.devDependencies['@su-record/vibe'];
656
- modified = true;
657
- }
658
- // 기존 postinstall/prepare에서 vibe update 제거
659
- if (pkg.scripts) {
660
- const oldPatterns = [
661
- /\s*&&\s*npx @su-record\/vibe update[^&|;]*/g,
662
- /npx @su-record\/vibe update[^&|;]*\s*&&\s*/g,
663
- /npx @su-record\/vibe update[^&|;]*/g,
664
- /\s*&&\s*node_modules\/\.bin\/vibe update[^&|;]*/g,
665
- /node_modules\/\.bin\/vibe update[^&|;]*\s*&&\s*/g,
666
- /node_modules\/\.bin\/vibe update[^&|;]*/g
667
- ];
668
- ['postinstall', 'prepare'].forEach(script => {
669
- if (pkg.scripts[script]?.includes('vibe update')) {
670
- let cleaned = pkg.scripts[script];
671
- oldPatterns.forEach(p => { cleaned = cleaned.replace(p, ''); });
672
- cleaned = cleaned.trim();
673
- if (cleaned) {
674
- pkg.scripts[script] = cleaned;
675
- }
676
- else {
677
- delete pkg.scripts[script];
678
- }
679
- modified = true;
680
- }
681
- });
682
- }
683
- if (modified) {
684
- fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
685
- log(' ✅ package.json 정리 완료 (레거시 vibe 설정 제거)\n');
686
- }
687
- }
688
- catch (e) {
689
- log(' ⚠️ package.json 수정 실패: ' + e.message + '\n');
690
- }
691
- }
692
- // 2. .claude/vibe/setup.sh 생성
693
- const setupShPath = path.join(vibeDir, 'setup.sh');
694
- if (!fs.existsSync(vibeDir)) {
695
- fs.mkdirSync(vibeDir, { recursive: true });
696
- }
697
- if (!fs.existsSync(setupShPath)) {
698
- const setupScript = `#!/bin/bash
699
- # Vibe 협업자 자동 설치 스크립트
700
- # 사용법: ./.claude/vibe/setup.sh
701
-
702
- set -e
703
-
704
- echo "🔧 Vibe 설치 확인 중..."
705
-
706
- # npm/npx 확인
707
- if ! command -v npx &> /dev/null; then
708
- echo "❌ Node.js/npm이 설치되어 있지 않습니다."
709
- echo " https://nodejs.org 에서 설치해주세요."
710
- exit 1
711
- fi
712
-
713
- # vibe 설치 확인 및 업데이트
714
- if command -v vibe &> /dev/null; then
715
- echo "✅ Vibe가 이미 설치되어 있습니다."
716
- vibe update --silent
717
- echo "✅ Vibe 업데이트 완료!"
718
- else
719
- echo "📦 Vibe 설치 중..."
720
- npm install -g @su-record/vibe
721
- vibe update --silent
722
- echo "✅ Vibe 설치 및 설정 완료!"
723
- fi
724
-
725
- echo ""
726
- echo "다음 명령어로 시작하세요:"
727
- echo " /vibe.spec \\"기능명\\" SPEC 작성"
728
- echo " /vibe.run \\"기능명\\" 구현 실행"
729
- `;
730
- fs.writeFileSync(setupShPath, setupScript);
731
- fs.chmodSync(setupShPath, '755');
732
- log(' ✅ 협업자 설치 스크립트 생성 (.claude/vibe/setup.sh)\n');
733
- }
734
- // 3. README.md에 협업자 안내 추가
735
- const readmePath = path.join(projectRoot, 'README.md');
736
- const vibeSetupSection = `
737
- ## Vibe Setup (AI Coding)
738
-
739
- 이 프로젝트는 [Vibe](https://github.com/su-record/vibe) AI 코딩 프레임워크를 사용합니다.
740
-
741
- ### 협업자 설치
742
-
743
- \`\`\`bash
744
- # 전역 설치 (권장)
745
- npm install -g @su-record/vibe
746
- vibe update
747
-
748
- # 또는 setup 스크립트 실행
749
- ./.claude/vibe/setup.sh
750
- \`\`\`
751
-
752
- ### 사용법
753
-
754
- Claude Code에서 슬래시 커맨드 사용:
755
- - \`/vibe.spec "기능명"\` - SPEC 문서 작성
756
- - \`/vibe.run "기능명"\` - 구현 실행
757
- `;
758
- if (fs.existsSync(readmePath)) {
759
- const readme = fs.readFileSync(readmePath, 'utf-8');
760
- if (!readme.includes('## Vibe Setup')) {
761
- fs.appendFileSync(readmePath, vibeSetupSection);
762
- log(' ✅ README.md에 협업자 안내 추가\n');
763
- }
764
- }
765
- else {
766
- fs.writeFileSync(readmePath, `# Project\n${vibeSetupSection}`);
767
- log(' ✅ README.md 생성 (협업자 안내 포함)\n');
768
- }
769
- }
770
- // ============================================================================
771
- // Stack Name Mapping
772
- // ============================================================================
773
- const STACK_NAMES = {
774
- 'python-fastapi': { lang: 'Python 3.11+', framework: 'FastAPI' },
775
- 'python-django': { lang: 'Python 3.11+', framework: 'Django' },
776
- 'python': { lang: 'Python 3.11+', framework: '-' },
777
- 'typescript-node': { lang: 'TypeScript/Node.js', framework: 'Express/Fastify' },
778
- 'typescript-nextjs': { lang: 'TypeScript', framework: 'Next.js' },
779
- 'typescript-react': { lang: 'TypeScript', framework: 'React' },
780
- 'typescript-vue': { lang: 'TypeScript', framework: 'Vue.js' },
781
- 'typescript-nuxt': { lang: 'TypeScript', framework: 'Nuxt 3' },
782
- 'typescript-react-native': { lang: 'TypeScript', framework: 'React Native' },
783
- 'dart-flutter': { lang: 'Dart', framework: 'Flutter' },
784
- 'go': { lang: 'Go', framework: '-' },
785
- 'rust': { lang: 'Rust', framework: '-' },
786
- 'java-spring': { lang: 'Java 17+', framework: 'Spring Boot' },
787
- 'kotlin-android': { lang: 'Kotlin', framework: 'Android' },
788
- 'swift-ios': { lang: 'Swift', framework: 'iOS/SwiftUI' }
789
- };
790
- // 언어별 CLAUDE.md 규칙
791
- const LANGUAGE_RULES = {
792
- typescript: `### TypeScript 규칙
793
- - \`any\` 타입 사용 금지 → \`unknown\` + 타입 가드 사용
794
- - \`as any\` 캐스팅 금지 → 적절한 인터페이스 정의
795
- - \`@ts-ignore\` 금지 → 타입 문제 근본 해결
796
- - 모든 함수에 반환 타입 명시`,
797
- python: `### Python 규칙
798
- - 타입 힌트 필수 (함수 매개변수, 반환값)
799
- - \`# type: ignore\` 금지 → 타입 문제 근본 해결
800
- - f-string 사용 권장 (format() 대신)
801
- - 리스트 컴프리헨션 적절히 활용`,
802
- go: `### Go 규칙
803
- - 에러 반환 즉시 처리 (if err != nil)
804
- - 명시적 에러 래핑 (fmt.Errorf with %w)
805
- - 인터페이스는 사용처에서 정의
806
- - 고루틴 누수 방지 (context 활용)`,
807
- rust: `### Rust 규칙
808
- - unwrap()/expect() 프로덕션 코드 금지 → Result/Option 처리
809
- - unsafe 블록 최소화
810
- - 명시적 에러 타입 정의
811
- - 소유권/수명 주석 명확히`,
812
- java: `### Java 규칙
813
- - Optional 적극 활용 (null 대신)
814
- - 불변 객체 선호 (final 필드)
815
- - 체크 예외 적절히 처리
816
- - 스트림 API 활용`,
817
- kotlin: `### Kotlin 규칙
818
- - nullable 타입 명시 (?)
819
- - !! 연산자 금지 → safe call (?.) 사용
820
- - data class 적극 활용
821
- - 확장 함수로 유틸리티 구현`,
822
- dart: `### Dart 규칙
823
- - null safety 준수 (? 및 ! 적절히 사용)
824
- - late 키워드 남용 금지
825
- - const 생성자 활용
826
- - 비동기 코드에 async/await 사용`,
827
- swift: `### Swift 규칙
828
- - 옵셔널 강제 언래핑 금지 → guard let / if let 사용
829
- - 프로토콜 지향 프로그래밍 권장
830
- - 값 타입(struct) 우선 사용
831
- - @escaping 클로저 메모리 관리 주의`
832
- };
833
- function getLanguageRulesForStacks(stacks) {
834
- const addedRules = new Set();
835
- const rules = [];
836
- for (const stack of stacks) {
837
- let langKey = '';
838
- if (stack.type.startsWith('typescript'))
839
- langKey = 'typescript';
840
- else if (stack.type.startsWith('python'))
841
- langKey = 'python';
842
- else if (stack.type === 'go')
843
- langKey = 'go';
844
- else if (stack.type === 'rust')
845
- langKey = 'rust';
846
- else if (stack.type.startsWith('java'))
847
- langKey = 'java';
848
- else if (stack.type.startsWith('kotlin'))
849
- langKey = 'kotlin';
850
- else if (stack.type.startsWith('dart'))
851
- langKey = 'dart';
852
- else if (stack.type.startsWith('swift'))
853
- langKey = 'swift';
854
- if (langKey && !addedRules.has(langKey) && LANGUAGE_RULES[langKey]) {
855
- addedRules.add(langKey);
856
- rules.push(LANGUAGE_RULES[langKey]);
857
- }
858
- }
859
- return rules.join('\n\n');
860
- }
30
+ // Silent 모드 설정
31
+ setSilentMode(options.silent);
861
32
  // ============================================================================
862
33
  // Main Commands
863
34
  // ============================================================================
@@ -875,7 +46,8 @@ async function init(projectName) {
875
46
  fs.mkdirSync(projectRoot, { recursive: true });
876
47
  isNewProject = true;
877
48
  }
878
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
49
+ const claudeDir = path.join(projectRoot, '.claude');
50
+ const vibeDir = path.join(claudeDir, 'vibe');
879
51
  if (fs.existsSync(vibeDir)) {
880
52
  log('❌ .claude/vibe/ 폴더가 이미 존재합니다.');
881
53
  return;
@@ -883,105 +55,17 @@ async function init(projectName) {
883
55
  ensureDir(vibeDir);
884
56
  // MCP 서버 등록
885
57
  log('🔧 Claude Code MCP 서버 등록 중 (전역)...\n');
886
- const geminiMcpPath = path.join(__dirname, '../lib/gemini-mcp.js');
887
- const gptMcpPath = path.join(__dirname, '../lib/gpt-mcp.js');
888
- // 0. 기존 hi-ai/vibe MCP 제거 (마이그레이션 - 내장 도구로 전환)
889
- unregisterMcp('vibe');
890
- // 1. vibe-gemini MCP
891
- if (fs.existsSync(geminiMcpPath)) {
892
- try {
893
- registerMcp('vibe-gemini', { command: 'node', args: [geminiMcpPath] });
894
- log(' ✅ vibe-gemini MCP 등록 완료 (전역)\n');
895
- }
896
- catch (e) {
897
- if (e.message?.includes('already exists')) {
898
- log(' ℹ️ vibe-gemini MCP 이미 등록됨\n');
899
- }
900
- }
901
- }
902
- // 3. vibe-gpt MCP
903
- if (fs.existsSync(gptMcpPath)) {
904
- try {
905
- registerMcp('vibe-gpt', { command: 'node', args: [gptMcpPath] });
906
- log(' ✅ vibe-gpt MCP 등록 완료 (전역)\n');
907
- }
908
- catch (e) {
909
- if (e.message?.includes('already exists')) {
910
- log(' ℹ️ vibe-gpt MCP 이미 등록됨\n');
911
- }
912
- }
913
- }
914
- // 4. Context7 MCP
915
- try {
916
- registerMcp('context7', { command: 'npx', args: ['-y', '@upstash/context7-mcp@latest'] });
917
- log(' ✅ Context7 MCP 등록 완료 (라이브러리 문서 검색)\n');
918
- }
919
- catch (e) {
920
- if (e.message?.includes('already exists')) {
921
- log(' ℹ️ Context7 MCP 이미 등록됨\n');
922
- }
923
- else {
924
- log(' ⚠️ Context7 MCP 수동 등록 필요\n');
925
- }
926
- }
927
- // Agent SDK 설치 (오케스트레이터용)
928
- log(' 🤖 Agent SDK 설치 중 (오케스트레이터용)...\n');
929
- try {
930
- const { execSync } = await import('child_process');
931
- execSync('npm install @anthropic-ai/claude-agent-sdk --save-dev', {
932
- cwd: projectRoot,
933
- stdio: 'pipe'
934
- });
935
- log(' ✅ Agent SDK 설치 완료\n');
936
- }
937
- catch (e) {
938
- log(' ⚠️ Agent SDK 설치 실패 - 수동 설치: npm i -D @anthropic-ai/claude-agent-sdk\n');
939
- }
58
+ registerMcpServers(false);
940
59
  // .claude/vibe 폴더 구조 생성
941
60
  ['specs', 'features'].forEach(dir => {
942
61
  ensureDir(path.join(vibeDir, dir));
943
62
  });
944
- // 기존 .vibe/ 폴더 마이그레이션 (레거시)
945
- const legacyVibeDir = path.join(projectRoot, '.vibe');
946
- if (fs.existsSync(legacyVibeDir)) {
947
- log(' 🔄 레거시 .vibe/ 폴더 마이그레이션 중...\n');
948
- try {
949
- // specs, features 등 데이터 폴더 이동
950
- ['specs', 'features', 'solutions', 'todos', 'memory'].forEach(dir => {
951
- const legacySrc = path.join(legacyVibeDir, dir);
952
- const newDest = path.join(vibeDir, dir);
953
- if (fs.existsSync(legacySrc) && !fs.existsSync(newDest)) {
954
- copyDirRecursive(legacySrc, newDest);
955
- }
956
- });
957
- removeDirRecursive(legacyVibeDir);
958
- log(' ✅ .vibe/ → .claude/vibe/ 마이그레이션 완료\n');
959
- }
960
- catch (e) {
961
- log(' ⚠️ 마이그레이션 실패 - .vibe/ 폴더 수동 삭제 필요\n');
962
- }
963
- }
63
+ // 레거시 마이그레이션
64
+ migrateLegacyVibe(projectRoot, vibeDir);
964
65
  // .gitignore 업데이트
965
- const gitignorePath = path.join(projectRoot, '.gitignore');
966
- const mcpIgnore = '.claude/vibe/mcp/';
967
- if (fs.existsSync(gitignorePath)) {
968
- let gitignore = fs.readFileSync(gitignorePath, 'utf-8');
969
- if (!gitignore.includes(mcpIgnore)) {
970
- gitignore += `\n# vibe MCP\n${mcpIgnore}\n`;
971
- fs.writeFileSync(gitignorePath, gitignore);
972
- }
973
- }
974
- else {
975
- fs.writeFileSync(gitignorePath, `# vibe MCP\n${mcpIgnore}\n`);
976
- }
977
- // .claude/commands 복사
978
- const claudeDir = path.join(projectRoot, '.claude');
979
- const commandsDir = path.join(claudeDir, 'commands');
980
- ensureDir(claudeDir);
981
- ensureDir(commandsDir);
982
- const sourceDir = path.join(__dirname, '../../commands');
983
- copyDirRecursive(sourceDir, commandsDir);
984
- log(' ✅ 슬래시 커맨드 설치 완료 (7개)\n');
66
+ updateGitignore(projectRoot);
67
+ // 전역 assets 설치
68
+ installGlobalAssets(false);
985
69
  // 기술 스택 감지
986
70
  const { stacks: detectedStacks, details: stackDetails } = detectTechStacks(projectRoot);
987
71
  if (detectedStacks.length > 0) {
@@ -997,171 +81,59 @@ async function init(projectName) {
997
81
  }
998
82
  }
999
83
  // constitution.md 생성
1000
- const templatePath = path.join(__dirname, '../../templates/constitution-template.md');
1001
- const constitutionPath = path.join(vibeDir, 'constitution.md');
1002
- if (fs.existsSync(templatePath)) {
1003
- let constitution = fs.readFileSync(templatePath, 'utf-8');
1004
- const backendStack = detectedStacks.find(s => s.type.includes('python') || s.type.includes('node') ||
1005
- s.type.includes('go') || s.type.includes('java') || s.type.includes('rust'));
1006
- const frontendStack = detectedStacks.find(s => s.type.includes('react') || s.type.includes('vue') ||
1007
- s.type.includes('flutter') || s.type.includes('swift') || s.type.includes('android'));
1008
- if (backendStack && STACK_NAMES[backendStack.type]) {
1009
- const info = STACK_NAMES[backendStack.type];
1010
- constitution = constitution.replace('- Language: {Python 3.11+ / Node.js / etc.}', `- Language: ${info.lang}`);
1011
- constitution = constitution.replace('- Framework: {FastAPI / Express / etc.}', `- Framework: ${info.framework}`);
1012
- }
1013
- if (frontendStack && STACK_NAMES[frontendStack.type]) {
1014
- const info = STACK_NAMES[frontendStack.type];
1015
- constitution = constitution.replace('- Framework: {Flutter / React / etc.}', `- Framework: ${info.framework}`);
1016
- }
1017
- constitution = constitution.replace('- Database: {PostgreSQL / MongoDB / etc.}', stackDetails.databases.length > 0 ? `- Database: ${stackDetails.databases.join(', ')}` : '- Database: (프로젝트에 맞게 설정)');
1018
- constitution = constitution.replace('- State Management: {Provider / Redux / etc.}', stackDetails.stateManagement.length > 0 ? `- State Management: ${stackDetails.stateManagement.join(', ')}` : '- State Management: (프로젝트에 맞게 설정)');
1019
- constitution = constitution.replace('- Hosting: {Cloud Run / Vercel / etc.}', stackDetails.hosting.length > 0 ? `- Hosting: ${stackDetails.hosting.join(', ')}` : '- Hosting: (프로젝트에 맞게 설정)');
1020
- constitution = constitution.replace('- CI/CD: {GitHub Actions / etc.}', stackDetails.cicd.length > 0 ? `- CI/CD: ${stackDetails.cicd.join(', ')}` : '- CI/CD: (프로젝트에 맞게 설정)');
1021
- fs.writeFileSync(constitutionPath, constitution);
1022
- }
84
+ updateConstitution(vibeDir, detectedStacks, stackDetails);
1023
85
  // config.json 생성
1024
- const config = {
1025
- language: 'ko',
1026
- quality: { strict: true, autoVerify: true },
1027
- stacks: detectedStacks,
1028
- details: stackDetails
1029
- };
1030
- fs.writeFileSync(path.join(vibeDir, 'config.json'), JSON.stringify(config, null, 2));
1031
- // CLAUDE.md 병합 (언어별 규칙 동적 추가)
1032
- const vibeClaudeMd = path.join(__dirname, '../../CLAUDE.md');
1033
- const projectClaudeMd = path.join(projectRoot, 'CLAUDE.md');
1034
- let vibeContent = fs.readFileSync(vibeClaudeMd, 'utf-8');
1035
- // 감지된 기술 스택에 따라 언어별 규칙 추가
1036
- const languageRules = getLanguageRulesForStacks(detectedStacks);
1037
- if (languageRules) {
1038
- // "### 에러 처리 필수" 앞에 언어별 규칙 삽입
1039
- vibeContent = vibeContent.replace('### 에러 처리 필수', languageRules + '\n\n### 에러 처리 필수');
1040
- }
1041
- if (fs.existsSync(projectClaudeMd)) {
1042
- const existingContent = fs.readFileSync(projectClaudeMd, 'utf-8');
1043
- if (!existingContent.includes('/vibe.spec')) {
1044
- const mergedContent = existingContent.trim() + '\n\n---\n\n' + vibeContent;
1045
- fs.writeFileSync(projectClaudeMd, mergedContent);
1046
- log(' ✅ CLAUDE.md에 vibe 섹션 추가\n');
1047
- }
1048
- else {
1049
- log(' ℹ️ CLAUDE.md에 vibe 섹션 이미 존재\n');
1050
- }
1051
- }
1052
- else {
1053
- fs.writeFileSync(projectClaudeMd, vibeContent);
1054
- log(' ✅ CLAUDE.md 생성\n');
1055
- }
1056
- // .claude/vibe/rules/ 복사
1057
- const rulesSource = path.join(__dirname, '../../.claude/vibe/rules');
1058
- const rulesTarget = path.join(vibeDir, 'rules');
1059
- const coreDirs = ['core', 'quality', 'standards', 'tools'];
1060
- coreDirs.forEach(dir => {
1061
- const src = path.join(rulesSource, dir);
1062
- const dst = path.join(rulesTarget, dir);
1063
- if (fs.existsSync(src)) {
1064
- copyDirRecursive(src, dst);
1065
- }
1066
- });
1067
- const langSource = path.join(rulesSource, 'languages');
1068
- const langTarget = path.join(rulesTarget, 'languages');
1069
- ensureDir(langTarget);
1070
- // 감지된 스택 타입을 그대로 파일명으로 매칭 (typescript-nextjs -> typescript-nextjs.md)
1071
- const detectedTypes = new Set(detectedStacks.map(s => s.type));
1072
- if (fs.existsSync(langSource)) {
1073
- const langFiles = fs.readdirSync(langSource);
1074
- langFiles.forEach(file => {
1075
- const langType = file.replace('.md', '');
1076
- if (detectedTypes.has(langType)) {
1077
- fs.copyFileSync(path.join(langSource, file), path.join(langTarget, file));
1078
- }
1079
- });
1080
- }
1081
- log(' ✅ 코딩 규칙 설치 완료 (.claude/vibe/rules/)\n');
1082
- // .claude/agents/ 복사
1083
- const agentsDir = path.join(claudeDir, 'agents');
1084
- ensureDir(agentsDir);
1085
- const agentsSourceDir = path.join(__dirname, '../../agents');
1086
- copyDirRecursive(agentsSourceDir, agentsDir);
1087
- log(' ✅ 서브에이전트 설치 완료 (.claude/agents/)\n');
1088
- // .claude/skills/ 복사
1089
- const skillsDir = path.join(claudeDir, 'skills');
1090
- ensureDir(skillsDir);
1091
- const skillsSourceDir = path.join(__dirname, '../../skills');
1092
- if (fs.existsSync(skillsSourceDir)) {
1093
- copyDirRecursive(skillsSourceDir, skillsDir);
1094
- log(' ✅ 스킬 설치 완료 (.claude/skills/)\n');
1095
- }
1096
- // .claude/settings.json 설정
1097
- const settingsPath = path.join(claudeDir, 'settings.json');
1098
- const hooksTemplate = path.join(__dirname, '../../hooks/hooks.json');
1099
- if (fs.existsSync(hooksTemplate)) {
1100
- const vibeHooks = JSON.parse(fs.readFileSync(hooksTemplate, 'utf-8'));
1101
- if (fs.existsSync(settingsPath)) {
1102
- const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1103
- existingSettings.hooks = vibeHooks.hooks;
1104
- fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2));
1105
- log(' ✅ Hooks 설정 업데이트 완료\n');
1106
- }
1107
- else {
1108
- fs.copyFileSync(hooksTemplate, settingsPath);
1109
- log(' ✅ Hooks 설정 설치 완료\n');
1110
- }
1111
- }
1112
- // .gitignore에서 settings.local.json 제거
1113
- if (fs.existsSync(gitignorePath)) {
1114
- let gitignore = fs.readFileSync(gitignorePath, 'utf-8');
1115
- if (gitignore.includes('settings.local.json')) {
1116
- gitignore = gitignore.replace(/\.claude\/settings\.local\.json\n?/g, '');
1117
- gitignore = gitignore.replace(/settings\.local\.json\n?/g, '');
1118
- fs.writeFileSync(gitignorePath, gitignore);
1119
- log(' ✅ .gitignore에서 settings.local.json 제거\n');
1120
- }
1121
- }
86
+ updateConfig(vibeDir, detectedStacks, stackDetails, false);
87
+ // CLAUDE.md 병합
88
+ updateClaudeMd(projectRoot, detectedStacks, false);
89
+ // 규칙 복사
90
+ updateRules(vibeDir, detectedStacks, false);
1122
91
  // 협업자 자동 설치 설정
1123
92
  setupCollaboratorAutoInstall(projectRoot);
1124
93
  // 완료 메시지
1125
- log(`
1126
- ✅ vibe 초기화 완료!
1127
-
1128
- ${isNewProject ? `프로젝트 위치:
1129
- ${projectRoot}/
1130
-
1131
- ` : ''}생성된 구조:
1132
- CLAUDE.md # 프로젝트 컨텍스트
1133
- .claude/
1134
- ├── commands/ # 슬래시 커맨드 (7개)
1135
- ├── agents/ # 서브에이전트
1136
- ├── skills/ # 자동 활성화 스킬 (7개)
1137
- ├── settings.json # Hooks 설정 (저장소 공유)
1138
- └── vibe/
1139
- ├── config.json # 프로젝트 설정
1140
- ├── constitution.md # 프로젝트 원칙
1141
- ├── setup.sh # 협업자 설치 스크립트
1142
- ├── rules/ # 코딩 규칙
1143
- ├── core/ # 핵심 원칙
1144
- ├── quality/ # 품질 체크리스트
1145
- └── languages/ # 언어별 규칙
1146
- ├── specs/ # SPEC 문서들
1147
- └── features/ # BDD Feature 파일들
1148
-
1149
- 내장 도구: (35+)
1150
- 협업자 자동 설치: ✓
1151
-
1152
- ${formatLLMStatus()}
1153
-
1154
- 사용법:
1155
- /vibe.spec "기능명" SPEC 작성 (대화형)
1156
- /vibe.run "기능명" 구현 실행
1157
- /vibe.verify "기능명" 검증
1158
-
1159
- 다음 단계:
1160
- ${isNewProject ? `cd ${projectName}\n ` : ''}/vibe.spec "기능명" 으로 시작하세요!
94
+ log(`
95
+ ✅ vibe 초기화 완료!
96
+
97
+ ${isNewProject ? `프로젝트 위치:
98
+ ${projectRoot}/
99
+
100
+ ` : ''}전역 설치 (~/.claude/):
101
+ ~/.claude/
102
+ ├── commands/ # 슬래시 커맨드 (7개)
103
+ ├── agents/ # 서브에이전트
104
+ ├── skills/ # 스킬 (7개)
105
+ └── settings.json # Hooks + MCP 설정
106
+
107
+ 프로젝트 설정 (.claude/vibe/):
108
+ CLAUDE.md # 프로젝트 컨텍스트
109
+ .claude/vibe/
110
+ ├── config.json # 프로젝트 설정
111
+ ├── constitution.md # 프로젝트 원칙
112
+ ├── setup.sh # 협업자 설치 스크립트
113
+ ├── rules/ # 코딩 규칙
114
+ ├── core/ # 핵심 원칙
115
+ ├── quality/ # 품질 체크리스트
116
+ └── languages/ # 언어별 규칙
117
+ ├── specs/ # SPEC 문서들
118
+ └── features/ # BDD Feature 파일들
119
+
120
+ 내장 도구: ✓ (35+)
121
+ 협업자 자동 설치: ✓
122
+
123
+ ${formatLLMStatus()}
124
+
125
+ 사용법:
126
+ /vibe.spec "기능명" SPEC 작성 (대화형)
127
+ /vibe.run "기능명" 구현 실행
128
+ /vibe.verify "기능명" 검증
129
+
130
+ 다음 단계:
131
+ ${isNewProject ? `cd ${projectName}\n ` : ''}/vibe.spec "기능명" 으로 시작하세요!
1161
132
  `);
1162
133
  }
1163
134
  catch (error) {
1164
- console.error('❌ 초기화 실패:', error.message);
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ console.error('❌ 초기화 실패:', message);
1165
137
  process.exit(1);
1166
138
  }
1167
139
  }
@@ -1172,7 +144,6 @@ async function checkAndUpgradeVibe() {
1172
144
  encoding: 'utf-8',
1173
145
  stdio: ['pipe', 'pipe', 'pipe']
1174
146
  }).trim();
1175
- // 버전 비교: 실제로 새 버전인 경우에만 업그레이드
1176
147
  const isNewer = compareVersions(latestVersion, currentVersion) > 0;
1177
148
  if (isNewer) {
1178
149
  log(` 📦 새 버전 발견: v${currentVersion} → v${latestVersion}\n`);
@@ -1193,7 +164,7 @@ async function checkAndUpgradeVibe() {
1193
164
  return false;
1194
165
  }
1195
166
  }
1196
- catch (e) {
167
+ catch { /* ignore: optional operation */
1197
168
  log(` ℹ️ 버전 확인 스킵 (오프라인 또는 네트워크 오류)\n`);
1198
169
  return false;
1199
170
  }
@@ -1208,29 +179,9 @@ async function update() {
1208
179
  if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') {
1209
180
  return;
1210
181
  }
1211
- // 레거시 .vibe/ 폴더가 있으면 마이그레이션
182
+ // 레거시 마이그레이션
1212
183
  if (fs.existsSync(legacyVibeDir) && !fs.existsSync(vibeDir)) {
1213
- log('🔄 .vibe/ → .claude/vibe/ 마이그레이션 중...\n');
1214
- ensureDir(vibeDir);
1215
- ['specs', 'features', 'solutions', 'todos', 'memory', 'rules', 'config.json', 'constitution.md'].forEach(item => {
1216
- const src = path.join(legacyVibeDir, item);
1217
- const dst = path.join(vibeDir, item);
1218
- if (fs.existsSync(src) && !fs.existsSync(dst)) {
1219
- if (fs.statSync(src).isDirectory()) {
1220
- copyDirRecursive(src, dst);
1221
- }
1222
- else {
1223
- fs.copyFileSync(src, dst);
1224
- }
1225
- }
1226
- });
1227
- try {
1228
- removeDirRecursive(legacyVibeDir);
1229
- log(' ✅ 마이그레이션 완료\n');
1230
- }
1231
- catch (e) {
1232
- log(' ⚠️ .vibe/ 폴더 수동 삭제 필요\n');
1233
- }
184
+ migrateLegacyVibe(projectRoot, vibeDir);
1234
185
  }
1235
186
  if (!fs.existsSync(vibeDir) && !fs.existsSync(legacyVibeDir)) {
1236
187
  if (!options.silent) {
@@ -1238,7 +189,6 @@ async function update() {
1238
189
  }
1239
190
  return;
1240
191
  }
1241
- // vibeDir이 없으면 생성
1242
192
  ensureDir(vibeDir);
1243
193
  log('🔄 vibe 업데이트 중...\n');
1244
194
  // 최신 버전 확인
@@ -1247,360 +197,53 @@ async function update() {
1247
197
  if (wasUpgraded)
1248
198
  return;
1249
199
  }
1250
- // 마이그레이션: .agent/rules/ → .claude/vibe/rules/
1251
- const oldRulesDir = path.join(projectRoot, '.agent/rules');
1252
- const oldAgentDir = path.join(projectRoot, '.agent');
1253
- if (fs.existsSync(oldRulesDir)) {
1254
- log(' 🔄 마이그레이션: .agent/rules/ → .claude/vibe/rules/\n');
1255
- removeDirRecursive(oldRulesDir);
1256
- if (fs.existsSync(oldAgentDir) && fs.readdirSync(oldAgentDir).length === 0) {
1257
- fs.rmdirSync(oldAgentDir);
1258
- }
1259
- log(' ✅ 기존 .agent/rules/ 폴더 정리 완료\n');
1260
- }
1261
- // .claude/commands 업데이트
1262
- const commandsDir = path.join(claudeDir, 'commands');
1263
- ensureDir(commandsDir);
1264
- // 레거시 커맨드 파일 정리 (v2.2.7 이전 버전에서 제거된 커맨드들)
1265
- const legacyCommands = [
1266
- 'vibe.analyze.md', // agents/analyzer.md로 통합 후 다시 commands로 복원
1267
- 'vibe.compound.md', // hooks 자동 트리거로 변경
1268
- 'vibe.continue.md', // SessionStart hook으로 변경
1269
- 'vibe.diagram.md', // vibe.utils --diagram으로 변경
1270
- 'vibe.e2e.md', // vibe.utils --e2e로 변경
1271
- 'vibe.reason.md', // agents/reasoner.md로 통합 후 다시 commands로 복원
1272
- 'vibe.setup.md', // 제거됨
1273
- 'vibe.ui.md', // vibe.utils --ui로 변경
1274
- ];
1275
- legacyCommands.forEach(cmd => {
1276
- const cmdPath = path.join(commandsDir, cmd);
1277
- if (fs.existsSync(cmdPath)) {
1278
- fs.unlinkSync(cmdPath);
1279
- }
1280
- });
1281
- const sourceDir = path.join(__dirname, '../../commands');
1282
- copyDirRecursive(sourceDir, commandsDir);
1283
- log(' ✅ 슬래시 커맨드 업데이트 완료 (7개)\n');
1284
- // 레거시 에이전트 파일 정리 (commands에 통합된 에이전트들)
1285
- const agentsDir = path.join(claudeDir, 'agents');
1286
- const legacyAgents = ['reviewer.md', 'analyzer.md', 'reasoner.md'];
1287
- legacyAgents.forEach(agent => {
1288
- const agentPath = path.join(agentsDir, agent);
1289
- if (fs.existsSync(agentPath)) {
1290
- fs.unlinkSync(agentPath);
1291
- }
1292
- });
1293
- // Agent SDK 설치 확인 (오케스트레이터용)
1294
- const packageJsonPath = path.join(projectRoot, 'package.json');
1295
- if (fs.existsSync(packageJsonPath)) {
1296
- try {
1297
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
1298
- const hasAgentSdk = pkg.dependencies?.['@anthropic-ai/claude-agent-sdk'] ||
1299
- pkg.devDependencies?.['@anthropic-ai/claude-agent-sdk'];
1300
- if (!hasAgentSdk) {
1301
- log(' 🤖 Agent SDK 설치 중 (오케스트레이터용)...\n');
1302
- try {
1303
- const { execSync } = await import('child_process');
1304
- execSync('npm install @anthropic-ai/claude-agent-sdk --save-dev', {
1305
- cwd: projectRoot,
1306
- stdio: 'pipe'
1307
- });
1308
- log(' ✅ Agent SDK 설치 완료\n');
1309
- }
1310
- catch (e) {
1311
- log(' ⚠️ Agent SDK 설치 실패 - 수동 설치: npm i -D @anthropic-ai/claude-agent-sdk\n');
1312
- }
1313
- }
1314
- }
1315
- catch (e) { }
1316
- }
200
+ // 레거시 정리
201
+ cleanupLegacy(projectRoot, claudeDir);
1317
202
  // 기술 스택 감지
1318
203
  const { stacks: detectedStacks, details: stackDetails } = detectTechStacks(projectRoot);
1319
204
  // config.json 업데이트
1320
- const configPath = path.join(vibeDir, 'config.json');
1321
- if (fs.existsSync(configPath)) {
1322
- try {
1323
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1324
- config.stacks = detectedStacks;
1325
- config.details = stackDetails;
1326
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1327
- }
1328
- catch (e) { }
1329
- }
205
+ updateConfig(vibeDir, detectedStacks, stackDetails, true);
1330
206
  // constitution.md 업데이트
1331
- const templatePath = path.join(__dirname, '../../templates/constitution-template.md');
1332
- const constitutionPath = path.join(vibeDir, 'constitution.md');
1333
- if (fs.existsSync(templatePath)) {
1334
- let constitution = fs.readFileSync(templatePath, 'utf-8');
1335
- const backendStack = detectedStacks.find(s => s.type.includes('python') || s.type.includes('node') ||
1336
- s.type.includes('go') || s.type.includes('java') || s.type.includes('rust'));
1337
- const frontendStack = detectedStacks.find(s => s.type.includes('react') || s.type.includes('vue') ||
1338
- s.type.includes('flutter') || s.type.includes('swift') || s.type.includes('android'));
1339
- if (backendStack && STACK_NAMES[backendStack.type]) {
1340
- const info = STACK_NAMES[backendStack.type];
1341
- constitution = constitution.replace('- Language: {Python 3.11+ / Node.js / etc.}', `- Language: ${info.lang}`);
1342
- constitution = constitution.replace('- Framework: {FastAPI / Express / etc.}', `- Framework: ${info.framework}`);
1343
- }
1344
- if (frontendStack && STACK_NAMES[frontendStack.type]) {
1345
- const info = STACK_NAMES[frontendStack.type];
1346
- constitution = constitution.replace('- Framework: {Flutter / React / etc.}', `- Framework: ${info.framework}`);
1347
- }
1348
- constitution = constitution.replace('- Database: {PostgreSQL / MongoDB / etc.}', stackDetails.databases.length > 0 ? `- Database: ${stackDetails.databases.join(', ')}` : '- Database: (프로젝트에 맞게 설정)');
1349
- constitution = constitution.replace('- State Management: {Provider / Redux / etc.}', stackDetails.stateManagement.length > 0 ? `- State Management: ${stackDetails.stateManagement.join(', ')}` : '- State Management: (프로젝트에 맞게 설정)');
1350
- constitution = constitution.replace('- Hosting: {Cloud Run / Vercel / etc.}', stackDetails.hosting.length > 0 ? `- Hosting: ${stackDetails.hosting.join(', ')}` : '- Hosting: (프로젝트에 맞게 설정)');
1351
- constitution = constitution.replace('- CI/CD: {GitHub Actions / etc.}', stackDetails.cicd.length > 0 ? `- CI/CD: ${stackDetails.cicd.join(', ')}` : '- CI/CD: (프로젝트에 맞게 설정)');
1352
- fs.writeFileSync(constitutionPath, constitution);
1353
- log(' ✅ constitution.md 업데이트 완료\n');
1354
- }
1355
- // CLAUDE.md 업데이트 (언어별 규칙 동적 추가)
1356
- const vibeClaudeMd = path.join(__dirname, '../../CLAUDE.md');
1357
- const projectClaudeMd = path.join(projectRoot, 'CLAUDE.md');
1358
- if (fs.existsSync(vibeClaudeMd)) {
1359
- let vibeContent = fs.readFileSync(vibeClaudeMd, 'utf-8');
1360
- // 감지된 기술 스택에 따라 언어별 규칙 추가
1361
- const languageRules = getLanguageRulesForStacks(detectedStacks);
1362
- if (languageRules) {
1363
- vibeContent = vibeContent.replace('### 에러 처리 필수', languageRules + '\n\n### 에러 처리 필수');
1364
- }
1365
- if (fs.existsSync(projectClaudeMd)) {
1366
- const existingContent = fs.readFileSync(projectClaudeMd, 'utf-8');
1367
- // vibe 섹션 찾아서 교체 (# VIBE 부터 다음 --- 또는 끝까지)
1368
- const vibeStartMarker = '# VIBE';
1369
- const sectionSeparator = '\n---\n';
1370
- if (existingContent.includes(vibeStartMarker)) {
1371
- // 기존 vibe 섹션 교체
1372
- const vibeStartIdx = existingContent.indexOf(vibeStartMarker);
1373
- const beforeVibe = existingContent.substring(0, vibeStartIdx).trimEnd();
1374
- // vibe 섹션 뒤에 다른 섹션이 있는지 확인
1375
- const afterVibeStart = existingContent.substring(vibeStartIdx);
1376
- const nextSeparatorIdx = afterVibeStart.indexOf(sectionSeparator);
1377
- let afterVibe = '';
1378
- if (nextSeparatorIdx !== -1) {
1379
- afterVibe = afterVibeStart.substring(nextSeparatorIdx);
1380
- }
1381
- const newContent = beforeVibe + (beforeVibe ? '\n\n---\n\n' : '') + vibeContent + afterVibe;
1382
- fs.writeFileSync(projectClaudeMd, newContent);
1383
- log(' ✅ CLAUDE.md vibe 섹션 업데이트 완료\n');
1384
- }
1385
- else if (!existingContent.includes('/vibe.spec')) {
1386
- // vibe 섹션이 없으면 추가
1387
- const mergedContent = existingContent.trim() + '\n\n---\n\n' + vibeContent;
1388
- fs.writeFileSync(projectClaudeMd, mergedContent);
1389
- log(' ✅ CLAUDE.md에 vibe 섹션 추가\n');
1390
- }
1391
- else {
1392
- log(' ℹ️ CLAUDE.md vibe 섹션 유지\n');
1393
- }
1394
- }
1395
- else {
1396
- fs.writeFileSync(projectClaudeMd, vibeContent);
1397
- log(' ✅ CLAUDE.md 생성\n');
1398
- }
1399
- }
1400
- // .claude/vibe/rules/ 업데이트
1401
- const rulesSource = path.join(__dirname, '../../.claude/vibe/rules');
1402
- const rulesTarget = path.join(vibeDir, 'rules');
1403
- const coreDirs = ['core', 'quality', 'standards', 'tools'];
1404
- coreDirs.forEach(dir => {
1405
- const src = path.join(rulesSource, dir);
1406
- const dst = path.join(rulesTarget, dir);
1407
- if (fs.existsSync(src)) {
1408
- copyDirRecursive(src, dst);
1409
- }
1410
- });
1411
- const langSource = path.join(rulesSource, 'languages');
1412
- const langTarget = path.join(rulesTarget, 'languages');
1413
- if (fs.existsSync(langTarget)) {
1414
- removeDirRecursive(langTarget);
1415
- }
1416
- ensureDir(langTarget);
1417
- // 감지된 스택 타입을 그대로 파일명으로 매칭 (typescript-nextjs -> typescript-nextjs.md)
1418
- const detectedTypes = new Set(detectedStacks.map(s => s.type));
1419
- if (fs.existsSync(langSource)) {
1420
- const langFiles = fs.readdirSync(langSource);
1421
- langFiles.forEach(file => {
1422
- const langType = file.replace('.md', '');
1423
- if (detectedTypes.has(langType)) {
1424
- fs.copyFileSync(path.join(langSource, file), path.join(langTarget, file));
1425
- }
1426
- });
1427
- }
207
+ updateConstitution(vibeDir, detectedStacks, stackDetails);
208
+ log(' ✅ constitution.md 업데이트 완료\n');
209
+ // CLAUDE.md 업데이트
210
+ updateClaudeMd(projectRoot, detectedStacks, true);
211
+ // 규칙 업데이트
212
+ updateRules(vibeDir, detectedStacks, true);
1428
213
  if (detectedStacks.length > 0) {
214
+ const detectedTypes = new Set(detectedStacks.map(s => s.type));
1429
215
  log(` 🔍 감지된 기술 스택: ${Array.from(detectedTypes).join(', ')}\n`);
1430
216
  }
1431
- log(' ✅ 코딩 규칙 업데이트 완료 (.claude/vibe/rules/)\n');
1432
- // .claude/agents/ 업데이트
1433
- ensureDir(agentsDir);
1434
- const agentsSourceDir = path.join(__dirname, '../../agents');
1435
- copyDirRecursive(agentsSourceDir, agentsDir);
1436
- log(' ✅ 서브에이전트 업데이트 완료 (.claude/agents/)\n');
1437
- // .claude/skills/ 업데이트
1438
- const skillsDir = path.join(claudeDir, 'skills');
1439
- ensureDir(skillsDir);
1440
- const skillsSourceDir = path.join(__dirname, '../../skills');
1441
- if (fs.existsSync(skillsSourceDir)) {
1442
- copyDirRecursive(skillsSourceDir, skillsDir);
1443
- log(' ✅ 스킬 업데이트 완료 (.claude/skills/)\n');
1444
- }
1445
- // settings.json 업데이트
1446
- const settingsPath = path.join(claudeDir, 'settings.json');
1447
- const hooksTemplate = path.join(__dirname, '../../hooks/hooks.json');
1448
- if (fs.existsSync(hooksTemplate)) {
1449
- const vibeHooks = JSON.parse(fs.readFileSync(hooksTemplate, 'utf-8'));
1450
- if (fs.existsSync(settingsPath)) {
1451
- const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1452
- existingSettings.hooks = vibeHooks.hooks;
1453
- fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2));
1454
- log(' ✅ Hooks 설정 업데이트 완료\n');
1455
- }
1456
- else {
1457
- fs.copyFileSync(hooksTemplate, settingsPath);
1458
- log(' ✅ Hooks 설정 생성 완료\n');
1459
- }
1460
- // settings.local.json도 업데이트
1461
- const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
1462
- if (fs.existsSync(settingsLocalPath)) {
1463
- try {
1464
- const localSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf-8'));
1465
- if (localSettings.hooks) {
1466
- localSettings.hooks = vibeHooks.hooks;
1467
- fs.writeFileSync(settingsLocalPath, JSON.stringify(localSettings, null, 2));
1468
- log(' ✅ 로컬 Hooks 설정 업데이트 완료\n');
1469
- }
1470
- }
1471
- catch (e) { }
1472
- }
1473
- }
1474
- // .gitignore에서 settings.local.json 제거
1475
- const gitignorePath = path.join(projectRoot, '.gitignore');
1476
- if (fs.existsSync(gitignorePath)) {
1477
- let gitignore = fs.readFileSync(gitignorePath, 'utf-8');
1478
- if (gitignore.includes('settings.local.json')) {
1479
- gitignore = gitignore.replace(/\.claude\/settings\.local\.json\n?/g, '');
1480
- gitignore = gitignore.replace(/settings\.local\.json\n?/g, '');
1481
- fs.writeFileSync(gitignorePath, gitignore);
1482
- log(' ✅ .gitignore에서 settings.local.json 제거\n');
1483
- }
1484
- }
217
+ // 전역 assets 업데이트
218
+ installGlobalAssets(true);
219
+ // 프로젝트 로컬 자산 제거
220
+ removeLocalAssets(claudeDir);
221
+ // .gitignore 업데이트
222
+ updateGitignore(projectRoot);
1485
223
  // 협업자 자동 설치 설정
1486
224
  setupCollaboratorAutoInstall(projectRoot);
1487
- // MCP 서버 등록
1488
- const geminiMcpPath = path.join(__dirname, '../lib/gemini-mcp.js');
1489
- const gptMcpPath = path.join(__dirname, '../lib/gpt-mcp.js');
1490
225
  // ~/.claude.json 정리
1491
- const claudeConfigPath = path.join(os.homedir(), '.claude.json');
1492
- if (fs.existsSync(claudeConfigPath)) {
1493
- try {
1494
- const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf-8'));
1495
- let configModified = false;
1496
- if (claudeConfig.projects) {
1497
- for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
1498
- if (projectConfig.mcpServers) {
1499
- if (projectConfig.mcpServers.vibe) {
1500
- const vibeArgs = projectConfig.mcpServers.vibe.args || [];
1501
- const isLocalPath = vibeArgs.some((arg) => arg.includes('.vibe/mcp/') || arg.includes('.vibe\\mcp\\'));
1502
- if (isLocalPath) {
1503
- delete projectConfig.mcpServers.vibe;
1504
- configModified = true;
1505
- log(` 🧹 ${projectPath}: 로컬 vibe MCP 제거\n`);
1506
- }
1507
- }
1508
- if (projectConfig.mcpServers['vibe-gemini']) {
1509
- const geminiArgs = projectConfig.mcpServers['vibe-gemini'].args || [];
1510
- const isLocalPath = geminiArgs.some((arg) => arg.includes('.vibe/') || arg.includes('.vibe\\'));
1511
- if (isLocalPath) {
1512
- delete projectConfig.mcpServers['vibe-gemini'];
1513
- configModified = true;
1514
- }
1515
- }
1516
- if (projectConfig.mcpServers.context7) {
1517
- delete projectConfig.mcpServers.context7;
1518
- configModified = true;
1519
- }
1520
- }
1521
- }
1522
- }
1523
- if (configModified) {
1524
- fs.writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
1525
- log(' ✅ ~/.claude.json 로컬 MCP 설정 정리 완료\n');
1526
- }
1527
- }
1528
- catch (e) {
1529
- log(' ⚠️ ~/.claude.json 정리 실패: ' + e.message + '\n');
1530
- }
1531
- }
1532
- // MCP 등록 (hi-ai는 내장 도구로 전환됨)
1533
- try {
1534
- // 기존 vibe MCP 제거 (hi-ai 기반 → 내장 도구로 마이그레이션)
1535
- unregisterMcp('vibe');
1536
- // vibe-gemini MCP 등록
1537
- unregisterMcp('vibe-gemini');
1538
- if (fs.existsSync(geminiMcpPath)) {
1539
- try {
1540
- registerMcp('vibe-gemini', { command: 'node', args: [geminiMcpPath] });
1541
- log(' ✅ vibe-gemini MCP 전역 등록 완료\n');
1542
- }
1543
- catch (e) {
1544
- if (e.message?.includes('already exists')) {
1545
- log(' ℹ️ vibe-gemini MCP 이미 등록됨\n');
1546
- }
1547
- }
1548
- }
1549
- // vibe-gpt MCP 등록
1550
- unregisterMcp('vibe-gpt');
1551
- if (fs.existsSync(gptMcpPath)) {
1552
- try {
1553
- registerMcp('vibe-gpt', { command: 'node', args: [gptMcpPath] });
1554
- log(' ✅ vibe-gpt MCP 전역 등록 완료\n');
1555
- }
1556
- catch (e) {
1557
- if (e.message?.includes('already exists')) {
1558
- log(' ℹ️ vibe-gpt MCP 이미 등록됨\n');
1559
- }
1560
- }
1561
- }
1562
- // context7 MCP 등록
1563
- unregisterMcp('context7');
1564
- try {
1565
- registerMcp('context7', { command: 'npx', args: ['-y', '@upstash/context7-mcp@latest'] });
1566
- log(' ✅ context7 MCP 전역 등록 완료\n');
1567
- }
1568
- catch (e) {
1569
- if (e.message?.includes('already exists')) {
1570
- log(' ℹ️ context7 MCP 이미 등록됨\n');
1571
- }
1572
- }
1573
- }
1574
- catch (e) {
1575
- log(' ⚠️ MCP 등록 실패\n');
1576
- }
1577
- // 기존 mcp/ 폴더 정리 (레거시)
1578
- const oldMcpDir = path.join(vibeDir, 'mcp');
1579
- if (fs.existsSync(oldMcpDir)) {
1580
- log(' 🧹 기존 mcp/ 폴더 정리 중...\n');
1581
- try {
1582
- removeDirRecursive(oldMcpDir);
1583
- log(' ✅ mcp/ 폴더 삭제 완료\n');
1584
- }
1585
- catch (e) {
1586
- log(' ⚠️ mcp/ 폴더 수동 삭제 필요\n');
1587
- }
1588
- }
226
+ cleanupClaudeConfig();
227
+ // MCP 서버 등록
228
+ registerMcpServers(true);
229
+ // 레거시 mcp 폴더 정리
230
+ cleanupLegacyMcp(vibeDir);
1589
231
  const packageJson = getPackageJson();
1590
- log(`
1591
- ✅ vibe 업데이트 완료! (v${packageJson.version})
1592
-
1593
- 업데이트된 항목:
1594
- - 슬래시 커맨드 (7개)
1595
- - 코딩 규칙 (.claude/vibe/rules/)
1596
- - 서브에이전트 (.claude/agents/)
1597
- - Hooks 설정
1598
-
1599
- ${formatLLMStatus()}
232
+ log(`
233
+ ✅ vibe 업데이트 완료! (v${packageJson.version})
234
+
235
+ 업데이트된 항목:
236
+ - 슬래시 커맨드 (7개)
237
+ - 코딩 규칙 (.claude/vibe/rules/)
238
+ - 서브에이전트 (.claude/agents/)
239
+ - Hooks 설정
240
+
241
+ ${formatLLMStatus()}
1600
242
  `);
1601
243
  }
1602
244
  catch (error) {
1603
- console.error('❌ 업데이트 실패:', error.message);
245
+ const message = error instanceof Error ? error.message : String(error);
246
+ console.error('❌ 업데이트 실패:', message);
1604
247
  process.exit(1);
1605
248
  }
1606
249
  }
@@ -1665,511 +308,72 @@ function remove() {
1665
308
  console.log(' ✅ Hooks 설정 제거 완료\n');
1666
309
  }
1667
310
  }
1668
- catch (e) { }
1669
- }
1670
- console.log(`
1671
- ✅ vibe 제거 완료!
1672
-
1673
- 제거된 항목:
1674
- - MCP 서버 (vibe, context7)
1675
- - .claude/vibe/ 폴더
1676
- - 슬래시 커맨드 (7개)
1677
- - 서브에이전트 (5개)
1678
- - Hooks 설정
1679
-
1680
- 다시 설치하려면: vibe init
1681
- `);
1682
- }
1683
- // ============================================================================
1684
- // External LLM Commands
1685
- // ============================================================================
1686
- function setupExternalLLM(llmType, apiKey) {
1687
- if (!apiKey) {
1688
- console.log(`
1689
- ❌ API 키가 필요합니다.
1690
-
1691
- 사용법:
1692
- vibe ${llmType} <api-key>
1693
-
1694
- ${llmType === 'gpt' ? 'OpenAI API 키: https://platform.openai.com/api-keys' : 'Google API 키: https://aistudio.google.com/apikey'}
1695
- `);
1696
- return;
1697
- }
1698
- const projectRoot = process.cwd();
1699
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
1700
- const configPath = path.join(vibeDir, 'config.json');
1701
- if (!fs.existsSync(vibeDir)) {
1702
- console.log('❌ vibe 프로젝트가 아닙니다. 먼저 vibe init을 실행하세요.');
1703
- return;
1704
- }
1705
- let config = {};
1706
- if (fs.existsSync(configPath)) {
1707
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1708
- }
1709
- if (!config.models) {
1710
- config.models = {};
1711
- }
1712
- const llmConfig = EXTERNAL_LLMS[llmType];
1713
- config.models[llmType] = {
1714
- enabled: true,
1715
- role: llmConfig.role,
1716
- description: llmConfig.description
1717
- };
1718
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1719
- const envKey = llmConfig.envKey;
1720
- try {
1721
- unregisterMcp(llmConfig.name);
1722
- registerMcp(llmConfig.name, {
1723
- command: 'npx',
1724
- args: ['-y', llmConfig.package],
1725
- env: { [envKey]: apiKey }
1726
- });
1727
- console.log(`
1728
- ✅ ${llmType.toUpperCase()} 활성화 완료! (전역)
1729
-
1730
- 역할: ${llmConfig.description}
1731
- MCP: ${llmConfig.name}
1732
-
1733
- 모든 프로젝트에서 /vibe.run 실행 시 자동으로 활용됩니다.
1734
-
1735
- 비활성화: vibe remove ${llmType}
1736
- `);
1737
- }
1738
- catch (e) {
1739
- console.log(`
1740
- ⚠️ MCP 등록 실패. ~/.claude/settings.json에 수동으로 추가하세요.
1741
- `);
1742
- }
1743
- }
1744
- function removeExternalLLM(llmType) {
1745
- const projectRoot = process.cwd();
1746
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
1747
- const configPath = path.join(vibeDir, 'config.json');
1748
- if (!fs.existsSync(vibeDir)) {
1749
- console.log('❌ vibe 프로젝트가 아닙니다.');
1750
- return;
1751
- }
1752
- if (fs.existsSync(configPath)) {
1753
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1754
- if (config.models?.[llmType]) {
1755
- config.models[llmType].enabled = false;
1756
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1757
- }
1758
- }
1759
- const llmConfig = EXTERNAL_LLMS[llmType];
1760
- unregisterMcp(llmConfig.name);
1761
- console.log(`✅ ${llmType.toUpperCase()} 비활성화 완료`);
1762
- }
1763
- // ============================================================================
1764
- // GPT OAuth Commands
1765
- // ============================================================================
1766
- async function gptAuth() {
1767
- console.log(`
1768
- 🔐 GPT Plus/Pro 인증 (OAuth)
1769
-
1770
- ChatGPT Plus 또는 Pro 구독이 있으면 Codex API를 사용할 수 있습니다.
1771
- 브라우저에서 OpenAI 계정으로 로그인하세요.
1772
- `);
1773
- try {
1774
- const gptOAuthPath = path.join(__dirname, '../lib/gpt-oauth.js');
1775
- const gptStoragePath = path.join(__dirname, '../lib/gpt-storage.js');
1776
- const { startOAuthFlow } = require(gptOAuthPath);
1777
- const storage = require(gptStoragePath);
1778
- const tokens = await startOAuthFlow();
1779
- storage.addAccount({
1780
- email: tokens.email,
1781
- accessToken: tokens.accessToken,
1782
- refreshToken: tokens.refreshToken,
1783
- idToken: tokens.idToken,
1784
- expires: tokens.expires,
1785
- accountId: tokens.accountId,
1786
- });
1787
- console.log(`
1788
- ✅ GPT 인증 완료!
1789
-
1790
- 계정: ${tokens.email}
1791
- 계정 ID: ${tokens.accountId || '(자동 감지)'}
1792
-
1793
- ⚠️ 참고: ChatGPT Plus/Pro 구독이 있어야 API 호출이 가능합니다.
1794
- 구독이 없으면 인증은 성공하지만 API 호출 시 오류가 발생합니다.
1795
-
1796
- 상태 확인: vibe status gpt
1797
- 로그아웃: vibe logout gpt
1798
- `);
1799
- // config.json 업데이트
1800
- const projectRoot = process.cwd();
1801
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
1802
- const configPath = path.join(vibeDir, 'config.json');
1803
- if (fs.existsSync(configPath)) {
1804
- try {
1805
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1806
- if (!config.models)
1807
- config.models = {};
1808
- config.models.gpt = {
1809
- enabled: true,
1810
- authType: 'oauth',
1811
- email: tokens.email,
1812
- role: 'architecture',
1813
- description: 'GPT (ChatGPT Plus/Pro)',
1814
- };
1815
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1816
- }
1817
- catch (e) { }
1818
- }
1819
- // MCP 서버 등록
1820
- try {
1821
- const mcpPath = path.join(__dirname, '../lib/gpt-mcp.js');
1822
- unregisterMcp('vibe-gpt');
1823
- registerMcp('vibe-gpt', { command: 'node', args: [mcpPath] });
1824
- console.log(`
1825
- ✅ vibe-gpt MCP 서버 등록 완료! (전역)
1826
-
1827
- 이제 모든 프로젝트에서 다음 도구를 사용할 수 있습니다:
1828
- - gpt_chat: GPT에 질문
1829
- - gpt_analyze_architecture: 아키텍처 분석
1830
- - gpt_debug: 디버깅
1831
- - gpt_quick_ask: 빠른 질문
1832
- `);
1833
- }
1834
- catch (mcpError) {
1835
- console.log(`
1836
- ⚠️ MCP 서버 등록 실패. ~/.claude/settings.json에 수동으로 추가하세요.
1837
- `);
1838
- }
1839
- process.exit(0);
1840
- }
1841
- catch (error) {
1842
- console.error(`
1843
- ❌ GPT 인증 실패
1844
-
1845
- 오류: ${error.message}
1846
-
1847
- 다시 시도하려면: vibe gpt --auth
1848
- `);
1849
- process.exit(1);
1850
- }
1851
- }
1852
- function gptStatus() {
1853
- try {
1854
- const gptStoragePath = path.join(__dirname, '../lib/gpt-storage.js');
1855
- const storage = require(gptStoragePath);
1856
- const accounts = storage.getAllAccounts();
1857
- if (accounts.length === 0) {
1858
- console.log(`
1859
- 📊 GPT 인증 상태
1860
-
1861
- 인증된 계정 없음
1862
-
1863
- 로그인: vibe gpt --auth
1864
- `);
1865
- return;
1866
- }
1867
- const activeAccount = storage.getActiveAccount();
1868
- const isExpired = storage.isTokenExpired(activeAccount);
1869
- console.log(`
1870
- 📊 GPT 인증 상태
1871
-
1872
- 활성 계정: ${activeAccount.email}
1873
- 계정 ID: ${activeAccount.accountId || '(없음)'}
1874
- 토큰 상태: ${isExpired ? '⚠️ 만료됨 (자동 갱신됨)' : '✅ 유효'}
1875
- 마지막 사용: ${new Date(activeAccount.lastUsed).toLocaleString()}
1876
-
1877
- 등록된 계정 (${accounts.length}개):
1878
- ${accounts.map((acc, i) => ` ${i === storage.loadAccounts()?.activeIndex ? '→' : ' '} ${acc.email}`).join('\n')}
1879
-
1880
- ⚠️ 참고: ChatGPT Plus/Pro 구독이 있어야 API 호출이 가능합니다.
1881
-
1882
- 로그아웃: vibe logout gpt
1883
- `);
1884
- }
1885
- catch (error) {
1886
- console.error('상태 확인 실패:', error.message);
1887
- }
1888
- }
1889
- function gptLogout() {
1890
- try {
1891
- const gptStoragePath = path.join(__dirname, '../lib/gpt-storage.js');
1892
- const storage = require(gptStoragePath);
1893
- const activeAccount = storage.getActiveAccount();
1894
- if (!activeAccount) {
1895
- console.log('로그인된 계정이 없습니다.');
1896
- return;
1897
- }
1898
- storage.clearAccounts();
1899
- console.log(`
1900
- ✅ GPT 로그아웃 완료
1901
-
1902
- ${activeAccount.email} 계정이 제거되었습니다.
1903
-
1904
- 다시 로그인: vibe gpt --auth
1905
- `);
1906
- // config.json 업데이트
1907
- const projectRoot = process.cwd();
1908
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
1909
- const configPath = path.join(vibeDir, 'config.json');
1910
- if (fs.existsSync(configPath)) {
1911
- try {
1912
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1913
- if (config.models?.gpt) {
1914
- config.models.gpt.enabled = false;
1915
- config.models.gpt.authType = undefined;
1916
- config.models.gpt.email = undefined;
1917
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1918
- }
1919
- }
1920
- catch (e) { }
1921
- }
311
+ catch { /* ignore: optional operation */ }
1922
312
  }
1923
- catch (error) {
1924
- console.error('로그아웃 실패:', error.message);
1925
- }
1926
- }
1927
- function showAuthHelp() {
1928
- console.log(`
1929
- 🔐 vibe auth - LLM 인증
1930
-
1931
- 사용법:
1932
- vibe auth gpt GPT Plus/Pro OAuth 인증
1933
- vibe auth gpt --key <key> GPT API 키로 설정
1934
- vibe auth gemini Gemini 구독 OAuth 인증 (권장)
1935
- vibe auth gemini --key <key> Gemini API 키로 설정
1936
-
1937
- 예시:
1938
- vibe auth gpt OpenAI 로그인 (Plus/Pro 구독 필요)
1939
- vibe auth gemini Google 로그인 (Gemini Advanced 구독 시 무료)
1940
- vibe auth gpt --key sk-xxx API 키로 설정 (사용량 과금)
313
+ console.log(`
314
+ vibe 제거 완료!
315
+
316
+ 제거된 항목:
317
+ - MCP 서버 (vibe, context7)
318
+ - .claude/vibe/ 폴더
319
+ - 슬래시 커맨드 (7개)
320
+ - 서브에이전트 (5개)
321
+ - Hooks 설정
322
+
323
+ 다시 설치하려면: vibe init
1941
324
  `);
1942
325
  }
1943
- function showLogoutHelp() {
1944
- console.log(`
1945
- 🚪 vibe logout - LLM 로그아웃
1946
-
1947
- 사용법:
1948
- vibe logout gpt GPT 로그아웃
1949
- vibe logout gemini Gemini 로그아웃
1950
- `);
1951
- }
1952
- // ============================================================================
1953
- // Gemini OAuth Commands
1954
- // ============================================================================
1955
- async function geminiAuth() {
1956
- console.log(`
1957
- 🔐 Gemini 구독 인증 (OAuth)
1958
-
1959
- Gemini Advanced 구독이 있으면 추가 비용 없이 사용할 수 있습니다.
1960
- 브라우저에서 Google 계정으로 로그인하세요.
1961
- `);
1962
- try {
1963
- const geminiOAuthPath = path.join(__dirname, '../lib/gemini-oauth.js');
1964
- const geminiStoragePath = path.join(__dirname, '../lib/gemini-storage.js');
1965
- const { startOAuthFlow } = require(geminiOAuthPath);
1966
- const storage = require(geminiStoragePath);
1967
- const tokens = await startOAuthFlow();
1968
- storage.addAccount({
1969
- email: tokens.email,
1970
- accessToken: tokens.accessToken,
1971
- refreshToken: tokens.refreshToken,
1972
- expires: tokens.expires,
1973
- projectId: tokens.projectId,
1974
- });
1975
- console.log(`
1976
- ✅ Gemini 인증 완료!
1977
-
1978
- 계정: ${tokens.email}
1979
- 프로젝트: ${tokens.projectId || '(자동 감지)'}
1980
-
1981
- 사용 가능한 모델:
1982
- - Gemini 3 Flash (빠른 응답, 탐색/검색)
1983
- - Gemini 3 Pro (높은 정확도)
1984
-
1985
- /vibe.run 실행 시 자동으로 Gemini가 보조 모델로 활용됩니다.
1986
-
1987
- 상태 확인: vibe status gemini
1988
- 로그아웃: vibe logout gemini
1989
- `);
1990
- // config.json 업데이트
1991
- const projectRoot = process.cwd();
1992
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
1993
- const configPath = path.join(vibeDir, 'config.json');
1994
- if (fs.existsSync(configPath)) {
1995
- try {
1996
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1997
- if (!config.models)
1998
- config.models = {};
1999
- config.models.gemini = {
2000
- enabled: true,
2001
- authType: 'oauth',
2002
- email: tokens.email,
2003
- role: 'exploration',
2004
- description: 'Gemini 3 Flash/Pro (탐색, UI/UX)',
2005
- };
2006
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2007
- }
2008
- catch (e) { }
2009
- }
2010
- // MCP 서버 등록
2011
- try {
2012
- const mcpPath = path.join(__dirname, '../lib/gemini-mcp.js');
2013
- unregisterMcp('vibe-gemini');
2014
- registerMcp('vibe-gemini', { command: 'node', args: [mcpPath] });
2015
- console.log(`
2016
- ✅ vibe-gemini MCP 서버 등록 완료! (전역)
2017
-
2018
- 이제 모든 프로젝트에서 다음 도구를 사용할 수 있습니다:
2019
- - gemini_chat: Gemini에 질문
2020
- - gemini_analyze_code: 코드 분석
2021
- - gemini_review_ui: UI/UX 리뷰
2022
- - gemini_quick_ask: 빠른 질문
2023
- `);
2024
- }
2025
- catch (mcpError) {
2026
- console.log(`
2027
- ⚠️ MCP 서버 등록 실패. ~/.claude/settings.json에 수동으로 추가하세요.
2028
- `);
2029
- }
2030
- process.exit(0);
2031
- }
2032
- catch (error) {
2033
- console.error(`
2034
- ❌ Gemini 인증 실패
2035
-
2036
- 오류: ${error.message}
2037
-
2038
- 다시 시도하려면: vibe gemini --auth
2039
- `);
2040
- process.exit(1);
2041
- }
2042
- }
2043
- function geminiStatus() {
2044
- try {
2045
- const geminiStoragePath = path.join(__dirname, '../lib/gemini-storage.js');
2046
- const geminiApiPath = path.join(__dirname, '../lib/gemini-api.js');
2047
- const storage = require(geminiStoragePath);
2048
- const { GEMINI_MODELS } = require(geminiApiPath);
2049
- const accounts = storage.getAllAccounts();
2050
- if (accounts.length === 0) {
2051
- console.log(`
2052
- 📊 Gemini 인증 상태
2053
-
2054
- 인증된 계정 없음
2055
-
2056
- 로그인: vibe gemini --auth
2057
- `);
2058
- return;
2059
- }
2060
- const activeAccount = storage.getActiveAccount();
2061
- const isExpired = storage.isTokenExpired(activeAccount);
2062
- console.log(`
2063
- 📊 Gemini 인증 상태
2064
-
2065
- 활성 계정: ${activeAccount.email}
2066
- 프로젝트: ${activeAccount.projectId || '(자동)'}
2067
- 토큰 상태: ${isExpired ? '⚠️ 만료됨 (자동 갱신됨)' : '✅ 유효'}
2068
- 마지막 사용: ${new Date(activeAccount.lastUsed).toLocaleString()}
2069
-
2070
- 등록된 계정 (${accounts.length}개):
2071
- ${accounts.map((acc, i) => ` ${i === storage.loadAccounts()?.activeIndex ? '→' : ' '} ${acc.email}`).join('\n')}
2072
-
2073
- 사용 가능한 모델:
2074
- ${Object.entries(GEMINI_MODELS).map(([id, info]) => ` - ${id}: ${info.description}`).join('\n')}
2075
-
2076
- 로그아웃: vibe logout gemini
2077
- `);
2078
- }
2079
- catch (error) {
2080
- console.error('상태 확인 실패:', error.message);
2081
- }
2082
- }
2083
- function geminiLogout() {
2084
- try {
2085
- const geminiStoragePath = path.join(__dirname, '../lib/gemini-storage.js');
2086
- const storage = require(geminiStoragePath);
2087
- const activeAccount = storage.getActiveAccount();
2088
- if (!activeAccount) {
2089
- console.log('로그인된 계정이 없습니다.');
2090
- return;
2091
- }
2092
- storage.clearAccounts();
2093
- console.log(`
2094
- ✅ Gemini 로그아웃 완료
2095
-
2096
- ${activeAccount.email} 계정이 제거되었습니다.
2097
-
2098
- 다시 로그인: vibe gemini --auth
2099
- `);
2100
- // config.json 업데이트
2101
- const projectRoot = process.cwd();
2102
- const vibeDir = path.join(projectRoot, '.claude', 'vibe');
2103
- const configPath = path.join(vibeDir, 'config.json');
2104
- if (fs.existsSync(configPath)) {
2105
- try {
2106
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
2107
- if (config.models?.gemini) {
2108
- config.models.gemini.enabled = false;
2109
- config.models.gemini.authType = undefined;
2110
- config.models.gemini.email = undefined;
2111
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2112
- }
2113
- }
2114
- catch (e) { }
2115
- }
2116
- }
2117
- catch (error) {
2118
- console.error('로그아웃 실패:', error.message);
2119
- }
2120
- }
2121
- // showGeminiHelp 제거됨 - showAuthHelp로 통합
2122
326
  // ============================================================================
2123
327
  // Info Commands
2124
328
  // ============================================================================
2125
329
  function showHelp() {
2126
- console.log(`
2127
- 📖 Vibe - SPEC-driven AI coding framework (Claude Code 전용)
2128
-
2129
- 기본 명령어:
2130
- vibe init [project] 프로젝트 초기화
2131
- vibe update 설정 업데이트
2132
- vibe status 현재 설정 상태
2133
- vibe help 도움말
2134
- vibe version 버전 정보
2135
-
2136
- 외부 LLM 인증:
2137
- vibe auth gpt GPT Plus/Pro OAuth 인증
2138
- vibe auth gemini Gemini 구독 OAuth 인증 (권장)
2139
- vibe auth gpt --key <key> GPT API 키 설정
2140
- vibe auth gemini --key <key> Gemini API 키 설정
2141
-
2142
- 상태 및 관리:
2143
- vibe status 전체 상태 확인
2144
- vibe status gpt GPT 인증 상태 확인
2145
- vibe status gemini Gemini 인증 상태 확인
2146
- vibe logout gpt GPT 로그아웃
2147
- vibe logout gemini Gemini 로그아웃
2148
- vibe remove gpt GPT 제거
2149
- vibe remove gemini Gemini 제거
2150
- vibe remove vibe 전체 제거 (MCP, 설정, 패키지)
2151
-
2152
- Claude Code 슬래시 커맨드:
2153
- /vibe.spec "기능명" SPEC 작성 (PTCF 구조)
2154
- /vibe.run "기능명" 구현 실행
2155
- /vibe.verify "기능명" 검증
2156
- /vibe.reason "문제" 체계적 추론
2157
- /vibe.analyze 프로젝트 분석
2158
- /vibe.ui "설명" UI 미리보기
2159
- /vibe.diagram 다이어그램 생성
2160
-
2161
- 모델 오케스트레이션:
2162
- Opus 4.5 오케스트레이터 (메인)
2163
- Sonnet 4 구현
2164
- Haiku 4.5 코드 탐색
2165
- GPT 5.2 아키텍처/디버깅 (선택적)
2166
- Gemini 3 UI/UX 설계 (선택적)
2167
-
2168
- Workflow:
2169
- /vibe.spec → /vibe.run → /vibe.verify
2170
-
2171
- 문서:
2172
- https://github.com/su-record/vibe
330
+ console.log(`
331
+ 📖 Vibe - SPEC-driven AI coding framework (Claude Code 전용)
332
+
333
+ 기본 명령어:
334
+ vibe init [project] 프로젝트 초기화
335
+ vibe update 설정 업데이트
336
+ vibe status 현재 설정 상태
337
+ vibe help 도움말
338
+ vibe version 버전 정보
339
+
340
+ 외부 LLM 인증:
341
+ vibe auth gpt GPT Plus/Pro OAuth 인증
342
+ vibe auth gemini Gemini 구독 OAuth 인증 (권장)
343
+ vibe auth gpt --key <key> GPT API 키 설정
344
+ vibe auth gemini --key <key> Gemini API 키 설정
345
+
346
+ 상태 및 관리:
347
+ vibe status 전체 상태 확인
348
+ vibe status gpt GPT 인증 상태 확인
349
+ vibe status gemini Gemini 인증 상태 확인
350
+ vibe logout gpt GPT 로그아웃
351
+ vibe logout gemini Gemini 로그아웃
352
+ vibe remove gpt GPT 제거
353
+ vibe remove gemini Gemini 제거
354
+ vibe remove vibe 전체 제거 (MCP, 설정, 패키지)
355
+
356
+ Claude Code 슬래시 커맨드:
357
+ /vibe.spec "기능명" SPEC 작성 (PTCF 구조)
358
+ /vibe.run "기능명" 구현 실행
359
+ /vibe.verify "기능명" 검증
360
+ /vibe.reason "문제" 체계적 추론
361
+ /vibe.analyze 프로젝트 분석
362
+ /vibe.ui "설명" UI 미리보기
363
+ /vibe.diagram 다이어그램 생성
364
+
365
+ 모델 오케스트레이션:
366
+ Opus 4.5 오케스트레이터 (메인)
367
+ Sonnet 4 구현
368
+ Haiku 4.5 코드 탐색
369
+ GPT 5.2 아키텍처/디버깅 (선택적)
370
+ Gemini 3 UI/UX 설계 (선택적)
371
+
372
+ Workflow:
373
+ /vibe.spec → /vibe.run → /vibe.verify
374
+
375
+ 문서:
376
+ https://github.com/su-record/vibe
2173
377
  `);
2174
378
  }
2175
379
  function showStatus() {
@@ -2185,35 +389,35 @@ function showStatus() {
2185
389
  if (fs.existsSync(configPath)) {
2186
390
  config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
2187
391
  }
2188
- const gptStatus = config.models?.gpt?.enabled ? '✅ 활성' : '⬚ 비활성';
2189
- const geminiStatus = config.models?.gemini?.enabled ? '✅ 활성' : '⬚ 비활성';
2190
- console.log(`
2191
- 📊 Vibe 상태 (v${packageJson.version})
2192
-
2193
- 프로젝트: ${projectRoot}
2194
- 언어: ${config.language || 'ko'}
2195
-
2196
- 모델 오케스트레이션:
2197
- ┌─────────────────────────────────────────┐
2198
- │ Opus 4.5 오케스트레이터 │
2199
- ├─────────────────────────────────────────┤
2200
- │ Sonnet 4 구현 │
2201
- │ Haiku 4.5 코드 탐색 │
2202
- ├─────────────────────────────────────────┤
2203
- │ GPT 5.2 ${gptStatus} 아키텍처/디버깅 │
2204
- │ Gemini 3 ${geminiStatus} UI/UX 설계 │
2205
- └─────────────────────────────────────────┘
2206
-
2207
- MCP 서버:
2208
- vibe-gemini Gemini API
2209
- vibe-gpt GPT API
2210
- context7 라이브러리 문서 검색
2211
-
2212
- 외부 LLM 설정:
2213
- vibe auth gpt GPT 활성화 (OAuth)
2214
- vibe auth gemini Gemini 활성화 (OAuth)
2215
- vibe remove gpt GPT 제거
2216
- vibe remove gemini Gemini 제거
392
+ const gptStatusText = config.models?.gpt?.enabled ? '✅ 활성' : '⬚ 비활성';
393
+ const geminiStatusText = config.models?.gemini?.enabled ? '✅ 활성' : '⬚ 비활성';
394
+ console.log(`
395
+ 📊 Vibe 상태 (v${packageJson.version})
396
+
397
+ 프로젝트: ${projectRoot}
398
+ 언어: ${config.language || 'ko'}
399
+
400
+ 모델 오케스트레이션:
401
+ ┌─────────────────────────────────────────┐
402
+ │ Opus 4.5 오케스트레이터 │
403
+ ├─────────────────────────────────────────┤
404
+ │ Sonnet 4 구현 │
405
+ │ Haiku 4.5 코드 탐색 │
406
+ ├─────────────────────────────────────────┤
407
+ │ GPT 5.2 ${gptStatusText} 아키텍처/디버깅 │
408
+ │ Gemini 3 ${geminiStatusText} UI/UX 설계 │
409
+ └─────────────────────────────────────────┘
410
+
411
+ MCP 서버:
412
+ vibe-gemini Gemini API
413
+ vibe-gpt GPT API
414
+ context7 라이브러리 문서 검색
415
+
416
+ 외부 LLM 설정:
417
+ vibe auth gpt GPT 활성화 (OAuth)
418
+ vibe auth gemini Gemini 활성화 (OAuth)
419
+ vibe remove gpt GPT 제거
420
+ vibe remove gemini Gemini 제거
2217
421
  `);
2218
422
  }
2219
423
  function showVersion() {
@@ -2276,7 +480,6 @@ switch (command) {
2276
480
  break;
2277
481
  case 'remove':
2278
482
  case 'uninstall':
2279
- // vibe remove gpt / vibe remove gemini
2280
483
  if (positionalArgs[1] === 'gpt' || positionalArgs[1] === 'gemini') {
2281
484
  removeExternalLLM(positionalArgs[1]);
2282
485
  }
@@ -2285,7 +488,6 @@ switch (command) {
2285
488
  }
2286
489
  break;
2287
490
  case 'auth':
2288
- // vibe auth gpt / vibe auth gemini
2289
491
  if (positionalArgs[1] === 'gpt') {
2290
492
  const keyIndex = args.indexOf('--key');
2291
493
  if (keyIndex !== -1 && args[keyIndex + 1]) {
@@ -2309,7 +511,6 @@ switch (command) {
2309
511
  }
2310
512
  break;
2311
513
  case 'logout':
2312
- // vibe logout gpt / vibe logout gemini
2313
514
  if (positionalArgs[1] === 'gpt') {
2314
515
  gptLogout();
2315
516
  }
@@ -2321,7 +522,6 @@ switch (command) {
2321
522
  }
2322
523
  break;
2323
524
  case 'status':
2324
- // vibe status / vibe status gpt / vibe status gemini
2325
525
  if (positionalArgs[1] === 'gpt') {
2326
526
  gptStatus();
2327
527
  }
@@ -2344,20 +544,20 @@ switch (command) {
2344
544
  showHelp();
2345
545
  break;
2346
546
  default:
2347
- console.log(`
2348
- ❌ 알 수 없는 명령어: ${command}
2349
-
2350
- 사용 가능한 명령어:
2351
- vibe init 프로젝트 초기화
2352
- vibe update 설정 업데이트
2353
- vibe auth LLM 인증 (gpt, gemini)
2354
- vibe status 상태 확인
2355
- vibe logout 로그아웃
2356
- vibe remove 제거
2357
- vibe help 도움말
2358
- vibe version 버전 정보
2359
-
2360
- 사용법: vibe help
547
+ console.log(`
548
+ ❌ 알 수 없는 명령어: ${command}
549
+
550
+ 사용 가능한 명령어:
551
+ vibe init 프로젝트 초기화
552
+ vibe update 설정 업데이트
553
+ vibe auth LLM 인증 (gpt, gemini)
554
+ vibe status 상태 확인
555
+ vibe logout 로그아웃
556
+ vibe remove 제거
557
+ vibe help 도움말
558
+ vibe version 버전 정보
559
+
560
+ 사용법: vibe help
2361
561
  `);
2362
562
  process.exit(1);
2363
563
  }