sh-ui-cli 0.78.0 → 0.80.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,34 @@
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.80.0",
7
+ "date": "2026-05-12",
8
+ "title": "describeTemplate — 프로젝트 생성 전 파일 트리 미리보기",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**`sh-ui-cli/api` 에 `describeTemplate()` 추가** — 옵션 조합(platform/structure/arch/plugins/cssFramework/appName) 으로 실제 생성 없이 emit 될 파일 경로 트리를 사전 계산. 베이스 템플릿 + arch 오버레이 + `plugin.files` + cssFramework 분기 + `plugin.transforms` (이동/삭제) 까지 정확히 반영. fs 접근 없는 순수 함수라 브라우저 번들 가능.",
12
+ "**`/create` 페이지 헤더에 파일 미리보기 별도 다이얼로그** — `FolderSearch` 아이콘 + 별도 진입. 옵션 picker(`ProjectOptionsForm` 으로 추출) + 요약/상세 토글 + 출처별 lucide 아이콘 분류(base=Package, arch=Layers, sentry=ShieldAlert, intl=Languages, …) + hover/진행막대 시각화. 상세 트리는 폴더/파일 lucide 아이콘 + 컴팩트 indent.",
13
+ "**상세 트리에서 파일 클릭 → 내용 viewer** — `/api/template-content` route 가 lazy fetch. 베이스/arch 오버레이 디스크 파일은 readFile, plugin.files 는 plugin 모듈 호출 결과, cssFramework 동적 변종은 placeholder. 출처 라벨 표시.",
14
+ "**MCP `sh_ui_describe_template` 툴 노출** — \"플러그인 켜면 어떤 파일 추가돼?\" 류 질문에 IDE-내 에이전트가 실제 스캐폴드 없이 답할 수 있게.",
15
+ "**docs UI raw 컴포넌트 일소** — `/create` 헤더 / TokenEditor / ShowcasePicker / ShowcaseCanvas / ExportBlock / ShadowBuilder / GradientBuilder / 검색 다이얼로그 / examples 갤러리의 raw `<button>`·`<input>` 을 sh-ui `Button`/`Input` 으로 통일. 풀 커스텀 className 영역도 `Button + className` 조합으로 base 동작(hover/focus/active) 받으면서 디자인 유지.",
16
+ "**빌드 인프라** — `scripts/build-template-manifest.mjs` 가 `packages/cli/templates/` 를 스캔해 `src/create/templateManifest.js` 로 emit (pretest / prepublishOnly 에서 자동 실행). 템플릿 변경 시 drift 없음."
17
+ ],
18
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.80.0"
19
+ },
20
+ {
21
+ "version": "0.79.0",
22
+ "date": "2026-05-12",
23
+ "title": "no-restricted toLocaleDateString — SSR hydration mismatch 회귀 가드",
24
+ "type": "minor",
25
+ "highlights": [
26
+ "**ESLint `no-restricted-syntax` 룰 추가** — 인자 0개의 `.toLocaleDateString()` / `.toLocaleString()` / `.toLocaleTimeString()` 호출을 error 로 차단. Node 기본 로케일(en-US) 과 브라우저 로케일(ko-KR) 이 달라 SSR 출력이 hydrate 시 mismatch 나던 함정의 회귀 가드.",
27
+ "**대체 경로 안내가 에러 메시지에 박힘** — `@/src/shared/lib/formatDate` 의 `formatDate` / `formatDateTime` (default `ko-KR`), next-intl locale 추종이면 `useFormatDate` 훅, 숫자는 `formatPrice`. AST selector 가 `arguments.length === 0` 만 매칭하므로 의도된 locale 명시 호출 (`toLocaleDateString('ko-KR', { ... })`) 은 통과.",
28
+ "**적용 범위** — monorepo `@workspace/eslint-config/base` (next.js · react-internal 등 모든 워크스페이스 config 가 상속) + `nextjs-standalone` 의 fsd / flat / mes arch 오버레이 3종.",
29
+ "**템플릿 CLAUDE.md 추가** — `nextjs-standalone` / `monorepo` 시작 템플릿에 `CLAUDE.md` 신규. 날짜·숫자 포맷 컨벤션 + sh-ui 컴포넌트 우선 + 토큰 사용 원칙 명시. AI 에이전트가 코드 작성 시점에 컨텍스트로 읽고 미리 피할 수 있게."
30
+ ],
31
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.79.0"
32
+ },
5
33
  {
6
34
  "version": "0.78.0",
7
35
  "date": "2026-05-12",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.78.0",
3
+ "version": "0.80.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,13 +44,15 @@
44
44
  "./api": {
45
45
  "types": "./src/api.d.ts",
46
46
  "default": "./src/api.js"
47
- }
47
+ },
48
+ "./package.json": "./package.json"
48
49
  },
49
50
  "scripts": {
50
51
  "bundle-data": "node scripts/copy-data.mjs",
51
- "pretest": "node scripts/copy-data.mjs",
52
+ "build:manifest": "node scripts/build-template-manifest.mjs",
53
+ "pretest": "node scripts/build-template-manifest.mjs && node scripts/copy-data.mjs",
52
54
  "test": "vitest run",
53
- "prepublishOnly": "node scripts/copy-data.mjs && node --check bin/sh-ui.mjs"
55
+ "prepublishOnly": "node scripts/build-template-manifest.mjs && node scripts/copy-data.mjs && node --check bin/sh-ui.mjs"
54
56
  },
55
57
  "files": [
56
58
  "bin",
package/src/api.d.ts CHANGED
@@ -120,3 +120,51 @@ export interface ThemePreset {
120
120
 
121
121
  export const THEME_PRESETS: Record<ThemePresetName, ThemePreset>;
122
122
  export const THEME_PRESET_NAMES: readonly ThemePresetName[];
123
+
124
+ /* ─────── 템플릿 트리 미리보기 ─────── */
125
+
126
+ /**
127
+ * 옵션 조합으로 프로젝트 생성 시 어떤 파일이 emit 되는지 사전 계산.
128
+ * fs 접근 없는 순수 함수 — 브라우저 사용 가능.
129
+ */
130
+ export interface DescribeTemplateOptions {
131
+ platform?: CreatePlatform;
132
+ /** next 일 때만 의미. */
133
+ structure?: CreateStructure;
134
+ /** next 일 때 'fsd' | 'flat' | 'mes'. */
135
+ arch?: string;
136
+ /** 플러그인 name 배열. */
137
+ plugins?: string[];
138
+ cssFramework?: CssFrameworkSupported;
139
+ projectName?: string;
140
+ /** monorepo 첫 앱 이름. 기본 'web'. */
141
+ appName?: string;
142
+ }
143
+
144
+ export interface DescribeTemplateGroup {
145
+ /** 'base' | 'arch' | `plugin-${name}` | 'css' | 'transform' | 'monorepo' | 'ui-app' | `app-${id}` */
146
+ id: string;
147
+ label: string;
148
+ paths: string[];
149
+ }
150
+
151
+ export interface DescribeTemplateResult {
152
+ /** 모든 파일 경로 (POSIX, 정렬). */
153
+ files: string[];
154
+ /** 출처별 분류 — 빈 그룹은 제외. */
155
+ groups: DescribeTemplateGroup[];
156
+ }
157
+
158
+ export function describeTemplate(
159
+ options?: DescribeTemplateOptions,
160
+ ): DescribeTemplateResult;
161
+
162
+ /**
163
+ * 빌드 타임에 emit 되는 raw 템플릿 인덱스. describeTemplate() 가 내부적으로 사용.
164
+ * 외부 노출은 디버깅 / 마이그레이션 도구용.
165
+ */
166
+ export interface TemplateManifestEntry {
167
+ base: string[];
168
+ arches?: Record<string, string[]>;
169
+ }
170
+ export const TEMPLATE_MANIFEST: Record<string, TemplateManifestEntry>;
package/src/api.js CHANGED
@@ -31,3 +31,5 @@ export {
31
31
  isKnownArch,
32
32
  } from './create/architectures/index.js';
33
33
  export { THEME_PRESETS, THEME_PRESET_NAMES } from './create/theme/presets.js';
34
+ export { describeTemplate } from './create/describeTemplate.js';
35
+ export { TEMPLATE_MANIFEST } from './create/templateManifest.js';
@@ -0,0 +1,274 @@
1
+ /**
2
+ * describeTemplate — "옵션 조합으로 프로젝트 만들면 어떤 파일이 생기는가" 를
3
+ * 실제 생성 없이 계산. apps/docs 의 CreateProjectDialog 가 옵션 토글 시 트리
4
+ * 미리보기를 그리는 데 사용. MCP 툴 `sh_ui_describe_template` 도 동일 함수.
5
+ *
6
+ * 입력: createProject() 가 받는 옵션의 부분집합 + appName(monorepo).
7
+ * 출력: { files: string[], groups: Group[] }
8
+ * - files: 모든 파일 경로(POSIX) 정렬
9
+ * - groups: 출처별 분류 — 'base'/'arch'/'plugin-*'/'css'/'transform' 등
10
+ *
11
+ * 정확도 계약:
12
+ * - 베이스 템플릿 + arch 오버레이 + plugin.files + cssFramework 분기 + plugin.transforms
13
+ * (move/delete) 까지 반영. plugin.transforms 의 replace 는 내용만 바꾸므로 트리 영향 X.
14
+ * - generator.js 가 emit 하는 *파일 경로* 와 1:1 일치하는 것을 목표. content 차이는 무시.
15
+ *
16
+ * 순수성: fs 접근 없음. templateManifest.js (build-template-manifest.mjs 가 emit) 와
17
+ * plugins/architectures 디스크립터만 사용. 브라우저 번들 가능.
18
+ */
19
+
20
+ import { TEMPLATE_MANIFEST } from './templateManifest.js';
21
+ import {
22
+ getArchByName,
23
+ DEFAULT_ARCH,
24
+ isKnownArch,
25
+ } from './architectures/index.js';
26
+ import { getPluginsByNames } from './plugins/index.js';
27
+ import { CSS_FRAMEWORK_DEFAULT } from '../constants.js';
28
+
29
+ /**
30
+ * @typedef {Object} DescribeOptions
31
+ * @property {'next'|'flutter'} [platform]
32
+ * @property {'standalone'|'monorepo'} [structure] next 일 때만 의미
33
+ * @property {string} [arch] next 일 때 'fsd'|'flat'|'mes'
34
+ * @property {string[]} [plugins] ['sentry', 'next-intl', 'auth-jwt']
35
+ * @property {'tailwind'|'plain'|'css-modules'} [cssFramework]
36
+ * @property {string} [projectName]
37
+ * @property {string} [appName] monorepo 첫 앱 이름. 기본 'web'
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} Group
42
+ * @property {string} id 'base' | 'arch' | `plugin-${name}` | 'css' | 'transform' | 'monorepo' | 'ui-app' | `app-${id}`
43
+ * @property {string} label 사용자가 보는 한국어 라벨
44
+ * @property {string[]} paths 이 그룹에 귀속된 파일 경로 (정렬)
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} DescribeResult
49
+ * @property {string[]} files 전체 파일 경로 정렬
50
+ * @property {Group[]} groups
51
+ */
52
+
53
+ /**
54
+ * @param {DescribeOptions} [opts]
55
+ * @returns {DescribeResult}
56
+ */
57
+ export function describeTemplate(opts = {}) {
58
+ const {
59
+ platform = 'next',
60
+ structure = 'standalone',
61
+ arch: archName = DEFAULT_ARCH,
62
+ plugins: pluginNames = [],
63
+ cssFramework = CSS_FRAMEWORK_DEFAULT,
64
+ appName: rawAppName = 'web',
65
+ } = opts;
66
+
67
+ if (platform === 'flutter') {
68
+ const base = TEMPLATE_MANIFEST['flutter-standalone'].base;
69
+ return finalize([
70
+ makeGroup('base', 'Flutter 베이스', base),
71
+ ]);
72
+ }
73
+
74
+ // platform === 'next'
75
+ const safeArchName = isKnownArch(archName) ? archName : DEFAULT_ARCH;
76
+ const archObj = getArchByName(safeArchName);
77
+ const plugins = getPluginsByNames(pluginNames).sort(
78
+ (a, b) => (a.priority ?? 0) - (b.priority ?? 0),
79
+ );
80
+
81
+ if (structure === 'standalone') {
82
+ const groups = buildNextGroups({
83
+ prefix: '',
84
+ templateKey: 'nextjs-standalone',
85
+ arch: archObj,
86
+ plugins,
87
+ cssFramework,
88
+ });
89
+ return finalize(groups);
90
+ }
91
+
92
+ // monorepo
93
+ const appName = rawAppName || 'web';
94
+ const groups = [];
95
+
96
+ groups.push(makeGroup(
97
+ 'monorepo',
98
+ '모노레포 루트',
99
+ TEMPLATE_MANIFEST['monorepo'].base.slice(),
100
+ ));
101
+
102
+ const appGroups = buildNextGroups({
103
+ prefix: `apps/${appName}/`,
104
+ templateKey: 'nextjs-app',
105
+ arch: archObj,
106
+ plugins,
107
+ cssFramework,
108
+ });
109
+ for (const g of appGroups) {
110
+ g.id = `app-${g.id}`;
111
+ g.label = `apps/${appName} — ${g.label}`;
112
+ groups.push(g);
113
+ }
114
+
115
+ groups.push(makeGroup(
116
+ 'ui-app',
117
+ `packages/ui/ui-apps/ui-${appName}`,
118
+ TEMPLATE_MANIFEST['ui-app-template'].base.map(
119
+ (p) => `packages/ui/ui-apps/ui-${appName}/${p}`,
120
+ ),
121
+ ));
122
+
123
+ return finalize(groups);
124
+ }
125
+
126
+ /** next 베이스 + arch + plugin.files + css 분기 + plugin.transforms 적용. */
127
+ function buildNextGroups({ prefix, templateKey, arch, plugins, cssFramework }) {
128
+ const tpl = TEMPLATE_MANIFEST[templateKey];
129
+ if (!tpl) {
130
+ throw new Error(`describeTemplate: 알 수 없는 템플릿 키 '${templateKey}'`);
131
+ }
132
+
133
+ const groups = [];
134
+
135
+ groups.push(makeGroup(
136
+ 'base',
137
+ `${templateKey} 베이스`,
138
+ tpl.base.map((p) => prefix + p),
139
+ ));
140
+
141
+ const archFiles = tpl.arches?.[arch.name] ?? [];
142
+ groups.push(makeGroup(
143
+ 'arch',
144
+ `${arch.label} 오버레이`,
145
+ archFiles.map((p) => prefix + p),
146
+ ));
147
+
148
+ // plugin.files — arch-aware. 정적이면 그대로, 함수면 호출.
149
+ for (const plugin of plugins) {
150
+ const filesField = resolveArchField(plugin.files, arch);
151
+ if (!filesField) continue;
152
+ const paths = Object.keys(filesField).map((p) => prefix + p);
153
+ if (paths.length === 0) continue;
154
+ groups.push(makeGroup(
155
+ `plugin-${plugin.name}`,
156
+ `+ ${plugin.label}`,
157
+ paths,
158
+ ));
159
+ }
160
+
161
+ // cssFramework: css-modules 면 page.module.css / error.module.css 추가.
162
+ // generator.js applyCssFrameworkVariant 와 동일한 분기 — intl 활성 시 [locale]/, 아니면 app/.
163
+ if (cssFramework === 'css-modules') {
164
+ const intlActive = plugins.some((p) => p.name === 'next-intl');
165
+ const sentryActive = plugins.some((p) => p.name === 'sentry');
166
+ const cssPaths = [];
167
+ const pageDir = intlActive ? 'app/[locale]' : 'app';
168
+ cssPaths.push(`${prefix}${pageDir}/page.module.css`);
169
+ if (sentryActive) {
170
+ cssPaths.push(`${prefix}${pageDir}/error.module.css`);
171
+ }
172
+ groups.push(makeGroup(
173
+ 'css',
174
+ 'CSS: css-modules 변종',
175
+ cssPaths,
176
+ ));
177
+ }
178
+
179
+ // plugin.transforms — move/delete 만 트리에 영향. replace 는 content-only.
180
+ const moves = [];
181
+ const deletes = [];
182
+ for (const plugin of plugins) {
183
+ const tr = resolveArchField(plugin.transforms, arch);
184
+ if (!tr) continue;
185
+ for (const t of tr) {
186
+ if (t.type === 'move') moves.push({ from: prefix + t.from, to: prefix + t.to });
187
+ if (t.type === 'delete') deletes.push(prefix + t.path);
188
+ }
189
+ }
190
+
191
+ if (moves.length || deletes.length) {
192
+ const added = applyMovesAndDeletes(groups, moves, deletes);
193
+ if (added.length) {
194
+ groups.push(makeGroup(
195
+ 'transform',
196
+ '플러그인 transform (이동/생성)',
197
+ added,
198
+ ));
199
+ }
200
+ }
201
+
202
+ return groups;
203
+ }
204
+
205
+ /** arch-aware 필드 resolve. 함수면 (arch) => value, 정적이면 그대로. */
206
+ function resolveArchField(field, arch) {
207
+ if (field == null) return null;
208
+ return typeof field === 'function' ? field(arch) : field;
209
+ }
210
+
211
+ /**
212
+ * 그룹 배열에서 moves/deletes 적용:
213
+ * - move from→to: 모든 그룹에서 from 제거, to 는 transform 그룹용으로 반환.
214
+ * - delete path: 모든 그룹에서 path 제거.
215
+ *
216
+ * @returns {string[]} transform 그룹에 추가될 새 경로
217
+ */
218
+ function applyMovesAndDeletes(groups, moves, deletes) {
219
+ const added = [];
220
+ for (const { from, to } of moves) {
221
+ const removed = removeFromAllGroups(groups, from);
222
+ // 원본이 어디에도 없었다면 (예: sentry 비활성 상태에서 error.tsx 이동) — 새로
223
+ // 추가하지 않는다. generator.js 의 `if (await fs.pathExists(fromPath))` 시맨틱.
224
+ if (removed) added.push(to);
225
+ }
226
+ for (const p of deletes) {
227
+ removeFromAllGroups(groups, p);
228
+ }
229
+ return added;
230
+ }
231
+
232
+ function removeFromAllGroups(groups, path) {
233
+ let removed = false;
234
+ for (const g of groups) {
235
+ const idx = g.paths.indexOf(path);
236
+ if (idx !== -1) {
237
+ g.paths.splice(idx, 1);
238
+ removed = true;
239
+ }
240
+ }
241
+ return removed;
242
+ }
243
+
244
+ function makeGroup(id, label, paths) {
245
+ return { id, label, paths: paths.slice() };
246
+ }
247
+
248
+ /**
249
+ * 후처리: 그룹 간 dedupe (같은 path 가 여러 그룹에 있으면 마지막 그룹이 소유),
250
+ * 그룹 안에서 path 정렬, 빈 그룹 제거, 전체 파일 목록 계산.
251
+ */
252
+ function finalize(groups) {
253
+ const seen = new Set();
254
+ // 뒤에서 앞으로 — 후순위 그룹(plugins/css/transform) 이 dedup 우선권.
255
+ for (let i = groups.length - 1; i >= 0; i--) {
256
+ const kept = [];
257
+ for (const p of groups[i].paths) {
258
+ if (!seen.has(p)) {
259
+ seen.add(p);
260
+ kept.push(p);
261
+ }
262
+ }
263
+ groups[i] = {
264
+ id: groups[i].id,
265
+ label: groups[i].label,
266
+ paths: kept.sort(),
267
+ };
268
+ }
269
+ const cleaned = groups.filter((g) => g.paths.length > 0);
270
+ const files = [];
271
+ for (const g of cleaned) files.push(...g.paths);
272
+ files.sort();
273
+ return { files, groups: cleaned };
274
+ }
@@ -0,0 +1,327 @@
1
+ // AUTO-GENERATED by scripts/build-template-manifest.mjs — do not edit by hand.
2
+ // 템플릿 디렉토리(`packages/cli/templates/`) 의 파일 경로 인덱스.
3
+ // describeTemplate() 의 입력. 템플릿 변경 후에는 빌드 스크립트 재실행 필요.
4
+
5
+ export const TEMPLATE_MANIFEST = {
6
+ "flutter-standalone": {
7
+ "base": [
8
+ "README.md",
9
+ "analysis_options.yaml",
10
+ "gitignore",
11
+ "lib/main.dart",
12
+ "lib/sh_ui/foundation/sh_ui_tokens.dart",
13
+ "pubspec.yaml",
14
+ "sh-ui.config.json"
15
+ ]
16
+ },
17
+ "monorepo": {
18
+ "base": [
19
+ ".dockerignore",
20
+ ".npmrc",
21
+ ".prettierrc",
22
+ "CLAUDE.md",
23
+ "README.md",
24
+ "gitignore",
25
+ "package.json",
26
+ "packages/eslint-config/base.js",
27
+ "packages/eslint-config/flat.js",
28
+ "packages/eslint-config/fsd.js",
29
+ "packages/eslint-config/mes.js",
30
+ "packages/eslint-config/next.js",
31
+ "packages/eslint-config/package.json",
32
+ "packages/eslint-config/react-internal.js",
33
+ "packages/typescript-config/base.json",
34
+ "packages/typescript-config/nextjs.json",
35
+ "packages/typescript-config/package.json",
36
+ "packages/typescript-config/react-library.json",
37
+ "packages/ui/ui-apps/.gitkeep",
38
+ "packages/ui/ui-core/eslint.config.js",
39
+ "packages/ui/ui-core/package.json",
40
+ "packages/ui/ui-core/sh-ui.config.json",
41
+ "packages/ui/ui-core/src/components/.gitkeep",
42
+ "packages/ui/ui-core/src/hooks/.gitkeep",
43
+ "packages/ui/ui-core/src/lib/utils.ts",
44
+ "packages/ui/ui-core/tsconfig.json",
45
+ "pnpm-workspace.yaml",
46
+ "tsconfig.json",
47
+ "turbo.json"
48
+ ]
49
+ },
50
+ "nextjs-app": {
51
+ "base": [
52
+ ".env.example",
53
+ "README.md",
54
+ "app/page.tsx",
55
+ "eslint.config.js",
56
+ "next.config.ts",
57
+ "package.json",
58
+ "postcss.config.mjs",
59
+ "vitest.config.ts",
60
+ "vitest.setup.ts"
61
+ ],
62
+ "arches": {
63
+ "flat": [
64
+ "app/api/proxy/[...path]/route.ts",
65
+ "app/layout.tsx",
66
+ "components/common/.gitkeep",
67
+ "components/common/FallbackBoundary/index.tsx",
68
+ "components/common/PrefetchBoundary/index.tsx",
69
+ "components/layouts/RootLayout.tsx",
70
+ "components/providers/GlobalProvider/index.tsx",
71
+ "components/providers/index.tsx",
72
+ "components/providers/tanstack/QueryClientProvider.tsx",
73
+ "components/providers/tanstack/TanstackDevtoolsProvider.tsx",
74
+ "components/providers/theme/ThemeProvider.tsx",
75
+ "eslint.config.js",
76
+ "lib/api/.gitkeep",
77
+ "lib/api/apiTypes.ts",
78
+ "lib/api/clientFetch.ts",
79
+ "lib/api/error.ts",
80
+ "lib/api/errorMessages.ts",
81
+ "lib/api/http.ts",
82
+ "lib/api/observability.ts",
83
+ "lib/api/queryClient.ts",
84
+ "lib/api/serverFetch.ts",
85
+ "lib/config/.gitkeep",
86
+ "lib/hooks/.gitkeep",
87
+ "lib/hooks/useAppMutation.ts",
88
+ "lib/test/createTestQueryClient.ts",
89
+ "lib/test/index.ts",
90
+ "lib/test/renderWithProviders.tsx",
91
+ "lib/utils/.gitkeep",
92
+ "lib/utils/formatDate.ts",
93
+ "lib/utils/formatPrice.ts",
94
+ "lib/utils/getQueryClient.ts",
95
+ "tsconfig.json"
96
+ ],
97
+ "fsd": [
98
+ "app/api/proxy/[...path]/route.ts",
99
+ "app/layout.tsx",
100
+ "src/app/layouts/RootLayout.tsx",
101
+ "src/app/providers/GlobalProvider/index.tsx",
102
+ "src/app/providers/index.tsx",
103
+ "src/app/providers/tanstack/QueryClientProvider.tsx",
104
+ "src/app/providers/tanstack/TanstackDevtoolsProvider.tsx",
105
+ "src/app/providers/theme/ThemeProvider.tsx",
106
+ "src/entities/.gitkeep",
107
+ "src/features/.gitkeep",
108
+ "src/shared/api/.gitkeep",
109
+ "src/shared/api/apiTypes.ts",
110
+ "src/shared/api/clientFetch.ts",
111
+ "src/shared/api/error.ts",
112
+ "src/shared/api/errorMessages.ts",
113
+ "src/shared/api/http.ts",
114
+ "src/shared/api/observability.ts",
115
+ "src/shared/api/queryClient.ts",
116
+ "src/shared/api/serverFetch.ts",
117
+ "src/shared/config/.gitkeep",
118
+ "src/shared/hooks/.gitkeep",
119
+ "src/shared/hooks/useAppMutation.ts",
120
+ "src/shared/lib/.gitkeep",
121
+ "src/shared/lib/formatDate.ts",
122
+ "src/shared/lib/formatPrice.ts",
123
+ "src/shared/lib/getQueryClient.ts",
124
+ "src/shared/model/.gitkeep",
125
+ "src/shared/test/createTestQueryClient.ts",
126
+ "src/shared/test/index.ts",
127
+ "src/shared/test/renderWithProviders.tsx",
128
+ "src/shared/ui/.gitkeep",
129
+ "src/shared/ui/FallbackBoundary/index.tsx",
130
+ "src/shared/ui/PrefetchBoundary/index.tsx",
131
+ "src/views/.gitkeep",
132
+ "src/widgets/.gitkeep",
133
+ "tsconfig.json"
134
+ ],
135
+ "mes": [
136
+ "app/api/proxy/[...path]/route.ts",
137
+ "app/layout.tsx",
138
+ "app/sign-in/page.tsx",
139
+ "eslint.config.js",
140
+ "src/components/common/.gitkeep",
141
+ "src/components/common/FallbackBoundary/index.tsx",
142
+ "src/components/common/PrefetchBoundary/index.tsx",
143
+ "src/components/layouts/RootLayout.tsx",
144
+ "src/components/providers/GlobalProvider/index.tsx",
145
+ "src/components/providers/index.tsx",
146
+ "src/components/providers/tanstack/QueryClientProvider.tsx",
147
+ "src/components/providers/tanstack/TanstackDevtoolsProvider.tsx",
148
+ "src/components/providers/theme/ThemeProvider.tsx",
149
+ "src/hooks/.gitkeep",
150
+ "src/hooks/useAppMutation.ts",
151
+ "src/lib/api/.gitkeep",
152
+ "src/lib/api/apiTypes.ts",
153
+ "src/lib/api/clientFetch.ts",
154
+ "src/lib/api/error.ts",
155
+ "src/lib/api/errorMessages.ts",
156
+ "src/lib/api/http.ts",
157
+ "src/lib/api/observability.ts",
158
+ "src/lib/api/queryClient.ts",
159
+ "src/lib/api/serverFetch.ts",
160
+ "src/lib/config/.gitkeep",
161
+ "src/lib/test/createTestQueryClient.ts",
162
+ "src/lib/test/index.ts",
163
+ "src/lib/test/renderWithProviders.tsx",
164
+ "src/lib/utils/.gitkeep",
165
+ "src/lib/utils/formatDate.ts",
166
+ "src/lib/utils/formatPrice.ts",
167
+ "src/lib/utils/getQueryClient.ts",
168
+ "src/pages/sign-in/api.ts",
169
+ "src/pages/sign-in/components/.gitkeep",
170
+ "src/pages/sign-in/hooks.ts",
171
+ "src/pages/sign-in/index.tsx",
172
+ "src/pages/sign-in/schema.ts",
173
+ "tsconfig.json"
174
+ ]
175
+ }
176
+ },
177
+ "nextjs-standalone": {
178
+ "base": [
179
+ ".env.example",
180
+ ".prettierrc",
181
+ "CLAUDE.md",
182
+ "README.md",
183
+ "app/globals.css",
184
+ "app/page.tsx",
185
+ "eslint.config.js",
186
+ "gitignore",
187
+ "next.config.ts",
188
+ "package.json",
189
+ "postcss.config.mjs",
190
+ "vitest.config.ts",
191
+ "vitest.setup.ts"
192
+ ],
193
+ "arches": {
194
+ "flat": [
195
+ "app/api/proxy/[...path]/route.ts",
196
+ "app/globals.css",
197
+ "app/layout.tsx",
198
+ "components/common/.gitkeep",
199
+ "components/common/FallbackBoundary/index.tsx",
200
+ "components/common/PrefetchBoundary/index.tsx",
201
+ "components/layouts/RootLayout.tsx",
202
+ "components/providers/GlobalProvider/index.tsx",
203
+ "components/providers/index.tsx",
204
+ "components/providers/tanstack/QueryClientProvider.tsx",
205
+ "components/providers/tanstack/TanstackDevtoolsProvider.tsx",
206
+ "components/providers/theme/ThemeProvider.tsx",
207
+ "eslint.config.js",
208
+ "lib/api/.gitkeep",
209
+ "lib/api/apiTypes.ts",
210
+ "lib/api/clientFetch.ts",
211
+ "lib/api/error.ts",
212
+ "lib/api/errorMessages.ts",
213
+ "lib/api/http.ts",
214
+ "lib/api/observability.ts",
215
+ "lib/api/queryClient.ts",
216
+ "lib/api/serverFetch.ts",
217
+ "lib/config/.gitkeep",
218
+ "lib/hooks/.gitkeep",
219
+ "lib/hooks/useAppMutation.ts",
220
+ "lib/styles/tokens.css",
221
+ "lib/test/createTestQueryClient.ts",
222
+ "lib/test/index.ts",
223
+ "lib/test/renderWithProviders.tsx",
224
+ "lib/utils/formatDate.ts",
225
+ "lib/utils/formatPrice.ts",
226
+ "lib/utils/getQueryClient.ts",
227
+ "lib/utils/utils.ts",
228
+ "sh-ui.config.json",
229
+ "tsconfig.json"
230
+ ],
231
+ "fsd": [
232
+ "app/api/proxy/[...path]/route.ts",
233
+ "app/layout.tsx",
234
+ "sh-ui.config.json",
235
+ "src/app/layouts/RootLayout.tsx",
236
+ "src/app/providers/GlobalProvider/index.tsx",
237
+ "src/app/providers/index.tsx",
238
+ "src/app/providers/tanstack/QueryClientProvider.tsx",
239
+ "src/app/providers/tanstack/TanstackDevtoolsProvider.tsx",
240
+ "src/app/providers/theme/ThemeProvider.tsx",
241
+ "src/entities/.gitkeep",
242
+ "src/features/.gitkeep",
243
+ "src/shared/api/.gitkeep",
244
+ "src/shared/api/apiTypes.ts",
245
+ "src/shared/api/clientFetch.ts",
246
+ "src/shared/api/error.ts",
247
+ "src/shared/api/errorMessages.ts",
248
+ "src/shared/api/http.ts",
249
+ "src/shared/api/observability.ts",
250
+ "src/shared/api/queryClient.ts",
251
+ "src/shared/api/serverFetch.ts",
252
+ "src/shared/config/.gitkeep",
253
+ "src/shared/hooks/.gitkeep",
254
+ "src/shared/hooks/useAppMutation.ts",
255
+ "src/shared/lib/formatDate.ts",
256
+ "src/shared/lib/formatPrice.ts",
257
+ "src/shared/lib/getQueryClient.ts",
258
+ "src/shared/lib/utils.ts",
259
+ "src/shared/model/.gitkeep",
260
+ "src/shared/styles/tokens.css",
261
+ "src/shared/test/createTestQueryClient.ts",
262
+ "src/shared/test/index.ts",
263
+ "src/shared/test/renderWithProviders.tsx",
264
+ "src/shared/ui/.gitkeep",
265
+ "src/shared/ui/FallbackBoundary/index.tsx",
266
+ "src/shared/ui/PrefetchBoundary/index.tsx",
267
+ "src/views/.gitkeep",
268
+ "src/widgets/.gitkeep",
269
+ "tsconfig.json"
270
+ ],
271
+ "mes": [
272
+ "app/api/proxy/[...path]/route.ts",
273
+ "app/globals.css",
274
+ "app/layout.tsx",
275
+ "app/sign-in/page.tsx",
276
+ "eslint.config.js",
277
+ "sh-ui.config.json",
278
+ "src/components/common/.gitkeep",
279
+ "src/components/common/FallbackBoundary/index.tsx",
280
+ "src/components/common/PrefetchBoundary/index.tsx",
281
+ "src/components/layouts/RootLayout.tsx",
282
+ "src/components/providers/GlobalProvider/index.tsx",
283
+ "src/components/providers/index.tsx",
284
+ "src/components/providers/tanstack/QueryClientProvider.tsx",
285
+ "src/components/providers/tanstack/TanstackDevtoolsProvider.tsx",
286
+ "src/components/providers/theme/ThemeProvider.tsx",
287
+ "src/hooks/.gitkeep",
288
+ "src/hooks/useAppMutation.ts",
289
+ "src/lib/api/.gitkeep",
290
+ "src/lib/api/apiTypes.ts",
291
+ "src/lib/api/clientFetch.ts",
292
+ "src/lib/api/error.ts",
293
+ "src/lib/api/errorMessages.ts",
294
+ "src/lib/api/http.ts",
295
+ "src/lib/api/observability.ts",
296
+ "src/lib/api/queryClient.ts",
297
+ "src/lib/api/serverFetch.ts",
298
+ "src/lib/config/.gitkeep",
299
+ "src/lib/styles/tokens.css",
300
+ "src/lib/test/createTestQueryClient.ts",
301
+ "src/lib/test/index.ts",
302
+ "src/lib/test/renderWithProviders.tsx",
303
+ "src/lib/utils/formatDate.ts",
304
+ "src/lib/utils/formatPrice.ts",
305
+ "src/lib/utils/getQueryClient.ts",
306
+ "src/lib/utils/utils.ts",
307
+ "src/pages/sign-in/api.ts",
308
+ "src/pages/sign-in/components/.gitkeep",
309
+ "src/pages/sign-in/hooks.ts",
310
+ "src/pages/sign-in/index.tsx",
311
+ "src/pages/sign-in/schema.ts",
312
+ "tsconfig.json"
313
+ ]
314
+ }
315
+ },
316
+ "ui-app-template": {
317
+ "base": [
318
+ "eslint.config.js",
319
+ "package.json",
320
+ "postcss.config.mjs",
321
+ "sh-ui.config.json",
322
+ "src/styles/globals.css",
323
+ "src/styles/tokens.css",
324
+ "tsconfig.json"
325
+ ]
326
+ }
327
+ };
package/src/mcp.mjs CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  } from "./constants.js";
50
50
  import { allPlugins } from "./create/plugins/index.js";
51
51
  import { allArchitectures } from "./create/architectures/index.js";
52
+ import { describeTemplate } from "./create/describeTemplate.js";
52
53
  import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
53
54
  import { decodeTheme } from "./create/theme/decode.js";
54
55
  import { encodeTheme } from "./create/theme/encode.js";
@@ -805,6 +806,51 @@ export async function startMcpServer() {
805
806
  },
806
807
  );
807
808
 
809
+ // 템플릿 트리 사전 미리보기 — 실제 생성 없이 옵션 조합으로 어떤 파일이 emit 되는지.
810
+ // apps/docs 의 CreateProjectDialog 와 같은 출력 (sh-ui-cli/api 의 describeTemplate).
811
+ server.registerTool(
812
+ "sh_ui_describe_template",
813
+ {
814
+ description:
815
+ "sh-ui create 호출 시 어떤 파일이 생기는지 사전 계산 — 실제 fs 변경 없음. " +
816
+ "사용자가 '미리 보고 싶다' / '어떤 파일 생기는지' / '플러그인 켜면 뭐가 추가돼?' 류 질문을 하면 이 툴 사용 (sh_ui_create_project 의 dry-run 대용). " +
817
+ "베이스 템플릿 + arch 오버레이 + plugin.files + cssFramework 분기 + plugin.transforms (이동/삭제) 까지 정확히 반영. " +
818
+ "반환: { files: 정렬된 전체 경로, groups: 출처별 분류 (base/arch/plugin-*/css/transform) }.",
819
+ inputSchema: {
820
+ platform: z.enum(CREATE_PLATFORMS)
821
+ .describe("타겟 플랫폼"),
822
+ structure: z.enum(CREATE_STRUCTURES).optional()
823
+ .describe("Next.js 구조. platform=next 일 때 의미. 기본 standalone"),
824
+ arch: z.enum(ARCH_NAMES).optional()
825
+ .describe("아키텍처. platform=next 일 때 의미. 기본 fsd"),
826
+ plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
827
+ .describe(`Next.js 플러그인 배열 (${PLUGIN_NAMES.join(', ')}). 미지정 빈 배열`),
828
+ cssFramework: z.enum(CSS_FRAMEWORKS).optional()
829
+ .describe("CSS 프레임워크. 기본 plain. css-modules 면 page.module.css 등 추가"),
830
+ appName: z.string().optional()
831
+ .describe("monorepo 첫 앱 이름. 기본 web"),
832
+ },
833
+ },
834
+ async (input) => {
835
+ try {
836
+ const result = describeTemplate({
837
+ platform: input.platform,
838
+ structure: input.structure,
839
+ arch: input.arch,
840
+ plugins: input.plugins,
841
+ cssFramework: input.cssFramework,
842
+ appName: input.appName,
843
+ });
844
+ return jsonResult(result);
845
+ } catch (e) {
846
+ return {
847
+ isError: true,
848
+ content: [{ type: "text", text: e.message }],
849
+ };
850
+ }
851
+ },
852
+ );
853
+
808
854
  // 변경 내역 조회 — 보너스: 사용자가 "최근 변경 알려줘" 류 요청 시
809
855
  server.registerTool(
810
856
  "sh_ui_get_changelog",
@@ -0,0 +1,40 @@
1
+ # 프로젝트 작업 규칙
2
+
3
+ sh-ui CLI 가 스캐폴드한 monorepo (Turborepo + pnpm workspace). AI 에이전트
4
+ (Claude / Cursor / Codex 등) 가 이 파일을 컨텍스트로 읽고 아래 규칙을 따른다.
5
+
6
+ ## 구조
7
+
8
+ - `apps/<name>/` — Next.js 앱. 라우트 + 비즈니스 로직.
9
+ - `packages/ui/ui-core/` — 모든 앱이 공유하는 sh-ui 컴포넌트 / 훅 / 유틸 SoT.
10
+ 컴포넌트 추가는 여기에 한 번만.
11
+ - `packages/ui/ui-apps/ui-<name>/` — 앱별 토큰 (color/spacing/font) 만 보관.
12
+ 컴포넌트는 두지 않음 (v0.65+ `tokens-only` 마커).
13
+ - `packages/eslint-config/` · `packages/typescript-config/` — 공용 설정.
14
+
15
+ ## 날짜 / 숫자 포맷
16
+
17
+ - raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`
18
+ 호출 **금지** — SSR(Node) 와 브라우저의 기본 로케일이 달라 hydration mismatch 의
19
+ 원인. ESLint `no-restricted-syntax` 룰이 인자 0개의 호출을 막는다
20
+ (`@workspace/eslint-config/base` 에 정의).
21
+ - 대신 `@/src/shared/lib/formatDate` 의 `formatDate(date)` / `formatDateTime(date)`
22
+ 사용 (default locale `ko-KR`, 서버·클라이언트 동일 출력 보장).
23
+ - next-intl locale 추종이 필요하면 `@/src/shared/hooks/useFormatDate` 훅 사용.
24
+ - 동일 원칙이 숫자에도 적용 — raw `Number.prototype.toLocaleString()` 금지,
25
+ `formatPrice` 사용.
26
+ - 인자가 명시된 호출 (`toLocaleDateString('ko-KR', { ... })`) 은 의도된 사용이므로
27
+ 허용. 다만 SSR 출력 결정성을 위해서는 util 경유가 안전.
28
+
29
+ ## 새 앱 추가
30
+
31
+ `sh_ui_add_app` MCP 툴 또는 `npx sh-ui-cli add app <name>` — `apps/<name>/` +
32
+ `packages/ui/ui-apps/ui-<name>/` 를 한 번에 만든다. 앱별로 다른 톤 가능 (예:
33
+ marketing = rose, admin = emerald). 컴포넌트는 `ui-core` 단일 SoT 라 두 앱이
34
+ 자동 공유.
35
+
36
+ ## 토큰 사용
37
+
38
+ - 색상 / 간격 / 폰트 크기는 항상 토큰 변수 경유 (`var(--space-3)`, `bg-fg`,
39
+ `text-fg-muted` 등). 매직 px / hex 직접 하드코딩 금지.
40
+ - 토큰 정의부는 `packages/ui/ui-apps/ui-<name>/src/styles/tokens.css`.
@@ -18,6 +18,15 @@ export const config = [
18
18
  "warn",
19
19
  { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
20
20
  ],
21
+ "no-restricted-syntax": [
22
+ "error",
23
+ {
24
+ selector:
25
+ "CallExpression[arguments.length=0][callee.type='MemberExpression'][callee.property.name=/^toLocale(Date|Time)?String$/]",
26
+ message:
27
+ "Argument-less .toLocaleDateString() / .toLocaleString() / .toLocaleTimeString() causes SSR hydration mismatch (Node default locale ≠ browser locale). Use `formatDate` from `@/src/shared/lib/formatDate`, or the `useFormatDate` hook for i18n-aware locale (numbers: `formatPrice`).",
28
+ },
29
+ ],
21
30
  },
22
31
  },
23
32
  {
@@ -0,0 +1,30 @@
1
+ # 프로젝트 작업 규칙
2
+
3
+ sh-ui CLI 가 스캐폴드한 Next.js standalone 프로젝트. AI 에이전트 (Claude / Cursor /
4
+ Codex 등) 가 이 파일을 컨텍스트로 읽고 아래 규칙을 따른다.
5
+
6
+ ## 날짜 / 숫자 포맷
7
+
8
+ - raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`
9
+ 호출 **금지** — SSR(Node) 와 브라우저의 기본 로케일이 달라 hydration mismatch 의
10
+ 원인. ESLint `no-restricted-syntax` 룰이 인자 0개의 호출을 막는다.
11
+ - 대신 `@/src/shared/lib/formatDate` 의 `formatDate(date)` / `formatDateTime(date)`
12
+ 사용 (default locale `ko-KR`, 서버·클라이언트 동일 출력 보장).
13
+ - next-intl locale 추종이 필요하면 `@/src/shared/hooks/useFormatDate` 훅 사용.
14
+ - 동일 원칙이 숫자에도 적용 — raw `Number.prototype.toLocaleString()` 금지,
15
+ `@/src/shared/lib/formatPrice` 의 `formatPrice` 사용.
16
+ - 인자가 명시된 호출 (`toLocaleDateString('ko-KR', { ... })`) 은 의도된 사용이므로
17
+ 허용. 다만 SSR 출력 결정성을 위해서는 util 경유가 안전.
18
+
19
+ ## sh-ui 컴포넌트 우선
20
+
21
+ - shadcn/ui 류 외부 라이브러리 대신 이 프로젝트의 `components/ui/*` (sh-ui 레지스트리)
22
+ 사용. Base UI (`@base-ui-components/react`) 위에 빌드되어 있음.
23
+ - 새 컴포넌트가 필요하면 `npx sh-ui-cli add <name>` 또는 sh-ui MCP 의
24
+ `sh_ui_add_component` 사용.
25
+
26
+ ## 토큰 사용
27
+
28
+ - 색상 / 간격 / 폰트 크기는 항상 토큰 변수 경유 (`var(--space-3)`, `bg-fg`,
29
+ `text-fg-muted` 등). 매직 px / hex 직접 하드코딩 금지.
30
+ - 토큰 정의부는 `app/globals.css` 또는 `src/shared/styles/tokens.css`.
@@ -27,6 +27,15 @@ export default [
27
27
  "warn",
28
28
  { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
29
29
  ],
30
+ "no-restricted-syntax": [
31
+ "error",
32
+ {
33
+ selector:
34
+ "CallExpression[arguments.length=0][callee.type='MemberExpression'][callee.property.name=/^toLocale(Date|Time)?String$/]",
35
+ message:
36
+ "Argument-less .toLocaleDateString() / .toLocaleString() / .toLocaleTimeString() causes SSR hydration mismatch (Node default locale ≠ browser locale). Use `formatDate` from `lib/utils/formatDate`, or the `useFormatDate` hook for i18n-aware locale (numbers: `formatPrice`).",
37
+ },
38
+ ],
30
39
  },
31
40
  },
32
41
 
@@ -27,6 +27,15 @@ export default [
27
27
  "warn",
28
28
  { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
29
29
  ],
30
+ "no-restricted-syntax": [
31
+ "error",
32
+ {
33
+ selector:
34
+ "CallExpression[arguments.length=0][callee.type='MemberExpression'][callee.property.name=/^toLocale(Date|Time)?String$/]",
35
+ message:
36
+ "Argument-less .toLocaleDateString() / .toLocaleString() / .toLocaleTimeString() causes SSR hydration mismatch (Node default locale ≠ browser locale). Use `formatDate` from `src/lib/utils/formatDate`, or the `useFormatDate` hook for i18n-aware locale (numbers: `formatPrice`).",
37
+ },
38
+ ],
30
39
  },
31
40
  },
32
41
 
@@ -27,6 +27,15 @@ export default [
27
27
  "warn",
28
28
  { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
29
29
  ],
30
+ "no-restricted-syntax": [
31
+ "error",
32
+ {
33
+ selector:
34
+ "CallExpression[arguments.length=0][callee.type='MemberExpression'][callee.property.name=/^toLocale(Date|Time)?String$/]",
35
+ message:
36
+ "Argument-less .toLocaleDateString() / .toLocaleString() / .toLocaleTimeString() causes SSR hydration mismatch (Node default locale ≠ browser locale). Use `formatDate` from `@/src/shared/lib/formatDate`, or the `useFormatDate` hook for i18n-aware locale (numbers: `formatPrice`).",
37
+ },
38
+ ],
30
39
  },
31
40
  },
32
41