byuckchon-frontend-cli 1.9.4 → 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 +7 -4
- package/package.json +1 -1
- package/src/commands/adopt.js +19 -2
- package/src/commands/chat.js +28 -1
- package/src/config/projectLinks.js +49 -0
- package/src/generators/createBaseFiles.js +5 -0
- package/src/generators/createPackageJson.js +1 -1
- 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
|
|
|
@@ -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` 필요) |
|
package/package.json
CHANGED
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);
|
|
@@ -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
|
+
}
|
|
@@ -233,6 +233,9 @@ node_modules
|
|
|
233
233
|
dist
|
|
234
234
|
dist-ssr
|
|
235
235
|
*.local
|
|
236
|
+
*.md
|
|
237
|
+
!README.md
|
|
238
|
+
.bc/
|
|
236
239
|
.env
|
|
237
240
|
.env.production
|
|
238
241
|
.history
|
|
@@ -704,6 +707,8 @@ declare module '*.webp' {
|
|
|
704
707
|
// ─── 진입점 ───────────────────────────────────────────────────────────────────
|
|
705
708
|
|
|
706
709
|
export async function createBaseFiles(rootDir, config) {
|
|
710
|
+
await write(path.join(rootDir, ".env"), "FIGMA_TOKEN=\n");
|
|
711
|
+
|
|
707
712
|
if (config.framework === "react") {
|
|
708
713
|
await createReactBaseFiles(rootDir, config);
|
|
709
714
|
} else if (config.framework === "next") {
|
|
@@ -33,10 +33,10 @@ export async function createPackageJson(rootDir, config) {
|
|
|
33
33
|
'react-dom': isReact ? versions['react-dom'] : versions['next-react-dom'],
|
|
34
34
|
...(isReact ? {} : { next: versions.next }),
|
|
35
35
|
zustand: versions.zustand,
|
|
36
|
+
'@tanstack/react-query': versions['@tanstack/react-query'],
|
|
36
37
|
...(isReact
|
|
37
38
|
? {
|
|
38
39
|
axios: versions.axios,
|
|
39
|
-
'@tanstack/react-query': versions['@tanstack/react-query'],
|
|
40
40
|
}
|
|
41
41
|
: {}),
|
|
42
42
|
zod: versions.zod,
|
package/src/ui/ChatApp.js
CHANGED
|
@@ -16,6 +16,7 @@ import { searchIndex } from '../indexer/search.js';
|
|
|
16
16
|
import { loadIndex, buildIndex } from '../indexer/store.js';
|
|
17
17
|
import { listSessions } from '../history/store.js';
|
|
18
18
|
import { findLastRetryableUser } from '../history/management.js';
|
|
19
|
+
import { updateProjectLink } from '../config/projectLinks.js';
|
|
19
20
|
|
|
20
21
|
const h = React.createElement;
|
|
21
22
|
|
|
@@ -292,6 +293,8 @@ const SLASH_COMMANDS = [
|
|
|
292
293
|
{ cmd: '/history', hint: '', desc: '프로젝트의 이전 대화 목록' },
|
|
293
294
|
{ cmd: '/retry', hint: '', desc: '마지막 사용자 요청 다시 실행' },
|
|
294
295
|
{ cmd: '/model', hint: '<id>', desc: '세션 모델 변경 (인자 없으면 목록)' },
|
|
296
|
+
{ cmd: '/figma-link', hint: '<url|off>', desc: '프로젝트 Figma 링크 변경/해제' },
|
|
297
|
+
{ cmd: '/openapi-link', hint: '<url|off>', desc: '프로젝트 OpenAPI 링크 변경/해제' },
|
|
295
298
|
{ cmd: '/cost', hint: '', desc: '누적 토큰/비용' },
|
|
296
299
|
{ cmd: '/image', hint: '<path>', desc: '이미지 첨부 (Finder 에서 끌어다 놔도 됨)' },
|
|
297
300
|
{ cmd: '/paste', hint: '', desc: '클립보드의 이미지(스크린샷)를 첨부 — macOS' },
|
|
@@ -422,6 +425,7 @@ export function ChatApp({
|
|
|
422
425
|
const [indexProgress, setIndexProgress] = useState('');
|
|
423
426
|
const [ragEnabled, setRagEnabled] = useState(true);
|
|
424
427
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
428
|
+
const [inputRevision, setInputRevision] = useState(0);
|
|
425
429
|
const [historyItems, setHistoryItems] = useState([]);
|
|
426
430
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
427
431
|
const [historyDeleteId, setHistoryDeleteId] = useState(null);
|
|
@@ -438,6 +442,16 @@ export function ChatApp({
|
|
|
438
442
|
setMenuIndex(0);
|
|
439
443
|
}, [input]);
|
|
440
444
|
|
|
445
|
+
// showCursor=false인 ink-text-input은 외부에서 value가 바뀌어도 내부 cursorOffset을
|
|
446
|
+
// 갱신하지 않는다. 자동완성/초기화 때 재마운트해 커서를 새 문자열 끝으로 맞춘다.
|
|
447
|
+
const replaceInput = useCallback((value) => {
|
|
448
|
+
setInput(value);
|
|
449
|
+
setInputRevision((revision) => revision + 1);
|
|
450
|
+
}, []);
|
|
451
|
+
const completeSlash = useCallback((item) => {
|
|
452
|
+
replaceInput(expandSlash(item));
|
|
453
|
+
}, [replaceInput]);
|
|
454
|
+
|
|
441
455
|
// 인덱스 빌드 헬퍼 — 자동/수동 양쪽에서 공유.
|
|
442
456
|
const runIndexBuild = useCallback(
|
|
443
457
|
async ({ rebuild = false, silent = false } = {}) => {
|
|
@@ -587,13 +601,13 @@ export function ChatApp({
|
|
|
587
601
|
return;
|
|
588
602
|
}
|
|
589
603
|
if (key.escape) {
|
|
590
|
-
|
|
604
|
+
replaceInput('');
|
|
591
605
|
return;
|
|
592
606
|
}
|
|
593
607
|
// Tab 자동완성 — ink-text-input 이 Tab 을 자체 처리하지 않아 충돌 없음.
|
|
594
608
|
if (key.tab) {
|
|
595
609
|
const sel = slashItems[Math.min(menuIndex, slashItems.length - 1)];
|
|
596
|
-
if (sel)
|
|
610
|
+
if (sel) completeSlash(sel);
|
|
597
611
|
return;
|
|
598
612
|
}
|
|
599
613
|
});
|
|
@@ -676,6 +690,27 @@ export function ChatApp({
|
|
|
676
690
|
}
|
|
677
691
|
return true;
|
|
678
692
|
}
|
|
693
|
+
if (cmd === 'figma-link' || cmd === 'openapi-link') {
|
|
694
|
+
const kind = cmd === 'figma-link' ? 'figma' : 'openapi';
|
|
695
|
+
if (!arg) {
|
|
696
|
+
const current = kind === 'figma' ? cfg.effective.design?.figma : cfg.effective.api?.openapi;
|
|
697
|
+
pushSystemInfo(`현재 ${kind === 'figma' ? 'Figma' : 'OpenAPI'}: ${current ?? '(미설정)'}\n사용법: /${cmd} <url|off>`);
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const nextCfg = await updateProjectLink(cfg, kind, arg);
|
|
702
|
+
const label = kind === 'figma' ? 'Figma' : 'OpenAPI';
|
|
703
|
+
const value = kind === 'figma' ? nextCfg.effective.design?.figma : nextCfg.effective.api?.openapi;
|
|
704
|
+
const sessionRule = value
|
|
705
|
+
? `\n\n[프로젝트 설정 변경] ${label} 소스는 ${value} 이다. 이전 ${label} 링크와 요약은 무시하고 이 값을 사용하라.`
|
|
706
|
+
: `\n\n[프로젝트 설정 변경] ${label} 링크가 해제되었다. 이전 ${label} 링크와 요약은 사용하지 마라.`;
|
|
707
|
+
setCfg({ ...nextCfg, system: cfg.system + sessionRule });
|
|
708
|
+
pushSystemInfo(`${label} 링크를 ${value ? '변경했습니다: ' + value : '해제했습니다.'}`);
|
|
709
|
+
} catch (err) {
|
|
710
|
+
pushSystemError(err.message);
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
679
714
|
if (cmd === 'image') {
|
|
680
715
|
if (!arg) {
|
|
681
716
|
pushSystemError('사용법: /image <파일 경로> (Finder 에서 입력창 위로 끌어다 놓으면 경로가 자동 입력됩니다.)');
|
|
@@ -1006,25 +1041,28 @@ export function ChatApp({
|
|
|
1006
1041
|
|
|
1007
1042
|
const onSubmit = async (raw) => {
|
|
1008
1043
|
const line = raw.trim();
|
|
1044
|
+
const exactCommand = SLASH_COMMANDS.find((item) => item.cmd === line);
|
|
1045
|
+
|
|
1046
|
+
// 인자가 없는 명령은 메뉴 선택 상태와 관계없이 정확히 일치하면 즉시 실행한다.
|
|
1047
|
+
if (exactCommand && !exactCommand.hint) {
|
|
1048
|
+
replaceInput('');
|
|
1049
|
+
await handleSlash(exactCommand.cmd);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1009
1052
|
|
|
1010
|
-
//
|
|
1011
|
-
// 1) input 이 메뉴의 아이템 cmd 와 정확히 일치 + 인자 필요 없는 명령 → 즉시 실행
|
|
1012
|
-
// 2) 그 외 → 선택 항목으로 자동완성만 (실행은 Enter 한 번 더)
|
|
1053
|
+
// 메뉴에서 선택한 명령도 인자가 없으면 즉시 실행하고, 인자가 필요할 때만 자동완성한다.
|
|
1013
1054
|
if (slashOpen) {
|
|
1014
1055
|
const sel = slashItems[Math.min(menuIndex, slashItems.length - 1)];
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
setInput('');
|
|
1019
|
-
await handleSlash(exact.cmd);
|
|
1056
|
+
if (sel && !sel.hint) {
|
|
1057
|
+
replaceInput('');
|
|
1058
|
+
await handleSlash(sel.cmd);
|
|
1020
1059
|
return;
|
|
1021
1060
|
}
|
|
1022
|
-
|
|
1023
|
-
if (sel) setInput(expandSlash(sel));
|
|
1061
|
+
if (sel) completeSlash(sel);
|
|
1024
1062
|
return;
|
|
1025
1063
|
}
|
|
1026
1064
|
|
|
1027
|
-
|
|
1065
|
+
replaceInput('');
|
|
1028
1066
|
if (!line) return;
|
|
1029
1067
|
|
|
1030
1068
|
if (line.startsWith('/')) {
|
|
@@ -1077,6 +1115,7 @@ export function ChatApp({
|
|
|
1077
1115
|
h(Text, { color: 'magenta', bold: true }, 'you › '),
|
|
1078
1116
|
state === 'idle' && !historyOpen
|
|
1079
1117
|
? h(TextInput, {
|
|
1118
|
+
key: inputRevision,
|
|
1080
1119
|
value: input,
|
|
1081
1120
|
onChange: setInput,
|
|
1082
1121
|
onSubmit,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { versions } from '../constants/versions.js';
|
|
5
|
+
|
|
6
|
+
const execFile = promisify(execFileCallback);
|
|
7
|
+
|
|
8
|
+
const COMMON_DEPENDENCIES = ['@tanstack/react-query', 'zod'];
|
|
9
|
+
|
|
10
|
+
const installCommands = {
|
|
11
|
+
npm: ['npm', ['install']],
|
|
12
|
+
pnpm: ['pnpm', ['add']],
|
|
13
|
+
yarn: ['yarn', ['add']],
|
|
14
|
+
bun: ['bun', ['add']],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function hasDependency(pkg, packageName) {
|
|
18
|
+
return Boolean(
|
|
19
|
+
pkg?.dependencies?.[packageName] ||
|
|
20
|
+
pkg?.devDependencies?.[packageName] ||
|
|
21
|
+
pkg?.peerDependencies?.[packageName] ||
|
|
22
|
+
pkg?.optionalDependencies?.[packageName],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requiredDependenciesForFramework(framework) {
|
|
27
|
+
return framework === 'next'
|
|
28
|
+
? COMMON_DEPENDENCIES
|
|
29
|
+
: [...COMMON_DEPENDENCIES, 'axios'];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function ensureRequiredDependencies({
|
|
33
|
+
cwd,
|
|
34
|
+
pkg,
|
|
35
|
+
framework,
|
|
36
|
+
packageManager,
|
|
37
|
+
run = execFile,
|
|
38
|
+
}) {
|
|
39
|
+
const missing = requiredDependenciesForFramework(framework).filter(
|
|
40
|
+
(packageName) => !hasDependency(pkg, packageName),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!missing.length) {
|
|
44
|
+
return { installed: [], packageManager };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selectedPackageManager = installCommands[packageManager]
|
|
48
|
+
? packageManager
|
|
49
|
+
: 'npm';
|
|
50
|
+
const [command, baseArgs] = installCommands[selectedPackageManager];
|
|
51
|
+
const packageSpecs = missing.map(
|
|
52
|
+
(packageName) => `${packageName}@${versions[packageName]}`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await run(command, [...baseArgs, ...packageSpecs], { cwd });
|
|
56
|
+
|
|
57
|
+
return { installed: missing, packageManager: selectedPackageManager };
|
|
58
|
+
}
|