byuckchon-frontend-cli 1.9.3 → 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
@@ -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.3",
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": {
@@ -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-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
+ }
@@ -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 };
@@ -0,0 +1,211 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { versions } from '../constants/versions.js';
5
+ import { createFolders } from './createFolders.js';
6
+ import { createBaseFiles } from './createBaseFiles.js';
7
+ import { createBcConfig } from './createBcConfig.js';
8
+ import { scaffoldApiConventionDoc } from './apiConventionDoc.js';
9
+
10
+ /**
11
+ * 모노레포(pnpm workspace) 안의 apps/<name> 으로 React/Next 앱을 하나 만든다.
12
+ *
13
+ * 단일 프로젝트용 createProject 와 달리:
14
+ * - package.json name 이 `@<scope>/<app>` 로 스코프됨
15
+ * - 공유 프리셋(@<scope>/config-eslint, @<scope>/config-typescript)을 workspace 로 참조
16
+ * - eslint / tsconfig 를 모노레포 프리셋을 extends 하도록 재작성
17
+ * - .gitignore / .vscode / .prettierrc 는 루트가 담당하므로 앱에서는 제거
18
+ * - 의존성 설치(pnpm install)는 호출자가 루트에서 일괄 수행
19
+ *
20
+ * @param {object} args
21
+ * @param {string} args.appDir 앱 절대경로 (…/apps/<name>)
22
+ * @param {object} args.config { projectName, framework, aiModel, figmaUrl, openapiUrl }
23
+ * @param {string} args.scope npm scope (예: 'marketd' → @marketd/<app>)
24
+ */
25
+ export async function createApp({ appDir, config, scope }) {
26
+ await fs.mkdir(appDir, { recursive: true });
27
+
28
+ await createFolders(appDir, config);
29
+ await createBaseFiles(appDir, config);
30
+ await scaffoldApiConventionDoc({ projectRoot: appDir, framework: config.framework });
31
+ await createBcConfig(appDir, config);
32
+
33
+ await createAppPackageJson(appDir, config, scope);
34
+ await applyMonorepoConventions(appDir, config, scope);
35
+ }
36
+
37
+ async function write(filePath, content) {
38
+ await fs.writeFile(filePath, content, 'utf-8');
39
+ }
40
+
41
+ async function rm(target) {
42
+ await fs.rm(target, { recursive: true, force: true });
43
+ }
44
+
45
+ /**
46
+ * 워크스페이스 앱용 package.json. 모노레포에서는 React 버전을 하나로 통일하려고
47
+ * (pnpm hoist 충돌 방지) react/react-dom 모두 19 계열로 맞춘다.
48
+ */
49
+ async function createAppPackageJson(appDir, config, scope) {
50
+ const isReact = config.framework === 'react';
51
+ const name = `@${scope}/${config.projectName}`;
52
+
53
+ const scripts = isReact
54
+ ? {
55
+ dev: 'vite',
56
+ build: 'vite build',
57
+ typecheck: 'tsc --noEmit',
58
+ lint: 'eslint .',
59
+ preview: 'vite preview',
60
+ 'tokens:build': 'style-dictionary build --config token.config.js',
61
+ clean: 'rm -rf dist node_modules .turbo',
62
+ }
63
+ : {
64
+ dev: 'next dev',
65
+ build: 'next build',
66
+ start: 'next start',
67
+ typecheck: 'tsc --noEmit',
68
+ lint: 'eslint .',
69
+ 'tokens:build': 'style-dictionary build --config token.config.js',
70
+ clean: 'rm -rf .next node_modules .turbo',
71
+ };
72
+
73
+ const pkg = {
74
+ name,
75
+ version: '0.0.0',
76
+ private: true,
77
+ type: 'module',
78
+ scripts,
79
+ dependencies: {
80
+ react: versions['next-react'],
81
+ 'react-dom': versions['next-react-dom'],
82
+ ...(isReact ? {} : { next: versions.next }),
83
+ zustand: versions.zustand,
84
+ ...(isReact
85
+ ? {
86
+ axios: versions.axios,
87
+ '@tanstack/react-query': versions['@tanstack/react-query'],
88
+ }
89
+ : {}),
90
+ zod: versions.zod,
91
+ },
92
+ devDependencies: {
93
+ [`@${scope}/config-eslint`]: 'workspace:*',
94
+ [`@${scope}/config-typescript`]: 'workspace:*',
95
+ '@types/react': versions['@types/react'],
96
+ '@types/react-dom': versions['@types/react-dom'],
97
+ '@types/node': versions['@types/node'],
98
+ // 모노레포 앱은 flat config(@scope/config-eslint) 를 쓰므로 ESLint 9 필요.
99
+ // (단일 프로젝트의 eslintrc + eslint 8 과 별개)
100
+ eslint: '^9.18.0',
101
+ 'eslint-plugin-unused-imports': versions['eslint-plugin-unused-imports'],
102
+ prettier: versions.prettier,
103
+ 'prettier-plugin-tailwindcss': versions['prettier-plugin-tailwindcss'],
104
+ 'style-dictionary': versions['style-dictionary'],
105
+ tailwindcss: versions.tailwindcss,
106
+ typescript: versions.typescript,
107
+ ...(isReact
108
+ ? {
109
+ '@tailwindcss/vite': versions['@tailwindcss/vite'],
110
+ '@vitejs/plugin-react': versions['@vitejs/plugin-react'],
111
+ vite: versions.vite,
112
+ 'vite-plugin-svgr': versions['vite-plugin-svgr'],
113
+ }
114
+ : {
115
+ '@tailwindcss/postcss': versions['@tailwindcss/postcss'],
116
+ '@svgr/webpack': versions['@svgr/webpack'],
117
+ }),
118
+ },
119
+ };
120
+
121
+ await write(path.join(appDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
122
+ }
123
+
124
+ /**
125
+ * createBaseFiles 가 깔아둔 단일 프로젝트용 설정 파일을 모노레포 규칙으로 교체한다.
126
+ */
127
+ async function applyMonorepoConventions(appDir, config, scope) {
128
+ const isReact = config.framework === 'react';
129
+
130
+ // 루트에서 관리하는 설정들은 앱 레벨에서 제거.
131
+ await rm(path.join(appDir, '.eslintrc.cjs'));
132
+ await rm(path.join(appDir, '.prettierrc'));
133
+ await rm(path.join(appDir, '.gitignore'));
134
+ await rm(path.join(appDir, '.vscode'));
135
+
136
+ // 공유 ESLint 프리셋을 extends 하는 flat config.
137
+ await write(
138
+ path.join(appDir, 'eslint.config.mjs'),
139
+ `import { reactConfig } from '@${scope}/config-eslint/react';
140
+ import unusedImports from 'eslint-plugin-unused-imports';
141
+
142
+ export default [
143
+ ...reactConfig,
144
+ {
145
+ plugins: { 'unused-imports': unusedImports },
146
+ rules: {
147
+ 'no-unused-vars': 'off',
148
+ '@typescript-eslint/no-unused-vars': 'off',
149
+ 'unused-imports/no-unused-imports': 'error',
150
+ 'unused-imports/no-unused-vars': [
151
+ 'warn',
152
+ { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
153
+ ],
154
+ },
155
+ },
156
+ ];
157
+ `,
158
+ );
159
+
160
+ if (isReact) {
161
+ // createBaseFiles 의 프로젝트 레퍼런스형 tsconfig 3종을 공유 프리셋 extends 로 교체.
162
+ await rm(path.join(appDir, 'tsconfig.app.json'));
163
+ await rm(path.join(appDir, 'tsconfig.node.json'));
164
+ await write(
165
+ path.join(appDir, 'tsconfig.json'),
166
+ JSON.stringify(
167
+ {
168
+ extends: `@${scope}/config-typescript/react.json`,
169
+ compilerOptions: {
170
+ tsBuildInfoFile: './node_modules/.tmp/tsconfig.tsbuildinfo',
171
+ noUnusedLocals: true,
172
+ noUnusedParameters: true,
173
+ noUncheckedSideEffectImports: true,
174
+ moduleDetection: 'force',
175
+ baseUrl: '.',
176
+ paths: {
177
+ '@/*': ['src/*'],
178
+ '@icons/*': ['src/assets/icons/*'],
179
+ '@images/*': ['src/assets/images/*'],
180
+ },
181
+ },
182
+ include: ['src', 'vite.config.ts'],
183
+ },
184
+ null,
185
+ 2,
186
+ ) + '\n',
187
+ );
188
+ } else {
189
+ await write(
190
+ path.join(appDir, 'tsconfig.json'),
191
+ JSON.stringify(
192
+ {
193
+ extends: `@${scope}/config-typescript/next.json`,
194
+ compilerOptions: {
195
+ baseUrl: '.',
196
+ paths: { '@/*': ['./src/*'] },
197
+ },
198
+ include: [
199
+ 'next-env.d.ts',
200
+ '**/*.ts',
201
+ '**/*.tsx',
202
+ '.next/types/**/*.ts',
203
+ ],
204
+ exclude: ['node_modules'],
205
+ },
206
+ null,
207
+ 2,
208
+ ) + '\n',
209
+ );
210
+ }
211
+ }