sh-ui-cli 0.22.2 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/bin/sh-ui.mjs +7 -0
- package/data/changelog/versions.json +26 -0
- package/package.json +13 -2
- package/src/create/cli-args.js +63 -0
- package/src/create/generator.js +542 -0
- package/src/create/index.mjs +68 -0
- package/src/create/plugins/index.js +17 -0
- package/src/create/plugins/nextIntl.js +197 -0
- package/src/create/plugins/sentry.js +689 -0
- package/src/create/theme/decode.js +66 -0
- package/src/create/theme/inject.js +111 -0
- package/src/mcp.mjs +81 -27
- package/src/paths.mjs +5 -0
- package/templates/flutter-standalone/README.md +34 -0
- package/templates/flutter-standalone/analysis_options.yaml +1 -0
- package/templates/flutter-standalone/lib/main.dart +103 -0
- package/templates/flutter-standalone/lib/sh_ui/foundation/sh_ui_tokens.dart +389 -0
- package/templates/flutter-standalone/pubspec.yaml +20 -0
- package/templates/flutter-standalone/sh-ui.config.json +15 -0
- package/templates/monorepo/.dockerignore +7 -0
- package/templates/monorepo/.eslintrc.js +8 -0
- package/templates/monorepo/.prettierrc +17 -0
- package/templates/monorepo/README.md +103 -0
- package/templates/monorepo/package.json +24 -0
- package/templates/monorepo/packages/eslint-config/base.js +31 -0
- package/templates/monorepo/packages/eslint-config/fsd.js +119 -0
- package/templates/monorepo/packages/eslint-config/next.js +65 -0
- package/templates/monorepo/packages/eslint-config/package.json +31 -0
- package/templates/monorepo/packages/eslint-config/react-internal.js +36 -0
- package/templates/monorepo/packages/typescript-config/base.json +20 -0
- package/templates/monorepo/packages/typescript-config/nextjs.json +13 -0
- package/templates/monorepo/packages/typescript-config/package.json +5 -0
- package/templates/monorepo/packages/typescript-config/react-library.json +8 -0
- package/templates/monorepo/packages/ui/ui-apps/.gitkeep +0 -0
- package/templates/monorepo/packages/ui/ui-core/eslint.config.js +3 -0
- package/templates/monorepo/packages/ui/ui-core/package.json +23 -0
- package/templates/monorepo/packages/ui/ui-core/src/lib/utils.ts +6 -0
- package/templates/monorepo/packages/ui/ui-core/tsconfig.json +11 -0
- package/templates/monorepo/pnpm-workspace.yaml +5 -0
- package/templates/monorepo/tsconfig.json +3 -0
- package/templates/monorepo/turbo.json +26 -0
- package/templates/nextjs-app/.env.example +2 -0
- package/templates/nextjs-app/Dockerfile +11 -0
- package/templates/nextjs-app/README.md +64 -0
- package/templates/nextjs-app/app/layout.tsx +22 -0
- package/templates/nextjs-app/app/page.tsx +7 -0
- package/templates/nextjs-app/eslint.config.js +10 -0
- package/templates/nextjs-app/next.config.ts +12 -0
- package/templates/nextjs-app/package.json +45 -0
- package/templates/nextjs-app/postcss.config.mjs +1 -0
- package/templates/nextjs-app/src/app/layouts/.gitkeep +0 -0
- package/templates/nextjs-app/src/app/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-app/src/app/providers/index.tsx +1 -0
- package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
- package/templates/nextjs-app/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-app/src/app/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-app/src/entities/.gitkeep +0 -0
- package/templates/nextjs-app/src/features/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/api/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/config/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/hooks/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/lib/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/model/.gitkeep +0 -0
- package/templates/nextjs-app/src/shared/ui/.gitkeep +0 -0
- package/templates/nextjs-app/src/views/.gitkeep +0 -0
- package/templates/nextjs-app/src/widgets/.gitkeep +0 -0
- package/templates/nextjs-app/tsconfig.json +23 -0
- package/templates/nextjs-app/vitest.config.ts +15 -0
- package/templates/nextjs-app/vitest.setup.ts +1 -0
- package/templates/nextjs-standalone/.env.example +2 -0
- package/templates/nextjs-standalone/.prettierrc +17 -0
- package/templates/nextjs-standalone/README.md +77 -0
- package/templates/nextjs-standalone/app/globals.css +33 -0
- package/templates/nextjs-standalone/app/layout.tsx +22 -0
- package/templates/nextjs-standalone/app/page.tsx +7 -0
- package/templates/nextjs-standalone/eslint.config.js +162 -0
- package/templates/nextjs-standalone/next.config.ts +10 -0
- package/templates/nextjs-standalone/package.json +66 -0
- package/templates/nextjs-standalone/postcss.config.mjs +5 -0
- package/templates/nextjs-standalone/sh-ui.config.json +19 -0
- package/templates/nextjs-standalone/src/app/layouts/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/app/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/src/app/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
- package/templates/nextjs-standalone/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/src/app/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-standalone/src/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/features/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/shared/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/shared/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/shared/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/shared/lib/utils.ts +6 -0
- package/templates/nextjs-standalone/src/shared/model/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/shared/styles/tokens.css +95 -0
- package/templates/nextjs-standalone/src/shared/ui/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/views/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/widgets/.gitkeep +0 -0
- package/templates/nextjs-standalone/tsconfig.json +39 -0
- package/templates/nextjs-standalone/vitest.config.ts +15 -0
- package/templates/nextjs-standalone/vitest.setup.ts +1 -0
- package/templates/ui-app-template/eslint.config.js +3 -0
- package/templates/ui-app-template/package.json +38 -0
- package/templates/ui-app-template/postcss.config.mjs +5 -0
- package/templates/ui-app-template/sh-ui.config.json +14 -0
- package/templates/ui-app-template/src/components/.gitkeep +0 -0
- package/templates/ui-app-template/src/hooks/.gitkeep +0 -0
- package/templates/ui-app-template/src/lib/.gitkeep +0 -0
- package/templates/ui-app-template/src/styles/globals.css +37 -0
- package/templates/ui-app-template/src/styles/tokens.css +95 -0
- 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
|
-
## 새 프로젝트를 만드는 경우
|
|
107
|
+
## 새 프로젝트를 만드는 경우
|
|
106
108
|
|
|
107
|
-
빈 폴더에서 시작하거나 사용자가 "Next.js 앱 만들어줘", "Flutter 프로젝트 새로", "sh-ui 로 시작" 처럼 **스캐폴드부터**
|
|
109
|
+
빈 폴더에서 시작하거나 사용자가 "Next.js 앱 만들어줘", "Flutter 프로젝트 새로", "sh-ui 로 시작" 처럼 **스캐폴드부터** 요청하면:
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
138
|
+
{ name: "sh-ui", version: "0.23.1" }, // 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
|
-
"⚠️ 빈 폴더/새 프로젝트면 이 툴 대신
|
|
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
|
+
}
|