@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.
- package/bin/vibe +214 -5
- 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.
|
|
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
|
-
|
|
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/ 복사 (서브에이전트)
|