sh-ui-cli 0.93.0 → 0.95.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,32 @@
|
|
|
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.95.0",
|
|
7
|
+
"date": "2026-05-15",
|
|
8
|
+
"title": "MCP describe_template 옵션 누락 fix + staleness 경고 — 외부 AI 가 stale MCP 알아채게",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`sh_ui_describe_template` 가 tauri/i18n/locales/observability 받음** — describeTemplate 자체는 옵션을 처리하지만 MCP 핸들러의 inputSchema 에 누락되어 외부 AI 가 옵션을 명시해도 무시됐다. preview 가 항상 기본 파일만 나열되던 회귀 (#3) 해결. create_project ↔ describe_template 파일 plan 1:1 일치 보장.",
|
|
12
|
+
"**MCP staleness 경고 자동 prepend** — 서버 start 시 https://registry.npmjs.org/sh-ui-cli/latest 비동기 조회 (3s timeout). 현재 버전보다 latest 가 높으면 모든 tool 응답 상단에 `⚠ sh-ui MCP X.Y.Z (latest: …)` 한 줄 prepend. 외부 AI 가 stale MCP 를 정상 출력으로 오해하던 회귀 (#8) 차단. 오프라인 / DNS 실패는 조용히 skip — best-effort.",
|
|
13
|
+
"**`SH_UI_SKIP_STALENESS_CHECK=1` 옵트아웃** — CI / 오프라인 / 사용자가 경고 끄고 싶을 때 env var 한 줄. test runner 가 외부 fetch 없이 결정적으로 돌게 함."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.95.0"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.94.0",
|
|
19
|
+
"date": "2026-05-15",
|
|
20
|
+
"title": "vite 스캐폴드 P0 fix 묶음 — i18n locale 자동 미러 / monorepo .gitignore / CLAUDE.md platform 분기",
|
|
21
|
+
"type": "minor",
|
|
22
|
+
"highlights": [
|
|
23
|
+
"**`--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 에 자동 포함.",
|
|
24
|
+
"**Locale seed 콘텐츠 언어별 정합화** — ko = `{greeting: '안녕하세요', app_title: 'sh-ui 앱'}`, en = `{greeting: 'Hello', app_title: 'sh-ui app'}`, 그 외 locale 도 영어 placeholder 시드를 받아 translator 가 채울 키가 즉시 보임. 이전엔 fallback (보통 ko) 에만 영어 시드 + 나머지 빈 객체였음.",
|
|
25
|
+
"**monorepo `apps/<name>/gitignore` 도 dot-prefix** — finalizeProject 가 root 만 처리하던 동작을 recursive 로 변경. vite-app 처럼 sub-app 에 자체 gitignore 가 있는 케이스에서 점 없는 파일이 남아 node_modules / dist 가 git staged 될 위험 제거. 회귀 가드 smoke 추가.",
|
|
26
|
+
"**CLAUDE.md `{{PLATFORM_APP_DESCRIPTION}}` 분기** — monorepo CLAUDE.md 가 `apps/<name>` 을 'Next.js 앱' 으로 hardcode 하던 문제 해결. `--platform vite` 면 'Vite SPA' + Tauri 옵션 시 추가 문장이 자동 치환되어 AI 에이전트가 잘못된 컨벤션 (App Router/RSC) 으로 코드를 작성하는 회귀 차단.",
|
|
27
|
+
"**mes arch 옵션 docs 명시** — monorepo / nextjs-standalone CLAUDE.md 에 `--arch` 옵션 (`fsd` / `flat` / `mes`) 섹션 추가. 외부 에이전트가 `packages/eslint-config/mes.js` 를 deprecated 잔재로 오해하던 문제 해결 — mes 는 MES(Backoffice) 전용 별도 arch, 모두 emit 되는 건 라이브러리 형태이기 때문."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.94.0"
|
|
30
|
+
},
|
|
5
31
|
{
|
|
6
32
|
"version": "0.93.0",
|
|
7
33
|
"date": "2026-05-14",
|
package/package.json
CHANGED
|
@@ -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 양쪽에서 호출.
|
package/src/create/generator.js
CHANGED
|
@@ -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
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
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
|
|
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
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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";
|
|
@@ -113,11 +113,66 @@ async function captureConsole(fn) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function textResult(text) {
|
|
116
|
-
return { content: [{ type: "text", text }] };
|
|
116
|
+
return { content: withStaleness([{ type: "text", text }]) };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function jsonResult(data) {
|
|
120
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
120
|
+
return { content: withStaleness([{ type: "text", text: JSON.stringify(data, null, 2) }]) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Staleness 경고 — npm registry 에서 latest sh-ui-cli 버전을 조회해 현재와 다르면
|
|
125
|
+
* 모든 tool 응답 상단에 한 줄 경고 prepend (v0.95.0+).
|
|
126
|
+
*
|
|
127
|
+
* 왜 필요: npx 가 sh-ui-cli 를 한 번 fetch 한 뒤 ~/.npm/_npx 캐시에 박아두면, 사용자
|
|
128
|
+
* `~/.claude.json` 에 `sh-ui-cli` (버전 비고정) 로 등록해도 MCP 가 오래된 버전으로
|
|
129
|
+
* 계속 실행된다. AI 에이전트는 "응답이 정상 응답" 으로 받아들이므로 stale 임을 의심
|
|
130
|
+
* 못 함 — 사용자 (ai-org 케이스) 가 직접 발견할 때까지 잘못된 plan 으로 작업.
|
|
131
|
+
*
|
|
132
|
+
* 모듈 변수 — startMcpServer 가 fire-and-forget 으로 채움. 첫 응답 직전에 미완료
|
|
133
|
+
* 면 경고 없이 진행 (best-effort).
|
|
134
|
+
*/
|
|
135
|
+
let STALE_WARNING = "";
|
|
136
|
+
|
|
137
|
+
function withStaleness(content) {
|
|
138
|
+
if (!STALE_WARNING) return content;
|
|
139
|
+
return [{ type: "text", text: STALE_WARNING }, ...content];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 비동기로 npm registry 조회 → 더 높은 버전이 있으면 STALE_WARNING 셋업.
|
|
144
|
+
* 3초 timeout · 모든 실패 (오프라인 / DNS / 차단) 는 조용히 무시.
|
|
145
|
+
*/
|
|
146
|
+
async function checkStaleness(currentVersion, cliName) {
|
|
147
|
+
try {
|
|
148
|
+
const ctrl = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => ctrl.abort(), 3000);
|
|
150
|
+
const res = await fetch(`https://registry.npmjs.org/${cliName}/latest`, {
|
|
151
|
+
signal: ctrl.signal,
|
|
152
|
+
});
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
if (!res.ok) return;
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
const latest = typeof data.version === "string" ? data.version : null;
|
|
157
|
+
if (!latest || latest === currentVersion) return;
|
|
158
|
+
if (!isSemverGreater(latest, currentVersion)) return;
|
|
159
|
+
STALE_WARNING =
|
|
160
|
+
`⚠ sh-ui MCP ${currentVersion} (latest: ${latest}) — stale. ` +
|
|
161
|
+
`최신 옵션·버그픽스를 받으려면: \n` +
|
|
162
|
+
` • ~/.claude.json (또는 다른 MCP 설정) 의 args 를 ["sh-ui-cli@latest", "mcp"] 로 변경 후 재시작\n` +
|
|
163
|
+
` • npx 캐시가 박혀 있으면 \`npx clear-npx-cache\` 한 번 실행`;
|
|
164
|
+
} catch {
|
|
165
|
+
// best-effort.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isSemverGreater(a, b) {
|
|
170
|
+
const parse = (s) => s.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
171
|
+
const [aMaj, aMin, aPatch] = parse(a);
|
|
172
|
+
const [bMaj, bMin, bPatch] = parse(b);
|
|
173
|
+
if (aMaj !== bMaj) return aMaj > bMaj;
|
|
174
|
+
if (aMin !== bMin) return aMin > bMin;
|
|
175
|
+
return aPatch > bPatch;
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
async function loadRegistry(platform) {
|
|
@@ -346,6 +401,13 @@ return (
|
|
|
346
401
|
|
|
347
402
|
export async function startMcpServer() {
|
|
348
403
|
const { version, name: cliName } = await readPackageMeta();
|
|
404
|
+
|
|
405
|
+
// fire-and-forget — server 가 첫 tool 요청 받기 전에 끝나면 경고 prepend, 아니면 다음 요청부터.
|
|
406
|
+
// env SH_UI_SKIP_STALENESS_CHECK=1 로 disable 가능 (CI / 오프라인 환경).
|
|
407
|
+
if (!process.env.SH_UI_SKIP_STALENESS_CHECK) {
|
|
408
|
+
void checkStaleness(version, cliName);
|
|
409
|
+
}
|
|
410
|
+
|
|
349
411
|
const server = new McpServer(
|
|
350
412
|
{ name: "sh-ui", version },
|
|
351
413
|
{
|
|
@@ -386,9 +448,10 @@ export async function startMcpServer() {
|
|
|
386
448
|
arch: z.enum(ARCH_NAMES).optional()
|
|
387
449
|
.describe(
|
|
388
450
|
`프로젝트 아키텍처 — 플랫폼별로 사용 가능한 값이 다름. ` +
|
|
389
|
-
|
|
390
|
-
`flutter 는 현재 arch 디스크립터 없음 (
|
|
391
|
-
`arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은
|
|
451
|
+
`사용 가능한 아키텍처 (의미 포함): ${describeArchOptions()}. ` +
|
|
452
|
+
`next 기본: fsd · vite 지원: fsd/flat · flutter 는 현재 arch 디스크립터 없음 (host 자체 default). ` +
|
|
453
|
+
`arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능. ` +
|
|
454
|
+
`mes 는 폐기된 옵션이 아니라 별도 도메인-특화 아키텍처 (관리자/MES 류).`,
|
|
392
455
|
),
|
|
393
456
|
theme: z.string().optional()
|
|
394
457
|
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
|
|
@@ -930,13 +993,27 @@ export async function startMcpServer() {
|
|
|
930
993
|
structure: z.enum(CREATE_STRUCTURES).optional()
|
|
931
994
|
.describe("Next.js 구조. platform=next | vite 일 때 의미. 기본 standalone"),
|
|
932
995
|
arch: z.enum(ARCH_NAMES).optional()
|
|
933
|
-
.describe(
|
|
996
|
+
.describe(
|
|
997
|
+
`아키텍처. 기본 fsd. ` +
|
|
998
|
+
`옵션 (의미 포함): ${describeArchOptions()}. ` +
|
|
999
|
+
`mes 는 deprecated 가 아니라 도메인-특화 옵션.`,
|
|
1000
|
+
),
|
|
934
1001
|
plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
|
|
935
1002
|
.describe(`Next.js 플러그인 배열 (${PLUGIN_NAMES.join(', ')}). 미지정 빈 배열`),
|
|
936
1003
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
937
1004
|
.describe("CSS 프레임워크. 기본 plain. css-modules 면 page.module.css 등 추가"),
|
|
938
1005
|
appName: z.string().optional()
|
|
939
1006
|
.describe("monorepo 첫 앱 이름. 기본 web"),
|
|
1007
|
+
// vite 전용 — sh_ui_create_project 의 동일 옵션과 1:1 대응. describe ↔ create 가 같은
|
|
1008
|
+
// file-plan 을 보장하려면 여기서 받아 describeTemplate 에 전달해야 한다 (v0.95.0+).
|
|
1009
|
+
tauri: z.boolean().optional()
|
|
1010
|
+
.describe("Tauri 2.x 데스크탑 셸 emit (platform=vite 전용). 기본 false"),
|
|
1011
|
+
i18n: z.enum(I18N_LIBRARIES).optional()
|
|
1012
|
+
.describe(`i18n 라이브러리 (platform=vite 전용). 옵션: ${I18N_LIBRARIES.join(', ')}. 기본 none`),
|
|
1013
|
+
locales: z.string().optional()
|
|
1014
|
+
.describe(`i18n 활성화 시 locale 코드 (comma-separated, 예: "ko,en"). 기본 "${I18N_DEFAULT_LOCALES}"`),
|
|
1015
|
+
observability: z.enum(OBSERVABILITY_PROVIDERS).optional()
|
|
1016
|
+
.describe(`observability 백엔드 (platform=vite 전용). 옵션: ${OBSERVABILITY_PROVIDERS.join(', ')}. 기본 none`),
|
|
940
1017
|
},
|
|
941
1018
|
},
|
|
942
1019
|
async (input) => {
|
|
@@ -948,6 +1025,10 @@ export async function startMcpServer() {
|
|
|
948
1025
|
plugins: input.plugins,
|
|
949
1026
|
cssFramework: input.cssFramework,
|
|
950
1027
|
appName: input.appName,
|
|
1028
|
+
tauri: input.tauri,
|
|
1029
|
+
i18n: input.i18n,
|
|
1030
|
+
locales: input.locales,
|
|
1031
|
+
observability: input.observability,
|
|
951
1032
|
});
|
|
952
1033
|
return jsonResult(result);
|
|
953
1034
|
} catch (e) {
|
|
@@ -5,13 +5,26 @@ sh-ui CLI 가 스캐폴드한 monorepo (Turborepo + pnpm workspace). AI 에이
|
|
|
5
5
|
|
|
6
6
|
## 구조
|
|
7
7
|
|
|
8
|
-
- `apps/<name>/` —
|
|
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()`
|