byuckchon-frontend-cli 1.9.2 → 1.9.4

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 CHANGED
@@ -180,10 +180,10 @@ FE 전반의 규칙(폴더 구조, 네이밍, 스웨거 → 코드 변환 규칙
180
180
  | 프레임워크 | 위치 |
181
181
  | --- | --- |
182
182
  | React (Vite/CRA 등) | `src/api/api-codegen.md` |
183
- | Next.js | `lib/api/api-codegen.md` |
183
+ | Next.js | `src/lib/api/api-codegen.md` |
184
184
 
185
185
  - 이 파일은 `bc.config.json` 의 `docs` 에 자동 등록되어 **chat 시작 시 주입**됩니다.
186
- - 하나의 .md React/Next 모두 다루며(차이는 위치뿐), 규칙에 맞게 직접 다듬어 쓰면 됩니다.
186
+ - 하나의 문서에 React axios 규칙과 Next.js의 fetch Server/Client 경계 규칙이 함께 들어갑니다.
187
187
  - 이미 파일이 있으면 덮어쓰지 않습니다.
188
188
 
189
189
  ### OpenAPI / 코드 컨텍스트 — 자동 주입 (v1.4+)
@@ -261,7 +261,7 @@ Figma·OpenAPI URL 만 추가로 묻고 `bc.config.json` 만 떨궈줍니다.
261
261
  bc # 인자 없이도 chat 진입 (제일 짧은 단축키)
262
262
  bc start # chat 의 alias
263
263
  bc chat # ink 풀 TUI (기본)
264
- bc chat --model claude-haiku-4 # 이번 세션만 모델 지정
264
+ bc chat --model claude-fable-5 # 이번 세션만 모델 지정
265
265
  bc chat --plain # 단순 readline 모드
266
266
  bc chat --once "useEffect 의존성 배열 누락된 거 어떻게 찾아?" # 1회성 호출 (CI/스크립트)
267
267
  bc chat -c # 가장 최근 세션 이어가기
@@ -346,7 +346,7 @@ bc config set-ui ink
346
346
  ```bash
347
347
  bc config show # 현재 적용 중인 설정 확인
348
348
  bc config set-model # 대화형 모델 선택
349
- bc config set-model claude-sonnet-4-5 # 직접 지정
349
+ bc config set-model claude-sonnet-5 # 직접 지정
350
350
  bc config set-key anthropic # 키 안전 입력 (가려짐)
351
351
  bc config set-key anthropic sk-ant-... # 직접 지정
352
352
  bc config set-gateway https://ai.example.com # 사내 게이트웨이 모드
@@ -369,9 +369,10 @@ bc config set-ui ink # 풀 TUI 복귀
369
369
 
370
370
  | id | provider | 추천 용도 |
371
371
  | ------------------- | --------- | ---------------------------------- |
372
- | `claude-sonnet-4-5` | anthropic | 기본 — 코드 Q&A, 리팩터, 컴포넌트 |
373
- | `claude-haiku-4` | anthropic | 짧은 작업, 커밋 메시지 (저렴) |
374
- | `claude-opus-4-5` | anthropic | 큰 리팩터, 아키텍처 설계 (고가) |
372
+ | `claude-sonnet-5` | anthropic | 기본 — 코드 Q&A, 리팩터, 컴포넌트 |
373
+ | `claude-sonnet-4-6` | anthropic | 안정 이전 세대 밸런스 |
374
+ | `claude-opus-4-8` | anthropic | 큰 리팩터, 아키텍처 설계 (고가) |
375
+ | `claude-fable-5` | anthropic | 짧은 작업, 커밋 메시지 (저렴·빠름) |
375
376
  | `gpt-5` | openai | 일반 코드 |
376
377
  | `gpt-5-mini` | openai | 저렴한 OpenAI |
377
378
 
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.js)')
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.2",
3
+ "version": "1.9.4",
4
4
  "description": "Byuckchon Frontend Workbench — project starter + AI chat + codebase RAG + OpenAPI codegen",
5
5
  "type": "module",
6
6
  "engines": {
package/src/ai/models.js CHANGED
@@ -9,32 +9,41 @@
9
9
  */
10
10
  export const MODEL_CATALOG = [
11
11
  {
12
- id: 'claude-sonnet-4-5',
13
- label: 'Claude Sonnet 4.5 (권장 · 코드 품질·속도 밸런스)',
12
+ id: 'claude-sonnet-5',
13
+ label: 'Claude Sonnet 5 (권장 · 코드 품질·속도 밸런스)',
14
14
  provider: 'anthropic',
15
- apiModel: 'claude-sonnet-4-5',
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-haiku-4',
22
- label: 'Claude Haiku 4 (저렴 · 짧은 작업·커밋 메시지)',
21
+ id: 'claude-sonnet-4-6',
22
+ label: 'Claude Sonnet 4.6 (안정 · 이전 세대 밸런스)',
23
23
  provider: 'anthropic',
24
- apiModel: 'claude-haiku-4',
24
+ apiModel: 'claude-sonnet-4-6',
25
25
  contextWindow: 200_000,
26
- pricing: { input: 0.8, output: 4, cachedInput: 0.08 },
27
- tier: 'fast',
26
+ pricing: { input: 3, output: 15, cachedInput: 0.3 },
27
+ tier: 'balanced',
28
28
  },
29
29
  {
30
- id: 'claude-opus-4-5',
31
- label: 'Claude Opus 4.5 (고성능 · 큰 리팩터·아키텍처)',
30
+ id: 'claude-opus-4-8',
31
+ label: 'Claude Opus 4.8 (고성능 · 큰 리팩터·아키텍처)',
32
32
  provider: 'anthropic',
33
- apiModel: 'claude-opus-4-5',
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-4-5';
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
+ }
@@ -145,7 +145,7 @@ export async function adoptCommand(opts = {}) {
145
145
  console.log(chalk.green(`\n ✓ ${CONFIG_PATHS.projectFileName} 작성 완료.`));
146
146
  console.log(chalk.dim(` ${targetFile}`));
147
147
 
148
- // API 코드 컨벤션 .md 를 API 루트(src/api | lib/api)에 깐다 (이미 있으면 유지).
148
+ // API 코드 컨벤션 .md 를 API 루트(src/api | src/lib/api)에 깐다 (이미 있으면 유지).
149
149
  try {
150
150
  const { relPath, written } = await scaffoldApiConventionDoc({
151
151
  projectRoot: cwd,
@@ -1,7 +1,13 @@
1
1
  import chalk from "chalk";
2
2
 
3
3
  import { createProject } from "../generators/createProject.js";
4
- import { askInitQuestions } from "../prompts/initPrompts.js";
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 answers = await askInitQuestions();
18
+ const projectType = await askProjectType();
13
19
 
14
- console.log(
15
- chalk.dim(
16
- `\n ${answers.framework} 프로젝트를 생성하는 중... (${answers.projectName})\n`
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
- console.log(
44
- chalk.dim(
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,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 };
@@ -9,15 +9,15 @@ const TEMPLATE_PATH = path.resolve(
9
9
 
10
10
  /**
11
11
  * 프레임워크에 맞는 API 루트 폴더.
12
- * - Next.js → lib/api
12
+ * - Next.js → src/lib/api
13
13
  * - 그 외 React 계열 → src/api
14
14
  */
15
15
  export function apiRootForFramework(framework) {
16
- return framework === 'next' ? 'lib/api' : 'src/api';
16
+ return framework === 'next' ? 'src/lib/api' : 'src/api';
17
17
  }
18
18
 
19
19
  /**
20
- * API 코드 컨벤션 .md 를 프로젝트의 API 루트(`src/api` 또는 `lib/api`)에 깐다.
20
+ * API 코드 컨벤션 .md 를 프로젝트의 API 루트(`src/api` 또는 `src/lib/api`)에 깐다.
21
21
  *
22
22
  * @param {object} args
23
23
  * @param {string} args.projectRoot