sh-ui-cli 0.56.4 → 0.58.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 (152) hide show
  1. package/bin/sh-ui.mjs +21 -2
  2. package/data/changelog/versions.json +32 -0
  3. package/package.json +1 -1
  4. package/src/api.d.ts +34 -0
  5. package/src/api.js +7 -0
  6. package/src/create/architectures/archSchema.js +69 -0
  7. package/src/create/architectures/flat.js +51 -0
  8. package/src/create/architectures/fsd.js +42 -0
  9. package/src/create/architectures/index.js +55 -0
  10. package/src/create/cli-args.js +8 -1
  11. package/src/create/generator.js +101 -32
  12. package/src/create/index.mjs +7 -0
  13. package/src/create/plugins/authJwt.js +14 -8
  14. package/src/create/plugins/nextIntl.js +53 -44
  15. package/src/create/plugins/pluginSchema.js +26 -10
  16. package/src/create/plugins/sentry.js +9 -5
  17. package/src/mcp.mjs +50 -0
  18. package/src/rename-app.mjs +321 -0
  19. package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  20. package/templates/nextjs-app/_arch/flat/app/layout.tsx +16 -0
  21. package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  22. package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  23. package/templates/nextjs-app/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  24. package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  25. package/templates/nextjs-app/_arch/flat/tsconfig.json +25 -0
  26. package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  27. package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +16 -0
  28. package/templates/nextjs-standalone/_arch/flat/components/common/FallbackBoundary/index.tsx +89 -0
  29. package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  30. package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  31. package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +23 -0
  32. package/templates/nextjs-standalone/_arch/flat/components/providers/index.tsx +1 -0
  33. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  34. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  35. package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +12 -0
  36. package/templates/nextjs-standalone/_arch/flat/lib/api/apiTypes.ts +21 -0
  37. package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +40 -0
  38. package/templates/nextjs-standalone/_arch/flat/lib/api/error.ts +12 -0
  39. package/templates/nextjs-standalone/_arch/flat/lib/api/http.ts +13 -0
  40. package/templates/nextjs-standalone/_arch/flat/lib/api/observability.ts +20 -0
  41. package/templates/nextjs-standalone/_arch/flat/lib/api/queryClient.ts +30 -0
  42. package/templates/nextjs-standalone/_arch/flat/lib/api/serverFetch.ts +59 -0
  43. package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +52 -0
  44. package/templates/nextjs-standalone/_arch/flat/lib/test/createTestQueryClient.ts +18 -0
  45. package/templates/nextjs-standalone/_arch/flat/lib/test/index.ts +2 -0
  46. package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +40 -0
  47. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +22 -0
  48. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +10 -0
  49. package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  50. package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +19 -0
  51. package/templates/nextjs-standalone/_arch/flat/tsconfig.json +41 -0
  52. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +23 -0
  53. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/index.tsx +1 -0
  54. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  55. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +12 -0
  56. package/templates/nextjs-standalone/_arch/fsd/src/entities/.gitkeep +0 -0
  57. package/templates/nextjs-standalone/_arch/fsd/src/features/.gitkeep +0 -0
  58. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/.gitkeep +0 -0
  59. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/apiTypes.ts +21 -0
  60. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +40 -0
  61. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/error.ts +12 -0
  62. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/http.ts +13 -0
  63. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/observability.ts +20 -0
  64. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/queryClient.ts +30 -0
  65. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/serverFetch.ts +59 -0
  66. package/templates/nextjs-standalone/_arch/fsd/src/shared/config/.gitkeep +0 -0
  67. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/.gitkeep +0 -0
  68. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +52 -0
  69. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +22 -0
  70. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +10 -0
  71. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
  72. package/templates/nextjs-standalone/_arch/fsd/src/shared/model/.gitkeep +0 -0
  73. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +135 -0
  74. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/createTestQueryClient.ts +18 -0
  75. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/index.ts +2 -0
  76. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +40 -0
  77. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/.gitkeep +0 -0
  78. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/FallbackBoundary/index.tsx +89 -0
  79. package/templates/nextjs-standalone/_arch/fsd/src/views/.gitkeep +0 -0
  80. package/templates/nextjs-standalone/_arch/fsd/src/widgets/.gitkeep +0 -0
  81. /package/templates/nextjs-app/{src/entities → _arch/flat/components/common}/.gitkeep +0 -0
  82. /package/templates/nextjs-app/{src/shared/ui → _arch/flat/components/common}/FallbackBoundary/index.tsx +0 -0
  83. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/GlobalProvider/index.tsx +0 -0
  84. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/index.tsx +0 -0
  85. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  86. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/theme/ThemeProviders.tsx +0 -0
  87. /package/templates/nextjs-app/{src/features → _arch/flat/lib/api}/.gitkeep +0 -0
  88. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/apiTypes.ts +0 -0
  89. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/clientFetch.ts +0 -0
  90. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/error.ts +0 -0
  91. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/http.ts +0 -0
  92. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/observability.ts +0 -0
  93. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/queryClient.ts +0 -0
  94. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/serverFetch.ts +0 -0
  95. /package/templates/nextjs-app/{src/shared/api → _arch/flat/lib/config}/.gitkeep +0 -0
  96. /package/templates/nextjs-app/{src/shared/config → _arch/flat/lib/hooks}/.gitkeep +0 -0
  97. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/hooks/useAppMutation.ts +0 -0
  98. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/createTestQueryClient.ts +0 -0
  99. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/index.ts +0 -0
  100. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/renderWithProviders.tsx +0 -0
  101. /package/templates/nextjs-app/{src/shared/hooks → _arch/flat/lib/utils}/.gitkeep +0 -0
  102. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatDate.ts +0 -0
  103. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatPrice.ts +0 -0
  104. /package/templates/nextjs-app/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  105. /package/templates/nextjs-app/{app → _arch/fsd/app}/layout.tsx +0 -0
  106. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  107. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/GlobalProvider/index.tsx +0 -0
  108. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/index.tsx +0 -0
  109. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  110. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  111. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/theme/ThemeProviders.tsx +0 -0
  112. /package/templates/nextjs-app/{src/shared/lib → _arch/fsd/src/entities}/.gitkeep +0 -0
  113. /package/templates/nextjs-app/{src/shared/model → _arch/fsd/src/features}/.gitkeep +0 -0
  114. /package/templates/nextjs-app/{src/shared/ui → _arch/fsd/src/shared/api}/.gitkeep +0 -0
  115. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/apiTypes.ts +0 -0
  116. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/clientFetch.ts +0 -0
  117. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/error.ts +0 -0
  118. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/http.ts +0 -0
  119. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/observability.ts +0 -0
  120. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/queryClient.ts +0 -0
  121. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/serverFetch.ts +0 -0
  122. /package/templates/nextjs-app/{src/views → _arch/fsd/src/shared/config}/.gitkeep +0 -0
  123. /package/templates/nextjs-app/{src/widgets → _arch/fsd/src/shared/hooks}/.gitkeep +0 -0
  124. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/hooks/useAppMutation.ts +0 -0
  125. /package/templates/{nextjs-standalone/src/entities → nextjs-app/_arch/fsd/src/shared/lib}/.gitkeep +0 -0
  126. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatDate.ts +0 -0
  127. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatPrice.ts +0 -0
  128. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  129. /package/templates/{nextjs-standalone/src/features → nextjs-app/_arch/fsd/src/shared/model}/.gitkeep +0 -0
  130. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/createTestQueryClient.ts +0 -0
  131. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/index.ts +0 -0
  132. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/renderWithProviders.tsx +0 -0
  133. /package/templates/{nextjs-standalone/src/shared/api → nextjs-app/_arch/fsd/src/shared/ui}/.gitkeep +0 -0
  134. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/ui/FallbackBoundary/index.tsx +0 -0
  135. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  136. /package/templates/{nextjs-standalone/src/shared/config → nextjs-app/_arch/fsd/src/views}/.gitkeep +0 -0
  137. /package/templates/{nextjs-standalone/src/shared/hooks → nextjs-app/_arch/fsd/src/widgets}/.gitkeep +0 -0
  138. /package/templates/nextjs-app/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
  139. /package/templates/nextjs-standalone/{src/shared/model → _arch/flat/components/common}/.gitkeep +0 -0
  140. /package/templates/nextjs-standalone/{src/shared/ui → _arch/flat/lib/api}/.gitkeep +0 -0
  141. /package/templates/nextjs-standalone/{src/views → _arch/flat/lib/config}/.gitkeep +0 -0
  142. /package/templates/nextjs-standalone/{src/widgets → _arch/flat/lib/hooks}/.gitkeep +0 -0
  143. /package/templates/nextjs-standalone/{src/shared → _arch/flat/lib}/styles/tokens.css +0 -0
  144. /package/templates/nextjs-standalone/{src/shared/lib → _arch/flat/lib/utils}/utils.ts +0 -0
  145. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  146. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/layout.tsx +0 -0
  147. /package/templates/nextjs-standalone/{sh-ui.config.json → _arch/fsd/sh-ui.config.json} +0 -0
  148. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  149. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  150. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  151. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  152. /package/templates/nextjs-standalone/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
package/src/mcp.mjs CHANGED
@@ -15,6 +15,7 @@
15
15
  // sh_ui_get_changelog - 변경 내역(versions.json) 반환
16
16
  // sh_ui_encode_theme - 토큰 객체 → base64 (사용자가 손본 톤을 영구 보관)
17
17
  // sh_ui_decode_theme - base64 → 토큰 객체 (기존 테마 일부만 수정 후 재인코딩)
18
+ // sh_ui_rename_app - monorepo 의 앱 이름 일괄 변경 (디렉토리 + import/path)
18
19
 
19
20
  import { readFile } from "node:fs/promises";
20
21
  import { existsSync } from "node:fs";
@@ -27,6 +28,7 @@ import { init } from "./init.mjs";
27
28
  import { add } from "./add.mjs";
28
29
  import { list } from "./list.mjs";
29
30
  import { remove } from "./remove.mjs";
31
+ import { renameApp } from "./rename-app.mjs";
30
32
  import { createProject } from "./create/generator.js";
31
33
  import {
32
34
  getRegistryRoot,
@@ -43,6 +45,7 @@ import {
43
45
  CSS_FRAMEWORKS_SUPPORTED,
44
46
  } from "./constants.js";
45
47
  import { allPlugins } from "./create/plugins/index.js";
48
+ import { allArchitectures } from "./create/architectures/index.js";
46
49
  import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
47
50
  import { decodeTheme } from "./create/theme/decode.js";
48
51
  import { encodeTheme } from "./create/theme/encode.js";
@@ -53,6 +56,7 @@ const RADII = THEME_RADII;
53
56
  const MODES = THEME_MODES;
54
57
  const CSS_FRAMEWORKS = CSS_FRAMEWORKS_SUPPORTED;
55
58
  const PLUGIN_NAMES = allPlugins.map((p) => p.name);
59
+ const ARCH_NAMES = allArchitectures.map((a) => a.name);
56
60
  const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join(", ");
57
61
 
58
62
  const INIT_DESCRIPTIONS = {
@@ -171,6 +175,10 @@ function buildServerInstructions(cliName) {
171
175
  - \`sh_ui_add_component\` / \`sh_ui_remove_component\` — 설치/삭제
172
176
  - \`sh_ui_get_changelog\` — 최근 변경 내역
173
177
 
178
+ ## 앱 이름 변경 (monorepo)
179
+
180
+ 사용자가 "apps/web 을 apps/dashboard 로 바꿔줘" 같이 모노레포 앱 이름 변경을 요청하면 \`sh_ui_rename_app\` 사용 — 손으로 6~10 군데 (디렉토리, package.json name, tsconfig paths, Dockerfile WORKDIR, next.config transpilePackages, sh-ui.config aliases, README, .github/workflows) 갈아엎지 않도록 자동화. \`dryRun: true\` 로 먼저 변경 매트릭스 보여주고 사용자 확인 후 실행 권장.
181
+
174
182
  ## 테마 커스터마이징 (스캐폴드 결과 톤이 마음에 안 들 때)
175
183
 
176
184
  스캐폴드 후 사용자가 "눈 아프다" / "Linear 톤으로" 같이 톤 조정을 요청하면, **\`tokens.css\` 직접 편집** + **편집 결과를 base64 로 백업** 두 단계를 같이 한다 — 그래야 다음에 같은 프로젝트를 재생성해도 톤이 보존된다.
@@ -221,6 +229,13 @@ export async function startMcpServer() {
221
229
  .describe("Next.js 구조 — platform=next 일 때 필수. standalone(단독) | monorepo(Turborepo)"),
222
230
  plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
223
231
  .describe(`Next.js 플러그인 (${PLUGIN_NAMES.join(', ')}). 미지정시 빈 배열`),
232
+ arch: z.enum(ARCH_NAMES).optional()
233
+ .describe(
234
+ `프로젝트 아키텍처 — 플랫폼별로 사용 가능한 값이 다름. ` +
235
+ `현재 next 에서 사용 가능: ${allArchitectures.filter((a) => a.platforms.includes('next')).map((a) => a.name).join(', ')} (기본 fsd). ` +
236
+ `flutter 는 현재 arch 디스크립터 없음 (미지정 또는 host 자체 default). ` +
237
+ `arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능.`,
238
+ ),
224
239
  theme: z.string().optional()
225
240
  .describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
226
241
  cssFramework: z.enum(CSS_FRAMEWORKS).optional()
@@ -265,6 +280,7 @@ export async function startMcpServer() {
265
280
  platform: input.platform,
266
281
  structure: input.structure,
267
282
  plugins: input.plugins,
283
+ arch: input.arch,
268
284
  theme: input.theme,
269
285
  css: input.cssFramework,
270
286
  yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
@@ -524,6 +540,40 @@ export async function startMcpServer() {
524
540
  },
525
541
  );
526
542
 
543
+ // 모노레포 앱 이름 일괄 변경 — 디렉토리 이동 + import/path 패턴 치환 + lockfile 재생성.
544
+ // dryRun=true 면 변경 매트릭스만 반환해 AI 가 사용자에게 미리보기 가능.
545
+ server.registerTool(
546
+ "sh_ui_rename_app",
547
+ {
548
+ description:
549
+ "monorepo 의 앱 이름 일괄 변경 — apps/<old>/, packages/ui/ui-apps/ui-<old>/ 두 디렉토리 이동 + " +
550
+ "@workspace/ui-<old> / apps/<old> / --filter <old> / --app <old> / cd apps/<old> 패턴 치환 + " +
551
+ "사용자 코드, package.json name, tsconfig paths, Dockerfile WORKDIR, sh-ui.config.json aliases, README, .github/workflows 모두 자동 갱신. " +
552
+ "monorepo 전용 (pnpm-workspace.yaml 필수). dryRun 으로 변경 매트릭스 미리보기 가능. " +
553
+ "false-positive 방지를 위해 bare 단어(예: 'web')는 절대 치환하지 않고 컨텍스트(슬래시·공백·따옴표) 로 묶인 패턴만 처리.",
554
+ inputSchema: {
555
+ oldName: z.string().min(1).describe("현재 앱 이름 (예: 'web'). apps/<old>/ 가 존재해야 함."),
556
+ newName: z.string().min(1).describe("새 앱 이름 (예: 'dashboard'). 영숫자 + 하이픈만 허용."),
557
+ cwd: z.string().optional().describe("monorepo 루트 디렉토리. 기본 process.cwd()"),
558
+ dryRun: z.boolean().optional().describe("변경 매트릭스만 반환, 실제 파일 변경 X. 기본 false"),
559
+ skipInstall: z.boolean().optional().describe("마지막 pnpm install 생략. 기본 false"),
560
+ },
561
+ },
562
+ async (input) => {
563
+ const result = await captureConsole(() =>
564
+ renameApp({
565
+ cwd: resolveCwd(input),
566
+ oldName: input.oldName,
567
+ newName: input.newName,
568
+ yes: true, // MCP 컨텍스트는 비대화형 — 호출자(AI) 가 사용자 확인을 이미 받았다고 가정
569
+ dryRun: input.dryRun === true,
570
+ skipInstall: input.skipInstall === true,
571
+ }),
572
+ );
573
+ return textResult(result || "✓ rename-app 완료");
574
+ },
575
+ );
576
+
527
577
  // 변경 내역 조회 — 보너스: 사용자가 "최근 변경 알려줘" 류 요청 시
528
578
  server.registerTool(
529
579
  "sh_ui_get_changelog",
@@ -0,0 +1,321 @@
1
+ // monorepo 의 앱 이름 (apps/<old>/ + packages/ui/ui-apps/ui-<old>/) 을 일괄 변경.
2
+ //
3
+ // 디렉토리 이동 + 정해진 6개 패턴 치환을 자동화. 사용자가 손으로
4
+ // 6~10 군데 (package.json 이름, tsconfig paths, Dockerfile WORKDIR,
5
+ // next.config transpilePackages, sh-ui.config aliases, README, ...) 를
6
+ // 일일이 갈아엎지 않도록.
7
+ //
8
+ // false-positive 방지를 위해 bare 단어 (`web`) 는 절대 치환하지 않고,
9
+ // 컨텍스트(슬래시·따옴표·`--filter ` 공백) 로 묶인 패턴만 치환한다.
10
+ //
11
+ // 사용:
12
+ // sh-ui rename-app web dashboard 대화형 확인 후 실행
13
+ // sh-ui rename-app web dashboard --yes 비대화형
14
+ // sh-ui rename-app web dashboard --dry-run 변경 매트릭스만 출력
15
+
16
+ import { existsSync, statSync } from "node:fs";
17
+ import { readdir, readFile, rename, writeFile } from "node:fs/promises";
18
+ import { spawn } from "node:child_process";
19
+ import { join, relative, resolve } from "node:path";
20
+ import { createInterface } from "node:readline/promises";
21
+
22
+ /** 텍스트 파일로 처리할 확장자 (전체 파일을 읽어 치환). */
23
+ const TEXT_EXT = new Set([
24
+ ".ts", ".tsx", ".js", ".mjs", ".cjs",
25
+ ".json", ".jsonc",
26
+ ".css", ".scss",
27
+ ".md", ".mdx",
28
+ ".yml", ".yaml",
29
+ ".html", ".env", ".sh",
30
+ ]);
31
+
32
+ /** 확장자 없는 텍스트 파일들 (Dockerfile 등) — 정확한 파일명으로 매칭. */
33
+ const TEXT_BASENAMES = new Set([
34
+ "Dockerfile",
35
+ "Dockerfile.dev",
36
+ "Dockerfile.prod",
37
+ ".env",
38
+ ".env.example",
39
+ ".env.local",
40
+ ".gitignore",
41
+ ".dockerignore",
42
+ ]);
43
+
44
+ /** 스캔하지 않을 디렉토리 — 빌드 산출물·캐시·의존성. */
45
+ const SKIP_DIRS = new Set([
46
+ "node_modules",
47
+ ".next",
48
+ ".turbo",
49
+ ".git",
50
+ "dist",
51
+ "build",
52
+ ".cache",
53
+ ]);
54
+
55
+ /** 모노레포 루트에서 추가로 스캔할 파일/디렉토리 (디렉토리 이동 외). */
56
+ const ROOT_TARGETS = [
57
+ "package.json",
58
+ "README.md",
59
+ "turbo.json",
60
+ "docker-compose.yml",
61
+ "docker-compose.yaml",
62
+ "vercel.json",
63
+ ".github", // 디렉토리 — 안의 yml/yaml 파일들 스캔
64
+ ];
65
+
66
+ function buildPatterns(oldName, newName) {
67
+ // false-positive 방지 위해 컨텍스트(/, ", ', 공백) 로 묶인 패턴만.
68
+ // 순서는 더 긴 패턴부터 (안 그러면 prefix 매치되어 부정확).
69
+ return [
70
+ [`@workspace/ui-${oldName}`, `@workspace/ui-${newName}`],
71
+ [`ui-apps/ui-${oldName}`, `ui-apps/ui-${newName}`],
72
+ [`packages/ui/ui-apps/ui-${oldName}`, `packages/ui/ui-apps/ui-${newName}`],
73
+ [`apps/${oldName}/`, `apps/${newName}/`],
74
+ [`apps/${oldName}"`, `apps/${newName}"`],
75
+ [`apps/${oldName}'`, `apps/${newName}'`],
76
+ [`apps/${oldName}\n`, `apps/${newName}\n`],
77
+ [`apps/${oldName} `, `apps/${newName} `],
78
+ [`--filter ${oldName} `, `--filter ${newName} `],
79
+ [`--filter ${oldName}\n`, `--filter ${newName}\n`],
80
+ [`--filter ${oldName}"`, `--filter ${newName}"`],
81
+ [`--filter ${oldName}'`, `--filter ${newName}'`],
82
+ [`--filter ${oldName}\``, `--filter ${newName}\``],
83
+ [`--app ${oldName} `, `--app ${newName} `],
84
+ [`--app ${oldName}\n`, `--app ${newName}\n`],
85
+ [`--app ${oldName}"`, `--app ${newName}"`],
86
+ [`--app ${oldName}'`, `--app ${newName}'`],
87
+ [`--app ${oldName}\``, `--app ${newName}\``],
88
+ ];
89
+ }
90
+
91
+ /** package.json 내부의 정확한 name 필드만 따로 처리 (정규식 1개). */
92
+ function rewritePackageJsonName(content, oldName, newName) {
93
+ // "name": "old" 또는 "name": "@workspace/ui-old" 두 케이스.
94
+ // app 자체의 name 은 정확히 oldName 이어야 매칭.
95
+ return content.replace(
96
+ new RegExp(`"name"\\s*:\\s*"${escapeRegex(oldName)}"`),
97
+ `"name": "${newName}"`,
98
+ );
99
+ }
100
+
101
+ function escapeRegex(s) {
102
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
+ }
104
+
105
+ /** 한 파일의 텍스트 치환. 변경 횟수 + 새 내용 반환. */
106
+ function applyPatternsToContent(content, patterns, oldName, newName) {
107
+ let next = content;
108
+ let hits = 0;
109
+ for (const [from, to] of patterns) {
110
+ if (next.includes(from)) {
111
+ const before = next;
112
+ next = next.split(from).join(to);
113
+ hits += (before.length - next.length) / (from.length - to.length) || 0;
114
+ }
115
+ }
116
+ // package.json name 필드도 (별도 처리 — 패턴에 안 잡힘)
117
+ const afterPkg = rewritePackageJsonName(next, oldName, newName);
118
+ if (afterPkg !== next) {
119
+ hits += 1;
120
+ next = afterPkg;
121
+ }
122
+ return { content: next, hits };
123
+ }
124
+
125
+ function isTextFile(filePath) {
126
+ const base = filePath.split("/").pop();
127
+ if (!base) return false;
128
+ if (TEXT_BASENAMES.has(base)) return true;
129
+ const dot = base.lastIndexOf(".");
130
+ if (dot < 0) return false;
131
+ return TEXT_EXT.has(base.slice(dot));
132
+ }
133
+
134
+ /** 디렉토리 재귀 순회. SKIP_DIRS 는 건너뜀. 텍스트 파일 경로 yield. */
135
+ async function* walkTextFiles(dir) {
136
+ let entries;
137
+ try {
138
+ entries = await readdir(dir, { withFileTypes: true });
139
+ } catch {
140
+ return;
141
+ }
142
+ for (const entry of entries) {
143
+ const full = join(dir, entry.name);
144
+ if (entry.isDirectory()) {
145
+ if (SKIP_DIRS.has(entry.name)) continue;
146
+ yield* walkTextFiles(full);
147
+ } else if (entry.isFile()) {
148
+ if (isTextFile(full)) yield full;
149
+ }
150
+ }
151
+ }
152
+
153
+ /** Symlink 안 따라가는 단순 안전한 fs.rename. cross-device 시 fallback 없음 (모노레포 내부). */
154
+ async function moveDir(from, to) {
155
+ await rename(from, to);
156
+ }
157
+
158
+ async function confirm(message) {
159
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
160
+ try {
161
+ const ans = await rl.question(`${message} [y/N] `);
162
+ return /^y(es)?$/i.test(ans.trim());
163
+ } finally {
164
+ rl.close();
165
+ }
166
+ }
167
+
168
+ function runPnpmInstall(cwd) {
169
+ console.log("\npnpm install — workspace 링크 재생성");
170
+ return new Promise((ok, bad) => {
171
+ const child = spawn("pnpm", ["install"], { cwd, stdio: "inherit", shell: process.platform === "win32" });
172
+ child.on("error", bad);
173
+ child.on("exit", (code) =>
174
+ code === 0 ? ok() : bad(new Error(`pnpm install exited with code ${code}`)),
175
+ );
176
+ });
177
+ }
178
+
179
+ /**
180
+ * @param {object} opts
181
+ * @param {string} opts.cwd
182
+ * @param {string} opts.oldName
183
+ * @param {string} opts.newName
184
+ * @param {boolean} [opts.yes] — 대화형 확인 생략
185
+ * @param {boolean} [opts.dryRun] — 변경 매트릭스만 출력, 실제 변경 X
186
+ * @param {boolean} [opts.skipInstall] — 마지막 pnpm install 생략
187
+ * @returns {Promise<{moves: Array<[string,string]>, edits: Array<{path:string, hits:number}>}>}
188
+ */
189
+ export async function renameApp({ cwd, oldName, newName, yes = false, dryRun = false, skipInstall = false }) {
190
+ const root = resolve(cwd);
191
+
192
+ // ─── 환경 검증 ───
193
+ if (!existsSync(join(root, "pnpm-workspace.yaml"))) {
194
+ throw new Error(
195
+ `${root} 가 pnpm 모노레포 루트가 아닙니다 (pnpm-workspace.yaml 없음). rename-app 은 모노레포 전용입니다.`,
196
+ );
197
+ }
198
+ if (!oldName || !newName) {
199
+ throw new Error("oldName / newName 둘 다 필요합니다.");
200
+ }
201
+ if (oldName === newName) {
202
+ throw new Error(`oldName 과 newName 이 같습니다 (${oldName}).`);
203
+ }
204
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(newName)) {
205
+ throw new Error(`newName 은 영숫자 + 하이픈만 허용 (받은 값: ${newName}).`);
206
+ }
207
+
208
+ const oldAppDir = join(root, "apps", oldName);
209
+ const newAppDir = join(root, "apps", newName);
210
+ const oldUiDir = join(root, "packages", "ui", "ui-apps", `ui-${oldName}`);
211
+ const newUiDir = join(root, "packages", "ui", "ui-apps", `ui-${newName}`);
212
+
213
+ if (!existsSync(oldAppDir)) throw new Error(`apps/${oldName} 가 존재하지 않습니다.`);
214
+ if (existsSync(newAppDir)) throw new Error(`apps/${newName} 가 이미 존재합니다.`);
215
+
216
+ const hasUiPkg = existsSync(oldUiDir);
217
+ if (hasUiPkg && existsSync(newUiDir)) {
218
+ throw new Error(`packages/ui/ui-apps/ui-${newName} 가 이미 존재합니다.`);
219
+ }
220
+
221
+ const patterns = buildPatterns(oldName, newName);
222
+
223
+ // ─── 변경 매트릭스 계산 (디렉토리 이동 전 — 원본 경로 기준 스캔) ───
224
+ const moves = [[oldAppDir, newAppDir]];
225
+ if (hasUiPkg) moves.push([oldUiDir, newUiDir]);
226
+
227
+ const edits = [];
228
+
229
+ async function scanFile(absPath, displayPath) {
230
+ const raw = await readFile(absPath, "utf-8");
231
+ const { content, hits } = applyPatternsToContent(raw, patterns, oldName, newName);
232
+ if (hits > 0 && content !== raw) {
233
+ edits.push({ path: displayPath, abs: absPath, content, hits });
234
+ }
235
+ }
236
+
237
+ // 두 패키지 디렉토리 (원본 경로) 안 텍스트 파일들
238
+ for await (const file of walkTextFiles(oldAppDir)) {
239
+ await scanFile(file, relative(root, file));
240
+ }
241
+ if (hasUiPkg) {
242
+ for await (const file of walkTextFiles(oldUiDir)) {
243
+ await scanFile(file, relative(root, file));
244
+ }
245
+ }
246
+
247
+ // 루트 추가 타겟
248
+ for (const target of ROOT_TARGETS) {
249
+ const abs = join(root, target);
250
+ if (!existsSync(abs)) continue;
251
+ const stat = statSync(abs);
252
+ if (stat.isDirectory()) {
253
+ for await (const file of walkTextFiles(abs)) {
254
+ await scanFile(file, relative(root, file));
255
+ }
256
+ } else if (stat.isFile() && isTextFile(abs)) {
257
+ await scanFile(abs, relative(root, abs));
258
+ }
259
+ }
260
+
261
+ // ─── 미리보기 출력 ───
262
+ console.log(`\n📦 ${oldName} → ${newName}`);
263
+ console.log("\n디렉토리 이동:");
264
+ for (const [from, to] of moves) {
265
+ console.log(` ${relative(root, from)} → ${relative(root, to)}`);
266
+ }
267
+ console.log(`\n파일 내용 수정 (${edits.length}개):`);
268
+ for (const e of edits) {
269
+ console.log(` ${e.path} (${e.hits}곳)`);
270
+ }
271
+ if (edits.length === 0) {
272
+ console.log(" (없음)");
273
+ }
274
+
275
+ if (dryRun) {
276
+ console.log("\n--dry-run 모드 — 실제 변경하지 않았습니다.");
277
+ return { moves, edits: edits.map(({ path, hits }) => ({ path, hits })) };
278
+ }
279
+
280
+ // ─── 확인 ───
281
+ if (!yes) {
282
+ const ok = await confirm("\n계속 진행할까요?");
283
+ if (!ok) {
284
+ console.log("취소했습니다.");
285
+ return { moves: [], edits: [] };
286
+ }
287
+ }
288
+
289
+ // ─── 디렉토리 이동 먼저 ───
290
+ for (const [from, to] of moves) {
291
+ await moveDir(from, to);
292
+ }
293
+
294
+ // ─── 파일 내용 수정 (이동된 새 경로로 매핑) ───
295
+ for (const edit of edits) {
296
+ let newAbs = edit.abs;
297
+ if (newAbs.startsWith(oldAppDir + "/")) {
298
+ newAbs = newAppDir + newAbs.slice(oldAppDir.length);
299
+ } else if (hasUiPkg && newAbs.startsWith(oldUiDir + "/")) {
300
+ newAbs = newUiDir + newAbs.slice(oldUiDir.length);
301
+ }
302
+ await writeFile(newAbs, edit.content, "utf-8");
303
+ }
304
+
305
+ console.log(`\n✓ ${moves.length}개 디렉토리 이동, ${edits.length}개 파일 수정`);
306
+
307
+ // ─── pnpm install 로 lockfile 재생성 ───
308
+ if (!skipInstall) {
309
+ try {
310
+ await runPnpmInstall(root);
311
+ console.log("✓ pnpm install 완료 — workspace 링크 갱신됨");
312
+ } catch (e) {
313
+ console.warn(`⚠ pnpm install 실패: ${e.message}`);
314
+ console.warn(" 수동으로 'pnpm install' 을 실행해 lockfile 을 재생성하세요.");
315
+ }
316
+ } else {
317
+ console.log("\n⚠ --skip-install — lockfile 재생성을 위해 'pnpm install' 을 직접 실행하세요.");
318
+ }
319
+
320
+ return { moves, edits: edits.map(({ path, hits }) => ({ path, hits })) };
321
+ }
@@ -0,0 +1,112 @@
1
+ import { cookies } from 'next/headers';
2
+ import { NextResponse, type NextRequest } from 'next/server';
3
+
4
+ import {
5
+ captureApiError,
6
+ logApiError,
7
+ } from '@/lib/api/observability';
8
+
9
+ const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
10
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
11
+ const LOCALE_COOKIE = 'NEXT_LOCALE';
12
+
13
+ const proxyRequest = async (
14
+ request: NextRequest,
15
+ ctx: { params: Promise<{ path: string[] }> },
16
+ method: string,
17
+ ) => {
18
+ const { path } = await ctx.params;
19
+ const apiPath = path.join('/');
20
+ const url = new URL(`${API_URL}/${apiPath}`);
21
+
22
+ request.nextUrl.searchParams.forEach((value, key) => {
23
+ url.searchParams.set(key, value);
24
+ });
25
+
26
+ const cookieStore = await cookies();
27
+ const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
28
+ const locale =
29
+ cookieStore.get(LOCALE_COOKIE)?.value ??
30
+ request.headers.get('Accept-Language') ??
31
+ undefined;
32
+
33
+ const headers: Record<string, string> = {};
34
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
35
+ if (locale) headers['Accept-Language'] = locale;
36
+
37
+ let body: BodyInit | undefined;
38
+ if (method !== 'GET' && method !== 'HEAD') {
39
+ const contentType = request.headers.get('Content-Type');
40
+ if (contentType?.includes('multipart/form-data')) {
41
+ body = await request.formData();
42
+ } else {
43
+ headers['Content-Type'] = 'application/json';
44
+ body = await request.text();
45
+ }
46
+ }
47
+
48
+ let response: Response;
49
+ try {
50
+ response = await fetch(url.toString(), { method, headers, body });
51
+ } catch (error) {
52
+ console.error(`[PROXY] ${method} ${url.toString()} —`, error);
53
+ return NextResponse.json(
54
+ {
55
+ result: 'ERROR',
56
+ data: null,
57
+ error: {
58
+ code: 'NETWORK_ERROR',
59
+ message: '서버에 연결할 수 없습니다.',
60
+ },
61
+ },
62
+ { status: 502 },
63
+ );
64
+ }
65
+
66
+ const data = await response.json();
67
+
68
+ if (!response.ok) {
69
+ logApiError('PROXY', {
70
+ url: url.toString(),
71
+ method,
72
+ status: response.status,
73
+ requestBody: typeof body === 'string' ? body : undefined,
74
+ responseBody: data,
75
+ });
76
+
77
+ captureApiError({
78
+ url: url.toString(),
79
+ apiPath,
80
+ method,
81
+ status: response.status,
82
+ responseBody: data,
83
+ });
84
+ }
85
+
86
+ return NextResponse.json(data, { status: response.status });
87
+ };
88
+
89
+ export const GET = (
90
+ req: NextRequest,
91
+ ctx: { params: Promise<{ path: string[] }> },
92
+ ) => proxyRequest(req, ctx, 'GET');
93
+
94
+ export const POST = (
95
+ req: NextRequest,
96
+ ctx: { params: Promise<{ path: string[] }> },
97
+ ) => proxyRequest(req, ctx, 'POST');
98
+
99
+ export const PUT = (
100
+ req: NextRequest,
101
+ ctx: { params: Promise<{ path: string[] }> },
102
+ ) => proxyRequest(req, ctx, 'PUT');
103
+
104
+ export const PATCH = (
105
+ req: NextRequest,
106
+ ctx: { params: Promise<{ path: string[] }> },
107
+ ) => proxyRequest(req, ctx, 'PATCH');
108
+
109
+ export const DELETE = (
110
+ req: NextRequest,
111
+ ctx: { params: Promise<{ path: string[] }> },
112
+ ) => proxyRequest(req, ctx, 'DELETE');
@@ -0,0 +1,16 @@
1
+ import type { Metadata } from 'next';
2
+ import '@workspace/ui-app-name/globals.css';
3
+ import { RootLayout } from '@/components/layouts/RootLayout';
4
+
5
+ export const metadata: Metadata = {
6
+ title: 'App Name',
7
+ description: 'App Description',
8
+ };
9
+
10
+ export default function Layout({
11
+ children,
12
+ }: Readonly<{
13
+ children: React.ReactNode;
14
+ }>) {
15
+ return <RootLayout>{children}</RootLayout>;
16
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import {
4
+ dehydrate,
5
+ type FetchQueryOptions,
6
+ HydrationBoundary,
7
+ } from '@tanstack/react-query';
8
+
9
+ import getQueryClient from '@/lib/utils/getQueryClient';
10
+
11
+ export type FetchOptions = Pick<FetchQueryOptions, 'queryKey' | 'queryFn'>;
12
+
13
+ type Props = {
14
+ fetchOptions?: FetchOptions[] | FetchOptions | null;
15
+ children: ReactNode;
16
+ };
17
+
18
+ /**
19
+ * RSC 에서 prefetch 를 끝낸 뒤 dehydrated state 로 클라이언트에 hydrate.
20
+ * 단일/배열 둘 다 받는다.
21
+ */
22
+ export async function PrefetchBoundary({ fetchOptions, children }: Props) {
23
+ const queryClient = getQueryClient();
24
+
25
+ if (fetchOptions) {
26
+ const list = Array.isArray(fetchOptions) ? fetchOptions : [fetchOptions];
27
+ await Promise.all(list.map((opt) => queryClient.prefetchQuery(opt)));
28
+ }
29
+
30
+ return (
31
+ <HydrationBoundary state={dehydrate(queryClient)}>
32
+ {children}
33
+ </HydrationBoundary>
34
+ );
35
+ }
@@ -0,0 +1,11 @@
1
+ import { GlobalProvider } from '@/components/providers';
2
+
3
+ export function RootLayout({ children }: { children: React.ReactNode }) {
4
+ return (
5
+ <html lang='ko' suppressHydrationWarning>
6
+ <body>
7
+ <GlobalProvider>{children}</GlobalProvider>
8
+ </body>
9
+ </html>
10
+ );
11
+ }
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
4
+ import type { ReactNode } from 'react';
5
+
6
+ import { getBrowserQueryClient } from '@/lib/api/queryClient';
7
+
8
+ export function QueryClientProvider({ children }: { children: ReactNode }) {
9
+ return (
10
+ <TanstackQueryClientProvider client={getBrowserQueryClient()}>
11
+ {children}
12
+ </TanstackQueryClientProvider>
13
+ );
14
+ }
@@ -0,0 +1,14 @@
1
+ import { isServer } from '@tanstack/react-query';
2
+
3
+ import {
4
+ getBrowserQueryClient,
5
+ getServerQueryClient,
6
+ } from '@/lib/api/queryClient';
7
+
8
+ /**
9
+ * RSC 와 클라이언트 양쪽에서 안전하게 호출 가능한 QueryClient 핸들.
10
+ * 서버에서는 React `cache()` 로 요청 단위 싱글턴, 브라우저에서는 모듈 싱글턴.
11
+ */
12
+ export default function getQueryClient() {
13
+ return isServer ? getServerQueryClient() : getBrowserQueryClient();
14
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "@workspace/typescript-config/nextjs.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/lib/*": ["./lib/*"],
7
+ "@/components/*": ["./components/*"],
8
+ "@/app/*": ["./app/*"],
9
+ "@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"]
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "next"
14
+ }
15
+ ]
16
+ },
17
+ "include": [
18
+ "next-env.d.ts",
19
+ "next.config.ts",
20
+ "**/*.ts",
21
+ "**/*.tsx",
22
+ ".next/types/**/*.ts"
23
+ ],
24
+ "exclude": ["node_modules"]
25
+ }