@su-record/vibe 1.0.2 → 1.0.4

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 (2) hide show
  1. package/bin/vibe +214 -5
  2. package/package.json +1 -1
package/bin/vibe CHANGED
@@ -78,6 +78,118 @@ function copyDirRecursive(sourceDir, targetDir) {
78
78
  });
79
79
  }
80
80
 
81
+ // 기술 스택 감지 (루트 + 1레벨 하위 폴더)
82
+ function detectTechStacks(projectRoot) {
83
+ const stacks = [];
84
+
85
+ const detectInDir = (dir, prefix = '') => {
86
+ const detected = [];
87
+
88
+ // Node.js / TypeScript
89
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
90
+ try {
91
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
92
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
93
+
94
+ if (deps['next']) detected.push({ type: 'typescript-nextjs', path: prefix });
95
+ else if (deps['react-native']) detected.push({ type: 'typescript-react-native', path: prefix });
96
+ else if (deps['react']) detected.push({ type: 'typescript-react', path: prefix });
97
+ else if (deps['vue']) detected.push({ type: 'typescript-vue', path: prefix });
98
+ else if (deps['express'] || deps['fastify'] || deps['koa']) detected.push({ type: 'typescript-node', path: prefix });
99
+ else if (pkg.name) detected.push({ type: 'typescript-node', path: prefix });
100
+ } catch (e) {}
101
+ }
102
+
103
+ // Python
104
+ if (fs.existsSync(path.join(dir, 'pyproject.toml'))) {
105
+ try {
106
+ const content = fs.readFileSync(path.join(dir, 'pyproject.toml'), 'utf-8');
107
+ if (content.includes('fastapi')) detected.push({ type: 'python-fastapi', path: prefix });
108
+ else if (content.includes('django')) detected.push({ type: 'python-django', path: prefix });
109
+ else detected.push({ type: 'python', path: prefix });
110
+ } catch (e) {}
111
+ } else if (fs.existsSync(path.join(dir, 'requirements.txt'))) {
112
+ try {
113
+ const content = fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf-8');
114
+ if (content.includes('fastapi')) detected.push({ type: 'python-fastapi', path: prefix });
115
+ else if (content.includes('django')) detected.push({ type: 'python-django', path: prefix });
116
+ else detected.push({ type: 'python', path: prefix });
117
+ } catch (e) {}
118
+ }
119
+
120
+ // Flutter / Dart
121
+ if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
122
+ detected.push({ type: 'dart-flutter', path: prefix });
123
+ }
124
+
125
+ // Go
126
+ if (fs.existsSync(path.join(dir, 'go.mod'))) {
127
+ detected.push({ type: 'go', path: prefix });
128
+ }
129
+
130
+ // Rust
131
+ if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
132
+ detected.push({ type: 'rust', path: prefix });
133
+ }
134
+
135
+ // Java / Kotlin
136
+ if (fs.existsSync(path.join(dir, 'build.gradle')) || fs.existsSync(path.join(dir, 'build.gradle.kts'))) {
137
+ try {
138
+ const gradleFile = fs.existsSync(path.join(dir, 'build.gradle.kts'))
139
+ ? path.join(dir, 'build.gradle.kts')
140
+ : path.join(dir, 'build.gradle');
141
+ const content = fs.readFileSync(gradleFile, 'utf-8');
142
+ if (content.includes('com.android')) detected.push({ type: 'kotlin-android', path: prefix });
143
+ else if (content.includes('kotlin')) detected.push({ type: 'kotlin', path: prefix });
144
+ else if (content.includes('spring')) detected.push({ type: 'java-spring', path: prefix });
145
+ else detected.push({ type: 'java', path: prefix });
146
+ } catch (e) {}
147
+ } else if (fs.existsSync(path.join(dir, 'pom.xml'))) {
148
+ try {
149
+ const content = fs.readFileSync(path.join(dir, 'pom.xml'), 'utf-8');
150
+ if (content.includes('spring')) detected.push({ type: 'java-spring', path: prefix });
151
+ else detected.push({ type: 'java', path: prefix });
152
+ } catch (e) {}
153
+ }
154
+
155
+ // Swift / iOS
156
+ if (fs.existsSync(path.join(dir, 'Package.swift')) ||
157
+ fs.readdirSync(dir).some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
158
+ detected.push({ type: 'swift-ios', path: prefix });
159
+ }
160
+
161
+ return detected;
162
+ };
163
+
164
+ // 루트 디렉토리 검사
165
+ stacks.push(...detectInDir(projectRoot));
166
+
167
+ // 1레벨 하위 폴더 검사 (일반적인 모노레포 구조)
168
+ const subDirs = ['backend', 'frontend', 'server', 'client', 'api', 'web', 'mobile', 'app', 'packages', 'apps'];
169
+ for (const subDir of subDirs) {
170
+ const subPath = path.join(projectRoot, subDir);
171
+ if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
172
+ stacks.push(...detectInDir(subPath, subDir));
173
+ }
174
+ }
175
+
176
+ // packages/* 또는 apps/* 내부 검사 (monorepo)
177
+ for (const monoDir of ['packages', 'apps']) {
178
+ const monoPath = path.join(projectRoot, monoDir);
179
+ if (fs.existsSync(monoPath) && fs.statSync(monoPath).isDirectory()) {
180
+ const subPackages = fs.readdirSync(monoPath).filter(f => {
181
+ const fullPath = path.join(monoPath, f);
182
+ return fs.statSync(fullPath).isDirectory() && !f.startsWith('.');
183
+ });
184
+ for (const pkg of subPackages) {
185
+ stacks.push(...detectInDir(path.join(monoPath, pkg), `${monoDir}/${pkg}`));
186
+ }
187
+ }
188
+ }
189
+
190
+ return stacks;
191
+ }
192
+
81
193
  // 협업자 자동 설치 설정
82
194
  function setupCollaboratorAutoInstall(projectRoot) {
83
195
  const packageJsonPath = path.join(projectRoot, 'package.json');
@@ -240,16 +352,83 @@ async function init(projectName) {
240
352
  copyDirContents(sourceDir, commandsDir);
241
353
  log(' ✅ 슬래시 커맨드 설치 완료 (7개)\n');
242
354
 
243
- // 설정 파일 생성
355
+ // 기술 스택 감지
356
+ const detectedStacks = detectTechStacks(projectRoot);
357
+ if (detectedStacks.length > 0) {
358
+ log(` 🔍 감지된 기술 스택:\n`);
359
+ detectedStacks.forEach(s => {
360
+ log(` - ${s.type}${s.path ? ` (${s.path}/)` : ''}\n`);
361
+ });
362
+ }
363
+
364
+ // constitution.md 생성 (감지된 스택으로 플레이스홀더 업데이트)
244
365
  const templatePath = path.join(__dirname, '../templates/constitution-template.md');
245
366
  const constitutionPath = path.join(vibeDir, 'constitution.md');
246
367
  if (fs.existsSync(templatePath)) {
247
- fs.copyFileSync(templatePath, constitutionPath);
368
+ let constitution = fs.readFileSync(templatePath, 'utf-8');
369
+
370
+ // 기술 스택 정보 생성
371
+ const backendStack = detectedStacks.find(s =>
372
+ s.type.includes('python') || s.type.includes('node') ||
373
+ s.type.includes('go') || s.type.includes('java') || s.type.includes('rust')
374
+ );
375
+ const frontendStack = detectedStacks.find(s =>
376
+ s.type.includes('react') || s.type.includes('vue') ||
377
+ s.type.includes('flutter') || s.type.includes('swift') || s.type.includes('android')
378
+ );
379
+
380
+ // 스택 이름 매핑
381
+ const stackNames = {
382
+ 'python-fastapi': { lang: 'Python 3.11+', framework: 'FastAPI' },
383
+ 'python-django': { lang: 'Python 3.11+', framework: 'Django' },
384
+ 'python': { lang: 'Python 3.11+', framework: '-' },
385
+ 'typescript-node': { lang: 'TypeScript/Node.js', framework: 'Express/Fastify' },
386
+ 'typescript-nextjs': { lang: 'TypeScript', framework: 'Next.js' },
387
+ 'typescript-react': { lang: 'TypeScript', framework: 'React' },
388
+ 'typescript-vue': { lang: 'TypeScript', framework: 'Vue.js' },
389
+ 'typescript-react-native': { lang: 'TypeScript', framework: 'React Native' },
390
+ 'dart-flutter': { lang: 'Dart', framework: 'Flutter' },
391
+ 'go': { lang: 'Go', framework: '-' },
392
+ 'rust': { lang: 'Rust', framework: '-' },
393
+ 'java-spring': { lang: 'Java 17+', framework: 'Spring Boot' },
394
+ 'kotlin-android': { lang: 'Kotlin', framework: 'Android' },
395
+ 'swift-ios': { lang: 'Swift', framework: 'iOS/SwiftUI' }
396
+ };
397
+
398
+ // 플레이스홀더 업데이트
399
+ if (backendStack && stackNames[backendStack.type]) {
400
+ const info = stackNames[backendStack.type];
401
+ constitution = constitution.replace(
402
+ '- Language: {Python 3.11+ / Node.js / etc.}',
403
+ `- Language: ${info.lang}`
404
+ );
405
+ constitution = constitution.replace(
406
+ '- Framework: {FastAPI / Express / etc.}',
407
+ `- Framework: ${info.framework}`
408
+ );
409
+ }
410
+
411
+ if (frontendStack && stackNames[frontendStack.type]) {
412
+ const info = stackNames[frontendStack.type];
413
+ constitution = constitution.replace(
414
+ '- Framework: {Flutter / React / etc.}',
415
+ `- Framework: ${info.framework}`
416
+ );
417
+ }
418
+
419
+ // 날짜 업데이트
420
+ const today = new Date().toISOString().split('T')[0];
421
+ constitution = constitution.replace('{날짜}', today);
422
+ constitution = constitution.replace('{이름}', 'vibe init');
423
+ constitution = constitution.replace('토큰 유효 기간: {기간}', '토큰 유효 기간: 1시간');
424
+
425
+ fs.writeFileSync(constitutionPath, constitution);
248
426
  }
249
427
 
250
428
  const config = {
251
429
  language: 'ko',
252
- quality: { strict: true, autoVerify: true }
430
+ quality: { strict: true, autoVerify: true },
431
+ stacks: detectedStacks
253
432
  };
254
433
  fs.writeFileSync(path.join(vibeDir, 'config.json'), JSON.stringify(config, null, 2));
255
434
 
@@ -276,10 +455,40 @@ async function init(projectName) {
276
455
  log(' ✅ CLAUDE.md 생성\n');
277
456
  }
278
457
 
279
- // .vibe/rules/ 복사 (코딩 규칙)
458
+ // .vibe/rules/ 복사 (감지된 스택에 해당하는 언어 규칙만)
280
459
  const rulesSource = path.join(__dirname, '../.vibe/rules');
281
460
  const rulesTarget = path.join(vibeDir, 'rules');
282
- copyDirRecursive(rulesSource, rulesTarget);
461
+
462
+ // core, quality, standards, tools는 전체 복사
463
+ const coreDirs = ['core', 'quality', 'standards', 'tools'];
464
+ coreDirs.forEach(dir => {
465
+ const src = path.join(rulesSource, dir);
466
+ const dst = path.join(rulesTarget, dir);
467
+ if (fs.existsSync(src)) {
468
+ copyDirRecursive(src, dst);
469
+ }
470
+ });
471
+
472
+ // languages는 감지된 스택만 복사
473
+ const langSource = path.join(rulesSource, 'languages');
474
+ const langTarget = path.join(rulesTarget, 'languages');
475
+ ensureDir(langTarget);
476
+
477
+ const detectedTypes = detectedStacks.map(s => s.type);
478
+ if (fs.existsSync(langSource)) {
479
+ const langFiles = fs.readdirSync(langSource);
480
+ langFiles.forEach(file => {
481
+ const langType = file.replace('.md', '');
482
+ // 감지된 스택에 해당하는 파일만 복사
483
+ if (detectedTypes.includes(langType)) {
484
+ fs.copyFileSync(path.join(langSource, file), path.join(langTarget, file));
485
+ }
486
+ });
487
+ }
488
+
489
+ const copiedLangs = detectedTypes.filter(t =>
490
+ fs.existsSync(path.join(langTarget, `${t}.md`))
491
+ );
283
492
  log(' ✅ 코딩 규칙 설치 완료 (.vibe/rules/)\n');
284
493
 
285
494
  // .claude/agents/ 복사 (서브에이전트)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Vibe - Claude Code exclusive SPEC-driven AI coding framework",
5
5
  "bin": {
6
6
  "vibe": "./bin/vibe"