@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.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/cli/detect/matcher.d.ts +15 -0
- package/dist/cli/detect/matcher.d.ts.map +1 -0
- package/dist/cli/detect/matcher.js +278 -0
- package/dist/cli/detect/matcher.js.map +1 -0
- package/dist/cli/detect/signatures.d.ts +76 -0
- package/dist/cli/detect/signatures.d.ts.map +1 -0
- package/dist/cli/detect/signatures.js +175 -0
- package/dist/cli/detect/signatures.js.map +1 -0
- package/dist/cli/detect/workspace.d.ts +7 -0
- package/dist/cli/detect/workspace.d.ts.map +1 -0
- package/dist/cli/detect/workspace.js +112 -0
- package/dist/cli/detect/workspace.js.map +1 -0
- package/dist/cli/detect.characterization.test.d.ts +7 -0
- package/dist/cli/detect.characterization.test.d.ts.map +1 -0
- package/dist/cli/detect.characterization.test.js +294 -0
- package/dist/cli/detect.characterization.test.js.map +1 -0
- package/dist/cli/detect.d.ts.map +1 -1
- package/dist/cli/detect.js +64 -488
- package/dist/cli/detect.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +1 -1
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/ui-ux/CsvDataLoader.d.ts +10 -1
- package/dist/infra/lib/ui-ux/CsvDataLoader.d.ts.map +1 -1
- package/dist/infra/lib/ui-ux/CsvDataLoader.js +11 -5
- package/dist/infra/lib/ui-ux/CsvDataLoader.js.map +1 -1
- package/dist/infra/lib/ui-ux/CsvDataLoader.test.js +8 -8
- package/dist/infra/lib/ui-ux/CsvDataLoader.test.js.map +1 -1
- package/dist/infra/lib/ui-ux/SearchService.test.js +1 -1
- package/dist/infra/lib/ui-ux/SearchService.test.js.map +1 -1
- package/hooks/scripts/__tests__/.vibe/command-log.txt +18 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/package.json +3 -2
package/dist/cli/detect.js
CHANGED
|
@@ -3,510 +3,87 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 = {
|
|
127
|
-
|
|
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
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
472
|
-
|
|
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
|
-
|
|
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.
|
|
506
|
-
details.
|
|
507
|
-
|
|
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
|
* 스택에 맞는 언어 규칙 내용 반환
|