byuckchon-frontend-cli 1.0.3 → 1.4.0
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.md +186 -8
- package/bin/index.js +157 -1
- package/package.json +32 -6
- package/src/ai/messageContent.js +84 -0
- package/src/ai/models.js +66 -0
- package/src/ai/provider.js +54 -0
- package/src/ai/systemPrompt.js +75 -0
- package/src/ai/tokenMeter.js +65 -0
- package/src/commands/adopt.js +146 -0
- package/src/commands/chat.js +300 -0
- package/src/commands/config.js +142 -0
- package/src/commands/genCmd.js +61 -0
- package/src/commands/indexCmd.js +89 -0
- package/src/commands/init.js +36 -13
- package/src/config/index.js +185 -0
- package/src/context/detect.js +228 -0
- package/src/generators/createBaseFiles.js +211 -190
- package/src/generators/createBcConfig.js +58 -0
- package/src/generators/createPackageJson.js +1 -1
- package/src/generators/createProject.js +2 -0
- package/src/history/store.js +117 -0
- package/src/indexer/chunker.js +42 -0
- package/src/indexer/embed.js +57 -0
- package/src/indexer/search.js +41 -0
- package/src/indexer/store.js +137 -0
- package/src/indexer/walker.js +45 -0
- package/src/openapi/cache.js +60 -0
- package/src/openapi/codegen.js +45 -0
- package/src/openapi/fetch.js +69 -0
- package/src/openapi/summary.js +75 -0
- package/src/prompts/initPrompts.js +24 -0
- package/src/ui/ChatApp.js +812 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 프로젝트 디렉터리를 분석해서 "AI 가 알아야 할 사실들" 을 뽑아낸다.
|
|
6
|
+
*
|
|
7
|
+
* 입력: projectDir (절대 경로)
|
|
8
|
+
* 출력:
|
|
9
|
+
* {
|
|
10
|
+
* framework: 'next' | 'expo' | 'electron' | 'vite-react' | 'cra' | 'react' | 'remix' | 'unknown',
|
|
11
|
+
* language: 'ts' | 'js',
|
|
12
|
+
* styling: { tailwind, cssModules, styledComponents, emotion, vanillaExtract },
|
|
13
|
+
* packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun' | 'unknown',
|
|
14
|
+
* routing: 'app-router' | 'pages-router' | 'expo-router' | 'react-router' | null,
|
|
15
|
+
* componentDirs: string[], // 후보 컴포넌트 폴더 (있는 것만)
|
|
16
|
+
* designTokensFiles: string[], // tailwind.config / theme 토큰 파일들
|
|
17
|
+
* hasStorybook: boolean,
|
|
18
|
+
* hasTests: 'jest' | 'vitest' | 'playwright' | null,
|
|
19
|
+
* pkg: { name, version, dependencies, devDependencies },
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* 모든 값은 best-effort. 못 찾으면 보수적으로 unknown / null / [] 로 채운다.
|
|
23
|
+
*/
|
|
24
|
+
export async function detectProjectContext(projectDir = process.cwd()) {
|
|
25
|
+
const root = path.resolve(projectDir);
|
|
26
|
+
|
|
27
|
+
const pkg = await readJsonSafe(path.join(root, 'package.json'));
|
|
28
|
+
if (!pkg) {
|
|
29
|
+
return {
|
|
30
|
+
framework: 'unknown',
|
|
31
|
+
language: 'js',
|
|
32
|
+
styling: emptyStyling(),
|
|
33
|
+
packageManager: await detectPackageManager(root),
|
|
34
|
+
routing: null,
|
|
35
|
+
componentDirs: [],
|
|
36
|
+
designTokensFiles: [],
|
|
37
|
+
hasStorybook: false,
|
|
38
|
+
hasTests: null,
|
|
39
|
+
pkg: null,
|
|
40
|
+
isProject: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
45
|
+
const has = (name) => Object.prototype.hasOwnProperty.call(deps, name);
|
|
46
|
+
|
|
47
|
+
// ── 프레임워크 ─────────────────────────────────────────────
|
|
48
|
+
let framework = 'unknown';
|
|
49
|
+
if (has('expo') || has('expo-router')) framework = 'expo';
|
|
50
|
+
else if (has('electron')) framework = 'electron';
|
|
51
|
+
else if (has('next')) framework = 'next';
|
|
52
|
+
else if (has('@remix-run/react') || has('@remix-run/dev')) framework = 'remix';
|
|
53
|
+
else if (has('vite') && has('react')) framework = 'vite-react';
|
|
54
|
+
else if (has('react-scripts')) framework = 'cra';
|
|
55
|
+
else if (has('react')) framework = 'react';
|
|
56
|
+
|
|
57
|
+
// ── 언어 ────────────────────────────────────────────────
|
|
58
|
+
const hasTsConfig = await exists(path.join(root, 'tsconfig.json'));
|
|
59
|
+
const language = hasTsConfig || has('typescript') ? 'ts' : 'js';
|
|
60
|
+
|
|
61
|
+
// ── 스타일 ─────────────────────────────────────────────
|
|
62
|
+
const styling = {
|
|
63
|
+
tailwind:
|
|
64
|
+
has('tailwindcss') ||
|
|
65
|
+
(await anyExists(root, [
|
|
66
|
+
'tailwind.config.js',
|
|
67
|
+
'tailwind.config.ts',
|
|
68
|
+
'tailwind.config.cjs',
|
|
69
|
+
'tailwind.config.mjs',
|
|
70
|
+
])),
|
|
71
|
+
cssModules: false, // 모듈 css 는 파일 패턴으로 추정 (아래에서 처리)
|
|
72
|
+
styledComponents: has('styled-components'),
|
|
73
|
+
emotion: has('@emotion/react') || has('@emotion/styled'),
|
|
74
|
+
vanillaExtract: has('@vanilla-extract/css'),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ── 라우팅 ─────────────────────────────────────────────
|
|
78
|
+
let routing = null;
|
|
79
|
+
if (framework === 'next') {
|
|
80
|
+
if (await exists(path.join(root, 'app'))) routing = 'app-router';
|
|
81
|
+
else if (await exists(path.join(root, 'src/app'))) routing = 'app-router';
|
|
82
|
+
else if (await exists(path.join(root, 'pages'))) routing = 'pages-router';
|
|
83
|
+
else if (await exists(path.join(root, 'src/pages'))) routing = 'pages-router';
|
|
84
|
+
} else if (framework === 'expo') {
|
|
85
|
+
routing = (await exists(path.join(root, 'app'))) ? 'expo-router' : null;
|
|
86
|
+
} else if (has('react-router-dom') || has('react-router')) {
|
|
87
|
+
routing = 'react-router';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 컴포넌트 디렉터리 후보 ─────────────────────────────
|
|
91
|
+
const componentCandidates = [
|
|
92
|
+
'src/components',
|
|
93
|
+
'src/ui',
|
|
94
|
+
'app/components',
|
|
95
|
+
'app/_components',
|
|
96
|
+
'components',
|
|
97
|
+
'ui',
|
|
98
|
+
];
|
|
99
|
+
const componentDirs = [];
|
|
100
|
+
for (const c of componentCandidates) {
|
|
101
|
+
if (await exists(path.join(root, c))) componentDirs.push(c);
|
|
102
|
+
}
|
|
103
|
+
// .module.css 파일이 한 개라도 있으면 cssModules = true 로 추정.
|
|
104
|
+
if (componentDirs.length) {
|
|
105
|
+
styling.cssModules = await hasFileWithExt(
|
|
106
|
+
path.join(root, componentDirs[0]),
|
|
107
|
+
'.module.css',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 디자인 토큰 후보 파일 ─────────────────────────────
|
|
112
|
+
const tokenCandidates = [
|
|
113
|
+
'tailwind.config.js',
|
|
114
|
+
'tailwind.config.ts',
|
|
115
|
+
'tailwind.config.cjs',
|
|
116
|
+
'tailwind.config.mjs',
|
|
117
|
+
'src/styles/tokens.ts',
|
|
118
|
+
'src/styles/tokens.css',
|
|
119
|
+
'src/styles/theme.ts',
|
|
120
|
+
'src/theme.ts',
|
|
121
|
+
'theme.config.ts',
|
|
122
|
+
'token.config.js',
|
|
123
|
+
];
|
|
124
|
+
const designTokensFiles = [];
|
|
125
|
+
for (const f of tokenCandidates) {
|
|
126
|
+
if (await exists(path.join(root, f))) designTokensFiles.push(f);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 테스트/스토리북 ─────────────────────────────────
|
|
130
|
+
const hasStorybook = has('@storybook/react') || has('@storybook/nextjs') || has('storybook');
|
|
131
|
+
let hasTests = null;
|
|
132
|
+
if (has('vitest')) hasTests = 'vitest';
|
|
133
|
+
else if (has('jest') || has('@testing-library/react')) hasTests = 'jest';
|
|
134
|
+
if (has('@playwright/test')) hasTests = hasTests ? hasTests : 'playwright';
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
framework,
|
|
138
|
+
language,
|
|
139
|
+
styling,
|
|
140
|
+
packageManager: await detectPackageManager(root),
|
|
141
|
+
routing,
|
|
142
|
+
componentDirs,
|
|
143
|
+
designTokensFiles,
|
|
144
|
+
hasStorybook,
|
|
145
|
+
hasTests,
|
|
146
|
+
pkg: {
|
|
147
|
+
name: pkg.name,
|
|
148
|
+
version: pkg.version,
|
|
149
|
+
dependencies: pkg.dependencies ?? {},
|
|
150
|
+
devDependencies: pkg.devDependencies ?? {},
|
|
151
|
+
},
|
|
152
|
+
isProject: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function emptyStyling() {
|
|
157
|
+
return {
|
|
158
|
+
tailwind: false,
|
|
159
|
+
cssModules: false,
|
|
160
|
+
styledComponents: false,
|
|
161
|
+
emotion: false,
|
|
162
|
+
vanillaExtract: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function exists(p) {
|
|
167
|
+
try {
|
|
168
|
+
await fs.access(p);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function anyExists(root, files) {
|
|
176
|
+
for (const f of files) {
|
|
177
|
+
if (await exists(path.join(root, f))) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readJsonSafe(file) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
185
|
+
return JSON.parse(raw);
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function detectPackageManager(root) {
|
|
192
|
+
if (await exists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
193
|
+
if (await exists(path.join(root, 'yarn.lock'))) return 'yarn';
|
|
194
|
+
if (await exists(path.join(root, 'bun.lockb'))) return 'bun';
|
|
195
|
+
if (await exists(path.join(root, 'package-lock.json'))) return 'npm';
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function hasFileWithExt(dir, ext) {
|
|
200
|
+
try {
|
|
201
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
if (e.isFile() && e.name.endsWith(ext)) return true;
|
|
204
|
+
if (e.isDirectory()) {
|
|
205
|
+
if (await hasFileWithExt(path.join(dir, e.name), ext)) return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
/* noop */
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 사람이 읽기 좋은 라벨로 압축. */
|
|
215
|
+
export function summarizeContext(ctx) {
|
|
216
|
+
if (!ctx?.isProject) return '(package.json 없음 — 프로젝트 외부)';
|
|
217
|
+
const styling = Object.entries(ctx.styling)
|
|
218
|
+
.filter(([, v]) => v)
|
|
219
|
+
.map(([k]) => k);
|
|
220
|
+
const parts = [
|
|
221
|
+
ctx.framework,
|
|
222
|
+
ctx.language === 'ts' ? 'TypeScript' : 'JavaScript',
|
|
223
|
+
styling.length ? styling.join('+') : 'no styling detected',
|
|
224
|
+
ctx.packageManager,
|
|
225
|
+
];
|
|
226
|
+
if (ctx.routing) parts.push(ctx.routing);
|
|
227
|
+
return parts.join(' · ');
|
|
228
|
+
}
|