@su-record/vibe 2.14.0 → 2.14.1

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 (35) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/cli/detect/matcher.d.ts +15 -0
  4. package/dist/cli/detect/matcher.d.ts.map +1 -0
  5. package/dist/cli/detect/matcher.js +278 -0
  6. package/dist/cli/detect/matcher.js.map +1 -0
  7. package/dist/cli/detect/signatures.d.ts +76 -0
  8. package/dist/cli/detect/signatures.d.ts.map +1 -0
  9. package/dist/cli/detect/signatures.js +175 -0
  10. package/dist/cli/detect/signatures.js.map +1 -0
  11. package/dist/cli/detect/workspace.d.ts +7 -0
  12. package/dist/cli/detect/workspace.d.ts.map +1 -0
  13. package/dist/cli/detect/workspace.js +112 -0
  14. package/dist/cli/detect/workspace.js.map +1 -0
  15. package/dist/cli/detect.characterization.test.d.ts +7 -0
  16. package/dist/cli/detect.characterization.test.d.ts.map +1 -0
  17. package/dist/cli/detect.characterization.test.js +294 -0
  18. package/dist/cli/detect.characterization.test.js.map +1 -0
  19. package/dist/cli/detect.d.ts.map +1 -1
  20. package/dist/cli/detect.js +64 -488
  21. package/dist/cli/detect.js.map +1 -1
  22. package/dist/cli/setup/ProjectSetup.js +1 -1
  23. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  24. package/dist/infra/lib/ui-ux/CsvDataLoader.d.ts +10 -1
  25. package/dist/infra/lib/ui-ux/CsvDataLoader.d.ts.map +1 -1
  26. package/dist/infra/lib/ui-ux/CsvDataLoader.js +11 -5
  27. package/dist/infra/lib/ui-ux/CsvDataLoader.js.map +1 -1
  28. package/dist/infra/lib/ui-ux/CsvDataLoader.test.js +8 -8
  29. package/dist/infra/lib/ui-ux/CsvDataLoader.test.js.map +1 -1
  30. package/dist/infra/lib/ui-ux/SearchService.test.js +1 -1
  31. package/dist/infra/lib/ui-ux/SearchService.test.js.map +1 -1
  32. package/hooks/scripts/__tests__/.vibe/command-log.txt +18 -0
  33. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  34. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  35. package/package.json +3 -2
@@ -3,510 +3,87 @@
3
3
  */
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
- /**
7
- * 모노레포 워크스페이스 경로 감지
8
- * 지원: pnpm-workspace.yaml, package.json workspaces, lerna.json, nx.json, turbo.json
9
- */
10
- function detectWorkspacePaths(projectRoot) {
11
- const workspacePaths = new Set();
12
- // 1. pnpm-workspace.yaml
13
- const pnpmWorkspacePath = path.join(projectRoot, 'pnpm-workspace.yaml');
14
- if (fs.existsSync(pnpmWorkspacePath)) {
15
- try {
16
- const content = fs.readFileSync(pnpmWorkspacePath, 'utf-8');
17
- // 간단한 YAML 파싱 (packages: 배열)
18
- const packagesMatch = content.match(/packages:\s*\n((?:\s*-\s*.+\n?)+)/);
19
- if (packagesMatch) {
20
- const lines = packagesMatch[1].split('\n');
21
- for (const line of lines) {
22
- const match = line.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?\s*$/);
23
- if (match) {
24
- const pattern = match[1].trim();
25
- // glob 패턴 확장 (예: packages/*)
26
- expandGlobPattern(projectRoot, pattern, workspacePaths);
27
- }
28
- }
29
- }
6
+ import { detectInDir, detectHosting, detectCicd } from './detect/matcher.js';
7
+ import { detectWorkspacePaths } from './detect/workspace.js';
8
+ // ── conventional sub-directory names ──────────────────────────────────────
9
+ const CONVENTIONAL_SUBDIRS = ['backend', 'frontend', 'server', 'client', 'api', 'web', 'mobile', 'app'];
10
+ const MONOREPO_FALLBACK_DIRS = ['packages', 'apps', 'libs'];
11
+ // ── helpers ────────────────────────────────────────────────────────────────
12
+ function dedupeDetails(details) {
13
+ details.databases = [...new Set(details.databases)];
14
+ details.stateManagement = [...new Set(details.stateManagement)];
15
+ details.hosting = [...new Set(details.hosting)];
16
+ details.cicd = [...new Set(details.cicd)];
17
+ details.capabilities = [...new Set(details.capabilities)];
18
+ }
19
+ function scanDir(dir, prefix, details, stacks) {
20
+ stacks.push(...detectInDir(dir, prefix, details));
21
+ }
22
+ function scanConventionalSubdirs(projectRoot, details, stacks) {
23
+ for (const sub of CONVENTIONAL_SUBDIRS) {
24
+ const full = path.join(projectRoot, sub);
25
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
26
+ scanDir(full, sub, details, stacks);
30
27
  }
31
- catch { /* ignore */ }
32
28
  }
33
- // 2. package.json workspaces
34
- const packageJsonPath = path.join(projectRoot, 'package.json');
35
- if (fs.existsSync(packageJsonPath)) {
36
- try {
37
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
38
- const workspaces = pkg.workspaces;
39
- if (Array.isArray(workspaces)) {
40
- for (const pattern of workspaces) {
41
- expandGlobPattern(projectRoot, pattern, workspacePaths);
42
- }
43
- }
44
- else if (workspaces?.packages && Array.isArray(workspaces.packages)) {
45
- // yarn workspaces 형식: { packages: [...] }
46
- for (const pattern of workspaces.packages) {
47
- expandGlobPattern(projectRoot, pattern, workspacePaths);
48
- }
49
- }
29
+ }
30
+ function scanWorkspacePaths(projectRoot, workspacePaths, scanned, details, stacks) {
31
+ for (const ws of workspacePaths) {
32
+ if (scanned.has(ws))
33
+ continue;
34
+ scanned.add(ws);
35
+ const full = path.join(projectRoot, ws);
36
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
37
+ scanDir(full, ws, details, stacks);
50
38
  }
51
- catch { /* ignore */ }
52
39
  }
53
- // 3. lerna.json
54
- const lernaPath = path.join(projectRoot, 'lerna.json');
55
- if (fs.existsSync(lernaPath)) {
40
+ }
41
+ function scanMonorepoFallback(projectRoot, scanned, details, stacks) {
42
+ for (const monoDir of MONOREPO_FALLBACK_DIRS) {
43
+ const monoPath = path.join(projectRoot, monoDir);
44
+ if (!fs.existsSync(monoPath) || !fs.statSync(monoPath).isDirectory())
45
+ continue;
56
46
  try {
57
- const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf-8'));
58
- const packages = lerna.packages || ['packages/*'];
59
- for (const pattern of packages) {
60
- expandGlobPattern(projectRoot, pattern, workspacePaths);
47
+ const entries = fs.readdirSync(monoPath).filter(f => {
48
+ const fp = path.join(monoPath, f);
49
+ return fs.statSync(fp).isDirectory() && !f.startsWith('.');
50
+ });
51
+ for (const entry of entries) {
52
+ const rel = `${monoDir}/${entry}`;
53
+ if (scanned.has(rel))
54
+ continue;
55
+ scanned.add(rel);
56
+ scanDir(path.join(monoPath, entry), rel, details, stacks);
61
57
  }
62
58
  }
63
59
  catch { /* ignore */ }
64
60
  }
65
- // 4. nx.json (projects 경로 확인)
66
- const nxPath = path.join(projectRoot, 'nx.json');
67
- if (fs.existsSync(nxPath)) {
68
- // nx는 보통 apps/, libs/, packages/ 구조
69
- for (const dir of ['apps', 'libs', 'packages']) {
70
- expandGlobPattern(projectRoot, `${dir}/*`, workspacePaths);
71
- }
72
- }
73
- // 5. turbo.json (pipeline이 있으면 모노레포)
74
- const turboPath = path.join(projectRoot, 'turbo.json');
75
- if (fs.existsSync(turboPath)) {
76
- // turbo는 package.json workspaces를 따르므로 추가 처리 불필요
77
- // 기본 폴더 검사
78
- for (const dir of ['apps', 'packages']) {
79
- expandGlobPattern(projectRoot, `${dir}/*`, workspacePaths);
80
- }
81
- }
82
- return [...workspacePaths];
83
- }
84
- /**
85
- * glob 패턴을 실제 디렉토리 경로로 확장
86
- */
87
- function expandGlobPattern(projectRoot, pattern, paths) {
88
- // 패턴 정규화
89
- const cleanPattern = pattern.replace(/['"]/g, '').trim();
90
- // ! 로 시작하면 제외 패턴 - 무시
91
- if (cleanPattern.startsWith('!'))
92
- return;
93
- // * 없으면 직접 경로
94
- if (!cleanPattern.includes('*')) {
95
- const fullPath = path.join(projectRoot, cleanPattern);
96
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
97
- paths.add(cleanPattern);
98
- }
99
- return;
100
- }
101
- // 단일 레벨 glob 처리 (예: packages/*, apps/*)
102
- if (cleanPattern.endsWith('/*') || cleanPattern.endsWith('/**/')) {
103
- const baseDir = cleanPattern.replace(/\/\*+\/?$/, '');
104
- const basePath = path.join(projectRoot, baseDir);
105
- if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
106
- try {
107
- const entries = fs.readdirSync(basePath);
108
- for (const entry of entries) {
109
- if (entry.startsWith('.'))
110
- continue;
111
- const entryPath = path.join(basePath, entry);
112
- if (fs.statSync(entryPath).isDirectory()) {
113
- paths.add(`${baseDir}/${entry}`);
114
- }
115
- }
116
- }
117
- catch { /* ignore */ }
118
- }
119
- }
120
61
  }
62
+ // ── public API ─────────────────────────────────────────────────────────────
121
63
  /**
122
64
  * 프로젝트 기술 스택 감지
123
65
  */
124
66
  export function detectTechStacks(projectRoot) {
125
67
  const stacks = [];
126
- const details = { databases: [], stateManagement: [], hosting: [], cicd: [], capabilities: [] };
127
- const detectInDir = (dir, prefix = '') => {
128
- const detected = [];
129
- // Node.js / TypeScript
130
- if (fs.existsSync(path.join(dir, 'package.json'))) {
131
- try {
132
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
133
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
134
- // 프레임워크 감지 (우선순위: 특수 프레임워크 → 범용 프레임워크)
135
- // Desktop/Mobile 프레임워크 (최우선)
136
- if (deps['@tauri-apps/cli'] || deps['@tauri-apps/api']) {
137
- detected.push({ type: 'typescript-tauri', path: prefix });
138
- }
139
- else if (deps['electron']) {
140
- detected.push({ type: 'typescript-electron', path: prefix });
141
- }
142
- else if (deps['react-native']) {
143
- detected.push({ type: 'typescript-react-native', path: prefix });
144
- }
145
- // 풀스택/SSR 프레임워크
146
- else if (deps['next']) {
147
- detected.push({ type: 'typescript-nextjs', path: prefix });
148
- }
149
- else if (deps['nuxt'] || deps['nuxt3']) {
150
- detected.push({ type: 'typescript-nuxt', path: prefix });
151
- }
152
- else if (deps['astro']) {
153
- detected.push({ type: 'typescript-astro', path: prefix });
154
- }
155
- // 프론트엔드 프레임워크
156
- else if (deps['@angular/core']) {
157
- detected.push({ type: 'typescript-angular', path: prefix });
158
- }
159
- else if (deps['svelte']) {
160
- detected.push({ type: 'typescript-svelte', path: prefix });
161
- }
162
- else if (deps['vue']) {
163
- detected.push({ type: 'typescript-vue', path: prefix });
164
- }
165
- else if (deps['react']) {
166
- detected.push({ type: 'typescript-react', path: prefix });
167
- }
168
- // 백엔드 프레임워크
169
- else if (deps['@nestjs/core']) {
170
- detected.push({ type: 'typescript-nestjs', path: prefix });
171
- }
172
- else if (deps['express'] || deps['fastify'] || deps['koa'] || deps['hono']) {
173
- detected.push({ type: 'typescript-node', path: prefix });
174
- }
175
- // 기본 Node.js
176
- else if (pkg.name) {
177
- detected.push({ type: 'typescript-node', path: prefix });
178
- }
179
- // DB 감지
180
- if (deps['pg'] || deps['postgres'] || deps['@prisma/client'])
181
- details.databases.push('PostgreSQL');
182
- if (deps['mysql'] || deps['mysql2'])
183
- details.databases.push('MySQL');
184
- if (deps['mongodb'] || deps['mongoose'])
185
- details.databases.push('MongoDB');
186
- if (deps['redis'] || deps['ioredis'])
187
- details.databases.push('Redis');
188
- if (deps['sqlite3'] || deps['better-sqlite3'])
189
- details.databases.push('SQLite');
190
- if (deps['typeorm'])
191
- details.databases.push('TypeORM');
192
- if (deps['prisma'] || deps['@prisma/client'])
193
- details.databases.push('Prisma');
194
- if (deps['drizzle-orm'])
195
- details.databases.push('Drizzle');
196
- if (deps['sequelize'])
197
- details.databases.push('Sequelize');
198
- // 상태관리 감지
199
- if (deps['redux'] || deps['@reduxjs/toolkit'])
200
- details.stateManagement.push('Redux');
201
- if (deps['zustand'])
202
- details.stateManagement.push('Zustand');
203
- if (deps['jotai'])
204
- details.stateManagement.push('Jotai');
205
- if (deps['recoil'])
206
- details.stateManagement.push('Recoil');
207
- if (deps['mobx'])
208
- details.stateManagement.push('MobX');
209
- if (deps['@tanstack/react-query'] || deps['react-query'])
210
- details.stateManagement.push('React Query');
211
- if (deps['swr'])
212
- details.stateManagement.push('SWR');
213
- if (deps['pinia'])
214
- details.stateManagement.push('Pinia');
215
- if (deps['vuex'])
216
- details.stateManagement.push('Vuex');
217
- // Capability 감지: Commerce
218
- if (deps['stripe'] || deps['@stripe/stripe-js'] || deps['@stripe/react-stripe-js']
219
- || deps['@shopify/shopify-api'] || deps['shopify-api-node']
220
- || deps['@medusajs/medusa'] || deps['@paypal/checkout-server-sdk']
221
- || deps['toss-payments'] || deps['iamport-rest-client']) {
222
- details.capabilities.push('commerce');
223
- }
224
- // Capability 감지: Video
225
- if (deps['fluent-ffmpeg'] || deps['@ffmpeg/ffmpeg'] || deps['ffmpeg-static']
226
- || deps['remotion'] || deps['video.js'] || deps['@mux/mux-node']) {
227
- details.capabilities.push('video');
228
- }
229
- // Capability 감지: Event Automation
230
- if (deps['@notionhq/client'] || deps['aligo-smartsms'] || deps['nodemailer']
231
- || deps['python-pptx'] || deps['google-generativeai']) {
232
- // Check for event-specific directory structures
233
- if (fs.existsSync(path.join(dir, 'agents')) || fs.existsSync(path.join(dir, 'schedules'))
234
- || fs.existsSync(path.join(dir, '.event_state.json')) || fs.existsSync(path.join(dir, 'prompts'))) {
235
- details.capabilities.push('event-automation');
236
- }
237
- }
238
- }
239
- catch { /* ignore: optional operation */ }
240
- }
241
- // Python
242
- if (fs.existsSync(path.join(dir, 'pyproject.toml'))) {
243
- try {
244
- const content = fs.readFileSync(path.join(dir, 'pyproject.toml'), 'utf-8');
245
- if (content.includes('fastapi'))
246
- detected.push({ type: 'python-fastapi', path: prefix });
247
- else if (content.includes('django'))
248
- detected.push({ type: 'python-django', path: prefix });
249
- else
250
- detected.push({ type: 'python', path: prefix });
251
- if (content.includes('psycopg') || content.includes('asyncpg'))
252
- details.databases.push('PostgreSQL');
253
- if (content.includes('pymongo'))
254
- details.databases.push('MongoDB');
255
- if (content.includes('sqlalchemy'))
256
- details.databases.push('SQLAlchemy');
257
- if (content.includes('prisma'))
258
- details.databases.push('Prisma');
259
- // Python capability 감지
260
- if (content.includes('stripe') || content.includes('shopify') || content.includes('saleor')) {
261
- details.capabilities.push('commerce');
262
- }
263
- if (content.includes('moviepy') || content.includes('ffmpeg') || content.includes('opencv') || content.includes('vidgear')) {
264
- details.capabilities.push('video');
265
- }
266
- if (content.includes('notion-client') || content.includes('python-pptx') || content.includes('google-generativeai')
267
- || content.includes('aligo')) {
268
- if (fs.existsSync(path.join(dir, 'agents')) || fs.existsSync(path.join(dir, 'schedules'))
269
- || fs.existsSync(path.join(dir, '.event_state.json')) || fs.existsSync(path.join(dir, 'prompts'))) {
270
- details.capabilities.push('event-automation');
271
- }
272
- }
273
- }
274
- catch { /* ignore: optional operation */ }
275
- }
276
- else if (fs.existsSync(path.join(dir, 'requirements.txt'))) {
277
- try {
278
- const content = fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf-8');
279
- if (content.includes('fastapi'))
280
- detected.push({ type: 'python-fastapi', path: prefix });
281
- else if (content.includes('django'))
282
- detected.push({ type: 'python-django', path: prefix });
283
- else
284
- detected.push({ type: 'python', path: prefix });
285
- if (content.includes('psycopg') || content.includes('asyncpg'))
286
- details.databases.push('PostgreSQL');
287
- if (content.includes('pymongo'))
288
- details.databases.push('MongoDB');
289
- if (content.includes('sqlalchemy'))
290
- details.databases.push('SQLAlchemy');
291
- // Python capability 감지
292
- if (content.includes('stripe') || content.includes('shopify') || content.includes('saleor')) {
293
- details.capabilities.push('commerce');
294
- }
295
- if (content.includes('moviepy') || content.includes('ffmpeg') || content.includes('opencv') || content.includes('vidgear')) {
296
- details.capabilities.push('video');
297
- }
298
- if (content.includes('notion-client') || content.includes('python-pptx') || content.includes('google-generativeai')
299
- || content.includes('aligo')) {
300
- if (fs.existsSync(path.join(dir, 'agents')) || fs.existsSync(path.join(dir, 'schedules'))
301
- || fs.existsSync(path.join(dir, '.event_state.json')) || fs.existsSync(path.join(dir, 'prompts'))) {
302
- details.capabilities.push('event-automation');
303
- }
304
- }
305
- }
306
- catch { /* ignore: optional operation */ }
307
- }
308
- // Flutter / Dart
309
- if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
310
- detected.push({ type: 'dart-flutter', path: prefix });
311
- try {
312
- const content = fs.readFileSync(path.join(dir, 'pubspec.yaml'), 'utf-8');
313
- if (content.includes('flutter_riverpod') || content.includes('riverpod'))
314
- details.stateManagement.push('Riverpod');
315
- else if (content.includes('provider'))
316
- details.stateManagement.push('Provider');
317
- if (content.includes('bloc'))
318
- details.stateManagement.push('BLoC');
319
- if (content.includes('getx') || content.includes('get:'))
320
- details.stateManagement.push('GetX');
321
- }
322
- catch { /* ignore: optional operation */ }
323
- }
324
- // Go
325
- if (fs.existsSync(path.join(dir, 'go.mod'))) {
326
- detected.push({ type: 'go', path: prefix });
327
- try {
328
- const content = fs.readFileSync(path.join(dir, 'go.mod'), 'utf-8');
329
- if (content.includes('pgx') || content.includes('pq'))
330
- details.databases.push('PostgreSQL');
331
- if (content.includes('go-redis'))
332
- details.databases.push('Redis');
333
- if (content.includes('mongo-driver'))
334
- details.databases.push('MongoDB');
335
- }
336
- catch { /* ignore: optional operation */ }
337
- }
338
- // Rust
339
- if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
340
- detected.push({ type: 'rust', path: prefix });
341
- try {
342
- const content = fs.readFileSync(path.join(dir, 'Cargo.toml'), 'utf-8');
343
- if (content.includes('sqlx') || content.includes('diesel'))
344
- details.databases.push('PostgreSQL');
345
- if (content.includes('mongodb'))
346
- details.databases.push('MongoDB');
347
- }
348
- catch { /* ignore: optional operation */ }
349
- }
350
- // Java / Kotlin
351
- if (fs.existsSync(path.join(dir, 'build.gradle')) || fs.existsSync(path.join(dir, 'build.gradle.kts'))) {
352
- try {
353
- const gradleFile = fs.existsSync(path.join(dir, 'build.gradle.kts'))
354
- ? path.join(dir, 'build.gradle.kts')
355
- : path.join(dir, 'build.gradle');
356
- const content = fs.readFileSync(gradleFile, 'utf-8');
357
- if (content.includes('com.android'))
358
- detected.push({ type: 'kotlin-android', path: prefix });
359
- else if (content.includes('kotlin'))
360
- detected.push({ type: 'kotlin', path: prefix });
361
- else if (content.includes('spring'))
362
- detected.push({ type: 'java-spring', path: prefix });
363
- else
364
- detected.push({ type: 'java', path: prefix });
365
- if (content.includes('postgresql'))
366
- details.databases.push('PostgreSQL');
367
- if (content.includes('mysql'))
368
- details.databases.push('MySQL');
369
- if (content.includes('jpa') || content.includes('hibernate'))
370
- details.databases.push('JPA/Hibernate');
371
- }
372
- catch { /* ignore: optional operation */ }
373
- }
374
- else if (fs.existsSync(path.join(dir, 'pom.xml'))) {
375
- try {
376
- const content = fs.readFileSync(path.join(dir, 'pom.xml'), 'utf-8');
377
- if (content.includes('spring'))
378
- detected.push({ type: 'java-spring', path: prefix });
379
- else
380
- detected.push({ type: 'java', path: prefix });
381
- if (content.includes('postgresql'))
382
- details.databases.push('PostgreSQL');
383
- if (content.includes('mysql'))
384
- details.databases.push('MySQL');
385
- }
386
- catch { /* ignore: optional operation */ }
387
- }
388
- // Swift / iOS
389
- if (fs.existsSync(path.join(dir, 'Package.swift')) ||
390
- fs.readdirSync(dir).some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
391
- detected.push({ type: 'swift-ios', path: prefix });
392
- }
393
- // Ruby / Rails
394
- if (fs.existsSync(path.join(dir, 'Gemfile'))) {
395
- try {
396
- const content = fs.readFileSync(path.join(dir, 'Gemfile'), 'utf-8');
397
- if (content.includes('rails')) {
398
- detected.push({ type: 'ruby-rails', path: prefix });
399
- }
400
- if (content.includes('pg'))
401
- details.databases.push('PostgreSQL');
402
- if (content.includes('mysql2'))
403
- details.databases.push('MySQL');
404
- if (content.includes('sqlite3'))
405
- details.databases.push('SQLite');
406
- }
407
- catch { /* ignore: optional operation */ }
408
- }
409
- // C# / Unity
410
- if (fs.readdirSync(dir).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
411
- // Unity 프로젝트 판별: ProjectSettings/ProjectVersion.txt 존재 여부
412
- if (fs.existsSync(path.join(dir, 'ProjectSettings', 'ProjectVersion.txt')) ||
413
- fs.existsSync(path.join(dir, 'Assets'))) {
414
- detected.push({ type: 'csharp-unity', path: prefix });
415
- }
416
- }
417
- // GDScript / Godot
418
- if (fs.existsSync(path.join(dir, 'project.godot')) ||
419
- fs.readdirSync(dir).some(f => f.endsWith('.gd'))) {
420
- detected.push({ type: 'gdscript-godot', path: prefix });
421
- }
422
- return detected;
68
+ const details = {
69
+ databases: [], stateManagement: [], hosting: [], cicd: [], capabilities: [],
423
70
  };
424
- // CI/CD 감지
425
- if (fs.existsSync(path.join(projectRoot, '.github', 'workflows'))) {
426
- details.cicd.push('GitHub Actions');
427
- }
428
- if (fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml'))) {
429
- details.cicd.push('GitLab CI');
430
- }
431
- if (fs.existsSync(path.join(projectRoot, 'Jenkinsfile'))) {
432
- details.cicd.push('Jenkins');
433
- }
434
- if (fs.existsSync(path.join(projectRoot, '.circleci'))) {
435
- details.cicd.push('CircleCI');
436
- }
437
- // Hosting 감지
438
- if (fs.existsSync(path.join(projectRoot, 'vercel.json')) ||
439
- fs.existsSync(path.join(projectRoot, '.vercel'))) {
440
- details.hosting.push('Vercel');
441
- }
442
- if (fs.existsSync(path.join(projectRoot, 'netlify.toml'))) {
443
- details.hosting.push('Netlify');
444
- }
445
- if (fs.existsSync(path.join(projectRoot, 'app.yaml')) ||
446
- fs.existsSync(path.join(projectRoot, 'cloudbuild.yaml'))) {
447
- details.hosting.push('Google Cloud');
448
- }
449
- if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
450
- fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
451
- details.hosting.push('Docker');
452
- }
453
- if (fs.existsSync(path.join(projectRoot, 'fly.toml'))) {
454
- details.hosting.push('Fly.io');
455
- }
456
- if (fs.existsSync(path.join(projectRoot, 'railway.json'))) {
457
- details.hosting.push('Railway');
458
- }
459
- // 루트 디렉토리 검사
460
- stacks.push(...detectInDir(projectRoot));
461
- // 1레벨 하위 폴더 검사 (전통적인 폴더 구조)
462
- const subDirs = ['backend', 'frontend', 'server', 'client', 'api', 'web', 'mobile', 'app'];
463
- for (const subDir of subDirs) {
464
- const subPath = path.join(projectRoot, subDir);
465
- if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
466
- stacks.push(...detectInDir(subPath, subDir));
467
- }
468
- }
469
- // 모노레포 워크스페이스 감지 및 검사
71
+ // Root
72
+ scanDir(projectRoot, '', details, stacks);
73
+ // Conventional sub-directories (backend/, frontend/, …)
74
+ scanConventionalSubdirs(projectRoot, details, stacks);
75
+ // Monorepo workspace paths
470
76
  const workspacePaths = detectWorkspacePaths(projectRoot);
471
- const scannedPaths = new Set();
472
- for (const workspacePath of workspacePaths) {
473
- // 이미 검사한 경로는 건너뛰기
474
- if (scannedPaths.has(workspacePath))
475
- continue;
476
- scannedPaths.add(workspacePath);
477
- const fullPath = path.join(projectRoot, workspacePath);
478
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
479
- stacks.push(...detectInDir(fullPath, workspacePath));
480
- }
481
- }
482
- // 워크스페이스 설정이 없으면 기본 폴더 검사 (fallback)
77
+ const scanned = new Set();
78
+ scanWorkspacePaths(projectRoot, workspacePaths, scanned, details, stacks);
79
+ // Fallback: scan packages/, apps/, libs/ if no workspace config found
483
80
  if (workspacePaths.length === 0) {
484
- for (const monoDir of ['packages', 'apps', 'libs']) {
485
- const monoPath = path.join(projectRoot, monoDir);
486
- if (fs.existsSync(monoPath) && fs.statSync(monoPath).isDirectory()) {
487
- try {
488
- const subPackages = fs.readdirSync(monoPath).filter(f => {
489
- const fullPath = path.join(monoPath, f);
490
- return fs.statSync(fullPath).isDirectory() && !f.startsWith('.');
491
- });
492
- for (const pkg of subPackages) {
493
- const pkgPath = `${monoDir}/${pkg}`;
494
- if (!scannedPaths.has(pkgPath)) {
495
- scannedPaths.add(pkgPath);
496
- stacks.push(...detectInDir(path.join(monoPath, pkg), pkgPath));
497
- }
498
- }
499
- }
500
- catch { /* ignore */ }
501
- }
502
- }
81
+ scanMonorepoFallback(projectRoot, scanned, details, stacks);
503
82
  }
504
- // 중복 제거
505
- details.databases = [...new Set(details.databases)];
506
- details.stateManagement = [...new Set(details.stateManagement)];
507
- details.hosting = [...new Set(details.hosting)];
508
- details.cicd = [...new Set(details.cicd)];
509
- details.capabilities = [...new Set(details.capabilities)];
83
+ // Project-root-level details (hosting, CI/CD)
84
+ details.hosting.push(...detectHosting(projectRoot));
85
+ details.cicd.push(...detectCicd(projectRoot));
86
+ dedupeDetails(details);
510
87
  return { stacks, details };
511
88
  }
512
89
  /**
@@ -552,9 +129,8 @@ export const STACK_NAMES = {
552
129
  export function getLanguageRulesForStacks(stacks) {
553
130
  const ruleFiles = [];
554
131
  for (const stack of stacks) {
555
- if (STACK_NAMES[stack.type]) {
132
+ if (STACK_NAMES[stack.type])
556
133
  ruleFiles.push(`${stack.type}.md`);
557
- }
558
134
  }
559
135
  return [...new Set(ruleFiles)].join(', ');
560
136
  }
@@ -601,7 +177,7 @@ export const LANGUAGE_RULES = {
601
177
  - No force unwrapping → use guard let / if let
602
178
  - Prefer protocol-oriented programming
603
179
  - Prefer value types (struct)
604
- - Watch memory management with @escaping closures`
180
+ - Watch memory management with @escaping closures`,
605
181
  };
606
182
  /**
607
183
  * 스택에 맞는 언어 규칙 내용 반환