sh-ui-cli 0.32.1 → 0.33.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.33.0",
7
+ "date": "2026-04-29",
8
+ "title": "중앙관리형 SSOT — sh-ui-cli/api 노출 + 플러그인 스키마 + --dry-run + CI 스캐폴드 게이트",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "신규 public API — `import { allPlugins, CREATE_PLATFORMS, ... } from 'sh-ui-cli/api'`. 다른 워크스페이스 (apps/docs 등) 가 플러그인 메타와 enum 을 동기화 없이 사용. CreateProjectDialog / 사이드바 / 플러그인 허브 가 모두 derive 하도록 변경 — 새 플러그인 추가 시 plugins/ 폴더 한 군데만 건드리면 됨",
12
+ "신규 --dry-run 플래그 — 실제 파일 쓰지 않고 스캐폴드 결과 파일 목록만 출력. 디버깅 + CI 검증 보조. tmpdir 에 생성 후 즉시 정리해 사용자 cwd 무영향",
13
+ "플러그인 manifest zod 스키마 검증 — 모듈 로드 시점에 모든 플러그인 형태 체크. 'src/proxy.ts' 같은 잘못된 경로는 빌드 타임 거부 (v0.32.0 에서 발생한 회귀 차단)",
14
+ "CI 스캐폴드 게이트 — publish 전에 5가지 플러그인 조합으로 실제 스캐폴드 → fresh install → tsc --noEmit 까지 돌리는 스모크 단계 추가. 사용자가 받게 될 결과물을 publish 직전에 검증",
15
+ "통합 테스트 7개 추가 — 각 플러그인 + 조합의 핵심 파일 위치 / 내용 / 합성 검증 (proxy.ts root 위치, 합성된 proxy.ts 의 intl + 인증 가드, observability 의 Sentry vs no-op, axios 부재 등)",
16
+ "내부 정리 — packages/cli/src/constants.js 신설로 PLATFORMS/STRUCTURES/THEME_* 단일화. cli-args / mcp / init / generator 도움말 모두 derive"
17
+ ],
18
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.33.0"
19
+ },
5
20
  {
6
21
  "version": "0.32.1",
7
22
  "date": "2026-04-29",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,12 @@
40
40
  "bin": {
41
41
  "sh-ui": "./bin/sh-ui.mjs"
42
42
  },
43
+ "exports": {
44
+ "./api": {
45
+ "types": "./src/api.d.ts",
46
+ "default": "./src/api.js"
47
+ }
48
+ },
43
49
  "scripts": {
44
50
  "bundle-data": "node scripts/copy-data.mjs",
45
51
  "test": "vitest run",
package/src/api.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * sh-ui-cli 외부 노출 API 의 타입 선언.
3
+ * apps/docs 등 TypeScript 사용자가 자동완성과 타입 안전을 받을 수 있게.
4
+ */
5
+
6
+ export type CreatePlatform = 'next' | 'flutter';
7
+ export type CreateStructure = 'standalone' | 'monorepo';
8
+ export type InitPlatform = 'react' | 'flutter';
9
+ export type ThemeBase = 'neutral' | 'zinc' | 'slate';
10
+ export type ThemeRadius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
11
+ export type ThemeMode = 'light-dark' | 'light' | 'dark';
12
+
13
+ export const CREATE_PLATFORMS: readonly CreatePlatform[];
14
+ export const CREATE_STRUCTURES: readonly CreateStructure[];
15
+ export const INIT_PLATFORMS: readonly InitPlatform[];
16
+ export const THEME_BASES: readonly ThemeBase[];
17
+ export const THEME_RADII: readonly ThemeRadius[];
18
+ export const THEME_MODES: readonly ThemeMode[];
19
+
20
+ export const INIT_DEFAULTS: {
21
+ platform: InitPlatform;
22
+ base: ThemeBase;
23
+ radius: ThemeRadius;
24
+ mode: ThemeMode;
25
+ };
26
+
27
+ export type PluginManifest = {
28
+ name: string;
29
+ label: string;
30
+ description?: string;
31
+ priority: number;
32
+ dependencies?: Record<string, string>;
33
+ devDependencies?: Record<string, string>;
34
+ envVars?: string[];
35
+ turboEnvVars?: string[];
36
+ imports?: string[];
37
+ providerImports?: string[];
38
+ providerWrappers?: Array<{ open: string; close: string } | string>;
39
+ files?: Record<string, string>;
40
+ };
41
+
42
+ export const allPlugins: readonly PluginManifest[];
package/src/api.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * sh-ui-cli 외부 노출 API.
3
+ *
4
+ * apps/docs 같은 다른 워크스페이스 패키지가 플러그인 메타데이터와 enum 을
5
+ * 동기화 없이 사용하도록. package.json 의 "exports": { "./api": "./src/api.js" }
6
+ * 로 노출되며, 사용자는 다음과 같이 import:
7
+ *
8
+ * import { allPlugins, CREATE_PLATFORMS } from 'sh-ui-cli/api';
9
+ */
10
+
11
+ export {
12
+ CREATE_PLATFORMS,
13
+ CREATE_STRUCTURES,
14
+ INIT_PLATFORMS,
15
+ THEME_BASES,
16
+ THEME_RADII,
17
+ THEME_MODES,
18
+ INIT_DEFAULTS,
19
+ } from './constants.js';
20
+
21
+ export { allPlugins } from './create/plugins/index.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * sh-ui-cli 의 모든 enum / 상수 단일 진실.
3
+ *
4
+ * 다른 모듈 (cli-args, mcp, init, generator) 과 외부 패키지 (apps/docs) 는
5
+ * 이 파일을 import 해서 사용한다. 새 값 추가 시 여기만 고치면 된다.
6
+ */
7
+
8
+ // ─── 프로젝트 생성 (sh-ui-cli create) ───
9
+
10
+ export const CREATE_PLATFORMS = ['next', 'flutter'];
11
+
12
+ export const CREATE_STRUCTURES = ['standalone', 'monorepo'];
13
+
14
+ // ─── 초기화 (sh-ui-cli init) — sh-ui 컴포넌트 설정 ───
15
+
16
+ export const INIT_PLATFORMS = ['react', 'flutter'];
17
+
18
+ export const THEME_BASES = ['neutral', 'zinc', 'slate'];
19
+
20
+ export const THEME_RADII = ['none', 'sm', 'md', 'lg', 'xl', 'full'];
21
+
22
+ export const THEME_MODES = ['light-dark', 'light', 'dark'];
23
+
24
+ // ─── 기본값 ───
25
+
26
+ export const INIT_DEFAULTS = {
27
+ platform: 'react',
28
+ base: 'neutral',
29
+ radius: 'md',
30
+ mode: 'light-dark',
31
+ };
@@ -1,9 +1,12 @@
1
- const VALID_PLATFORMS = ['next', 'flutter'];
2
- const VALID_STRUCTURES = ['standalone', 'monorepo'];
3
- const VALID_PLUGINS = ['sentry', 'next-intl', 'auth-jwt'];
1
+ import { CREATE_PLATFORMS, CREATE_STRUCTURES } from '../constants.js';
2
+ import { allPlugins } from './plugins/index.js';
3
+
4
+ const VALID_PLATFORMS = CREATE_PLATFORMS;
5
+ const VALID_STRUCTURES = CREATE_STRUCTURES;
6
+ const VALID_PLUGINS = allPlugins.map((p) => p.name);
4
7
 
5
8
  const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app'];
6
- const BOOL_FLAGS = ['yes', 'help'];
9
+ const BOOL_FLAGS = ['yes', 'help', 'dry-run'];
7
10
 
8
11
  const SUBCOMMANDS = ['add-app', 'add-component'];
9
12
 
@@ -29,7 +32,9 @@ export const parseArgs = (argv) => {
29
32
  }
30
33
  const name = arg.slice(2);
31
34
  if (BOOL_FLAGS.includes(name)) {
32
- flags[name] = true;
35
+ // dry-run 은 dryRun 으로 캐멀 케이스
36
+ const key = name === 'dry-run' ? 'dryRun' : name;
37
+ flags[key] = true;
33
38
  continue;
34
39
  }
35
40
  if (!VALUE_FLAGS.includes(name)) {
@@ -44,7 +49,9 @@ export const parseArgs = (argv) => {
44
49
  const list = value === '' ? [] : value.split(',').map((s) => s.trim()).filter(Boolean);
45
50
  for (const p of list) {
46
51
  if (!VALID_PLUGINS.includes(p)) {
47
- throw new Error(`알 수 없는 플러그인: ${p}`);
52
+ throw new Error(
53
+ `알 수 없는 플러그인: ${p} (지원: ${VALID_PLUGINS.join(', ')})`,
54
+ );
48
55
  }
49
56
  }
50
57
  flags.plugins = list;
@@ -1,6 +1,7 @@
1
1
  import { input, select, checkbox, confirm } from '@inquirer/prompts';
2
2
  import { execSync } from 'node:child_process';
3
3
  import fs from 'fs-extra';
4
+ import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { getPluginChoices, getPluginsByNames } from './plugins/index.js';
6
7
  import { decodeTheme } from './theme/decode.js';
@@ -53,9 +54,13 @@ export async function createProject(options = {}) {
53
54
 
54
55
  const theme = options.theme ? decodeTheme(options.theme) : null;
55
56
 
56
- const targetDir = path.resolve(process.cwd(), projectName);
57
+ // dry-run tmpdir 에 그대로 생성한 뒤 파일 목록 출력 + 정리.
58
+ // 사용자 cwd 를 건드리지 않으면서 실제 generation 흐름을 그대로 검증한다.
59
+ const targetDir = options.dryRun
60
+ ? await fs.mkdtemp(path.join(os.tmpdir(), 'sh-ui-dry-'))
61
+ : path.resolve(process.cwd(), projectName);
57
62
 
58
- if (await fs.pathExists(targetDir)) {
63
+ if (!options.dryRun && await fs.pathExists(targetDir)) {
59
64
  if (options.yes) {
60
65
  await fs.remove(targetDir);
61
66
  } else {
@@ -102,12 +107,44 @@ export async function createProject(options = {}) {
102
107
  await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme });
103
108
  }
104
109
 
110
+ if (options.dryRun) {
111
+ const files = await listAllFiles(targetDir);
112
+ console.log(`\n[DRY RUN] ${projectName} 스캐폴드 시 작성될 파일 (${files.length}개):\n`);
113
+ for (const f of files.sort()) {
114
+ console.log(` ${f}`);
115
+ }
116
+ await fs.remove(targetDir);
117
+ console.log(`\n실제 스캐폴드: --dry-run 제거 후 같은 명령 실행.`);
118
+ return;
119
+ }
120
+
105
121
  console.log(`\n✅ ${projectName} 프로젝트가 생성되었습니다!`);
106
122
  console.log(`\n cd ${projectName}`);
107
123
  console.log(' pnpm install');
108
124
  console.log(' pnpm dev\n');
109
125
  }
110
126
 
127
+ /**
128
+ * targetDir 아래 모든 파일을 상대 경로로 나열. node_modules 등은 자동 스킵
129
+ * (스캐폴드 직후라 install 안 한 상태기 때문).
130
+ */
131
+ async function listAllFiles(targetDir) {
132
+ const collected = [];
133
+ async function walk(dir) {
134
+ const entries = await fs.readdir(dir, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ const full = path.join(dir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ await walk(full);
139
+ } else if (entry.isFile()) {
140
+ collected.push(path.relative(targetDir, full));
141
+ }
142
+ }
143
+ }
144
+ await walk(targetDir);
145
+ return collected;
146
+ }
147
+
111
148
  // ─── Add app to existing monorepo ───
112
149
 
113
150
  export async function addApp() {
@@ -2,6 +2,11 @@
2
2
 
3
3
  import { parseArgs } from './cli-args.js';
4
4
  import { createProject, addApp, addComponent } from './generator.js';
5
+ import { allPlugins } from './plugins/index.js';
6
+ import { CREATE_PLATFORMS, CREATE_STRUCTURES } from '../constants.js';
7
+
8
+ const PLUGIN_NAMES = allPlugins.map((p) => p.name);
9
+ const PLUGINS_LIST = PLUGIN_NAMES.join(', ');
5
10
 
6
11
  export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next.js / Flutter)
7
12
 
@@ -11,11 +16,12 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
11
16
  sh-ui create add-component <name> [--app <name>]
12
17
 
13
18
  옵션:
14
- --platform <next|flutter> 타겟 플랫폼
15
- --structure <standalone|monorepo> Next.js 프로젝트 구조 (next 일 때)
16
- --plugins <a,b> 플러그인 (sentry, next-intl, auth-jwt). 미지정/"" → 없음
19
+ --platform <${CREATE_PLATFORMS.join('|')}> 타겟 플랫폼
20
+ --structure <${CREATE_STRUCTURES.join('|')}> Next.js 프로젝트 구조 (next 일 때)
21
+ --plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
17
22
  --theme <base64> 테마 JSON (base64). 선택
18
23
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
24
+ --dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
19
25
  -h, --help 이 도움말
20
26
 
21
27
  예 (대화형):
@@ -23,7 +29,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
23
29
 
24
30
  예 (비대화형 / 에이전트 / CI):
25
31
  sh-ui create my-app --platform next --structure standalone --yes
26
- sh-ui create my-app --platform next --structure monorepo --plugins sentry,next-intl,auth-jwt --yes
32
+ sh-ui create my-app --platform next --structure monorepo --plugins ${PLUGIN_NAMES.slice(0, 3).join(',')} --yes
27
33
  sh-ui create my-app --platform flutter --yes
28
34
 
29
35
  비대화형 환경(TTY 없음)에서는 누락된 필수 인자가 있으면 prompt 대신 에러로 종료한다.
@@ -62,6 +68,7 @@ export async function runCreate(rest) {
62
68
  plugins: flags.plugins,
63
69
  theme: flags.theme,
64
70
  yes: flags.yes,
71
+ dryRun: flags.dryRun,
65
72
  });
66
73
  }
67
74
  }
@@ -1,6 +1,8 @@
1
1
  export const authJwtPlugin = {
2
2
  name: 'auth-jwt',
3
3
  label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
4
+ description:
5
+ '쿠키 기반 JWT 인증. Next 16 proxy.ts 미들웨어, refresh-aware BFF, withAuthRetry 헬퍼. refresh 본문은 placeholder — 백엔드 명세 확정 후 한 파일 채우면 자동 활성화.',
4
6
  priority: 2,
5
7
 
6
8
  // 의존성 추가 없음 — 베이스의 fetch + cookies + react-query 만 사용
@@ -1,9 +1,14 @@
1
1
  import { sentryPlugin } from './sentry.js';
2
2
  import { nextIntlPlugin } from './nextIntl.js';
3
3
  import { authJwtPlugin } from './authJwt.js';
4
+ import { validatePlugins } from './pluginSchema.js';
4
5
 
5
6
  export const allPlugins = [sentryPlugin, nextIntlPlugin, authJwtPlugin];
6
7
 
8
+ // 모듈 로드 시점에 모든 플러그인 manifest 검증 — 잘못된 형태가 있으면 즉시 실패.
9
+ // 예: src/proxy.ts 같은 잘못된 경로, name 이 kebab-case 가 아닌 경우 등.
10
+ validatePlugins(allPlugins);
11
+
7
12
  export function getPluginChoices() {
8
13
  return allPlugins.map((p) => ({
9
14
  name: p.label,
@@ -1,6 +1,8 @@
1
1
  export const nextIntlPlugin = {
2
2
  name: 'next-intl',
3
3
  label: 'next-intl (다국어 지원)',
4
+ description:
5
+ '다국어. 라우트 기반 로케일, NEXT_LOCALE 쿠키, BFF 가 백엔드로 Accept-Language 자동 전달.',
4
6
  priority: 2,
5
7
 
6
8
  dependencies: {
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * 플러그인 manifest 스키마.
5
+ * plugins/index.js 로딩 시 모든 플러그인을 이 스키마로 validate 한다.
6
+ *
7
+ * 디자인 가드:
8
+ * - file path 가 'src/proxy.ts' 면 거부 — Next 16 은 root proxy.ts 만 인식하므로
9
+ * (베이스 템플릿이 app/ 을 root 에 두는 한). 이런 종류의 사고를 빌드 타임에 차단.
10
+ */
11
+
12
+ const filePath = z
13
+ .string()
14
+ .min(1)
15
+ .refine((p) => p !== 'src/proxy.ts', {
16
+ message:
17
+ "proxy.ts must be at root (Next 16 convention with app/ at root). " +
18
+ "Use 'proxy.ts' instead of 'src/proxy.ts'.",
19
+ })
20
+ .refine((p) => p !== 'src/middleware.ts', {
21
+ message:
22
+ "middleware.ts (deprecated, use proxy.ts in Next 16+) must be at root. " +
23
+ "Use 'middleware.ts' instead of 'src/middleware.ts'.",
24
+ });
25
+
26
+ const wrapperFn = z
27
+ .function()
28
+ .args(z.string())
29
+ .returns(z.string())
30
+ .optional();
31
+
32
+ export const PluginSchema = z.object({
33
+ name: z.string().regex(/^[a-z][a-z0-9-]*$/, {
34
+ message: 'Plugin name must be lowercase kebab-case (e.g., "auth-jwt")',
35
+ }),
36
+ label: z.string().min(1),
37
+ description: z.string().min(1).optional(),
38
+ priority: z.number().int().nonnegative(),
39
+
40
+ dependencies: z.record(z.string(), z.string()).optional(),
41
+ devDependencies: z.record(z.string(), z.string()).optional(),
42
+
43
+ imports: z.array(z.string()).optional(),
44
+ wrapExport: wrapperFn,
45
+
46
+ envVars: z.array(z.string()).optional(),
47
+ turboEnvVars: z.array(z.string()).optional(),
48
+
49
+ providerImports: z.array(z.string()).optional(),
50
+ providerWrappers: z
51
+ .array(
52
+ z.union([
53
+ z.object({ open: z.string(), close: z.string() }),
54
+ z.string(),
55
+ ]),
56
+ )
57
+ .optional(),
58
+
59
+ files: z.record(filePath, z.string()).optional(),
60
+
61
+ // 향후 확장 — moves, transforms, etc 는 nextIntl.js 에서 사용하므로 허용
62
+ moves: z.array(z.any()).optional(),
63
+ transforms: z.array(z.any()).optional(),
64
+ });
65
+
66
+ /**
67
+ * 모든 플러그인을 검증. 실패 시 첫 번째 에러로 throw.
68
+ */
69
+ export function validatePlugins(plugins) {
70
+ for (const plugin of plugins) {
71
+ const result = PluginSchema.safeParse(plugin);
72
+ if (!result.success) {
73
+ const issues = result.error.issues
74
+ .map((i) => ` - ${i.path.join('.')}: ${i.message}`)
75
+ .join('\n');
76
+ throw new Error(
77
+ `Invalid plugin manifest "${plugin?.name ?? '(unknown)'}":\n${issues}`,
78
+ );
79
+ }
80
+ }
81
+ }
@@ -1,6 +1,8 @@
1
1
  export const sentryPlugin = {
2
2
  name: 'sentry',
3
3
  label: 'Sentry (에러 모니터링)',
4
+ description:
5
+ '에러 모니터링. 클라/서버/엣지 init, 라우트 에러 페이지, observability 브릿지로 다른 플러그인의 5xx 자동 캡처.',
4
6
  priority: 1,
5
7
 
6
8
  dependencies: {
package/src/init.mjs CHANGED
@@ -3,19 +3,22 @@ import { resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin, stdout } from "node:process";
5
5
 
6
+ import {
7
+ INIT_PLATFORMS,
8
+ THEME_BASES,
9
+ THEME_RADII,
10
+ THEME_MODES,
11
+ INIT_DEFAULTS,
12
+ } from "./constants.js";
13
+
6
14
  const CHOICES = {
7
- platform: ["react", "flutter"],
8
- base: ["neutral", "zinc", "slate"],
9
- radius: ["none", "sm", "md", "lg", "xl", "full"],
10
- mode: ["light-dark", "light", "dark"],
15
+ platform: INIT_PLATFORMS,
16
+ base: THEME_BASES,
17
+ radius: THEME_RADII,
18
+ mode: THEME_MODES,
11
19
  };
12
20
 
13
- const DEFAULTS = {
14
- platform: "react",
15
- base: "neutral",
16
- radius: "md",
17
- mode: "light-dark",
18
- };
21
+ const DEFAULTS = INIT_DEFAULTS;
19
22
 
20
23
  const PATHS = {
21
24
  react: {
package/src/mcp.mjs CHANGED
@@ -29,11 +29,21 @@ import {
29
29
  getSummariesPath,
30
30
  getVersionsPath,
31
31
  } from "./paths.mjs";
32
+ import {
33
+ CREATE_PLATFORMS,
34
+ CREATE_STRUCTURES,
35
+ INIT_PLATFORMS,
36
+ THEME_BASES,
37
+ THEME_RADII,
38
+ THEME_MODES,
39
+ } from "./constants.js";
40
+ import { allPlugins } from "./create/plugins/index.js";
32
41
 
33
- const PLATFORMS = ["react", "flutter"];
34
- const BASES = ["neutral", "zinc", "slate"];
35
- const RADII = ["none", "sm", "md", "lg", "xl", "full"];
36
- const MODES = ["light-dark", "light", "dark"];
42
+ const PLATFORMS = INIT_PLATFORMS;
43
+ const BASES = THEME_BASES;
44
+ const RADII = THEME_RADII;
45
+ const MODES = THEME_MODES;
46
+ const PLUGIN_NAMES = allPlugins.map((p) => p.name);
37
47
 
38
48
  const INIT_DESCRIPTIONS = {
39
49
  platform: {
@@ -161,12 +171,12 @@ export async function startMcpServer() {
161
171
  inputSchema: {
162
172
  name: z.string().min(1)
163
173
  .describe("프로젝트 디렉토리 이름. 예: my-app"),
164
- platform: z.enum(["next", "flutter"])
174
+ platform: z.enum(CREATE_PLATFORMS)
165
175
  .describe("타겟 플랫폼"),
166
- structure: z.enum(["standalone", "monorepo"]).optional()
176
+ structure: z.enum(CREATE_STRUCTURES).optional()
167
177
  .describe("Next.js 구조 — platform=next 일 때 필수. standalone(단독) | monorepo(Turborepo)"),
168
- plugins: z.array(z.enum(["sentry", "next-intl", "auth-jwt"])).optional()
169
- .describe("Next.js 플러그인. 미지정시 빈 배열"),
178
+ plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
179
+ .describe(`Next.js 플러그인 (${PLUGIN_NAMES.join(', ')}). 미지정시 빈 배열`),
170
180
  theme: z.string().optional()
171
181
  .describe("base64 인코딩된 테마 JSON (선택)"),
172
182
  cwd: z.string().optional()