sh-ui-cli 0.22.2 → 0.23.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.
Files changed (111) hide show
  1. package/README.md +19 -2
  2. package/bin/sh-ui.mjs +7 -0
  3. package/data/changelog/versions.json +14 -0
  4. package/package.json +13 -2
  5. package/src/create/cli-args.js +63 -0
  6. package/src/create/generator.js +542 -0
  7. package/src/create/index.mjs +68 -0
  8. package/src/create/plugins/index.js +17 -0
  9. package/src/create/plugins/nextIntl.js +197 -0
  10. package/src/create/plugins/sentry.js +689 -0
  11. package/src/create/theme/decode.js +66 -0
  12. package/src/create/theme/inject.js +111 -0
  13. package/src/mcp.mjs +81 -27
  14. package/src/paths.mjs +5 -0
  15. package/templates/flutter-standalone/README.md +34 -0
  16. package/templates/flutter-standalone/analysis_options.yaml +1 -0
  17. package/templates/flutter-standalone/lib/main.dart +103 -0
  18. package/templates/flutter-standalone/lib/sh_ui/foundation/sh_ui_tokens.dart +389 -0
  19. package/templates/flutter-standalone/pubspec.yaml +20 -0
  20. package/templates/flutter-standalone/sh-ui.config.json +15 -0
  21. package/templates/monorepo/.dockerignore +7 -0
  22. package/templates/monorepo/.eslintrc.js +8 -0
  23. package/templates/monorepo/.prettierrc +17 -0
  24. package/templates/monorepo/README.md +103 -0
  25. package/templates/monorepo/package.json +24 -0
  26. package/templates/monorepo/packages/eslint-config/base.js +31 -0
  27. package/templates/monorepo/packages/eslint-config/fsd.js +119 -0
  28. package/templates/monorepo/packages/eslint-config/next.js +65 -0
  29. package/templates/monorepo/packages/eslint-config/package.json +31 -0
  30. package/templates/monorepo/packages/eslint-config/react-internal.js +36 -0
  31. package/templates/monorepo/packages/typescript-config/base.json +20 -0
  32. package/templates/monorepo/packages/typescript-config/nextjs.json +13 -0
  33. package/templates/monorepo/packages/typescript-config/package.json +5 -0
  34. package/templates/monorepo/packages/typescript-config/react-library.json +8 -0
  35. package/templates/monorepo/packages/ui/ui-apps/.gitkeep +0 -0
  36. package/templates/monorepo/packages/ui/ui-core/eslint.config.js +3 -0
  37. package/templates/monorepo/packages/ui/ui-core/package.json +23 -0
  38. package/templates/monorepo/packages/ui/ui-core/src/lib/utils.ts +6 -0
  39. package/templates/monorepo/packages/ui/ui-core/tsconfig.json +11 -0
  40. package/templates/monorepo/pnpm-workspace.yaml +5 -0
  41. package/templates/monorepo/tsconfig.json +3 -0
  42. package/templates/monorepo/turbo.json +26 -0
  43. package/templates/nextjs-app/.env.example +2 -0
  44. package/templates/nextjs-app/Dockerfile +11 -0
  45. package/templates/nextjs-app/README.md +64 -0
  46. package/templates/nextjs-app/app/layout.tsx +22 -0
  47. package/templates/nextjs-app/app/page.tsx +7 -0
  48. package/templates/nextjs-app/eslint.config.js +10 -0
  49. package/templates/nextjs-app/next.config.ts +12 -0
  50. package/templates/nextjs-app/package.json +45 -0
  51. package/templates/nextjs-app/postcss.config.mjs +1 -0
  52. package/templates/nextjs-app/src/app/layouts/.gitkeep +0 -0
  53. package/templates/nextjs-app/src/app/providers/GlobalProvider/index.tsx +23 -0
  54. package/templates/nextjs-app/src/app/providers/index.tsx +1 -0
  55. package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
  56. package/templates/nextjs-app/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  57. package/templates/nextjs-app/src/app/providers/theme/ThemeProviders.tsx +12 -0
  58. package/templates/nextjs-app/src/entities/.gitkeep +0 -0
  59. package/templates/nextjs-app/src/features/.gitkeep +0 -0
  60. package/templates/nextjs-app/src/shared/api/.gitkeep +0 -0
  61. package/templates/nextjs-app/src/shared/config/.gitkeep +0 -0
  62. package/templates/nextjs-app/src/shared/hooks/.gitkeep +0 -0
  63. package/templates/nextjs-app/src/shared/lib/.gitkeep +0 -0
  64. package/templates/nextjs-app/src/shared/model/.gitkeep +0 -0
  65. package/templates/nextjs-app/src/shared/ui/.gitkeep +0 -0
  66. package/templates/nextjs-app/src/views/.gitkeep +0 -0
  67. package/templates/nextjs-app/src/widgets/.gitkeep +0 -0
  68. package/templates/nextjs-app/tsconfig.json +23 -0
  69. package/templates/nextjs-app/vitest.config.ts +15 -0
  70. package/templates/nextjs-app/vitest.setup.ts +1 -0
  71. package/templates/nextjs-standalone/.env.example +2 -0
  72. package/templates/nextjs-standalone/.prettierrc +17 -0
  73. package/templates/nextjs-standalone/README.md +77 -0
  74. package/templates/nextjs-standalone/app/globals.css +33 -0
  75. package/templates/nextjs-standalone/app/layout.tsx +22 -0
  76. package/templates/nextjs-standalone/app/page.tsx +7 -0
  77. package/templates/nextjs-standalone/eslint.config.js +162 -0
  78. package/templates/nextjs-standalone/next.config.ts +10 -0
  79. package/templates/nextjs-standalone/package.json +66 -0
  80. package/templates/nextjs-standalone/postcss.config.mjs +5 -0
  81. package/templates/nextjs-standalone/sh-ui.config.json +19 -0
  82. package/templates/nextjs-standalone/src/app/layouts/.gitkeep +0 -0
  83. package/templates/nextjs-standalone/src/app/providers/GlobalProvider/index.tsx +23 -0
  84. package/templates/nextjs-standalone/src/app/providers/index.tsx +1 -0
  85. package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
  86. package/templates/nextjs-standalone/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  87. package/templates/nextjs-standalone/src/app/providers/theme/ThemeProviders.tsx +12 -0
  88. package/templates/nextjs-standalone/src/entities/.gitkeep +0 -0
  89. package/templates/nextjs-standalone/src/features/.gitkeep +0 -0
  90. package/templates/nextjs-standalone/src/shared/api/.gitkeep +0 -0
  91. package/templates/nextjs-standalone/src/shared/config/.gitkeep +0 -0
  92. package/templates/nextjs-standalone/src/shared/hooks/.gitkeep +0 -0
  93. package/templates/nextjs-standalone/src/shared/lib/utils.ts +6 -0
  94. package/templates/nextjs-standalone/src/shared/model/.gitkeep +0 -0
  95. package/templates/nextjs-standalone/src/shared/styles/tokens.css +95 -0
  96. package/templates/nextjs-standalone/src/shared/ui/.gitkeep +0 -0
  97. package/templates/nextjs-standalone/src/views/.gitkeep +0 -0
  98. package/templates/nextjs-standalone/src/widgets/.gitkeep +0 -0
  99. package/templates/nextjs-standalone/tsconfig.json +39 -0
  100. package/templates/nextjs-standalone/vitest.config.ts +15 -0
  101. package/templates/nextjs-standalone/vitest.setup.ts +1 -0
  102. package/templates/ui-app-template/eslint.config.js +3 -0
  103. package/templates/ui-app-template/package.json +38 -0
  104. package/templates/ui-app-template/postcss.config.mjs +5 -0
  105. package/templates/ui-app-template/sh-ui.config.json +14 -0
  106. package/templates/ui-app-template/src/components/.gitkeep +0 -0
  107. package/templates/ui-app-template/src/hooks/.gitkeep +0 -0
  108. package/templates/ui-app-template/src/lib/.gitkeep +0 -0
  109. package/templates/ui-app-template/src/styles/globals.css +37 -0
  110. package/templates/ui-app-template/src/styles/tokens.css +95 -0
  111. package/templates/ui-app-template/tsconfig.json +11 -0
@@ -0,0 +1,66 @@
1
+ const TOKEN_KEYS = [
2
+ 'background', 'background-subtle', 'background-muted',
3
+ 'foreground', 'foreground-muted',
4
+ 'border', 'border-strong',
5
+ 'primary', 'primary-foreground', 'primary-hover',
6
+ 'danger', 'danger-foreground',
7
+ ];
8
+
9
+ const HEX_REGEX = /^#[0-9A-Fa-f]{6}$/;
10
+ const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
11
+ const MAX_THEME_BYTES = 10 * 1024; // 정상 테마 ~860 바이트. 10KB 면 10× 여유.
12
+
13
+ const validateTokenMap = (name, map) => {
14
+ if (!map || typeof map !== 'object') {
15
+ throw new Error(`theme 디코드 실패: ${name} 가 객체가 아님`);
16
+ }
17
+ for (const key of TOKEN_KEYS) {
18
+ if (!(key in map)) {
19
+ throw new Error(`theme 디코드 실패: ${name}.${key} 누락`);
20
+ }
21
+ const value = map[key];
22
+ if (typeof value !== 'string' || !HEX_REGEX.test(value)) {
23
+ throw new Error(
24
+ `theme 디코드 실패: ${name}.${key} 가 hex 포맷이 아님 (받은 값: ${JSON.stringify(value)})`,
25
+ );
26
+ }
27
+ }
28
+ };
29
+
30
+ export const decodeTheme = (b64) => {
31
+ if (typeof b64 !== 'string') {
32
+ throw new Error(`theme 디코드 실패: 문자열이 아님`);
33
+ }
34
+ if (b64.length > MAX_THEME_BYTES) {
35
+ throw new Error(
36
+ `theme 크기가 허용 범위를 초과함 (${b64.length} > ${MAX_THEME_BYTES} 바이트). ` +
37
+ `playground 에서 생성한 값만 사용하세요.`
38
+ );
39
+ }
40
+ if (!BASE64_REGEX.test(b64)) {
41
+ throw new Error(`theme 디코드 실패: base64 포맷이 아님`);
42
+ }
43
+ let json;
44
+ try {
45
+ json = Buffer.from(b64, 'base64').toString('utf-8');
46
+ } catch (e) {
47
+ throw new Error(`theme 디코드 실패: base64 디코드 실패 (${e.message})`);
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(json);
52
+ } catch (e) {
53
+ throw new Error(`theme 디코드 실패: JSON 파싱 실패 (${e.message})`);
54
+ }
55
+ validateTokenMap('light', parsed.light);
56
+ validateTokenMap('dark', parsed.dark);
57
+ if (typeof parsed.radius !== 'number' || Number.isNaN(parsed.radius)) {
58
+ throw new Error(`theme 디코드 실패: radius 가 숫자가 아님`);
59
+ }
60
+ if (parsed.radius < 0 || parsed.radius > 1.5) {
61
+ throw new Error(`theme 디코드 실패: radius 가 허용 범위(0~1.5)를 벗어남 (${parsed.radius})`);
62
+ }
63
+ return parsed;
64
+ };
65
+
66
+ export { TOKEN_KEYS };
@@ -0,0 +1,111 @@
1
+ import { TOKEN_KEYS } from './decode.js';
2
+
3
+ /**
4
+ * 파일 내용에서 sh-ui:<section>-start / -end 마커 사이 내용을 교체.
5
+ * commentOpen / commentClose 는 파일 형식에 따라 주어짐:
6
+ * CSS → '/*', '*' + '/'
7
+ * Dart → '//', ''
8
+ */
9
+ export const replaceSection = (content, section, commentOpen, commentClose, replacement) => {
10
+ const startMarker = commentClose
11
+ ? `${commentOpen} sh-ui:${section}-start ${commentClose}`
12
+ : `${commentOpen} sh-ui:${section}-start`;
13
+ const endMarker = commentClose
14
+ ? `${commentOpen} sh-ui:${section}-end ${commentClose}`
15
+ : `${commentOpen} sh-ui:${section}-end`;
16
+
17
+ const startIdx = content.indexOf(startMarker);
18
+ const endIdx = content.indexOf(endMarker);
19
+ if (startIdx < 0 || endIdx < 0 || endIdx < startIdx) {
20
+ throw new Error(`inject 실패: 섹션 ${section} 마커 없음`);
21
+ }
22
+
23
+ const before = content.slice(0, startIdx + startMarker.length);
24
+ const after = content.slice(endIdx);
25
+ return `${before}\n${replacement}\n${after}`;
26
+ };
27
+
28
+ // ─── CSS 블록 빌더 ───
29
+
30
+ const cssColorLine = (key, value) => ` --${key}: ${value};`;
31
+
32
+ export const buildCssColorsBlock = (theme) => {
33
+ const lightLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.light[k])).join('\n');
34
+ const darkLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.dark[k])).join('\n');
35
+ return [
36
+ ':root {',
37
+ lightLines,
38
+ '}',
39
+ '.dark {',
40
+ darkLines,
41
+ '}',
42
+ ].join('\n');
43
+ };
44
+
45
+ export const buildCssRadiusBlock = (theme) => {
46
+ return ` --radius: ${theme.radius}rem;`;
47
+ };
48
+
49
+ // ─── Dart 블록 빌더 ───
50
+
51
+ const toDartColor = (hex) => `Color(0xFF${hex.replace('#', '').toUpperCase()})`;
52
+
53
+ /**
54
+ * Dart 의 ShUiColorTokens 필드 순서 + 각 필드가 어떤 소스에서 값을 가져오는지.
55
+ * self — 현재 모드의 편집값
56
+ * inverse — 반대 모드의 편집값
57
+ * default — playground 가 노출하지 않음, 고정 기본값 사용
58
+ */
59
+ const DART_FIELD_SOURCES = [
60
+ { field: 'background', source: { kind: 'self', key: 'background' } },
61
+ { field: 'backgroundSubtle', source: { kind: 'self', key: 'background-subtle' } },
62
+ { field: 'backgroundMuted', source: { kind: 'self', key: 'background-muted' } },
63
+ { field: 'backgroundInverse', source: { kind: 'inverse', key: 'background' } },
64
+ { field: 'foreground', source: { kind: 'self', key: 'foreground' } },
65
+ { field: 'foregroundMuted', source: { kind: 'self', key: 'foreground-muted' } },
66
+ { field: 'foregroundSubtle', source: { kind: 'default' } },
67
+ { field: 'foregroundInverse', source: { kind: 'inverse', key: 'foreground' } },
68
+ { field: 'border', source: { kind: 'self', key: 'border' } },
69
+ { field: 'borderStrong', source: { kind: 'self', key: 'border-strong' } },
70
+ { field: 'primary', source: { kind: 'self', key: 'primary' } },
71
+ { field: 'primaryForeground', source: { kind: 'self', key: 'primary-foreground' } },
72
+ { field: 'primaryHover', source: { kind: 'self', key: 'primary-hover' } },
73
+ { field: 'danger', source: { kind: 'self', key: 'danger' } },
74
+ { field: 'dangerForeground', source: { kind: 'self', key: 'danger-foreground' } },
75
+ ];
76
+
77
+ const DART_DEFAULTS = {
78
+ light: { foregroundSubtle: '0xFFA3A3A3' },
79
+ dark: { foregroundSubtle: '0xFF737373' },
80
+ };
81
+
82
+ const buildDartStaticConst = (mode, self, opposite) => {
83
+ const lines = DART_FIELD_SOURCES.map(({ field, source }) => {
84
+ switch (source.kind) {
85
+ case 'self':
86
+ return ` ${field}: ${toDartColor(self[source.key])},`;
87
+ case 'inverse':
88
+ return ` ${field}: ${toDartColor(opposite[source.key])},`;
89
+ case 'default':
90
+ return ` ${field}: Color(${DART_DEFAULTS[mode][field]}),`;
91
+ }
92
+ }).join('\n');
93
+ return [
94
+ ` static const ${mode} = ShUiColorTokens(`,
95
+ lines,
96
+ ' );',
97
+ ].join('\n');
98
+ };
99
+
100
+ export const buildDartColorsBlock = (theme) => {
101
+ return [
102
+ buildDartStaticConst('light', theme.light, theme.dark),
103
+ '',
104
+ buildDartStaticConst('dark', theme.dark, theme.light),
105
+ ].join('\n');
106
+ };
107
+
108
+ export const buildDartRadiusBlock = (theme) => {
109
+ const px = (theme.radius * 16).toFixed(1);
110
+ return ` defaultRadius: ${px},`;
111
+ };
package/src/mcp.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
  // sh_ui_remove_component - 컴포넌트 삭제
14
14
 
15
15
  import { readFile } from "node:fs/promises";
16
+ import { existsSync } from "node:fs";
16
17
  import { resolve } from "node:path";
17
18
  import { z } from "zod";
18
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -22,6 +23,7 @@ import { init } from "./init.mjs";
22
23
  import { add } from "./add.mjs";
23
24
  import { list } from "./list.mjs";
24
25
  import { remove } from "./remove.mjs";
26
+ import { createProject } from "./create/generator.js";
25
27
  import {
26
28
  getRegistryRoot,
27
29
  getSummariesPath,
@@ -102,35 +104,19 @@ function resolveCwd(input) {
102
104
 
103
105
  const SERVER_INSTRUCTIONS = `sh-ui — Base UI 위에 빌드된 React/Flutter 디자인 시스템.
104
106
 
105
- ## 새 프로젝트를 만드는 경우 (이 MCP 툴 호출 금지)
107
+ ## 새 프로젝트를 만드는 경우
106
108
 
107
- 빈 폴더에서 시작하거나 사용자가 "Next.js 앱 만들어줘", "Flutter 프로젝트 새로", "sh-ui 로 시작" 처럼 **스캐폴드부터** 요청하면 \`sh-ui-create\` 사용:
109
+ 빈 폴더에서 시작하거나 사용자가 "Next.js 앱 만들어줘", "Flutter 프로젝트 새로", "sh-ui 로 시작" 처럼 **스캐폴드부터** 요청하면:
108
110
 
109
- npm create sh-ui my-app # 권장 (대화형)
110
- npx sh-ui-create my-app # 동일
111
+ **1차 \`sh_ui_create_project\` MCP 툴** (선호):
112
+ - 인자: name, platform (next|flutter), structure (next 일 때 standalone|monorepo), plugins (선택), force (덮어쓰기)
113
+ - 인터랙티브 프롬프트 없이 한 번에 스캐폴드 + 토큰 + sh-ui.config.json 생성
111
114
 
112
- 비대화형(에이전트/CI)은 **모든 필수 플래그를 명시**해야 한다 \`sh-ui-create\` \`--help\` 없고, 누락 prompt 에서 멈춘다:
115
+ **2차Bash** (사용자가 직접 셸에서 돌리고 싶다고 명시할 때만):
116
+ npm create sh-ui my-app
117
+ npx sh-ui-cli create my-app --platform next --structure standalone --yes
113
118
 
114
- # Next.js 단독:
115
- npx sh-ui-create my-app --platform next --structure standalone --plugins "" --yes
116
- # Next.js 모노레포 (Turborepo):
117
- npx sh-ui-create my-app --platform next --structure monorepo --plugins "" --yes
118
- # Flutter:
119
- npx sh-ui-create my-app --platform flutter --yes
120
-
121
- 플래그:
122
- - \`--platform\` next | flutter (필수)
123
- - \`--structure\` standalone | monorepo (next 일 때 필수)
124
- - \`--plugins\` 콤마 구분 (sentry,next-intl) — 없으면 \`""\` 명시 (필수)
125
- - \`--theme\` base64 인코딩된 테마 JSON (선택)
126
- - \`--yes\` 디렉토리 덮어쓰기/모노레포 기본값 자동 채택
127
-
128
- 이게 한 번에 해주는 일:
129
- - Next.js (standalone / Turborepo 모노레포) 또는 Flutter 프로젝트 스캐폴드
130
- - FSD 폴더 구조, 토큰, \`sh-ui.config.json\` 일괄 생성
131
- - 옵션 플러그인 (Sentry, next-intl)
132
-
133
- \`create-next-app\` + \`sh_ui_init\` 조합은 **쓰지 말 것** — 위 명령이 더 짧고 sh-ui 관용에 맞다.
119
+ \`create-next-app\` + \`sh_ui_init\` 조합은 **쓰지 말 것** — 위 두 경로가 더 짧고 sh-ui 관용에 맞다.
134
120
 
135
121
  ## 이미 있는 프로젝트에 sh-ui 를 얹는 경우 (MCP 툴 사용)
136
122
 
@@ -149,7 +135,7 @@ const SERVER_INSTRUCTIONS = `sh-ui — Base UI 위에 빌드된 React/Flutter
149
135
 
150
136
  export async function startMcpServer() {
151
137
  const server = new McpServer(
152
- { name: "sh-ui", version: "0.22.2" }, // sh-ui-cli 와 동기화
138
+ { name: "sh-ui", version: "0.23.0" }, // sh-ui-cli 와 동기화
153
139
  {
154
140
  capabilities: { tools: {} },
155
141
  instructions: SERVER_INSTRUCTIONS,
@@ -167,11 +153,79 @@ export async function startMcpServer() {
167
153
  async () => jsonResult(INIT_DESCRIPTIONS),
168
154
  );
169
155
 
156
+ server.registerTool(
157
+ "sh_ui_create_project",
158
+ {
159
+ description:
160
+ "빈 폴더에 sh-ui 프로젝트 스캐폴드 — Next.js (standalone/monorepo) 또는 Flutter. " +
161
+ "FSD 폴더 구조 + 토큰 + sh-ui.config.json 일괄 생성. 사용자가 '새 프로젝트' / '빈 폴더' / '스캐폴드부터' 류 요청을 하면 이 툴 사용 (Bash 로 npx sh-ui-cli create 직접 호출보다 우선).",
162
+ inputSchema: {
163
+ name: z.string().min(1)
164
+ .describe("프로젝트 디렉토리 이름. 예: my-app"),
165
+ platform: z.enum(["next", "flutter"])
166
+ .describe("타겟 플랫폼"),
167
+ structure: z.enum(["standalone", "monorepo"]).optional()
168
+ .describe("Next.js 구조 — platform=next 일 때 필수. standalone(단독) | monorepo(Turborepo)"),
169
+ plugins: z.array(z.enum(["sentry", "next-intl"])).optional()
170
+ .describe("Next.js 플러그인. 미지정시 빈 배열"),
171
+ theme: z.string().optional()
172
+ .describe("base64 인코딩된 테마 JSON (선택)"),
173
+ cwd: z.string().optional()
174
+ .describe("부모 디렉토리. 기본 process.cwd()"),
175
+ force: z.boolean().optional()
176
+ .describe("기존 디렉토리 덮어쓰기. 기본 false (안전)"),
177
+ },
178
+ },
179
+ async (input) => {
180
+ if (input.platform === "next" && !input.structure) {
181
+ return {
182
+ isError: true,
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: "platform=next 일 때 structure ('standalone' | 'monorepo') 가 필요합니다.",
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ const targetParent = resolveCwd(input);
192
+ const targetDir = resolve(targetParent, input.name);
193
+ if (existsSync(targetDir) && !input.force) {
194
+ return {
195
+ isError: true,
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `'${targetDir}' 가 이미 존재합니다. 덮어쓰려면 force: true.`,
200
+ },
201
+ ],
202
+ };
203
+ }
204
+ const origCwd = process.cwd();
205
+ try {
206
+ process.chdir(targetParent);
207
+ const text = await captureConsole(() =>
208
+ createProject({
209
+ name: input.name,
210
+ platform: input.platform,
211
+ structure: input.structure,
212
+ plugins: input.plugins,
213
+ theme: input.theme,
214
+ yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
215
+ }),
216
+ );
217
+ return textResult(text || "✓ 프로젝트 생성 완료");
218
+ } finally {
219
+ process.chdir(origCwd);
220
+ }
221
+ },
222
+ );
223
+
170
224
  server.registerTool(
171
225
  "sh_ui_init",
172
226
  {
173
227
  description:
174
- "⚠️ 빈 폴더/새 프로젝트면 이 툴 대신 `npx sh-ui-create` 사용 — 스캐폴드 + 토큰 + config 일괄 처리. " +
228
+ "⚠️ 빈 폴더/새 프로젝트면 이 툴 대신 sh_ui_create_project 사용 — 스캐폴드 + 토큰 + config 일괄 처리. " +
175
229
  "이 툴은 **이미 있는** Next.js/Vite/Flutter 프로젝트에 sh-ui 만 얹을 때. " +
176
230
  "현재 디렉토리(또는 cwd)에 sh-ui.config.json 을 생성. 비대화형 — 누락된 값은 기본값 사용. " +
177
231
  "선택지 의미가 헷갈리면 먼저 sh_ui_describe_init 호출 권장.",
package/src/paths.mjs CHANGED
@@ -47,6 +47,11 @@ export function getSummariesPath(platform) {
47
47
  : resolve(MONOREPO_PACKAGES, "llms", "summaries", `${platform}.json`);
48
48
  }
49
49
 
50
+ /** sh-ui create 용 프로젝트 템플릿 루트 — 이 패키지 안에 직접 들어있다 */
51
+ export function getTemplatesRoot() {
52
+ return resolve(CLI_ROOT, "templates");
53
+ }
54
+
50
55
  /** 변경 내역 JSON */
51
56
  export function getVersionsPath() {
52
57
  return isBundled
@@ -0,0 +1,34 @@
1
+ # {{project_name}}
2
+
3
+ sh-ui 기반 Flutter 앱.
4
+
5
+ ## 시작하기
6
+
7
+ ```bash
8
+ flutter pub get
9
+ flutter run
10
+ ```
11
+
12
+ ## sh-ui 위젯 추가
13
+
14
+ ```bash
15
+ npx sh-ui add button
16
+ npx sh-ui add card input
17
+ ```
18
+
19
+ 위젯은 `lib/sh_ui/widgets/` 아래로 복사됩니다. 설정은 `sh-ui.config.json` 을 참조하세요.
20
+
21
+ ## 구조
22
+
23
+ ```
24
+ lib/
25
+ ├── main.dart # 앱 진입점
26
+ └── sh_ui/ # sh-ui 자산 (건드리지 말 것 — sh-ui CLI 가 관리)
27
+ ├── foundation/
28
+ │ └── sh_ui_tokens.dart # 디자인 토큰
29
+ └── widgets/ # sh-ui add 로 추가되는 위젯들
30
+ ```
31
+
32
+ ## 더 알아보기
33
+
34
+ - sh-ui 컴포넌트 목록 및 가이드: https://github.com/sanghyeonKim0201/sh-ui
@@ -0,0 +1 @@
1
+ include: package:flutter_lints/flutter.yaml
@@ -0,0 +1,103 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'sh_ui/foundation/sh_ui_tokens.dart';
3
+
4
+ void main() {
5
+ runApp(const MyApp());
6
+ }
7
+
8
+ class MyApp extends StatefulWidget {
9
+ const MyApp({super.key});
10
+
11
+ @override
12
+ State<MyApp> createState() => _MyAppState();
13
+ }
14
+
15
+ class _MyAppState extends State<MyApp> {
16
+ ThemeMode _themeMode = ThemeMode.light;
17
+
18
+ void _toggleTheme() {
19
+ setState(() {
20
+ _themeMode =
21
+ _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
22
+ });
23
+ }
24
+
25
+ @override
26
+ Widget build(BuildContext context) {
27
+ return MaterialApp(
28
+ title: '{{project_name}}',
29
+ debugShowCheckedModeBanner: false,
30
+ themeMode: _themeMode,
31
+ theme: ThemeData(
32
+ brightness: Brightness.light,
33
+ scaffoldBackgroundColor: ShUiColorTokens.light.background,
34
+ extensions: const [ShUiTheme.light],
35
+ ),
36
+ darkTheme: ThemeData(
37
+ brightness: Brightness.dark,
38
+ scaffoldBackgroundColor: ShUiColorTokens.dark.background,
39
+ extensions: const [ShUiTheme.dark],
40
+ ),
41
+ home: HomePage(
42
+ themeMode: _themeMode,
43
+ onToggleTheme: _toggleTheme,
44
+ ),
45
+ );
46
+ }
47
+ }
48
+
49
+ class HomePage extends StatelessWidget {
50
+ const HomePage({
51
+ super.key,
52
+ required this.themeMode,
53
+ required this.onToggleTheme,
54
+ });
55
+
56
+ final ThemeMode themeMode;
57
+ final VoidCallback onToggleTheme;
58
+
59
+ @override
60
+ Widget build(BuildContext context) {
61
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
62
+ final colors = shUi.colors;
63
+
64
+ return Scaffold(
65
+ appBar: AppBar(
66
+ title: const Text('{{project_name}}'),
67
+ actions: [
68
+ IconButton(
69
+ icon: Icon(
70
+ themeMode == ThemeMode.light
71
+ ? Icons.dark_mode_outlined
72
+ : Icons.light_mode_outlined,
73
+ ),
74
+ onPressed: onToggleTheme,
75
+ ),
76
+ ],
77
+ ),
78
+ body: Center(
79
+ child: Column(
80
+ mainAxisAlignment: MainAxisAlignment.center,
81
+ children: [
82
+ Text(
83
+ 'sh-ui 기반 Flutter 앱',
84
+ style: TextStyle(
85
+ color: colors.foreground,
86
+ fontSize: 20,
87
+ fontWeight: FontWeight.w600,
88
+ ),
89
+ ),
90
+ const SizedBox(height: 12),
91
+ Text(
92
+ 'sh-ui add <widget> 로 위젯을 추가해 보세요',
93
+ style: TextStyle(
94
+ color: colors.foregroundMuted,
95
+ fontSize: 14,
96
+ ),
97
+ ),
98
+ ],
99
+ ),
100
+ ),
101
+ );
102
+ }
103
+ }