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,69 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenAPI 스펙을 URL/파일경로/JSON객체 중 무엇으로 받든 정상화해서
|
|
6
|
+
* `{ source, doc }` 로 돌려준다.
|
|
7
|
+
*
|
|
8
|
+
* - URL ('http://' / 'https://') → fetch 후 파싱
|
|
9
|
+
* - 파일 경로 (.json / .yaml / .yml) → 디스크에서 읽기
|
|
10
|
+
* - YAML 은 openapi-typescript 에 그대로 넘겨도 되지만, 우리가 endpoint 요약을
|
|
11
|
+
* 뽑을 땐 JSON 객체가 필요해서 yaml 파서가 필요함. 의존성 키우지 않도록
|
|
12
|
+
* YAML 인 경우 doc 필드는 null 로 두고 source(원본 문자열)만 반환한다.
|
|
13
|
+
* - 그 외 → 에러
|
|
14
|
+
*
|
|
15
|
+
* 반환된 source 는 openapi-typescript 의 입력으로 그대로 쓸 수 있다.
|
|
16
|
+
*/
|
|
17
|
+
export async function loadOpenApi(input) {
|
|
18
|
+
if (!input) {
|
|
19
|
+
const e = new Error('OpenAPI 입력이 비어있습니다 (URL 또는 파일 경로 필요).');
|
|
20
|
+
e.code = 'BC_NO_OPENAPI_INPUT';
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// URL?
|
|
25
|
+
if (/^https?:\/\//.test(input)) {
|
|
26
|
+
const res = await fetch(input);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const e = new Error(`OpenAPI fetch 실패: ${res.status} ${res.statusText} (${input})`);
|
|
29
|
+
e.code = 'BC_OPENAPI_FETCH_FAILED';
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
let doc = null;
|
|
34
|
+
try {
|
|
35
|
+
doc = JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
// 서버가 YAML 을 줄 수도 있다 — 객체 추출은 포기하지만 source(URL) 는 그대로.
|
|
38
|
+
}
|
|
39
|
+
return { source: input, text, doc };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 로컬 파일?
|
|
43
|
+
const abs = path.resolve(input);
|
|
44
|
+
let stat;
|
|
45
|
+
try {
|
|
46
|
+
stat = await fs.stat(abs);
|
|
47
|
+
} catch {
|
|
48
|
+
const e = new Error(`OpenAPI 파일을 찾을 수 없습니다: ${input}`);
|
|
49
|
+
e.code = 'BC_OPENAPI_FILE_NOT_FOUND';
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
if (!stat.isFile()) {
|
|
53
|
+
const e = new Error(`OpenAPI 경로가 파일이 아닙니다: ${input}`);
|
|
54
|
+
e.code = 'BC_OPENAPI_NOT_A_FILE';
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
const text = await fs.readFile(abs, 'utf8');
|
|
58
|
+
let doc = null;
|
|
59
|
+
if (abs.endsWith('.json')) {
|
|
60
|
+
try {
|
|
61
|
+
doc = JSON.parse(text);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const e = new Error(`JSON 파싱 실패: ${err.message}`);
|
|
64
|
+
e.code = 'BC_OPENAPI_PARSE_FAILED';
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { source: abs, text, doc };
|
|
69
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 파싱된 OpenAPI 객체를 → 모델에 주입할 "압축 요약" 텍스트로 변환.
|
|
3
|
+
*
|
|
4
|
+
* 목표: 토큰 폭발 방지하면서도 모델이 "이 API 스펙이 있구나, 어떤 엔드포인트가
|
|
5
|
+
* 있구나" 를 알게 하는 정도. 자세한 스키마는 RAG 가 types.gen.ts 통해서 별도로
|
|
6
|
+
* 가져오게 함.
|
|
7
|
+
*
|
|
8
|
+
* 결과 형태 (예):
|
|
9
|
+
* API: Petstore v3.0
|
|
10
|
+
* base: /api/v3
|
|
11
|
+
* endpoints:
|
|
12
|
+
* GET /pet/{petId} Find pet by ID
|
|
13
|
+
* POST /pet Add a new pet to the store
|
|
14
|
+
* ...
|
|
15
|
+
*/
|
|
16
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
17
|
+
const MAX_ENDPOINTS = 200;
|
|
18
|
+
const MAX_BYTES = 8 * 1024;
|
|
19
|
+
|
|
20
|
+
export function summarizeOpenApi(doc) {
|
|
21
|
+
if (!doc || typeof doc !== 'object') return null;
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
const title = doc.info?.title ?? '(untitled)';
|
|
25
|
+
const version = doc.info?.version ?? '';
|
|
26
|
+
lines.push(`API: ${title}${version ? ' v' + version : ''}`);
|
|
27
|
+
|
|
28
|
+
const servers = (doc.servers ?? []).map((s) => s.url).filter(Boolean);
|
|
29
|
+
if (servers.length) {
|
|
30
|
+
lines.push(`base: ${servers.slice(0, 3).join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const endpoints = [];
|
|
34
|
+
for (const [pathStr, methods] of Object.entries(doc.paths ?? {})) {
|
|
35
|
+
if (!methods || typeof methods !== 'object') continue;
|
|
36
|
+
for (const m of METHODS) {
|
|
37
|
+
const op = methods[m];
|
|
38
|
+
if (!op || typeof op !== 'object') continue;
|
|
39
|
+
const desc = op.summary || op.operationId || op.description || '';
|
|
40
|
+
endpoints.push({
|
|
41
|
+
method: m.toUpperCase(),
|
|
42
|
+
path: pathStr,
|
|
43
|
+
desc: desc.split('\n')[0].slice(0, 90),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 너무 많으면 자른다 — 모델이 알아야 하는 건 "어떤 종류의 엔드포인트가 있다" 정도면 충분.
|
|
49
|
+
const total = endpoints.length;
|
|
50
|
+
const shown = endpoints.slice(0, MAX_ENDPOINTS);
|
|
51
|
+
|
|
52
|
+
lines.push(`endpoints: (${shown.length}/${total})`);
|
|
53
|
+
for (const e of shown) {
|
|
54
|
+
lines.push(` ${e.method.padEnd(6)} ${e.path}${e.desc ? ' -- ' + e.desc : ''}`);
|
|
55
|
+
}
|
|
56
|
+
if (shown.length < total) {
|
|
57
|
+
lines.push(` ... and ${total - shown.length} more`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let result = lines.join('\n');
|
|
61
|
+
if (Buffer.byteLength(result, 'utf8') > MAX_BYTES) {
|
|
62
|
+
// 안전 장치 — desc 다 자르고 다시.
|
|
63
|
+
const compact = [
|
|
64
|
+
lines[0],
|
|
65
|
+
servers.length ? lines[1] : null,
|
|
66
|
+
`endpoints: (${shown.length}/${total})`,
|
|
67
|
+
...shown.map((e) => ` ${e.method.padEnd(6)} ${e.path}`),
|
|
68
|
+
shown.length < total ? ` ... and ${total - shown.length} more` : null,
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join('\n');
|
|
72
|
+
result = compact;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
|
|
3
|
+
import { modelChoices, DEFAULT_MODEL_ID } from '../ai/models.js';
|
|
4
|
+
|
|
3
5
|
export async function askInitQuestions() {
|
|
4
6
|
return inquirer.prompt([
|
|
5
7
|
{
|
|
@@ -22,5 +24,27 @@ export async function askInitQuestions() {
|
|
|
22
24
|
{ name: 'Next.js (App Router + TypeScript)', value: 'next' },
|
|
23
25
|
],
|
|
24
26
|
},
|
|
27
|
+
{
|
|
28
|
+
type: 'list',
|
|
29
|
+
name: 'aiModel',
|
|
30
|
+
message: 'bc chat 에서 기본으로 쓸 AI 모델은?',
|
|
31
|
+
choices: [
|
|
32
|
+
...modelChoices(),
|
|
33
|
+
{ name: '나중에 설정 (bc config set-model)', value: null },
|
|
34
|
+
],
|
|
35
|
+
default: DEFAULT_MODEL_ID,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'figmaUrl',
|
|
40
|
+
message: 'Figma 파일 URL (선택, 엔터로 건너뛰기):',
|
|
41
|
+
default: '',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'input',
|
|
45
|
+
name: 'openapiUrl',
|
|
46
|
+
message: '백엔드 OpenAPI(Swagger) URL (선택, 엔터로 건너뛰기):',
|
|
47
|
+
default: '',
|
|
48
|
+
},
|
|
25
49
|
]);
|
|
26
50
|
}
|