sh-ui-cli 0.93.0 → 0.94.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.
@@ -2,6 +2,20 @@
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.94.0",
7
+ "date": "2026-05-15",
8
+ "title": "vite 스캐폴드 P0 fix 묶음 — i18n locale 자동 미러 / monorepo .gitignore / CLAUDE.md platform 분기",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**`--i18n` 시 vite-plugin-static-copy 자동 셋업** — emitI18n 이 `vite-plugin-static-copy` (devDeps) + `vite.config.ts` 의 plugins 배열 패치를 자동 처리. 사용자가 손으로 plugin 추가하지 않아도 dev/build 양쪽에서 `src/shared/i18n/locales/*` (또는 `src/lib/i18n/locales/*`) 가 `public/locales/*` 로 미러되어 `/locales/{{lng}}/{{ns}}.json` 404 없음. Tauri prod 빌드도 dist 에 자동 포함.",
12
+ "**Locale seed 콘텐츠 언어별 정합화** — ko = `{greeting: '안녕하세요', app_title: 'sh-ui 앱'}`, en = `{greeting: 'Hello', app_title: 'sh-ui app'}`, 그 외 locale 도 영어 placeholder 시드를 받아 translator 가 채울 키가 즉시 보임. 이전엔 fallback (보통 ko) 에만 영어 시드 + 나머지 빈 객체였음.",
13
+ "**monorepo `apps/<name>/gitignore` 도 dot-prefix** — finalizeProject 가 root 만 처리하던 동작을 recursive 로 변경. vite-app 처럼 sub-app 에 자체 gitignore 가 있는 케이스에서 점 없는 파일이 남아 node_modules / dist 가 git staged 될 위험 제거. 회귀 가드 smoke 추가.",
14
+ "**CLAUDE.md `{{PLATFORM_APP_DESCRIPTION}}` 분기** — monorepo CLAUDE.md 가 `apps/<name>` 을 'Next.js 앱' 으로 hardcode 하던 문제 해결. `--platform vite` 면 'Vite SPA' + Tauri 옵션 시 추가 문장이 자동 치환되어 AI 에이전트가 잘못된 컨벤션 (App Router/RSC) 으로 코드를 작성하는 회귀 차단.",
15
+ "**mes arch 옵션 docs 명시** — monorepo / nextjs-standalone CLAUDE.md 에 `--arch` 옵션 (`fsd` / `flat` / `mes`) 섹션 추가. 외부 에이전트가 `packages/eslint-config/mes.js` 를 deprecated 잔재로 오해하던 문제 해결 — mes 는 MES(Backoffice) 전용 별도 arch, 모두 emit 되는 건 라이브러리 형태이기 때문."
16
+ ],
17
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.94.0"
18
+ },
5
19
  {
6
20
  "version": "0.93.0",
7
21
  "date": "2026-05-14",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,6 +39,21 @@ export function getArchesForPlatform(platform) {
39
39
  return allArchitectures.filter((a) => a.platforms.includes(platform));
40
40
  }
41
41
 
42
+ /**
43
+ * MCP tool description / CLI --help / docs 어디서나 재사용 가능한 arch 설명 블록.
44
+ * "fsd (FSD) — ..., flat (Flat) — ..., mes (MES) — ..." 처럼 사람-읽기 좋은 한 줄로 직렬화.
45
+ *
46
+ * 외부 AI 에이전트 (Cursor / Codex / Copilot 등) 는 CLAUDE.md 를 읽지 않으므로
47
+ * MCP schema description 에 각 arch 의 의미를 노출해야 mes 같은 도메인-특화 arch
48
+ * 를 deprecated 잔재로 오해하지 않는다 (v0.94.0+).
49
+ */
50
+ export function describeArchOptions(platformFilter) {
51
+ const arches = platformFilter ? getArchesForPlatform(platformFilter) : allArchitectures;
52
+ return arches
53
+ .map((a) => `${a.name} (${a.label}) — ${a.description}`)
54
+ .join(' | ');
55
+ }
56
+
42
57
  /**
43
58
  * 주어진 arch 가 platform 과 호환되는지 검증. 호환 안 되면 친절한 에러.
44
59
  * generator/cli-args 양쪽에서 호출.
@@ -1046,9 +1046,9 @@ import HttpBackend from 'i18next-http-backend';
1046
1046
  import { initReactI18next } from 'react-i18next';
1047
1047
 
1048
1048
  // 클라이언트 측 lazy-load — 빌드 산출물에서 public/locales/{lng}/{ns}.json 경로로 fetch.
1049
- // dev 에선 vite ${i18nDirRel}/locales/* /locales 로 serve (vite.config 의 publicDir 기본 'public').
1050
- // 프로덕션 빌드 시 사용자가 vite-plugin-static-copy 등으로 public/locales 로 카피하거나
1051
- // 처음부터 public/locales 두면 됨. 디폴트 경로 ${i18nDirRel}/locales dev 편의용.
1049
+ // 원본 locale 파일은 ${i18nDirRel}/locales/* 두고, vite-plugin-static-copy
1050
+ // dev/build 양쪽에서 public/locales/*자동 미러링한다 (vite.config.ts 참고).
1051
+ // Tauri 빌드의 경우도 dist/locales 그대로 포함된다.
1052
1052
 
1053
1053
  i18n
1054
1054
  .use(HttpBackend)
@@ -1078,12 +1078,17 @@ export default i18n;
1078
1078
  `export { default } from './config';\n`,
1079
1079
  );
1080
1080
 
1081
+ // locale 별 시드 — 같은 key 를 모든 locale 에 emit (translator 가 무엇을 채워야 하는지 즉시 보임).
1082
+ // 핵심 locale (ko, en) 만 사람-언어 값, 그 외 locale 은 영어 placeholder.
1083
+ const seedByLocale = {
1084
+ ko: { greeting: '안녕하세요', app_title: 'sh-ui 앱' },
1085
+ en: { greeting: 'Hello', app_title: 'sh-ui app' },
1086
+ };
1087
+ const fallbackSeed = { greeting: 'Hello', app_title: 'sh-ui app' };
1081
1088
  for (const lng of locales) {
1082
1089
  const localeDir = path.join(i18nDir, 'locales', lng);
1083
1090
  await fs.ensureDir(localeDir);
1084
- const seed = lng === fallbackLng
1085
- ? { greeting: 'Hello World', app_title: 'sh-ui app' }
1086
- : {};
1091
+ const seed = seedByLocale[lng] ?? fallbackSeed;
1087
1092
  await fs.writeFile(
1088
1093
  path.join(localeDir, 'common.json'),
1089
1094
  JSON.stringify(seed, null, 2) + '\n',
@@ -1130,7 +1135,56 @@ export function GlobalProvider({ children }: { children: ReactNode }) {
1130
1135
  pkg.dependencies['i18next-http-backend'] = '^2.7.1';
1131
1136
  pkg.dependencies['react-i18next'] = '^15.1.0';
1132
1137
  pkg.dependencies = sortObjectKeys(pkg.dependencies);
1138
+ pkg.devDependencies = pkg.devDependencies ?? {};
1139
+ // dev/build 양쪽에서 src/shared/i18n/locales/* (또는 src/lib/i18n/locales/*) 를
1140
+ // public/locales/* 로 자동 미러 → i18next-http-backend 의 /locales/{{lng}}/{{ns}}.json 이 동작.
1141
+ pkg.devDependencies['vite-plugin-static-copy'] = '^2.2.0';
1142
+ pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
1133
1143
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1144
+
1145
+ // vite.config.ts 에 vite-plugin-static-copy 삽입.
1146
+ await patchViteConfigForI18n(targetDir, { i18nDirRel });
1147
+ }
1148
+
1149
+ /**
1150
+ * vite.config.ts 의 plugins 배열에 vite-plugin-static-copy 호출을 삽입한다.
1151
+ * i18n 의 locale 파일 (src/shared/i18n/locales/* 등) 을 public/locales/* 로 자동 미러.
1152
+ *
1153
+ * 이미 같은 호출이 있으면 no-op. parsing 실패하면 사용자에게 수동 작업을 알리고 abort 하지 않는다.
1154
+ */
1155
+ async function patchViteConfigForI18n(targetDir, { i18nDirRel }) {
1156
+ const viteConfigPath = path.join(targetDir, 'vite.config.ts');
1157
+ if (!(await fs.pathExists(viteConfigPath))) return;
1158
+ let src = await fs.readFile(viteConfigPath, 'utf-8');
1159
+
1160
+ if (src.includes('vite-plugin-static-copy')) {
1161
+ return; // 이미 셋업됨 (재진입 안전).
1162
+ }
1163
+
1164
+ const importLine = `import { viteStaticCopy } from 'vite-plugin-static-copy';`;
1165
+ // 다른 import 블록 뒤에 삽입.
1166
+ const lastImportIdx = src.lastIndexOf('import ');
1167
+ const insertImportAt = src.indexOf('\n', lastImportIdx) + 1;
1168
+ src = src.slice(0, insertImportAt) + importLine + '\n' + src.slice(insertImportAt);
1169
+
1170
+ const pluginCall = ` viteStaticCopy({
1171
+ // i18n locale 파일을 public/locales 로 미러 — i18next-http-backend 의 loadPath 와 매칭.
1172
+ targets: [
1173
+ { src: '${i18nDirRel}/locales/*', dest: 'locales' },
1174
+ ],
1175
+ }),`;
1176
+
1177
+ // plugins: [ ... ] 의 시작 직후에 새 plugin 삽입.
1178
+ const pluginsMatch = src.match(/plugins:\s*\[/);
1179
+ if (pluginsMatch) {
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
+ }
1186
+
1187
+ await fs.writeFile(viteConfigPath, src);
1134
1188
  }
1135
1189
 
1136
1190
  /**
@@ -1331,6 +1385,11 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
1331
1385
  rootPkg.name = projectName;
1332
1386
  await fs.writeJson(rootPkgPath, rootPkg, { spaces: 2 });
1333
1387
 
1388
+ // CLAUDE.md 의 platform 분기 placeholder 치환. AI 에이전트가 Next.js 가정으로
1389
+ // 잘못된 컨벤션을 적용하지 않도록 (v0.94.0+).
1390
+ const platformAppDesc = describeAppPlatform(platform, { tauri });
1391
+ await replaceInAllFiles(targetDir, '{{PLATFORM_APP_DESCRIPTION}}', platformAppDesc);
1392
+
1334
1393
  // Update turbo.json
1335
1394
  const turboPath = path.join(targetDir, 'turbo.json');
1336
1395
  const turbo = await fs.readJson(turboPath);
@@ -2149,11 +2208,9 @@ function buildErrorModuleCss() {
2149
2208
  * git init 은 dry-run 에서는 스킵하고, 실패해도(git 미설치 등) 조용히 넘어간다.
2150
2209
  */
2151
2210
  async function finalizeProject(targetDir, { dryRun = false } = {}) {
2152
- const noDot = path.join(targetDir, 'gitignore');
2153
- const withDot = path.join(targetDir, '.gitignore');
2154
- if (await fs.pathExists(noDot)) {
2155
- await fs.move(noDot, withDot, { overwrite: true });
2156
- }
2211
+ // 모노레포 / sub-app 까지 모든 `gitignore` 를 `.gitignore` 로 rename.
2212
+ // root 처리하면 apps/<name>/gitignore 가 그대로 남아 node_modules/dist 가 staged 된다 (v0.93.0 버그).
2213
+ await renameAllGitignoreRecursive(targetDir);
2157
2214
 
2158
2215
  if (dryRun) return;
2159
2216
 
@@ -2164,6 +2221,40 @@ async function finalizeProject(targetDir, { dryRun = false } = {}) {
2164
2221
  }
2165
2222
  }
2166
2223
 
2224
+ /**
2225
+ * CLAUDE.md 의 `{{PLATFORM_APP_DESCRIPTION}}` 치환용 문장 — AI 에이전트에게 어떤 플랫폼인지
2226
+ * 정확히 전달해서 잘못된 컨벤션 (예: vite 프로젝트에 App Router 가정) 적용 방지.
2227
+ */
2228
+ function describeAppPlatform(platform, { tauri = false } = {}) {
2229
+ if (platform === 'vite') {
2230
+ const tauriSuffix = tauri
2231
+ ? ' Tauri 데스크탑 셸이 동봉되어 있어 `src-tauri/` 가 native 진입점이다.'
2232
+ : '';
2233
+ return `Vite SPA (React + TypeScript). 라우트 + 비즈니스 로직. RSC / App Router 없음 — 모든 코드가 클라이언트 사이드 실행이다.${tauriSuffix}`;
2234
+ }
2235
+ // 디폴트: Next.js. 향후 platform 추가 시 분기 늘릴 것.
2236
+ return 'Next.js 앱 (App Router + Server Components). 라우트 + 비즈니스 로직.';
2237
+ }
2238
+
2239
+ async function renameAllGitignoreRecursive(dir) {
2240
+ let entries;
2241
+ try {
2242
+ entries = await fs.readdir(dir, { withFileTypes: true });
2243
+ } catch {
2244
+ return;
2245
+ }
2246
+ for (const entry of entries) {
2247
+ const fullPath = path.join(dir, entry.name);
2248
+ if (entry.isDirectory()) {
2249
+ // 스캐폴드 직후엔 node_modules / .git 가 없지만 방어적으로.
2250
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
2251
+ await renameAllGitignoreRecursive(fullPath);
2252
+ } else if (entry.name === 'gitignore') {
2253
+ await fs.move(fullPath, path.join(dir, '.gitignore'), { overwrite: true });
2254
+ }
2255
+ }
2256
+ }
2257
+
2167
2258
  async function replaceInAllFiles(dir, search, replace) {
2168
2259
  const entries = await fs.readdir(dir, { withFileTypes: true });
2169
2260
  for (const entry of entries) {
package/src/mcp.mjs CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  OBSERVABILITY_PROVIDERS,
52
52
  } from "./constants.js";
53
53
  import { allPlugins } from "./create/plugins/index.js";
54
- import { allArchitectures } from "./create/architectures/index.js";
54
+ import { allArchitectures, describeArchOptions } from "./create/architectures/index.js";
55
55
  import { describeTemplate } from "./create/describeTemplate.js";
56
56
  import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
57
57
  import { decodeTheme } from "./create/theme/decode.js";
@@ -386,9 +386,10 @@ export async function startMcpServer() {
386
386
  arch: z.enum(ARCH_NAMES).optional()
387
387
  .describe(
388
388
  `프로젝트 아키텍처 — 플랫폼별로 사용 가능한 값이 다름. ` +
389
- `현재 next 에서 사용 가능: ${allArchitectures.filter((a) => a.platforms.includes('next')).map((a) => a.name).join(', ')} (기본 fsd). ` +
390
- `flutter 는 현재 arch 디스크립터 없음 (미지정 또는 host 자체 default). ` +
391
- `arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능.`,
389
+ `사용 가능한 아키텍처 (의미 포함): ${describeArchOptions()}. ` +
390
+ `next 기본: fsd · vite 지원: fsd/flat · flutter 는 현재 arch 디스크립터 없음 (host 자체 default). ` +
391
+ `arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능. ` +
392
+ `mes 는 폐기된 옵션이 아니라 별도 도메인-특화 아키텍처 (관리자/MES 류).`,
392
393
  ),
393
394
  theme: z.string().optional()
394
395
  .describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
@@ -930,7 +931,11 @@ export async function startMcpServer() {
930
931
  structure: z.enum(CREATE_STRUCTURES).optional()
931
932
  .describe("Next.js 구조. platform=next | vite 일 때 의미. 기본 standalone"),
932
933
  arch: z.enum(ARCH_NAMES).optional()
933
- .describe("아키텍처. platform=next 일 때 의미. 기본 fsd"),
934
+ .describe(
935
+ `아키텍처. 기본 fsd. ` +
936
+ `옵션 (의미 포함): ${describeArchOptions()}. ` +
937
+ `mes 는 deprecated 가 아니라 도메인-특화 옵션.`,
938
+ ),
934
939
  plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
935
940
  .describe(`Next.js 플러그인 배열 (${PLUGIN_NAMES.join(', ')}). 미지정 빈 배열`),
936
941
  cssFramework: z.enum(CSS_FRAMEWORKS).optional()
@@ -5,13 +5,26 @@ sh-ui CLI 가 스캐폴드한 monorepo (Turborepo + pnpm workspace). AI 에이
5
5
 
6
6
  ## 구조
7
7
 
8
- - `apps/<name>/` — Next.js 앱. 라우트 + 비즈니스 로직.
8
+ - `apps/<name>/` — {{PLATFORM_APP_DESCRIPTION}}
9
9
  - `packages/ui/ui-core/` — 모든 앱이 공유하는 sh-ui 컴포넌트 / 훅 / 유틸 SoT.
10
10
  컴포넌트 추가는 여기에 한 번만.
11
11
  - `packages/ui/ui-apps/ui-<name>/` — 앱별 토큰 (color/spacing/font) 만 보관.
12
12
  컴포넌트는 두지 않음 (v0.65+ `tokens-only` 마커).
13
13
  - `packages/eslint-config/` · `packages/typescript-config/` — 공용 설정.
14
14
 
15
+ ## 아키텍처 옵션 (`--arch`)
16
+
17
+ - **`fsd`** (default) — Feature-Sliced Design. `src/{app,pages,widgets,features,entities,shared}`
18
+ 레이어로 단방향 의존(상위→하위). 일반적 SPA / 서비스에 적합.
19
+ - **`flat`** — `src/{components,hooks,lib,pages}` 단순 구조. 작은 앱 / 학습용.
20
+ - **`mes`** — MES (Backoffice) 전용. 페이지 격리 + 단방향 의존 강제. ERP/내부 관리도구
21
+ 처럼 페이지 간 분리도가 중요한 도메인. Next.js 만 지원 (vite-app 은 fsd/flat 만).
22
+
23
+ 세 arch 모두 `packages/eslint-config/` 에 별도 ruleset 으로 들어가 있다 — 라이브러리
24
+ 이므로 모두 emit 되지만, 본인 앱의 `eslint.config.js` 에서 선택한 arch 의 config 만
25
+ import 한다. 다른 arch 의 `.js` 파일이 보여도 deprecated 가 아니라 다른 앱이
26
+ 쓸 수 있는 옵션이다.
27
+
15
28
  ## 날짜 / 숫자 포맷
16
29
 
17
30
  - raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`
@@ -3,6 +3,16 @@
3
3
  sh-ui CLI 가 스캐폴드한 Next.js standalone 프로젝트. AI 에이전트 (Claude / Cursor /
4
4
  Codex 등) 가 이 파일을 컨텍스트로 읽고 아래 규칙을 따른다.
5
5
 
6
+ ## 아키텍처 옵션 (`--arch`)
7
+
8
+ - **`fsd`** (default) — Feature-Sliced Design. `src/{app,pages,widgets,features,entities,shared}`
9
+ 레이어로 단방향 의존(상위→하위). 일반적 SPA / 서비스에 적합.
10
+ - **`flat`** — `src/{components,hooks,lib}` 단순 구조. 작은 앱 / 학습용.
11
+ - **`mes`** — MES (Backoffice) 전용. 페이지 격리 + 단방향 의존 강제. ERP/내부 관리도구
12
+ 처럼 페이지 간 분리도가 중요한 도메인.
13
+
14
+ 선택한 arch 에 따라 `eslint.config.js` 가 다르게 emit 된다.
15
+
6
16
  ## 날짜 / 숫자 포맷
7
17
 
8
18
  - raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`