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 +8 -7
- package/bin/index.js +9 -1
- package/package.json +1 -1
- package/src/ai/models.js +21 -12
- package/src/commands/add.js +82 -0
- package/src/commands/adopt.js +1 -1
- package/src/commands/init.js +85 -36
- package/src/context/monorepo.js +98 -0
- package/src/generators/apiConventionDoc.js +3 -3
- package/src/generators/createApp.js +211 -0
- package/src/generators/createMonorepo.js +514 -0
- package/src/generators/createProject.js +1 -1
- package/src/generators/install.js +69 -0
- package/src/prompts/initPrompts.js +128 -0
- package/templates/conventions/api-codegen.md +99 -21
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
|
-
- 하나의
|
|
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-
|
|
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-
|
|
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-
|
|
373
|
-
| `claude-
|
|
374
|
-
| `claude-opus-4-
|
|
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
|
|
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
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
|
@@ -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,
|
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,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
|