sh-ui-cli 0.92.0 → 0.93.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,21 @@
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.93.0",
7
+ "date": "2026-05-14",
8
+ "title": "vite 프리셋 Sentry observability opt-in (--observability sentry)",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**`--observability sentry` 옵션** — vite preset 에 opt-in Sentry 셋업 추가. `@sentry/react` (deps) + `@sentry/vite-plugin` (devDeps) + `Sentry.init({...})` (DSN 가드, browserTracing + replay PII 보호 디폴트) + `SentryProvider` (ErrorBoundary wrap) + `.env.example` 자동 emit. DSN 빈 값이면 init 자동 skip — dev/CI 안전. ai-org 처럼 error tracking 부터 들어가는 워크로드 5분 boilerplate 제거.",
12
+ "**enum 디자인** — `observability: z.enum(['none', 'sentry'])` future-extensible. GlitchTip 등 Sentry-protocol 호환 서비스는 같은 SDK + DSN 만 변경하면 동작 (라이브러리 lock-in 아님). 추후 `glitchtip` / `bugsnag` 등 enum 확장 자연스럽게.",
13
+ "**Source map upload 자동 셋업** — `vite.config.ts` 에 `sentryVitePlugin({ org, project, authToken, disable: !SENTRY_AUTH_TOKEN })` 자동 추가 + `build.sourcemap: true`. SENTRY_AUTH_TOKEN 이 env 에 있으면 빌드 시 source map 자동 업로드, 없으면 plugin disable. CI 친화.",
14
+ "**i18n + Sentry 동시 시 wrapper 순서 보장** — SentryProvider 가 OUTERMOST (모든 children error 캐치) → I18nProvider → ThemeProvider → QueryClientProvider. Smoke V20 가 wrapper index 순서 단언으로 회귀 가드.",
15
+ "**Vite types 자동 reference** — emit 된 `sentry.ts` 가 `/// <reference types=\"vite/client\" />` triple-slash 로 `import.meta.env.VITE_SENTRY_DSN` 타입 정확. 사용자 tsconfig 손댈 필요 X.",
16
+ "**guards + 회귀** — `observability + platform !== 'vite'` 조합은 CLI + MCP 양쪽에서 명시적 Korean 에러. Smoke V18 (positive), V19 (default opt-out 회귀), V20 (i18n + sentry combo wrapper 순서). docs `/create` 페이지도 observability 토글 노출."
17
+ ],
18
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.93.0"
19
+ },
5
20
  {
6
21
  "version": "0.92.0",
7
22
  "date": "2026-05-14",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.92.0",
3
+ "version": "0.93.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/api.d.ts CHANGED
@@ -145,6 +145,8 @@ export interface DescribeTemplateOptions {
145
145
  i18n?: 'none' | 'react-i18next';
146
146
  /** i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en' */
147
147
  locales?: string;
148
+ /** vite 전용 — Sentry observability opt-in. v0.93.0+ */
149
+ observability?: 'none' | 'sentry';
148
150
  }
149
151
 
150
152
  export interface DescribeTemplateGroup {
package/src/constants.js CHANGED
@@ -60,3 +60,8 @@ export const INIT_DEFAULTS = {
60
60
  export const I18N_LIBRARIES = ['none', 'react-i18next'];
61
61
  export const I18N_DEFAULT = 'none';
62
62
  export const I18N_DEFAULT_LOCALES = 'ko,en';
63
+
64
+ // ─── observability (vite preset 전용 — v0.93.0+) ───
65
+ // 'none' 디폴트 — opt-in 으로 @sentry/react + @sentry/vite-plugin 설치 + 셋업.
66
+ // GlitchTip self-hosted 도 같은 SDK — DSN 만 변경. 향후 bugsnag 등 추가 시 enum 확장.
67
+ export const OBSERVABILITY_PROVIDERS = ['none', 'sentry'];
@@ -4,6 +4,7 @@ import {
4
4
  CSS_FRAMEWORKS_SUPPORTED,
5
5
  CSS_FRAMEWORKS_PLANNED,
6
6
  I18N_LIBRARIES,
7
+ OBSERVABILITY_PROVIDERS,
7
8
  } from '../constants.js';
8
9
  import { allPlugins } from './plugins/index.js';
9
10
  import { allArchitectures } from './architectures/index.js';
@@ -13,7 +14,7 @@ const VALID_STRUCTURES = CREATE_STRUCTURES;
13
14
  const VALID_PLUGINS = allPlugins.map((p) => p.name);
14
15
  const VALID_ARCHES = allArchitectures.map((a) => a.name);
15
16
 
16
- const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port', 'i18n', 'locales'];
17
+ const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port', 'i18n', 'locales', 'observability'];
17
18
  const BOOL_FLAGS = ['yes', 'help', 'dry-run', 'tauri'];
18
19
 
19
20
  const SUBCOMMANDS = ['add-app', 'add-component'];
@@ -79,6 +80,9 @@ export const parseArgs = (argv) => {
79
80
  if (name === 'i18n' && !I18N_LIBRARIES.includes(value)) {
80
81
  throw new Error(`--i18n 은 ${I18N_LIBRARIES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
81
82
  }
83
+ if (name === 'observability' && !OBSERVABILITY_PROVIDERS.includes(value)) {
84
+ throw new Error(`--observability 는 ${OBSERVABILITY_PROVIDERS.join('/')} 중 하나여야 함 (받은 값: ${value})`);
85
+ }
82
86
  if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
83
87
  // planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
84
88
  if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
@@ -38,6 +38,7 @@ import { CSS_FRAMEWORK_DEFAULT } from '../constants.js';
38
38
  * @property {boolean} [tauri] platform=vite (standalone/monorepo 둘 다) 일 때 Tauri 2.x 셸 같이 emit
39
39
  * @property {'none'|'react-i18next'} [i18n] vite 전용 — react-i18next 셋업
40
40
  * @property {string} [locales] i18n 활성화 시 생성할 locale 코드 (comma-separated, default 'ko,en')
41
+ * @property {'none'|'sentry'} [observability] vite 전용 — Sentry 셋업 (v0.93.0+)
41
42
  */
42
43
 
43
44
  /**
@@ -68,6 +69,7 @@ export function describeTemplate(opts = {}) {
68
69
  tauri = false,
69
70
  i18n = 'none',
70
71
  locales = 'ko,en',
72
+ observability = 'none',
71
73
  } = opts;
72
74
 
73
75
  if (platform === 'flutter') {
@@ -116,6 +118,17 @@ export function describeTemplate(opts = {}) {
116
118
  ];
117
119
  groups.push(makeGroup('i18n', `i18n (${i18n})`, i18nFiles));
118
120
  }
121
+ if (observability === 'sentry') {
122
+ const isFsd = safeArchName === 'fsd';
123
+ const obsBase = isFsd ? 'src/shared/observability' : 'src/lib/observability';
124
+ const providersBase = isFsd ? 'src/app/providers' : 'src/components/providers';
125
+ groups.push(makeGroup('sentry', `Sentry observability`, [
126
+ `${obsBase}/sentry.ts`,
127
+ `${obsBase}/index.ts`,
128
+ `${providersBase}/SentryProvider.tsx`,
129
+ '.env.example',
130
+ ]));
131
+ }
119
132
  return finalize(groups);
120
133
  }
121
134
 
@@ -180,6 +193,18 @@ export function describeTemplate(opts = {}) {
180
193
  groups.push(makeGroup('i18n', `i18n (${i18n}, apps/${appName}/)`, i18nFiles));
181
194
  }
182
195
 
196
+ if (observability === 'sentry') {
197
+ const isFsd = safeArchName === 'fsd';
198
+ const obsBase = isFsd ? 'src/shared/observability' : 'src/lib/observability';
199
+ const providersBase = isFsd ? 'src/app/providers' : 'src/components/providers';
200
+ groups.push(makeGroup('sentry', `Sentry observability (apps/${appName}/)`, [
201
+ `apps/${appName}/${obsBase}/sentry.ts`,
202
+ `apps/${appName}/${obsBase}/index.ts`,
203
+ `apps/${appName}/${providersBase}/SentryProvider.tsx`,
204
+ `apps/${appName}/.env.example`,
205
+ ]));
206
+ }
207
+
183
208
  return finalize(groups);
184
209
  }
185
210
 
@@ -231,6 +231,13 @@ export async function createProject(options = {}) {
231
231
  );
232
232
  }
233
233
 
234
+ // observability 옵션도 vite preset 전용. v0.93.0+.
235
+ if (options.observability && options.observability !== 'none' && platform !== 'vite') {
236
+ throw new Error(
237
+ `observability='${options.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --observability none 또는 --platform vite 사용.`,
238
+ );
239
+ }
240
+
234
241
  // arch 결정 — platform 확정 후. 사용자가 --arch 미지정 시:
235
242
  // - next → DEFAULT_ARCH ('fsd')
236
243
  // - flutter → 현재 Flutter arch 디스크립터 없음 → null. 미래에 flutter arch 추가되면
@@ -355,6 +362,7 @@ export async function createProject(options = {}) {
355
362
  tauri: !!options.tauri,
356
363
  i18n: options.i18n ?? 'none',
357
364
  locales: options.locales ?? 'ko,en',
365
+ observability: options.observability ?? 'none',
358
366
  });
359
367
  } else {
360
368
  await generateMonorepo(targetDir, projectName, [], {
@@ -363,6 +371,7 @@ export async function createProject(options = {}) {
363
371
  tauri: options.tauri,
364
372
  i18n: options.i18n ?? 'none',
365
373
  locales: options.locales ?? 'ko,en',
374
+ observability: options.observability ?? 'none',
366
375
  });
367
376
  }
368
377
 
@@ -529,6 +538,11 @@ export async function addApp(options = {}) {
529
538
  `i18n='${options.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}).`,
530
539
  );
531
540
  }
541
+ if (options.observability && options.observability !== 'none' && platform !== 'vite') {
542
+ throw new Error(
543
+ `observability='${options.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --observability none 또는 --platform vite 사용.`,
544
+ );
545
+ }
532
546
 
533
547
  const appName = validateProjectName(
534
548
  options.name ?? await input({
@@ -583,6 +597,7 @@ export async function addApp(options = {}) {
583
597
  tauri: !!options.tauri,
584
598
  i18n: options.i18n ?? 'none',
585
599
  locales: options.locales ?? 'ko,en',
600
+ observability: options.observability ?? 'none',
586
601
  });
587
602
  } else {
588
603
  await generateApp(appsDir, appName, port, plugins, arch, css);
@@ -846,7 +861,7 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
846
861
  await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
847
862
  }
848
863
 
849
- async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
864
+ async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
850
865
  // 베이스 (arch-neutral) + arch 오버레이 — generateStandalone 과 같은 패턴.
851
866
  await fs.copy(path.join(TEMPLATES_DIR, 'vite-standalone'), targetDir, {
852
867
  filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
@@ -892,6 +907,10 @@ async function generateViteStandalone(targetDir, projectName, theme, css, arch,
892
907
  const localesArr = parseLocales(locales);
893
908
  await emitI18n(targetDir, { arch, locales: localesArr });
894
909
  }
910
+
911
+ if (observability === 'sentry') {
912
+ await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
913
+ }
895
914
  }
896
915
 
897
916
  /**
@@ -1127,7 +1146,183 @@ function parseLocales(input) {
1127
1146
  return cleaned.length > 0 ? cleaned : ['ko', 'en'];
1128
1147
  }
1129
1148
 
1130
- async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
1149
+ /**
1150
+ * Sentry observability 셋업 emit (v0.93.0+). vite preset 전용 opt-in.
1151
+ *
1152
+ * - shared/observability/sentry.ts — Sentry.init (DSN 있을 때만)
1153
+ * - shared/observability/index.ts — Sentry + ErrorBoundary re-export
1154
+ * - app/providers/SentryProvider.tsx — ErrorBoundary wrapper
1155
+ * - GlobalProvider 재작성 — Sentry > [I18n?] > Theme > Query 순서
1156
+ * - package.json — @sentry/react + @sentry/vite-plugin 추가
1157
+ * - vite.config.ts — sentryVitePlugin 삽입 + sourcemap: true
1158
+ * - .env.example — Sentry 변수 안내 블록 추가
1159
+ *
1160
+ * @param {string} targetDir — 앱 디렉토리
1161
+ * @param {object} opts
1162
+ * @param {object} opts.arch — arch descriptor
1163
+ * @param {boolean} [opts.i18nActive] — i18n 도 같이 켜져 있는지 (GlobalProvider wrapping 순서)
1164
+ */
1165
+ async function emitSentry(targetDir, { arch, i18nActive = false }) {
1166
+ const isFsd = arch.name === 'fsd';
1167
+ const obsDirRel = isFsd ? 'src/shared/observability' : 'src/lib/observability';
1168
+ const obsAlias = isFsd ? '@/shared/observability' : '@/lib/observability';
1169
+ const providersDirRel = isFsd ? 'src/app/providers' : 'src/components/providers';
1170
+ const apiAlias = isFsd ? '@/shared/api/queryClient' : '@/lib/api/queryClient';
1171
+
1172
+ const obsDir = path.join(targetDir, obsDirRel);
1173
+ await fs.ensureDir(obsDir);
1174
+
1175
+ const sentryTs = `/// <reference types="vite/client" />
1176
+ import * as Sentry from '@sentry/react';
1177
+
1178
+ // VITE_SENTRY_DSN 이 있을 때만 init — 로컬 dev 에선 자동 skip.
1179
+ // GlitchTip self-hosted 도 같은 SDK — DSN 만 변경.
1180
+ if (import.meta.env.VITE_SENTRY_DSN) {
1181
+ Sentry.init({
1182
+ dsn: import.meta.env.VITE_SENTRY_DSN,
1183
+ release: import.meta.env.VITE_APP_VERSION,
1184
+ environment: import.meta.env.MODE,
1185
+ tracesSampleRate: 0.1,
1186
+ replaysOnErrorSampleRate: 1.0,
1187
+ replaysSessionSampleRate: 0,
1188
+ integrations: [
1189
+ Sentry.browserTracingIntegration(),
1190
+ Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
1191
+ ],
1192
+ });
1193
+ }
1194
+
1195
+ export { Sentry };
1196
+ `;
1197
+ await fs.writeFile(path.join(obsDir, 'sentry.ts'), sentryTs);
1198
+
1199
+ await fs.writeFile(
1200
+ path.join(obsDir, 'index.ts'),
1201
+ `export { Sentry } from './sentry';\nexport { ErrorBoundary } from '@sentry/react';\n`,
1202
+ );
1203
+
1204
+ const providersDir = path.join(targetDir, providersDirRel);
1205
+ await fs.ensureDir(providersDir);
1206
+ const sentryProvider = `import { type ReactNode } from 'react';
1207
+ import { ErrorBoundary } from '${obsAlias}';
1208
+
1209
+ function Fallback({ error }: { error: unknown }) {
1210
+ return (
1211
+ <div style={{ padding: '2rem', textAlign: 'center' }}>
1212
+ <h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>오류가 발생했습니다</h1>
1213
+ <p style={{ marginTop: '0.5rem', color: 'var(--foreground-muted)' }}>
1214
+ {error instanceof Error ? error.message : '알 수 없는 오류'}
1215
+ </p>
1216
+ </div>
1217
+ );
1218
+ }
1219
+
1220
+ export function SentryProvider({ children }: { children: ReactNode }) {
1221
+ return (
1222
+ <ErrorBoundary fallback={({ error }) => <Fallback error={error} />}>
1223
+ {children}
1224
+ </ErrorBoundary>
1225
+ );
1226
+ }
1227
+ `;
1228
+ await fs.writeFile(path.join(providersDir, 'SentryProvider.tsx'), sentryProvider);
1229
+
1230
+ // GlobalProvider rewrite — Sentry outermost, then optional I18n, then Theme + Query.
1231
+ const globalProviderPath = path.join(targetDir, providersDirRel, 'GlobalProvider', 'index.tsx');
1232
+ const i18nImport = i18nActive ? `import { I18nProvider } from '../I18nProvider';\n` : '';
1233
+ const innerOpen = i18nActive ? ' <I18nProvider>\n ' : ' ';
1234
+ const innerClose = i18nActive ? '\n </I18nProvider>' : '';
1235
+ const globalProvider = `import { QueryClientProvider } from '@tanstack/react-query';
1236
+ import { type ReactNode, useState } from 'react';
1237
+ import { createQueryClient } from '${apiAlias}';
1238
+ import { ThemeProvider } from '../theme/ThemeProvider';
1239
+ ${i18nImport}import { SentryProvider } from '../SentryProvider';
1240
+
1241
+ export function GlobalProvider({ children }: { children: ReactNode }) {
1242
+ const [queryClient] = useState(() => createQueryClient());
1243
+ return (
1244
+ <SentryProvider>
1245
+ ${innerOpen}<ThemeProvider>
1246
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
1247
+ </ThemeProvider>${innerClose}
1248
+ </SentryProvider>
1249
+ );
1250
+ }
1251
+ `;
1252
+ await fs.writeFile(globalProviderPath, globalProvider);
1253
+
1254
+ // package.json deps
1255
+ const pkgPath = path.join(targetDir, 'package.json');
1256
+ const pkg = await fs.readJson(pkgPath);
1257
+ pkg.dependencies = pkg.dependencies ?? {};
1258
+ pkg.devDependencies = pkg.devDependencies ?? {};
1259
+ pkg.dependencies['@sentry/react'] = '^8.45.0';
1260
+ pkg.devDependencies['@sentry/vite-plugin'] = '^2.22.7';
1261
+ pkg.dependencies = sortObjectKeys(pkg.dependencies);
1262
+ pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
1263
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1264
+
1265
+ await patchViteConfigForSentry(targetDir);
1266
+
1267
+ // .env.example
1268
+ const envExamplePath = path.join(targetDir, '.env.example');
1269
+ const envBlock = `# Sentry observability (v0.93.0+) — 비워두면 init 자동 skip.
1270
+ VITE_SENTRY_DSN=
1271
+ VITE_APP_VERSION=
1272
+ # Source map upload (vite build 시).
1273
+ SENTRY_AUTH_TOKEN=
1274
+ SENTRY_ORG=
1275
+ SENTRY_PROJECT=
1276
+ `;
1277
+ if (await fs.pathExists(envExamplePath)) {
1278
+ const existing = await fs.readFile(envExamplePath, 'utf-8');
1279
+ if (!existing.includes('VITE_SENTRY_DSN')) {
1280
+ await fs.writeFile(envExamplePath, existing + '\n' + envBlock);
1281
+ }
1282
+ } else {
1283
+ await fs.writeFile(envExamplePath, envBlock);
1284
+ }
1285
+ }
1286
+
1287
+ async function patchViteConfigForSentry(targetDir) {
1288
+ const viteCfgPath = path.join(targetDir, 'vite.config.ts');
1289
+ if (!(await fs.pathExists(viteCfgPath))) {
1290
+ throw new Error(`vite.config.ts 가 ${targetDir} 에 없습니다.`);
1291
+ }
1292
+ let cfg = await fs.readFile(viteCfgPath, 'utf-8');
1293
+
1294
+ if (!cfg.includes("@sentry/vite-plugin")) {
1295
+ cfg = cfg.replace(
1296
+ /import { defineConfig } from 'vite';/,
1297
+ `import { defineConfig } from 'vite';\nimport { sentryVitePlugin } from '@sentry/vite-plugin';`,
1298
+ );
1299
+ }
1300
+
1301
+ if (!cfg.includes("sentryVitePlugin(")) {
1302
+ cfg = cfg.replace(
1303
+ /(plugins:\s*\[[^\]]*?)(\s*\])/s,
1304
+ (_, before, close) => {
1305
+ const sep = /,\s*$/.test(before) ? ' ' : ', ';
1306
+ return `${before}${sep}sentryVitePlugin({\n org: process.env.SENTRY_ORG,\n project: process.env.SENTRY_PROJECT,\n authToken: process.env.SENTRY_AUTH_TOKEN,\n disable: !process.env.SENTRY_AUTH_TOKEN,\n })${close}`;
1307
+ },
1308
+ );
1309
+ }
1310
+
1311
+ if (!cfg.includes("sourcemap:")) {
1312
+ if (cfg.includes("server: {")) {
1313
+ cfg = cfg.replace(/(\n\s*server:\s*\{)/, `\n build: { sourcemap: true },$1`);
1314
+ } else if (cfg.includes("plugins: [")) {
1315
+ cfg = cfg.replace(
1316
+ /(plugins:\s*\[[^\]]*?\][^,\n]*[,;]?\n)/s,
1317
+ `$1 build: { sourcemap: true },\n`,
1318
+ );
1319
+ }
1320
+ }
1321
+
1322
+ await fs.writeFile(viteCfgPath, cfg);
1323
+ }
1324
+
1325
+ async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
1131
1326
  await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
1132
1327
 
1133
1328
  // Update root package.json
@@ -1159,7 +1354,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
1159
1354
 
1160
1355
  const appsDir = path.join(targetDir, 'apps', appName);
1161
1356
  if (platform === 'vite') {
1162
- await generateViteApp(appsDir, appName, port, arch, css, { tauri, i18n, locales });
1357
+ await generateViteApp(appsDir, appName, port, arch, css, { tauri, i18n, locales, observability });
1163
1358
  } else {
1164
1359
  await generateApp(appsDir, appName, port, plugins, arch, css);
1165
1360
  }
@@ -1260,7 +1455,7 @@ async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailw
1260
1455
  }
1261
1456
  }
1262
1457
 
1263
- async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
1458
+ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
1264
1459
  // 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 동일 패턴.
1265
1460
  await fs.copy(path.join(TEMPLATES_DIR, 'vite-app'), targetDir, {
1266
1461
  filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
@@ -1333,6 +1528,10 @@ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind',
1333
1528
  const localesArr = parseLocales(locales);
1334
1529
  await emitI18n(targetDir, { arch, locales: localesArr });
1335
1530
  }
1531
+
1532
+ if (observability === 'sentry') {
1533
+ await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
1534
+ }
1336
1535
  }
1337
1536
 
1338
1537
  /**
@@ -17,8 +17,8 @@ const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join('|');
17
17
  export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next.js / Flutter)
18
18
 
19
19
  사용법:
20
- sh-ui create [name] [options]
21
- sh-ui create add-app [name] [--port <n>] [--platform <next|vite>] [--plugins ..] [--theme ..] [--css ..] [--tauri] [--i18n <react-i18next|none>] [--locales ko,en]
20
+ sh-ui create [name] [options] [--observability <none|sentry>]
21
+ sh-ui create add-app [name] [--port <n>] [--platform <next|vite>] [--plugins ..] [--theme ..] [--css ..] [--tauri] [--i18n <react-i18next|none>] [--locales ko,en] [--observability <none|sentry>]
22
22
  sh-ui create add-component <name> [--app <name>]
23
23
 
24
24
  옵션:
@@ -30,6 +30,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
30
30
  --css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크. base 파일까지 분기 emit (tailwind/plain/css-modules)
31
31
  --i18n <react-i18next|none> vite 전용 — react-i18next 셋업 emit (i18n config + I18nProvider). 기본 none (v0.92.0+)
32
32
  --locales <ko,en> i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en'
33
+ --observability <none|sentry> vite 전용 — Sentry 셋업 emit (@sentry/react + vite-plugin + SentryProvider). 기본 none (v0.93.0+)
33
34
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
34
35
  --dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
35
36
  -h, --help 이 도움말
@@ -79,6 +80,7 @@ export async function runCreate(rest) {
79
80
  tauri: flags.tauri,
80
81
  i18n: flags.i18n,
81
82
  locales: flags.locales,
83
+ observability: flags.observability,
82
84
  });
83
85
  } else if (command === 'add-component') {
84
86
  // 호환 별칭 — 신규 진입점은 `sh-ui add <name>` (bin/sh-ui.mjs 가 walk-up 으로 라우팅).
@@ -98,6 +100,7 @@ export async function runCreate(rest) {
98
100
  tauri: flags.tauri,
99
101
  i18n: flags.i18n,
100
102
  locales: flags.locales,
103
+ observability: flags.observability,
101
104
  yes: flags.yes,
102
105
  dryRun: flags.dryRun,
103
106
  });
package/src/mcp.mjs CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  CSS_FRAMEWORKS_SUPPORTED,
49
49
  I18N_LIBRARIES,
50
50
  I18N_DEFAULT_LOCALES,
51
+ OBSERVABILITY_PROVIDERS,
51
52
  } from "./constants.js";
52
53
  import { allPlugins } from "./create/plugins/index.js";
53
54
  import { allArchitectures } from "./create/architectures/index.js";
@@ -414,6 +415,11 @@ export async function startMcpServer() {
414
415
  `i18n 활성화 시 생성할 locale 코드 (comma-separated, 2글자 또는 'ko-KR' 류). 기본 '${I18N_DEFAULT_LOCALES}'. 첫 locale 이 fallbackLng. ` +
415
416
  "i18n='none' 이면 무시.",
416
417
  ),
418
+ observability: z.enum(OBSERVABILITY_PROVIDERS).optional()
419
+ .describe(
420
+ "observability provider — platform=vite 일 때만 의미. 'sentry' 로 설정 시 @sentry/react + " +
421
+ "@sentry/vite-plugin 셋업 + SentryProvider + .env.example 자동 emit. GlitchTip self-hosted 도 같은 SDK — DSN 만 변경. 기본 'none'.",
422
+ ),
417
423
  },
418
424
  },
419
425
  async (input) => {
@@ -453,6 +459,15 @@ export async function startMcpServer() {
453
459
  }],
454
460
  };
455
461
  }
462
+ if (input.observability && input.observability !== "none" && input.platform !== "vite") {
463
+ return {
464
+ isError: true,
465
+ content: [{
466
+ type: "text",
467
+ text: `observability='${input.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${input.platform}).`,
468
+ }],
469
+ };
470
+ }
456
471
  const targetParent = resolveCwd(input);
457
472
  const targetDir = resolve(targetParent, input.name);
458
473
  if (existsSync(targetDir) && !input.force) {
@@ -481,6 +496,7 @@ export async function startMcpServer() {
481
496
  tauri: input.tauri,
482
497
  i18n: input.i18n,
483
498
  locales: input.locales,
499
+ observability: input.observability,
484
500
  yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
485
501
  }),
486
502
  );
@@ -524,6 +540,11 @@ export async function startMcpServer() {
524
540
  .describe(
525
541
  `i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 '${I18N_DEFAULT_LOCALES}'. 첫 locale 이 fallbackLng. i18n='none' 이면 무시.`,
526
542
  ),
543
+ observability: z.enum(OBSERVABILITY_PROVIDERS).optional()
544
+ .describe(
545
+ "observability provider — platform=vite 일 때만 의미. 'sentry' 로 설정 시 @sentry/react + " +
546
+ "@sentry/vite-plugin 셋업 + SentryProvider + .env.example 자동 emit. GlitchTip self-hosted 도 같은 SDK — DSN 만 변경. 기본 'none'.",
547
+ ),
527
548
  cwd: z.string().optional()
528
549
  .describe("모노레포 루트 (pnpm-workspace.yaml 있는 곳). 기본 process.cwd()"),
529
550
  },
@@ -552,6 +573,15 @@ export async function startMcpServer() {
552
573
  }],
553
574
  };
554
575
  }
576
+ if (input.observability && input.observability !== "none" && input.platform && input.platform !== "vite") {
577
+ return {
578
+ isError: true,
579
+ content: [{
580
+ type: "text",
581
+ text: `observability='${input.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${input.platform}).`,
582
+ }],
583
+ };
584
+ }
555
585
  const text = await captureConsole(() =>
556
586
  addApp({
557
587
  name: input.name,
@@ -563,6 +593,7 @@ export async function startMcpServer() {
563
593
  tauri: input.tauri,
564
594
  i18n: input.i18n,
565
595
  locales: input.locales,
596
+ observability: input.observability,
566
597
  cwd: resolveCwd(input),
567
598
  }),
568
599
  );