byuckchon-frontend-cli 1.9.3 → 1.9.5
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 +13 -9
- package/bin/index.js +9 -1
- package/package.json +8 -8
- package/src/ai/models.js +21 -12
- package/src/commands/add.js +82 -0
- package/src/commands/adopt.js +19 -2
- package/src/commands/chat.js +28 -1
- package/src/commands/init.js +85 -36
- package/src/config/projectLinks.js +49 -0
- package/src/context/monorepo.js +98 -0
- package/src/generators/createApp.js +211 -0
- package/src/generators/createBaseFiles.js +5 -0
- package/src/generators/createMonorepo.js +514 -0
- package/src/generators/createPackageJson.js +1 -1
- package/src/generators/install.js +69 -0
- package/src/prompts/initPrompts.js +128 -0
- package/src/ui/ChatApp.js +52 -13
- package/src/utils/ensureRequiredDependencies.js +58 -0
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ bc init
|
|
|
104
104
|
# 1) Figma → Settings → Personal access tokens → "Generate new token"
|
|
105
105
|
# Read 권한만 있으면 충분 (file 읽기 / image export 둘 다 read 로 됨)
|
|
106
106
|
|
|
107
|
-
# 2)
|
|
108
|
-
|
|
107
|
+
# 2) bc init이 만든 .env의 FIGMA_TOKEN 값 채우기 (gitignore 됨)
|
|
108
|
+
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxx
|
|
109
109
|
|
|
110
110
|
# 3) bc.config.json 의 design.figma 에 파일/노드 URL 박기 (bc adopt 시점에 입력하거나 직접 편집)
|
|
111
111
|
```
|
|
@@ -250,8 +250,9 @@ bc adopt
|
|
|
250
250
|
```
|
|
251
251
|
|
|
252
252
|
`package.json` 과 디렉터리를 스캔해서 **프레임워크/언어/스타일/라우팅/패키지 매니저** 를 자동 감지하고,
|
|
253
|
-
Figma·OpenAPI URL 만 추가로 묻고 `bc.config.json`
|
|
254
|
-
|
|
253
|
+
Figma·OpenAPI URL 만 추가로 묻고 `bc.config.json` 을 생성합니다.
|
|
254
|
+
필수 의존성(`@tanstack/react-query`, `zod`, React 계열의 `axios`)이 없으면 감지한 패키지 매니저로
|
|
255
|
+
자동 설치하며, 기존 소스 코드는 건드리지 않습니다.
|
|
255
256
|
|
|
256
257
|
지원 감지: Next.js · Expo · Electron · Vite+React · Remix · CRA · 일반 React.
|
|
257
258
|
|
|
@@ -261,7 +262,7 @@ Figma·OpenAPI URL 만 추가로 묻고 `bc.config.json` 만 떨궈줍니다.
|
|
|
261
262
|
bc # 인자 없이도 chat 진입 (제일 짧은 단축키)
|
|
262
263
|
bc start # chat 의 alias
|
|
263
264
|
bc chat # ink 풀 TUI (기본)
|
|
264
|
-
bc chat --model claude-
|
|
265
|
+
bc chat --model claude-fable-5 # 이번 세션만 모델 지정
|
|
265
266
|
bc chat --plain # 단순 readline 모드
|
|
266
267
|
bc chat --once "useEffect 의존성 배열 누락된 거 어떻게 찾아?" # 1회성 호출 (CI/스크립트)
|
|
267
268
|
bc chat -c # 가장 최근 세션 이어가기
|
|
@@ -291,6 +292,8 @@ TTY 안에서 자동으로 ink 모드로 뜨고, 파이프/CI 같은 비-TTY 환
|
|
|
291
292
|
| `/history` | 이전 대화 선택 후 해당 컨텍스트 이어가기 |
|
|
292
293
|
| `/retry` | 마지막 사용자 요청 다시 실행 |
|
|
293
294
|
| `/model [id]` | 세션 모델 변경 (인자 없으면 목록) |
|
|
295
|
+
| `/figma-link <url\|off>` | 프로젝트 Figma 링크 변경 또는 해제 |
|
|
296
|
+
| `/openapi-link <url\|off>` | 프로젝트 OpenAPI 링크 변경 또는 해제 |
|
|
294
297
|
| `/cost` | 누적 토큰/비용 |
|
|
295
298
|
| `/image <path>` | 다음 메시지에 이미지 첨부 (Vision 모델 권장) |
|
|
296
299
|
| `/paste` | 클립보드 이미지 첨부 (macOS, `pngpaste` 필요) |
|
|
@@ -346,7 +349,7 @@ bc config set-ui ink
|
|
|
346
349
|
```bash
|
|
347
350
|
bc config show # 현재 적용 중인 설정 확인
|
|
348
351
|
bc config set-model # 대화형 모델 선택
|
|
349
|
-
bc config set-model claude-sonnet-
|
|
352
|
+
bc config set-model claude-sonnet-5 # 직접 지정
|
|
350
353
|
bc config set-key anthropic # 키 안전 입력 (가려짐)
|
|
351
354
|
bc config set-key anthropic sk-ant-... # 직접 지정
|
|
352
355
|
bc config set-gateway https://ai.example.com # 사내 게이트웨이 모드
|
|
@@ -369,9 +372,10 @@ bc config set-ui ink # 풀 TUI 복귀
|
|
|
369
372
|
|
|
370
373
|
| id | provider | 추천 용도 |
|
|
371
374
|
| ------------------- | --------- | ---------------------------------- |
|
|
372
|
-
| `claude-sonnet-
|
|
373
|
-
| `claude-
|
|
374
|
-
| `claude-opus-4-
|
|
375
|
+
| `claude-sonnet-5` | anthropic | 기본 — 코드 Q&A, 리팩터, 컴포넌트 |
|
|
376
|
+
| `claude-sonnet-4-6` | anthropic | 안정 — 이전 세대 밸런스 |
|
|
377
|
+
| `claude-opus-4-8` | anthropic | 큰 리팩터, 아키텍처 설계 (고가) |
|
|
378
|
+
| `claude-fable-5` | anthropic | 짧은 작업, 커밋 메시지 (저렴·빠름) |
|
|
375
379
|
| `gpt-5` | openai | 일반 코드 |
|
|
376
380
|
| `gpt-5-mini` | openai | 저렴한 OpenAI |
|
|
377
381
|
|
package/bin/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
import { initCommand } from '../src/commands/init.js';
|
|
7
|
+
import { addCommand } from '../src/commands/add.js';
|
|
7
8
|
import { chatCommand } from '../src/commands/chat.js';
|
|
8
9
|
import { adoptCommand } from '../src/commands/adopt.js';
|
|
9
10
|
import {
|
|
@@ -32,11 +33,18 @@ program
|
|
|
32
33
|
|
|
33
34
|
program
|
|
34
35
|
.command('init')
|
|
35
|
-
.description('새 프론트엔드 프로젝트 생성 (React/Next
|
|
36
|
+
.description('새 프론트엔드 프로젝트 생성 (단일 React/Next 또는 pnpm 모노레포)')
|
|
36
37
|
.action(async () => {
|
|
37
38
|
await initCommand();
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
program
|
|
42
|
+
.command('add')
|
|
43
|
+
.description('기존 pnpm 모노레포에 React/Next 앱 추가 (apps/ 아래)')
|
|
44
|
+
.action(async () => {
|
|
45
|
+
await addCommand();
|
|
46
|
+
});
|
|
47
|
+
|
|
40
48
|
program
|
|
41
49
|
.command('adopt')
|
|
42
50
|
.description('기존 프로젝트에 bc 설정만 추가 (Expo/Electron/Next/Vite/CRA 자동 감지)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "byuckchon-frontend-cli",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "Byuckchon Frontend Workbench — project starter + AI chat + codebase RAG + OpenAPI codegen",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
"byuckchon-frontend-cli": "./bin/index.js",
|
|
11
11
|
"bc": "./bin/index.js"
|
|
12
12
|
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node bin/index.js",
|
|
15
|
+
"chat": "node bin/index.js chat",
|
|
16
|
+
"init": "node bin/index.js init",
|
|
17
|
+
"config": "node bin/index.js config show"
|
|
18
|
+
},
|
|
13
19
|
"dependencies": {
|
|
14
20
|
"@ai-sdk/anthropic": "^3.0.85",
|
|
15
21
|
"@ai-sdk/openai": "^3.0.73",
|
|
@@ -59,11 +65,5 @@
|
|
|
59
65
|
"repository": {
|
|
60
66
|
"type": "git",
|
|
61
67
|
"url": "git+https://github.com/RevolutionaryWarrior/byuckchon-frontend-cli.git"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"start": "node bin/index.js",
|
|
65
|
-
"chat": "node bin/index.js chat",
|
|
66
|
-
"init": "node bin/index.js init",
|
|
67
|
-
"config": "node bin/index.js config show"
|
|
68
68
|
}
|
|
69
|
-
}
|
|
69
|
+
}
|
package/src/ai/models.js
CHANGED
|
@@ -9,32 +9,41 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export const MODEL_CATALOG = [
|
|
11
11
|
{
|
|
12
|
-
id: 'claude-sonnet-
|
|
13
|
-
label: 'Claude Sonnet
|
|
12
|
+
id: 'claude-sonnet-5',
|
|
13
|
+
label: 'Claude Sonnet 5 (권장 · 코드 품질·속도 밸런스)',
|
|
14
14
|
provider: 'anthropic',
|
|
15
|
-
apiModel: 'claude-sonnet-
|
|
15
|
+
apiModel: 'claude-sonnet-5',
|
|
16
16
|
contextWindow: 200_000,
|
|
17
17
|
pricing: { input: 3, output: 15, cachedInput: 0.3 },
|
|
18
18
|
tier: 'balanced',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
id: 'claude-
|
|
22
|
-
label: 'Claude
|
|
21
|
+
id: 'claude-sonnet-4-6',
|
|
22
|
+
label: 'Claude Sonnet 4.6 (안정 · 이전 세대 밸런스)',
|
|
23
23
|
provider: 'anthropic',
|
|
24
|
-
apiModel: 'claude-
|
|
24
|
+
apiModel: 'claude-sonnet-4-6',
|
|
25
25
|
contextWindow: 200_000,
|
|
26
|
-
pricing: { input:
|
|
27
|
-
tier: '
|
|
26
|
+
pricing: { input: 3, output: 15, cachedInput: 0.3 },
|
|
27
|
+
tier: 'balanced',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
id: 'claude-opus-4-
|
|
31
|
-
label: 'Claude Opus 4.
|
|
30
|
+
id: 'claude-opus-4-8',
|
|
31
|
+
label: 'Claude Opus 4.8 (고성능 · 큰 리팩터·아키텍처)',
|
|
32
32
|
provider: 'anthropic',
|
|
33
|
-
apiModel: 'claude-opus-4-
|
|
33
|
+
apiModel: 'claude-opus-4-8',
|
|
34
34
|
contextWindow: 200_000,
|
|
35
35
|
pricing: { input: 15, output: 75, cachedInput: 1.5 },
|
|
36
36
|
tier: 'powerful',
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
id: 'claude-fable-5',
|
|
40
|
+
label: 'Claude Fable 5 (저렴·빠름 · 짧은 작업·커밋 메시지)',
|
|
41
|
+
provider: 'anthropic',
|
|
42
|
+
apiModel: 'claude-fable-5',
|
|
43
|
+
contextWindow: 200_000,
|
|
44
|
+
pricing: { input: 1, output: 5, cachedInput: 0.1 },
|
|
45
|
+
tier: 'fast',
|
|
46
|
+
},
|
|
38
47
|
{
|
|
39
48
|
id: 'gpt-5',
|
|
40
49
|
label: 'GPT-5 (OpenAI · 일반 코드)',
|
|
@@ -55,7 +64,7 @@ export const MODEL_CATALOG = [
|
|
|
55
64
|
},
|
|
56
65
|
];
|
|
57
66
|
|
|
58
|
-
export const DEFAULT_MODEL_ID = 'claude-sonnet-
|
|
67
|
+
export const DEFAULT_MODEL_ID = 'claude-sonnet-5';
|
|
59
68
|
|
|
60
69
|
export function findModel(id) {
|
|
61
70
|
return MODEL_CATALOG.find((m) => m.id === id);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
import { askAddQuestions } from '../prompts/initPrompts.js';
|
|
7
|
+
import { createApp } from '../generators/createApp.js';
|
|
8
|
+
import { installMonorepoDeps } from '../generators/install.js';
|
|
9
|
+
import {
|
|
10
|
+
detectMonorepoRoot,
|
|
11
|
+
detectScope,
|
|
12
|
+
appExists,
|
|
13
|
+
} from '../context/monorepo.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `bc add`
|
|
17
|
+
*
|
|
18
|
+
* 기존 pnpm 모노레포에 React/Next 앱을 하나 추가한다.
|
|
19
|
+
* (현재 위치에서 위로 올라가며 pnpm-workspace.yaml 을 찾는다.)
|
|
20
|
+
*/
|
|
21
|
+
export async function addCommand() {
|
|
22
|
+
console.log(chalk.bold.cyan('\n bc add — 모노레포에 앱 추가\n'));
|
|
23
|
+
|
|
24
|
+
const root = await detectMonorepoRoot(process.cwd());
|
|
25
|
+
if (!root) {
|
|
26
|
+
console.log(chalk.yellow(' ⚠ pnpm 모노레포를 찾지 못했습니다.'));
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.dim(
|
|
29
|
+
' 모노레포 루트(또는 그 하위)에서 실행하세요. 새로 만들려면 `bc init` → 모노레포.\n',
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const scope = await detectScope(root);
|
|
36
|
+
console.log(chalk.dim(` 모노레포: ${root}`));
|
|
37
|
+
console.log(chalk.dim(` scope: @${scope}\n`));
|
|
38
|
+
|
|
39
|
+
const answers = await askAddQuestions({ scope });
|
|
40
|
+
|
|
41
|
+
if (await appExists(root, answers.appName)) {
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.red(`\n 오류: apps/${answers.appName} 가 이미 존재합니다.\n`),
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config = { ...answers, projectName: answers.appName, typescript: true };
|
|
49
|
+
const appDir = path.join(root, 'apps', answers.appName);
|
|
50
|
+
const appPkgName = `@${scope}/${answers.appName}`;
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.dim(` ${answers.framework} 앱을 apps/${answers.appName} 에 생성하는 중...\n`),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await createApp({ appDir, config, scope });
|
|
57
|
+
await addRootDevScript(root, answers.appName, appPkgName);
|
|
58
|
+
await installMonorepoDeps({ root, appPkgName });
|
|
59
|
+
|
|
60
|
+
console.log(chalk.bold.green(`\n ✓ ${appPkgName} 앱을 추가했습니다!\n`));
|
|
61
|
+
console.log(chalk.yellow(' 실행:\n'));
|
|
62
|
+
console.log(chalk.white(` pnpm --filter ${appPkgName} dev`));
|
|
63
|
+
console.log(chalk.white(` # 또는 pnpm ${answers.appName}\n`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 루트 package.json 에 `"<app>": "pnpm --filter <pkg> dev"` 단축 스크립트를 추가.
|
|
68
|
+
*/
|
|
69
|
+
async function addRootDevScript(root, appName, appPkgName) {
|
|
70
|
+
const pkgPath = path.join(root, 'package.json');
|
|
71
|
+
let pkg;
|
|
72
|
+
try {
|
|
73
|
+
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
78
|
+
if (!pkg.scripts[appName]) {
|
|
79
|
+
pkg.scripts[appName] = `pnpm --filter ${appPkgName} dev`;
|
|
80
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/commands/adopt.js
CHANGED
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
apiRootForFramework,
|
|
12
12
|
scaffoldApiConventionDoc,
|
|
13
13
|
} from '../generators/apiConventionDoc.js';
|
|
14
|
+
import { ensureRequiredDependencies } from '../utils/ensureRequiredDependencies.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* `bc adopt`
|
|
17
18
|
*
|
|
18
|
-
* 기존 프로젝트(현재 디렉터리)에 bc.config.json
|
|
19
|
-
* 소스 코드는
|
|
19
|
+
* 기존 프로젝트(현재 디렉터리)에 bc.config.json 과 필수 의존성을 추가한다.
|
|
20
|
+
* 기존 소스 코드는 건드리지 않는다.
|
|
20
21
|
*
|
|
21
22
|
* 흐름:
|
|
22
23
|
* 1) 자동 감지(framework, styling, language, routing 등) 결과를 보여주고
|
|
@@ -145,6 +146,22 @@ export async function adoptCommand(opts = {}) {
|
|
|
145
146
|
console.log(chalk.green(`\n ✓ ${CONFIG_PATHS.projectFileName} 작성 완료.`));
|
|
146
147
|
console.log(chalk.dim(` ${targetFile}`));
|
|
147
148
|
|
|
149
|
+
const dependencies = await ensureRequiredDependencies({
|
|
150
|
+
cwd,
|
|
151
|
+
pkg: ctx.pkg,
|
|
152
|
+
framework: ctx.framework,
|
|
153
|
+
packageManager: ctx.packageManager,
|
|
154
|
+
});
|
|
155
|
+
if (dependencies.installed.length) {
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.green(
|
|
158
|
+
` ✓ 필수 의존성 설치 완료 (${dependencies.packageManager}): ${dependencies.installed.join(', ')}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(chalk.dim(' 필수 의존성 유지 (이미 설치됨)'));
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
// API 코드 컨벤션 .md 를 API 루트(src/api | src/lib/api)에 깐다 (이미 있으면 유지).
|
|
149
166
|
try {
|
|
150
167
|
const { relPath, written } = await scaffoldApiConventionDoc({
|
package/src/commands/chat.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { findLastRetryableUser, formatSessionList } from '../history/management.js';
|
|
26
26
|
import { getCachedOpenApi } from '../openapi/cache.js';
|
|
27
27
|
import { summarizeOpenApi } from '../openapi/summary.js';
|
|
28
|
+
import { updateProjectLink } from '../config/projectLinks.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* `bc chat`
|
|
@@ -330,7 +331,7 @@ async function runReadlineFallback({ cfg, resolved, system, session, openapiInfo
|
|
|
330
331
|
}
|
|
331
332
|
console.log(
|
|
332
333
|
chalk.dim(
|
|
333
|
-
' /history · /retry · /
|
|
334
|
+
' /history · /retry · /figma-link <url|off> · /openapi-link <url|off> · /image <경로> · /exit\n',
|
|
334
335
|
),
|
|
335
336
|
);
|
|
336
337
|
|
|
@@ -425,6 +426,32 @@ async function runReadlineFallback({ cfg, resolved, system, session, openapiInfo
|
|
|
425
426
|
ask();
|
|
426
427
|
continue;
|
|
427
428
|
}
|
|
429
|
+
if (/^\/(figma-link|openapi-link)(\s|$)/.test(line)) {
|
|
430
|
+
const isFigma = line === '/figma-link' || line.startsWith('/figma-link ');
|
|
431
|
+
const kind = isFigma ? 'figma' : 'openapi';
|
|
432
|
+
const label = isFigma ? 'Figma' : 'OpenAPI';
|
|
433
|
+
const command = line.match(/^\/(figma-link|openapi-link)/)[1];
|
|
434
|
+
const arg = line.slice(command.length + 1).trim();
|
|
435
|
+
if (!arg) {
|
|
436
|
+
const current = isFigma ? cfg.effective.design?.figma : cfg.effective.api?.openapi;
|
|
437
|
+
console.log(chalk.dim(` 현재 ${label}: ${current ?? '(미설정)'}`));
|
|
438
|
+
console.log(chalk.dim(` 사용법: /${kind} <url|off>`));
|
|
439
|
+
ask();
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
cfg = await updateProjectLink(cfg, kind, arg);
|
|
444
|
+
const value = isFigma ? cfg.effective.design?.figma : cfg.effective.api?.openapi;
|
|
445
|
+
system += value
|
|
446
|
+
? `\n\n[프로젝트 설정 변경] ${label} 소스는 ${value} 이다. 이전 ${label} 링크와 요약은 무시하고 이 값을 사용하라.`
|
|
447
|
+
: `\n\n[프로젝트 설정 변경] ${label} 링크가 해제되었다. 이전 ${label} 링크와 요약은 사용하지 마라.`;
|
|
448
|
+
console.log(chalk.green(` ${label} 링크를 ${value ? '변경했습니다: ' + value : '해제했습니다.'}`));
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.log(chalk.red(' ' + err.message));
|
|
451
|
+
}
|
|
452
|
+
ask();
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
428
455
|
let retryContent = null;
|
|
429
456
|
if (line === '/retry') {
|
|
430
457
|
const retry = findLastRetryableUser(history);
|
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
|
|
3
3
|
import { createProject } from "../generators/createProject.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createMonorepo } from "../generators/createMonorepo.js";
|
|
5
|
+
import { installMonorepoDeps } from "../generators/install.js";
|
|
6
|
+
import {
|
|
7
|
+
askInitQuestions,
|
|
8
|
+
askProjectType,
|
|
9
|
+
askMonorepoQuestions,
|
|
10
|
+
} from "../prompts/initPrompts.js";
|
|
5
11
|
|
|
6
12
|
export async function initCommand() {
|
|
7
13
|
console.log(
|
|
@@ -9,43 +15,14 @@ export async function initCommand() {
|
|
|
9
15
|
);
|
|
10
16
|
|
|
11
17
|
try {
|
|
12
|
-
const
|
|
18
|
+
const projectType = await askProjectType();
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
await createProject({ ...answers, typescript: true });
|
|
21
|
-
|
|
22
|
-
console.log(
|
|
23
|
-
chalk.bold.green(
|
|
24
|
-
`\n ✓ ${answers.projectName} 프로젝트가 생성되었습니다!\n`
|
|
25
|
-
)
|
|
26
|
-
);
|
|
27
|
-
console.log(chalk.yellow(" 다음 명령어로 시작하세요:\n"));
|
|
28
|
-
console.log(chalk.white(` cd ${answers.projectName}`));
|
|
29
|
-
console.log(chalk.white(" npm run dev\n"));
|
|
30
|
-
|
|
31
|
-
console.log(chalk.dim(" AI 어시스턴트:"));
|
|
32
|
-
if (answers.aiModel) {
|
|
33
|
-
console.log(
|
|
34
|
-
chalk.dim(
|
|
35
|
-
` 이 프로젝트의 기본 모델 = ${answers.aiModel} (bc.config.json 에 저장됨)`
|
|
36
|
-
)
|
|
37
|
-
);
|
|
38
|
-
} else {
|
|
39
|
-
console.log(
|
|
40
|
-
chalk.dim(" 모델 미설정 — `bc config set-model` 로 나중에 지정")
|
|
41
|
-
);
|
|
20
|
+
if (projectType === "monorepo") {
|
|
21
|
+
await runMonorepoInit();
|
|
22
|
+
return;
|
|
42
23
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
" API 키: bc config set-key anthropic (또는 ANTHROPIC_API_KEY 환경변수)"
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
console.log(chalk.dim(" 실행: bc chat\n"));
|
|
24
|
+
|
|
25
|
+
await runSingleInit();
|
|
49
26
|
} catch (error) {
|
|
50
27
|
if (error.code === "EEXIST") {
|
|
51
28
|
console.error(
|
|
@@ -60,3 +37,75 @@ export async function initCommand() {
|
|
|
60
37
|
process.exit(1);
|
|
61
38
|
}
|
|
62
39
|
}
|
|
40
|
+
|
|
41
|
+
async function runSingleInit() {
|
|
42
|
+
const answers = await askInitQuestions();
|
|
43
|
+
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.dim(
|
|
46
|
+
`\n ${answers.framework} 프로젝트를 생성하는 중... (${answers.projectName})\n`
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await createProject({ ...answers, typescript: true });
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.bold.green(
|
|
54
|
+
`\n ✓ ${answers.projectName} 프로젝트가 생성되었습니다!\n`
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
console.log(chalk.yellow(" 다음 명령어로 시작하세요:\n"));
|
|
58
|
+
console.log(chalk.white(` cd ${answers.projectName}`));
|
|
59
|
+
console.log(chalk.white(" npm run dev\n"));
|
|
60
|
+
|
|
61
|
+
printAiHints(answers.aiModel);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runMonorepoInit() {
|
|
65
|
+
const answers = await askMonorepoQuestions();
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.dim(
|
|
69
|
+
`\n 모노레포 '${answers.projectName}' 생성 중... (scope: @${answers.scope}, 첫 앱: ${answers.framework} / apps/${answers.appName})\n`
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const { root, scope, appName } = await createMonorepo({
|
|
74
|
+
...answers,
|
|
75
|
+
typescript: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await installMonorepoDeps({ root, appPkgName: `@${scope}/${appName}` });
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.bold.green(`\n ✓ 모노레포 '${answers.projectName}' 가 생성되었습니다!\n`)
|
|
82
|
+
);
|
|
83
|
+
console.log(chalk.yellow(" 다음 명령어로 시작하세요:\n"));
|
|
84
|
+
console.log(chalk.white(` cd ${answers.projectName}`));
|
|
85
|
+
console.log(chalk.white(" pnpm install"));
|
|
86
|
+
console.log(chalk.white(` pnpm ${appName} # ${appName} 앱 실행\n`));
|
|
87
|
+
console.log(chalk.dim(" 앱을 더 추가하려면 루트에서: bc add\n"));
|
|
88
|
+
|
|
89
|
+
printAiHints(answers.aiModel);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printAiHints(aiModel) {
|
|
93
|
+
console.log(chalk.dim(" AI 어시스턴트:"));
|
|
94
|
+
if (aiModel) {
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.dim(
|
|
97
|
+
` 이 프로젝트의 기본 모델 = ${aiModel} (bc.config.json 에 저장됨)`
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
console.log(
|
|
102
|
+
chalk.dim(" 모델 미설정 — `bc config set-model` 로 나중에 지정")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
console.log(
|
|
106
|
+
chalk.dim(
|
|
107
|
+
" API 키: bc config set-key anthropic (또는 ANTHROPIC_API_KEY 환경변수)"
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
console.log(chalk.dim(" 실행: bc chat\n"));
|
|
111
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseFigmaUrl } from '../figma/url.js';
|
|
2
|
+
import { saveProjectConfig } from './index.js';
|
|
3
|
+
|
|
4
|
+
function normalizeUrl(raw, kind) {
|
|
5
|
+
const value = raw.trim();
|
|
6
|
+
if (['off', 'none', 'clear'].includes(value.toLowerCase())) return null;
|
|
7
|
+
|
|
8
|
+
let url;
|
|
9
|
+
try {
|
|
10
|
+
url = new URL(value);
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(`${kind} 링크가 올바른 URL이 아닙니다.`);
|
|
13
|
+
}
|
|
14
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
15
|
+
throw new Error(`${kind} 링크는 http 또는 https URL이어야 합니다.`);
|
|
16
|
+
}
|
|
17
|
+
if (kind === 'Figma' && !parseFigmaUrl(value)) {
|
|
18
|
+
throw new Error('지원하는 Figma file/design/proto 링크가 아닙니다.');
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function updateProjectLink(cfg, kind, rawValue) {
|
|
24
|
+
if (!cfg.paths.projectFile) {
|
|
25
|
+
throw new Error('bc.config.json이 없습니다. 먼저 `bc adopt` 또는 `bc init`을 실행하세요.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const value = normalizeUrl(rawValue, kind === 'figma' ? 'Figma' : 'OpenAPI');
|
|
29
|
+
const project = {
|
|
30
|
+
...cfg.project,
|
|
31
|
+
design: { ...(cfg.project.design ?? {}) },
|
|
32
|
+
api: { ...(cfg.project.api ?? {}) },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (kind === 'figma') project.design.figma = value;
|
|
36
|
+
else project.api.openapi = value;
|
|
37
|
+
|
|
38
|
+
await saveProjectConfig(cfg.paths.projectFile, project);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...cfg,
|
|
42
|
+
project,
|
|
43
|
+
effective: {
|
|
44
|
+
...cfg.effective,
|
|
45
|
+
design: project.design,
|
|
46
|
+
api: project.api,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 모노레포(pnpm workspace) 관련 감지/유틸.
|
|
6
|
+
*
|
|
7
|
+
* - detectMonorepoRoot: 현재 위치에서 위로 올라가며 pnpm-workspace.yaml 을 찾는다.
|
|
8
|
+
* - detectScope: 기존 워크스페이스 패키지 이름에서 `@scope` 를 추출한다.
|
|
9
|
+
* - deriveScope: 모노레포 이름에서 scope 후보를 만든다 (marketd-frontend → marketd).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
async function exists(p) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(p);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readJsonSafe(file) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* pnpm-workspace.yaml 을 기준으로 모노레포 루트를 찾는다.
|
|
31
|
+
* @returns {Promise<string|null>} 루트 절대경로 또는 null
|
|
32
|
+
*/
|
|
33
|
+
export async function detectMonorepoRoot(startDir = process.cwd()) {
|
|
34
|
+
let dir = path.resolve(startDir);
|
|
35
|
+
// 파일시스템 루트까지 올라간다.
|
|
36
|
+
while (true) {
|
|
37
|
+
if (await exists(path.join(dir, 'pnpm-workspace.yaml'))) return dir;
|
|
38
|
+
const parent = path.dirname(dir);
|
|
39
|
+
if (parent === dir) return null;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 모노레포에서 사용 중인 npm scope (`@marketd` 의 marketd) 를 추론한다.
|
|
46
|
+
* apps/*, packages/* 의 package.json name 에서 우선 추출하고,
|
|
47
|
+
* 실패하면 루트 package.json name 을 fallback 으로 쓴다.
|
|
48
|
+
*/
|
|
49
|
+
export async function detectScope(root) {
|
|
50
|
+
const candidateDirs = ['packages', 'apps'];
|
|
51
|
+
for (const base of candidateDirs) {
|
|
52
|
+
const baseDir = path.join(root, base);
|
|
53
|
+
let entries = [];
|
|
54
|
+
try {
|
|
55
|
+
entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.isDirectory()) continue;
|
|
61
|
+
const pkg = await readJsonSafe(path.join(baseDir, entry.name, 'package.json'));
|
|
62
|
+
const name = pkg?.name;
|
|
63
|
+
if (typeof name === 'string' && name.startsWith('@')) {
|
|
64
|
+
const scope = name.slice(1).split('/')[0];
|
|
65
|
+
if (scope) return scope;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rootPkg = await readJsonSafe(path.join(root, 'package.json'));
|
|
71
|
+
if (rootPkg?.name) return deriveScope(rootPkg.name);
|
|
72
|
+
return 'workspace';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 모노레포 이름에서 scope 를 뽑아낸다.
|
|
77
|
+
* marketd-frontend → marketd
|
|
78
|
+
* acme-fe → acme
|
|
79
|
+
* @acme/repo → acme
|
|
80
|
+
*/
|
|
81
|
+
export function deriveScope(name) {
|
|
82
|
+
let base = String(name || '').trim();
|
|
83
|
+
if (base.startsWith('@')) base = base.slice(1).split('/')[0];
|
|
84
|
+
base = base
|
|
85
|
+
.replace(/-(frontend|front|fe|monorepo|mono|workspace|repo)$/i, '')
|
|
86
|
+
.replace(/[^a-z0-9-]/gi, '')
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
return base || 'workspace';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* apps/<name> 이 이미 존재하는지 확인.
|
|
93
|
+
*/
|
|
94
|
+
export async function appExists(root, appName) {
|
|
95
|
+
return exists(path.join(root, 'apps', appName));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { exists as pathExists, readJsonSafe };
|