sh-ui-cli 0.95.0 → 0.96.0
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/data/changelog/versions.json +13 -0
- package/package.json +1 -1
- package/src/create/describeTemplate.js +24 -3
- package/src/create/generator.js +191 -32
- package/src/create/index.mjs +4 -0
- package/src/mcp.mjs +11 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.96.0",
|
|
7
|
+
"date": "2026-05-15",
|
|
8
|
+
"title": "vite 스캐폴드 P0 fix — i18n + sentry plugins 패치 충돌 + create_project appName 인자",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**P0 — vite.config.ts plugins 배열 패치 anchor 경합 fix** — `--i18n react-i18next` 와 `--observability sentry` 를 동시에 켜면 sentry plugin 호출이 viteStaticCopy 의 `targets: [...]` 배열 안에 inject 되던 회귀 (피드백 #3). 두 patch 가 같은 정규식 anchor 를 두고 경합한 결과. balanced-bracket walker + top-level entry splitter 로 통합 (`appendVitePluginEntry` helper) — 두 옵션 모두 plugins 의 top-level entry 로 안전하게 append. all-options 동시 ON smoke 회귀 가드 추가.",
|
|
12
|
+
"**`sh_ui_create_project` 의 `appName` 인자** — monorepo 첫 앱 이름을 호출 시점에 지정. describe_template 의 동일 인자와 1:1 시그니처 일치. 기존엔 'web' 고정이라 사용자가 12+ 파일을 손 rename 해야 했음. CLI `--app` 플래그도 같이 노출. 미지정 시 기본 'web' (호환 유지).",
|
|
13
|
+
"**i18n 켜면 Home.tsx / App.tsx 자동 patch** — `useTranslation` import + `{t('greeting')}` 사용으로 변환. locale 시드 (ko='안녕하세요', en='Hello') 덕분에 첫 화면이 사용자 언어로 즉시 표시 — i18n 셋업 동작을 한눈에 확인 (피드백 #2/#3). fsd/flat × standalone/monorepo 4 조합 모두 처리.",
|
|
14
|
+
"**`sh_ui_describe_template` file plan 에 `.gitignore` mock-rename** — finalizeProject 가 fs 단계에서 `gitignore` → `.gitignore` 로 바꾸지만, describe_template 출력엔 그 변환이 반영 안 돼 점 없는 raw 파일명이 노출되던 buglet (피드백 #3) 수정. file plan ↔ 실제 emit 1:1 정합성 유지."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.96.0"
|
|
17
|
+
},
|
|
5
18
|
{
|
|
6
19
|
"version": "0.95.0",
|
|
7
20
|
"date": "2026-05-15",
|
package/package.json
CHANGED
|
@@ -400,9 +400,10 @@ function finalize(groups) {
|
|
|
400
400
|
for (let i = groups.length - 1; i >= 0; i--) {
|
|
401
401
|
const kept = [];
|
|
402
402
|
for (const p of groups[i].paths) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
const finalPath = applyFinalizeRenames(p);
|
|
404
|
+
if (!seen.has(finalPath)) {
|
|
405
|
+
seen.add(finalPath);
|
|
406
|
+
kept.push(finalPath);
|
|
406
407
|
}
|
|
407
408
|
}
|
|
408
409
|
groups[i] = {
|
|
@@ -417,3 +418,23 @@ function finalize(groups) {
|
|
|
417
418
|
files.sort();
|
|
418
419
|
return { files, groups: cleaned };
|
|
419
420
|
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* generator.js 의 `finalizeProject` 가 실제 fs 단계에서 적용하는 rename 을
|
|
424
|
+
* file plan 텍스트 레벨에서 mock-apply (v0.96.0+ — 피드백 #3 buglet).
|
|
425
|
+
*
|
|
426
|
+
* 현재 규칙:
|
|
427
|
+
* - basename 이 정확히 'gitignore' 인 경로 → '.gitignore' 로 prefix dot 추가.
|
|
428
|
+
* (npm publish 가 .gitignore 를 strip 하므로 템플릿엔 점 없이 두고 emit 후 dot-prefix.)
|
|
429
|
+
* - 이미 '.gitignore' 인 경로 (예: tauri-shell 의 src-tauri/.gitignore) 는 그대로.
|
|
430
|
+
*
|
|
431
|
+
* 미래에 다른 fs-level rename 이 추가되면 여기에 같이 등록 (describeTemplate 의
|
|
432
|
+
* file-plan ↔ create_project 의 실제 emit 1:1 정합성 유지).
|
|
433
|
+
*/
|
|
434
|
+
function applyFinalizeRenames(p) {
|
|
435
|
+
const slash = p.lastIndexOf('/');
|
|
436
|
+
const dir = slash === -1 ? '' : p.slice(0, slash + 1);
|
|
437
|
+
const base = slash === -1 ? p : p.slice(slash + 1);
|
|
438
|
+
if (base === 'gitignore') return dir + '.gitignore';
|
|
439
|
+
return p;
|
|
440
|
+
}
|
package/src/create/generator.js
CHANGED
|
@@ -372,6 +372,8 @@ export async function createProject(options = {}) {
|
|
|
372
372
|
i18n: options.i18n ?? 'none',
|
|
373
373
|
locales: options.locales ?? 'ko,en',
|
|
374
374
|
observability: options.observability ?? 'none',
|
|
375
|
+
appName: options.appName ?? null,
|
|
376
|
+
port: options.port ?? null,
|
|
375
377
|
});
|
|
376
378
|
}
|
|
377
379
|
|
|
@@ -422,7 +424,11 @@ export async function createProject(options = {}) {
|
|
|
422
424
|
if (projectType === 'standalone') {
|
|
423
425
|
await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch, themeBase);
|
|
424
426
|
} else {
|
|
425
|
-
await generateMonorepo(targetDir, projectName, plugins, {
|
|
427
|
+
await generateMonorepo(targetDir, projectName, plugins, {
|
|
428
|
+
yes: options.yes, theme, css: cssFramework, arch, themeBase,
|
|
429
|
+
appName: options.appName ?? null,
|
|
430
|
+
port: options.port ?? null,
|
|
431
|
+
});
|
|
426
432
|
}
|
|
427
433
|
|
|
428
434
|
await finalizeProject(targetDir, { dryRun: options.dryRun });
|
|
@@ -1144,6 +1150,162 @@ export function GlobalProvider({ children }: { children: ReactNode }) {
|
|
|
1144
1150
|
|
|
1145
1151
|
// vite.config.ts 에 vite-plugin-static-copy 삽입.
|
|
1146
1152
|
await patchViteConfigForI18n(targetDir, { i18nDirRel });
|
|
1153
|
+
|
|
1154
|
+
// 랜딩 컴포넌트 (Home.tsx 또는 fsd 의 src/app/App.tsx) 가 i18n 을 즉시 사용하도록 패치.
|
|
1155
|
+
// v0.94 에서 ko 시드가 '안녕하세요' 로 들어가니 첫 화면이 한국어로 떠 사용자가 i18n 동작을 즉시 인지 (피드백 #2/#3).
|
|
1156
|
+
await patchLandingForI18n(targetDir, { arch });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* i18n 활성화 시 첫 화면을 `useTranslation` 으로 바꾸어 'greeting' 키를 사용한다.
|
|
1161
|
+
* 이 patch 가 없으면 사용자가 i18n 셋업이 동작하는지 즉시 확인할 길이 없음.
|
|
1162
|
+
*
|
|
1163
|
+
* 대상 파일 (존재하는 첫 번째):
|
|
1164
|
+
* - fsd + vite-standalone: src/app/App.tsx (inline `<h1>Hello World</h1>`)
|
|
1165
|
+
* - fsd + vite-app(monorepo): src/Home.tsx (App.tsx 가 <Home /> import)
|
|
1166
|
+
* - flat: src/Home.tsx
|
|
1167
|
+
*
|
|
1168
|
+
* 이미 useTranslation 을 쓰고 있으면 no-op (재진입 안전).
|
|
1169
|
+
* 형태가 예상과 다르면 silent skip — 사용자 코드를 깨뜨리지 않는 게 우선.
|
|
1170
|
+
*/
|
|
1171
|
+
async function patchLandingForI18n(targetDir, { arch }) {
|
|
1172
|
+
const candidates =
|
|
1173
|
+
arch.name === 'fsd'
|
|
1174
|
+
? [
|
|
1175
|
+
path.join(targetDir, 'src/Home.tsx'), // vite-app monorepo
|
|
1176
|
+
path.join(targetDir, 'src/app/App.tsx'), // vite-standalone fsd
|
|
1177
|
+
]
|
|
1178
|
+
: [
|
|
1179
|
+
path.join(targetDir, 'src/Home.tsx'), // flat (vite-app + vite-standalone 공통)
|
|
1180
|
+
];
|
|
1181
|
+
|
|
1182
|
+
for (const filePath of candidates) {
|
|
1183
|
+
if (!(await fs.pathExists(filePath))) continue;
|
|
1184
|
+
let src = await fs.readFile(filePath, 'utf-8');
|
|
1185
|
+
if (src.includes('useTranslation')) continue; // 이미 적용됨
|
|
1186
|
+
|
|
1187
|
+
// 형태가 예상과 다르면 (Hello World 가 없으면) skip.
|
|
1188
|
+
if (!src.includes('Hello World')) continue;
|
|
1189
|
+
|
|
1190
|
+
// 1) import { useTranslation } from 'react-i18next' 를 import 블록 뒤에 (없으면 파일 맨 앞에) 추가.
|
|
1191
|
+
const i18nImport = `import { useTranslation } from 'react-i18next';\n`;
|
|
1192
|
+
const lastImportIdx = src.lastIndexOf('import ');
|
|
1193
|
+
if (lastImportIdx >= 0) {
|
|
1194
|
+
const importLineEnd = src.indexOf('\n', lastImportIdx) + 1;
|
|
1195
|
+
src = src.slice(0, importLineEnd) + i18nImport + src.slice(importLineEnd);
|
|
1196
|
+
} else {
|
|
1197
|
+
// import 없는 파일 (flat Home.tsx) — 파일 맨 앞에 import + 빈 줄.
|
|
1198
|
+
src = i18nImport + '\n' + src;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// 2) 컴포넌트 함수 본문 첫 줄에 `const { t } = useTranslation();` 추가.
|
|
1202
|
+
// export default function ... () { 또는 export default function ... () {\n...return (
|
|
1203
|
+
// 형태를 가정. 매치 실패 시 silent skip.
|
|
1204
|
+
const fnMatch = src.match(/export default function \w+\s*\([^)]*\)\s*\{\n/);
|
|
1205
|
+
if (!fnMatch) continue;
|
|
1206
|
+
const insertAt = fnMatch.index + fnMatch[0].length;
|
|
1207
|
+
src = src.slice(0, insertAt) + ` const { t } = useTranslation();\n` + src.slice(insertAt);
|
|
1208
|
+
|
|
1209
|
+
// 3) Hello World 텍스트를 {t('greeting')} 으로 치환.
|
|
1210
|
+
src = src.replace(/Hello World/g, "{t('greeting')}");
|
|
1211
|
+
|
|
1212
|
+
await fs.writeFile(filePath, src);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* vite.config.ts 의 `plugins: [ ... ]` 배열에서 top-level entry 들을 분리해
|
|
1218
|
+
* 새 entry 를 append 한 뒤 canonical 멀티라인 형태로 재방출한다.
|
|
1219
|
+
*
|
|
1220
|
+
* - balanced bracket 으로 outer `]` 를 찾으므로 `viteStaticCopy({ targets: [...] })`
|
|
1221
|
+
* 처럼 안쪽에 `[]` 가 있어도 안전.
|
|
1222
|
+
* - 두 개 이상의 patch (i18n + sentry 등) 가 같은 vite.config.ts 를 순차로
|
|
1223
|
+
* 건드릴 때 anchor 경합으로 entry 가 안쪽 배열에 inject 되던 v0.92~0.95 회귀의 원인을 제거.
|
|
1224
|
+
*
|
|
1225
|
+
* 입력 src 가 예상 형태가 아니면 (`plugins: [` 못 찾음 / unbalanced bracket)
|
|
1226
|
+
* `null` 을 반환 → 호출부에서 patch 포기.
|
|
1227
|
+
*
|
|
1228
|
+
* @param {string} src — vite.config.ts 전체 소스
|
|
1229
|
+
* @param {string} callSource — 추가할 plugin 호출 소스 (예: "viteStaticCopy({ ... })"). 선행 indent / 후행 comma 없이 entry 본문만.
|
|
1230
|
+
* @returns {string | null}
|
|
1231
|
+
*/
|
|
1232
|
+
function appendVitePluginEntry(src, callSource) {
|
|
1233
|
+
const openMatch = src.match(/plugins:\s*\[/);
|
|
1234
|
+
if (!openMatch) return null;
|
|
1235
|
+
|
|
1236
|
+
// plugins: 라인의 들여쓰기 추출 → 안쪽 entry 는 한 단계(=2 space) 더 깊게.
|
|
1237
|
+
const pluginsLineStart = src.lastIndexOf('\n', openMatch.index) + 1;
|
|
1238
|
+
const pluginsIndent = src.slice(pluginsLineStart, openMatch.index).match(/^[ \t]*/)[0];
|
|
1239
|
+
const entryIndent = pluginsIndent + ' ';
|
|
1240
|
+
|
|
1241
|
+
const arrStart = openMatch.index + openMatch[0].length; // `[` 직후
|
|
1242
|
+
// balanced bracket walk — `]` 깊이 0 지점이 outer 닫는 `]`.
|
|
1243
|
+
let depth = 1;
|
|
1244
|
+
let i = arrStart;
|
|
1245
|
+
let inStr = null;
|
|
1246
|
+
while (i < src.length && depth > 0) {
|
|
1247
|
+
const ch = src[i];
|
|
1248
|
+
if (inStr) {
|
|
1249
|
+
if (ch === '\\' && i + 1 < src.length) { i += 2; continue; }
|
|
1250
|
+
if (ch === inStr) inStr = null;
|
|
1251
|
+
i++;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; i++; continue; }
|
|
1255
|
+
if (ch === '[' || ch === '(' || ch === '{') depth++;
|
|
1256
|
+
else if (ch === ']' || ch === ')' || ch === '}') depth--;
|
|
1257
|
+
if (depth === 0) break;
|
|
1258
|
+
i++;
|
|
1259
|
+
}
|
|
1260
|
+
if (depth !== 0) return null;
|
|
1261
|
+
const arrEnd = i; // outer `]` 의 인덱스
|
|
1262
|
+
|
|
1263
|
+
const inner = src.slice(arrStart, arrEnd);
|
|
1264
|
+
const entries = splitTopLevelPluginEntries(inner);
|
|
1265
|
+
entries.push(callSource);
|
|
1266
|
+
|
|
1267
|
+
const rebuilt =
|
|
1268
|
+
'\n' +
|
|
1269
|
+
entries.map((e) => entryIndent + e).join(',\n') +
|
|
1270
|
+
',\n' +
|
|
1271
|
+
pluginsIndent;
|
|
1272
|
+
return src.slice(0, arrStart) + rebuilt + src.slice(arrEnd);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* plugins 배열 내부 문자열을 top-level `,` 기준으로 split.
|
|
1277
|
+
* `()` `[]` `{}` 와 문자열 리터럴 ('`, ', ") 안의 `,` 는 무시.
|
|
1278
|
+
*
|
|
1279
|
+
* @param {string} s
|
|
1280
|
+
* @returns {string[]}
|
|
1281
|
+
*/
|
|
1282
|
+
function splitTopLevelPluginEntries(s) {
|
|
1283
|
+
const entries = [];
|
|
1284
|
+
let depth = 0;
|
|
1285
|
+
let inStr = null;
|
|
1286
|
+
let buf = '';
|
|
1287
|
+
for (let i = 0; i < s.length; i++) {
|
|
1288
|
+
const ch = s[i];
|
|
1289
|
+
if (inStr) {
|
|
1290
|
+
buf += ch;
|
|
1291
|
+
if (ch === '\\' && i + 1 < s.length) { buf += s[i + 1]; i++; continue; }
|
|
1292
|
+
if (ch === inStr) inStr = null;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; buf += ch; continue; }
|
|
1296
|
+
if (ch === '(' || ch === '[' || ch === '{') { depth++; buf += ch; continue; }
|
|
1297
|
+
if (ch === ')' || ch === ']' || ch === '}') { depth--; buf += ch; continue; }
|
|
1298
|
+
if (ch === ',' && depth === 0) {
|
|
1299
|
+
const t = buf.trim();
|
|
1300
|
+
if (t) entries.push(t);
|
|
1301
|
+
buf = '';
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
buf += ch;
|
|
1305
|
+
}
|
|
1306
|
+
const t = buf.trim();
|
|
1307
|
+
if (t) entries.push(t);
|
|
1308
|
+
return entries;
|
|
1147
1309
|
}
|
|
1148
1310
|
|
|
1149
1311
|
/**
|
|
@@ -1167,22 +1329,17 @@ async function patchViteConfigForI18n(targetDir, { i18nDirRel }) {
|
|
|
1167
1329
|
const insertImportAt = src.indexOf('\n', lastImportIdx) + 1;
|
|
1168
1330
|
src = src.slice(0, insertImportAt) + importLine + '\n' + src.slice(insertImportAt);
|
|
1169
1331
|
|
|
1170
|
-
|
|
1332
|
+
// entry 본문만 넘기고 indent / 후행 comma 는 helper 가 붙인다.
|
|
1333
|
+
const pluginCall = `viteStaticCopy({
|
|
1171
1334
|
// i18n locale 파일을 public/locales 로 미러 — i18next-http-backend 의 loadPath 와 매칭.
|
|
1172
1335
|
targets: [
|
|
1173
1336
|
{ src: '${i18nDirRel}/locales/*', dest: 'locales' },
|
|
1174
1337
|
],
|
|
1175
|
-
})
|
|
1338
|
+
})`;
|
|
1176
1339
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const insertAt = pluginsMatch.index + pluginsMatch[0].length;
|
|
1181
|
-
src = src.slice(0, insertAt) + '\n' + pluginCall + src.slice(insertAt);
|
|
1182
|
-
} else {
|
|
1183
|
-
// 형태가 예상과 다르면 패치 포기 — config 가 깨지지 않도록.
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1340
|
+
const patched = appendVitePluginEntry(src, pluginCall);
|
|
1341
|
+
if (!patched) return; // 형태가 예상과 다르면 patch 포기 — config 가 깨지지 않도록.
|
|
1342
|
+
src = patched;
|
|
1186
1343
|
|
|
1187
1344
|
await fs.writeFile(viteConfigPath, src);
|
|
1188
1345
|
}
|
|
@@ -1353,30 +1510,29 @@ async function patchViteConfigForSentry(targetDir) {
|
|
|
1353
1510
|
}
|
|
1354
1511
|
|
|
1355
1512
|
if (!cfg.includes("sentryVitePlugin(")) {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1513
|
+
// i18n 의 viteStaticCopy 처럼 plugins 배열 안에 nested `[]` 가 있을 수 있으므로
|
|
1514
|
+
// balanced-bracket helper 로 outer entry append.
|
|
1515
|
+
const sentryCall = `sentryVitePlugin({
|
|
1516
|
+
org: process.env.SENTRY_ORG,
|
|
1517
|
+
project: process.env.SENTRY_PROJECT,
|
|
1518
|
+
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
1519
|
+
disable: !process.env.SENTRY_AUTH_TOKEN,
|
|
1520
|
+
})`;
|
|
1521
|
+
const patched = appendVitePluginEntry(cfg, sentryCall);
|
|
1522
|
+
if (patched) cfg = patched;
|
|
1363
1523
|
}
|
|
1364
1524
|
|
|
1365
1525
|
if (!cfg.includes("sourcemap:")) {
|
|
1366
1526
|
if (cfg.includes("server: {")) {
|
|
1367
1527
|
cfg = cfg.replace(/(\n\s*server:\s*\{)/, `\n build: { sourcemap: true },$1`);
|
|
1368
|
-
} else if (cfg.includes("plugins: [")) {
|
|
1369
|
-
cfg = cfg.replace(
|
|
1370
|
-
/(plugins:\s*\[[^\]]*?\][^,\n]*[,;]?\n)/s,
|
|
1371
|
-
`$1 build: { sourcemap: true },\n`,
|
|
1372
|
-
);
|
|
1373
1528
|
}
|
|
1529
|
+
// server 가 없는 변종은 build 삽입 위치가 모호 → 보수적으로 skip (사용자가 수동 추가).
|
|
1374
1530
|
}
|
|
1375
1531
|
|
|
1376
1532
|
await fs.writeFile(viteCfgPath, cfg);
|
|
1377
1533
|
}
|
|
1378
1534
|
|
|
1379
|
-
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
|
|
1535
|
+
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none', appName: appNameOpt = null, port: portOpt = null } = {}) {
|
|
1380
1536
|
await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
|
|
1381
1537
|
|
|
1382
1538
|
// Update root package.json
|
|
@@ -1400,16 +1556,19 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
1400
1556
|
}
|
|
1401
1557
|
await fs.writeJson(turboPath, turbo, { spaces: 2 });
|
|
1402
1558
|
|
|
1403
|
-
// Create first app
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1559
|
+
// Create first app — `appName` 인자가 주어지면 그대로, 아니면 yes 모드 default 'web' / 대화모드 prompt.
|
|
1560
|
+
// v0.96.0+ — describe_template 와 동일한 시그니처 (피드백 #3 의 API 일관성 항목).
|
|
1561
|
+
const appName = appNameOpt
|
|
1562
|
+
? validateProjectName(appNameOpt, 'appName')
|
|
1563
|
+
: (yes ? 'web' : await input({
|
|
1564
|
+
message: '첫 번째 앱 이름:',
|
|
1565
|
+
default: 'web',
|
|
1566
|
+
}));
|
|
1408
1567
|
|
|
1409
|
-
const port = yes ? '3000' : await input({
|
|
1568
|
+
const port = portOpt ?? (yes ? '3000' : await input({
|
|
1410
1569
|
message: '포트 번호:',
|
|
1411
1570
|
default: '3000',
|
|
1412
|
-
});
|
|
1571
|
+
}));
|
|
1413
1572
|
|
|
1414
1573
|
const appsDir = path.join(targetDir, 'apps', appName);
|
|
1415
1574
|
if (platform === 'vite') {
|
package/src/create/index.mjs
CHANGED
|
@@ -101,6 +101,10 @@ export async function runCreate(rest) {
|
|
|
101
101
|
i18n: flags.i18n,
|
|
102
102
|
locales: flags.locales,
|
|
103
103
|
observability: flags.observability,
|
|
104
|
+
// create 컨텍스트에서는 --app 이 첫 앱 이름 (monorepo). 같은 플래그가
|
|
105
|
+
// add-component 컨텍스트에서는 대상 앱 선택 — 의미가 컨텍스트마다 다름.
|
|
106
|
+
appName: flags.app,
|
|
107
|
+
port: flags.port,
|
|
104
108
|
yes: flags.yes,
|
|
105
109
|
dryRun: flags.dryRun,
|
|
106
110
|
});
|
package/src/mcp.mjs
CHANGED
|
@@ -483,6 +483,15 @@ export async function startMcpServer() {
|
|
|
483
483
|
"observability provider — platform=vite 일 때만 의미. 'sentry' 로 설정 시 @sentry/react + " +
|
|
484
484
|
"@sentry/vite-plugin 셋업 + SentryProvider + .env.example 자동 emit. GlitchTip self-hosted 도 같은 SDK — DSN 만 변경. 기본 'none'.",
|
|
485
485
|
),
|
|
486
|
+
// monorepo 첫 앱 이름 — describe_template 와 시그니처 1:1 일치 (v0.96.0+).
|
|
487
|
+
// standalone 일 땐 무시 (프로젝트 루트가 곧 앱). 미지정 시 'web'.
|
|
488
|
+
appName: z.string().min(1).optional()
|
|
489
|
+
.describe(
|
|
490
|
+
"monorepo 첫 앱 이름. structure=monorepo 일 때만 의미 — apps/{appName}/ + packages/ui/ui-apps/ui-{appName}/ 동시 생성. " +
|
|
491
|
+
"기본 'web'. describe_template 의 동일 인자와 1:1 대응 (v0.96.0+).",
|
|
492
|
+
),
|
|
493
|
+
port: z.string().optional()
|
|
494
|
+
.describe("monorepo 첫 앱의 dev 포트. 기본 '3000'. structure=monorepo 일 때만 의미."),
|
|
486
495
|
},
|
|
487
496
|
},
|
|
488
497
|
async (input) => {
|
|
@@ -560,6 +569,8 @@ export async function startMcpServer() {
|
|
|
560
569
|
i18n: input.i18n,
|
|
561
570
|
locales: input.locales,
|
|
562
571
|
observability: input.observability,
|
|
572
|
+
appName: input.appName,
|
|
573
|
+
port: input.port,
|
|
563
574
|
yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
|
|
564
575
|
}),
|
|
565
576
|
);
|