sh-ui-cli 0.96.3 → 0.98.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.
@@ -213,17 +213,6 @@ export async function createProject(options = {}) {
213
213
  ],
214
214
  });
215
215
 
216
- // tauri 옵션은 platform=vite + structure=standalone 일 때만 의미. 다른 조합은 명시적 에러.
217
- // (MCP 진입점은 mcp.mjs 가 이미 동일 가드 — CLI 직접 호출에도 동일 안전망.)
218
- if (options.tauri) {
219
- if (platform !== 'vite') {
220
- throw new Error(
221
- `tauri: true 는 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). ` +
222
- `--tauri 옵션 제거 또는 --platform vite 사용.`,
223
- );
224
- }
225
- }
226
-
227
216
  // i18n 옵션도 vite preset 전용. v0.92.0+.
228
217
  if (options.i18n && options.i18n !== 'none' && platform !== 'vite') {
229
218
  throw new Error(
@@ -231,13 +220,6 @@ export async function createProject(options = {}) {
231
220
  );
232
221
  }
233
222
 
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
-
241
223
  // arch 결정 — platform 확정 후. 사용자가 --arch 미지정 시:
242
224
  // - next → DEFAULT_ARCH ('fsd')
243
225
  // - flutter → 현재 Flutter arch 디스크립터 없음 → null. 미래에 flutter arch 추가되면
@@ -359,19 +341,15 @@ export async function createProject(options = {}) {
359
341
 
360
342
  if (projectType === 'standalone') {
361
343
  await generateViteStandalone(targetDir, projectName, theme, cssFramework, arch, themeBase, {
362
- tauri: !!options.tauri,
363
344
  i18n: options.i18n ?? 'none',
364
345
  locales: options.locales ?? 'ko,en',
365
- observability: options.observability ?? 'none',
366
346
  });
367
347
  } else {
368
348
  await generateMonorepo(targetDir, projectName, [], {
369
349
  yes: options.yes, theme, css: cssFramework, arch, themeBase,
370
350
  platform: 'vite',
371
- tauri: options.tauri,
372
351
  i18n: options.i18n ?? 'none',
373
352
  locales: options.locales ?? 'ko,en',
374
- observability: options.observability ?? 'none',
375
353
  appName: options.appName ?? null,
376
354
  port: options.port ?? null,
377
355
  });
@@ -392,14 +370,6 @@ export async function createProject(options = {}) {
392
370
  console.log(`\n cd ${projectName}`);
393
371
  console.log(' pnpm install');
394
372
  console.log(' pnpm dev\n');
395
- if (options.tauri) {
396
- console.log('Tauri 데스크탑 셸:');
397
- if (projectType === 'monorepo') {
398
- console.log(' cd apps/web # 또는 첫 앱 이름');
399
- }
400
- console.log(' pnpm tauri dev # Rust 처음 빌드는 5~10분 — 캐시 후 5~10초');
401
- console.log(' (Rust 미설치 시 https://rustup.rs/ 참고)\n');
402
- }
403
373
  console.log('다음 단계 — 베이스 컴포넌트 추가 (예시):');
404
374
  console.log(' npx sh-ui-cli add button card input dialog\n');
405
375
  return;
@@ -415,7 +385,7 @@ export async function createProject(options = {}) {
415
385
  });
416
386
 
417
387
  // plugins 는 미지정시 기본 빈 배열 — prompt 띄우지 않는다.
418
- // (플러그인을 쓰려면 명시적으로 --plugins sentry,next-intl,auth-jwt 사용)
388
+ // (플러그인을 쓰려면 명시적으로 --plugins next-intl 사용)
419
389
  const selectedPluginNames = options.plugins ?? [];
420
390
 
421
391
  const plugins = getPluginsByNames(selectedPluginNames);
@@ -534,21 +504,11 @@ export async function addApp(options = {}) {
534
504
  `add-app 는 platform=next 또는 vite 만 지원 (받은 값: ${platform}). flutter 는 standalone 만 지원.`,
535
505
  );
536
506
  }
537
- if (options.tauri && platform !== 'vite') {
538
- throw new Error(
539
- `tauri 는 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --platform vite 사용 또는 tauri 옵션 제거.`,
540
- );
541
- }
542
507
  if (options.i18n && options.i18n !== 'none' && platform !== 'vite') {
543
508
  throw new Error(
544
509
  `i18n='${options.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}).`,
545
510
  );
546
511
  }
547
- if (options.observability && options.observability !== 'none' && platform !== 'vite') {
548
- throw new Error(
549
- `observability='${options.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --observability none 또는 --platform vite 사용.`,
550
- );
551
- }
552
512
 
553
513
  const appName = validateProjectName(
554
514
  options.name ?? await input({
@@ -600,10 +560,8 @@ export async function addApp(options = {}) {
600
560
 
601
561
  if (platform === 'vite') {
602
562
  await generateViteApp(appsDir, appName, port, arch, css, {
603
- tauri: !!options.tauri,
604
563
  i18n: options.i18n ?? 'none',
605
564
  locales: options.locales ?? 'ko,en',
606
- observability: options.observability ?? 'none',
607
565
  });
608
566
  } else {
609
567
  await generateApp(appsDir, appName, port, plugins, arch, css);
@@ -620,13 +578,6 @@ export async function addApp(options = {}) {
620
578
  console.log(`\n✅ apps/${appName} 이 추가되었습니다!`);
621
579
  console.log('\n pnpm install');
622
580
  console.log(` pnpm --filter ${appName} dev\n`);
623
-
624
- if (platform === 'vite' && options.tauri) {
625
- console.log('Tauri 데스크탑 셸:');
626
- console.log(` cd apps/${appName}`);
627
- console.log(' pnpm tauri dev # Rust 처음 빌드는 5~10분 — 캐시 후 5~10초');
628
- console.log(' (Rust 미설치 시 https://rustup.rs/ 참고)\n');
629
- }
630
581
  }
631
582
 
632
583
  /**
@@ -867,7 +818,7 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
867
818
  await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
868
819
  }
869
820
 
870
- async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
821
+ async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { i18n = 'none', locales = 'ko,en' } = {}) {
871
822
  // 베이스 (arch-neutral) + arch 오버레이 — generateStandalone 과 같은 패턴.
872
823
  await fs.copy(path.join(TEMPLATES_DIR, 'vite-standalone'), targetDir, {
873
824
  filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
@@ -903,115 +854,10 @@ async function generateViteStandalone(targetDir, projectName, theme, css, arch,
903
854
  await injectCssTheme(targetDir, theme);
904
855
  await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
905
856
 
906
- // Tauri 셸 emit (옵션) — vite SPA + native window. standalone 만 v1 지원.
907
- if (tauri) {
908
- await emitTauri(targetDir, projectName, { devPort: 5173 });
909
- await patchViteForTauri(targetDir, { port: 5173 });
910
- }
911
-
912
857
  if (i18n === 'react-i18next') {
913
858
  const localesArr = parseLocales(locales);
914
859
  await emitI18n(targetDir, { arch, locales: localesArr });
915
860
  }
916
-
917
- if (observability === 'sentry') {
918
- await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
919
- }
920
- }
921
-
922
- /**
923
- * tauri-shell 템플릿을 `<targetDir>/src-tauri/` 로 복사하고 placeholder 치환.
924
- *
925
- * - `{{project_name}}` → projectName (kebab-case 유지, npm 패키지명과 동일)
926
- * - `{{tauri_crate_name}}` → snake_case 변환 (Rust crate name 규칙: 영숫자+언더스코어).
927
- * 하이픈/점/대문자가 들어 있으면 모두 안전한 형태로 정규화.
928
- *
929
- * generateViteStandalone 에서 tauri: true 인 경우 호출. monorepo+tauri 는 v0.89 후속.
930
- */
931
- async function emitTauri(targetDir, projectName, { devPort = 5173 } = {}) {
932
- const srcTauriDir = path.join(targetDir, 'src-tauri');
933
- await fs.copy(path.join(TEMPLATES_DIR, 'tauri-shell'), srcTauriDir);
934
-
935
- // crate name: snake_case 강제 — Rust 식별자는 영숫자+'_' 만 허용
936
- const tauriCrateName = projectName
937
- .toLowerCase()
938
- .replace(/[^a-z0-9]+/g, '_')
939
- .replace(/^_+|_+$/g, '');
940
-
941
- await replaceInAllFiles(srcTauriDir, '{{tauri_crate_name}}', tauriCrateName);
942
- await replaceInAllFiles(srcTauriDir, '{{project_name}}', projectName);
943
- await replaceInAllFiles(srcTauriDir, '{{tauri_dev_url}}', `http://localhost:${devPort}`);
944
- }
945
-
946
- /**
947
- * vite 앱의 package.json + vite.config.ts 를 Tauri 친화적으로 패치.
948
- *
949
- * - package.json: `@tauri-apps/cli` (devDep), `@tauri-apps/api` (dep), `tauri`/`tauri:dev`/`tauri:build` scripts 추가
950
- * - vite.config.ts: Tauri 공식 권장값 추가 — `clearScreen: false`, `server.strictPort: true`,
951
- * `server.host: false`, `server.port: 5173`. 그래야 Tauri 가 dev server 를 안정적으로 wrap.
952
- *
953
- * NOTE: vite.config.ts 를 전부 재작성한다. 현재 base template 의 vite.config.ts 는 arch-neutral
954
- * 이라 안전. 후속 task 에서 arch-specific vite.config.ts overlay 가 생기면 이 자리에서 머지 전략
955
- * 필요 (현재는 단순 overwrite).
956
- */
957
- async function patchViteForTauri(targetDir, { port = 5173 } = {}) {
958
- const pkgPath = path.join(targetDir, 'package.json');
959
- const pkg = await fs.readJson(pkgPath);
960
-
961
- pkg.dependencies = pkg.dependencies ?? {};
962
- pkg.devDependencies = pkg.devDependencies ?? {};
963
- pkg.scripts = pkg.scripts ?? {};
964
-
965
- pkg.dependencies['@tauri-apps/api'] = '^2.0.0';
966
- pkg.dependencies['@tauri-apps/plugin-opener'] = '^2.0.0';
967
- pkg.devDependencies['@tauri-apps/cli'] = '^2.0.0';
968
-
969
- pkg.scripts.tauri = 'tauri';
970
- pkg.scripts['tauri:dev'] = 'tauri dev';
971
- pkg.scripts['tauri:build'] = 'tauri build';
972
-
973
- pkg.dependencies = sortObjectKeys(pkg.dependencies);
974
- pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
975
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
976
-
977
- // vite.config.ts 재작성 — Tauri 공식 권장 설정 추가.
978
- const viteCfgPath = path.join(targetDir, 'vite.config.ts');
979
- const viteCfg = `import { defineConfig } from 'vite';
980
- import react from '@vitejs/plugin-react';
981
- import tailwindcss from '@tailwindcss/vite';
982
- import tsconfigPaths from 'vite-tsconfig-paths';
983
-
984
- // Tauri 권장 설정 (https://v2.tauri.app/start/frontend/vite/)
985
- // - clearScreen: false — Rust 컴파일 에러가 터미널을 가리지 않게
986
- // - server.strictPort — Tauri 가 사용할 포트를 고정 (충돌 시 에러)
987
- // - server.host: false — Tauri dev 가 host network 안 열어도 됨
988
- export default defineConfig({
989
- plugins: [react(), tailwindcss(), tsconfigPaths()],
990
- clearScreen: false,
991
- server: {
992
- port: ${port},
993
- strictPort: true,
994
- host: false,
995
- },
996
- });
997
- `;
998
- await fs.writeFile(viteCfgPath, viteCfg);
999
-
1000
- // .gitignore 에 src-tauri/target 추가 — Rust 빌드 산출물 (수 GB 가능).
1001
- // 스캐폴드 단계에서는 파일명이 `gitignore` (점 없음); finalizeProject 가 나중에 `.gitignore` 로 rename.
1002
- // 양쪽 이름 모두 시도해서 호출 순서가 달라져도 안전하게 적용.
1003
- const gitignoreCandidates = ['.gitignore', 'gitignore'];
1004
- for (const name of gitignoreCandidates) {
1005
- const p = path.join(targetDir, name);
1006
- if (await fs.pathExists(p)) {
1007
- let ignore = await fs.readFile(p, 'utf-8');
1008
- if (!ignore.includes('src-tauri/target')) {
1009
- ignore += `\n# Tauri build artifacts\nsrc-tauri/target/\n`;
1010
- await fs.writeFile(p, ignore);
1011
- }
1012
- break;
1013
- }
1014
- }
1015
861
  }
1016
862
 
1017
863
  /**
@@ -1054,7 +900,6 @@ import { initReactI18next } from 'react-i18next';
1054
900
  // 클라이언트 측 lazy-load — 빌드 산출물에서 public/locales/{lng}/{ns}.json 경로로 fetch.
1055
901
  // 원본 locale 파일은 ${i18nDirRel}/locales/* 에 두고, vite-plugin-static-copy 가
1056
902
  // dev/build 양쪽에서 public/locales/* 로 자동 미러링한다 (vite.config.ts 참고).
1057
- // Tauri 빌드의 경우도 dist/locales 에 그대로 포함된다.
1058
903
 
1059
904
  i18n
1060
905
  .use(HttpBackend)
@@ -1219,8 +1064,8 @@ async function patchLandingForI18n(targetDir, { arch }) {
1219
1064
  *
1220
1065
  * - balanced bracket 으로 outer `]` 를 찾으므로 `viteStaticCopy({ targets: [...] })`
1221
1066
  * 처럼 안쪽에 `[]` 가 있어도 안전.
1222
- * - 이상의 patch (i18n + sentry 등) 같은 vite.config.ts 순차로
1223
- * 건드릴 때 anchor 경합으로 entry 가 안쪽 배열에 inject 되던 v0.92~0.95 회귀의 원인을 제거.
1067
+ * - i18n viteStaticCopy 처럼 nested `[]` 가진 plugin append
1068
+ * anchor 경합으로 entry 가 안쪽 배열에 inject 되던 v0.92~0.95 회귀의 원인을 제거.
1224
1069
  *
1225
1070
  * 입력 src 가 예상 형태가 아니면 (`plugins: [` 못 찾음 / unbalanced bracket)
1226
1071
  * `null` 을 반환 → 호출부에서 patch 포기.
@@ -1357,182 +1202,7 @@ function parseLocales(input) {
1357
1202
  return cleaned.length > 0 ? cleaned : ['ko', 'en'];
1358
1203
  }
1359
1204
 
1360
- /**
1361
- * Sentry observability 셋업 emit (v0.93.0+). vite preset 전용 opt-in.
1362
- *
1363
- * - shared/observability/sentry.ts — Sentry.init (DSN 있을 때만)
1364
- * - shared/observability/index.ts — Sentry + ErrorBoundary re-export
1365
- * - app/providers/SentryProvider.tsx — ErrorBoundary wrapper
1366
- * - GlobalProvider 재작성 — Sentry > [I18n?] > Theme > Query 순서
1367
- * - package.json — @sentry/react + @sentry/vite-plugin 추가
1368
- * - vite.config.ts — sentryVitePlugin 삽입 + sourcemap: true
1369
- * - .env.example — Sentry 변수 안내 블록 추가
1370
- *
1371
- * @param {string} targetDir — 앱 디렉토리
1372
- * @param {object} opts
1373
- * @param {object} opts.arch — arch descriptor
1374
- * @param {boolean} [opts.i18nActive] — i18n 도 같이 켜져 있는지 (GlobalProvider wrapping 순서)
1375
- */
1376
- async function emitSentry(targetDir, { arch, i18nActive = false }) {
1377
- const isFsd = arch.name === 'fsd';
1378
- const obsDirRel = isFsd ? 'src/shared/observability' : 'src/lib/observability';
1379
- const obsAlias = isFsd ? '@/shared/observability' : '@/lib/observability';
1380
- const providersDirRel = isFsd ? 'src/app/providers' : 'src/components/providers';
1381
- const apiAlias = isFsd ? '@/shared/api/queryClient' : '@/lib/api/queryClient';
1382
-
1383
- const obsDir = path.join(targetDir, obsDirRel);
1384
- await fs.ensureDir(obsDir);
1385
-
1386
- const sentryTs = `/// <reference types="vite/client" />
1387
- import * as Sentry from '@sentry/react';
1388
-
1389
- // VITE_SENTRY_DSN 이 있을 때만 init — 로컬 dev 에선 자동 skip.
1390
- // GlitchTip self-hosted 도 같은 SDK — DSN 만 변경.
1391
- if (import.meta.env.VITE_SENTRY_DSN) {
1392
- Sentry.init({
1393
- dsn: import.meta.env.VITE_SENTRY_DSN,
1394
- release: import.meta.env.VITE_APP_VERSION,
1395
- environment: import.meta.env.MODE,
1396
- tracesSampleRate: 0.1,
1397
- replaysOnErrorSampleRate: 1.0,
1398
- replaysSessionSampleRate: 0,
1399
- integrations: [
1400
- Sentry.browserTracingIntegration(),
1401
- Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
1402
- ],
1403
- });
1404
- }
1405
-
1406
- export { Sentry };
1407
- `;
1408
- await fs.writeFile(path.join(obsDir, 'sentry.ts'), sentryTs);
1409
-
1410
- await fs.writeFile(
1411
- path.join(obsDir, 'index.ts'),
1412
- `export { Sentry } from './sentry';\nexport { ErrorBoundary } from '@sentry/react';\n`,
1413
- );
1414
-
1415
- const providersDir = path.join(targetDir, providersDirRel);
1416
- await fs.ensureDir(providersDir);
1417
- const sentryProvider = `import { type ReactNode } from 'react';
1418
- import { ErrorBoundary } from '${obsAlias}';
1419
-
1420
- function Fallback({ error }: { error: unknown }) {
1421
- return (
1422
- <div style={{ padding: '2rem', textAlign: 'center' }}>
1423
- <h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>오류가 발생했습니다</h1>
1424
- <p style={{ marginTop: '0.5rem', color: 'var(--foreground-muted)' }}>
1425
- {error instanceof Error ? error.message : '알 수 없는 오류'}
1426
- </p>
1427
- </div>
1428
- );
1429
- }
1430
-
1431
- export function SentryProvider({ children }: { children: ReactNode }) {
1432
- return (
1433
- <ErrorBoundary fallback={({ error }) => <Fallback error={error} />}>
1434
- {children}
1435
- </ErrorBoundary>
1436
- );
1437
- }
1438
- `;
1439
- await fs.writeFile(path.join(providersDir, 'SentryProvider.tsx'), sentryProvider);
1440
-
1441
- // GlobalProvider rewrite — Sentry outermost, then optional I18n, then Theme + Query.
1442
- const globalProviderPath = path.join(targetDir, providersDirRel, 'GlobalProvider', 'index.tsx');
1443
- const i18nImport = i18nActive ? `import { I18nProvider } from '../I18nProvider';\n` : '';
1444
- const innerOpen = i18nActive ? ' <I18nProvider>\n ' : ' ';
1445
- const innerClose = i18nActive ? '\n </I18nProvider>' : '';
1446
- const globalProvider = `import { QueryClientProvider } from '@tanstack/react-query';
1447
- import { type ReactNode, useState } from 'react';
1448
- import { createQueryClient } from '${apiAlias}';
1449
- import { ThemeProvider } from '../theme/ThemeProvider';
1450
- ${i18nImport}import { SentryProvider } from '../SentryProvider';
1451
-
1452
- export function GlobalProvider({ children }: { children: ReactNode }) {
1453
- const [queryClient] = useState(() => createQueryClient());
1454
- return (
1455
- <SentryProvider>
1456
- ${innerOpen}<ThemeProvider>
1457
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
1458
- </ThemeProvider>${innerClose}
1459
- </SentryProvider>
1460
- );
1461
- }
1462
- `;
1463
- await fs.writeFile(globalProviderPath, globalProvider);
1464
-
1465
- // package.json deps
1466
- const pkgPath = path.join(targetDir, 'package.json');
1467
- const pkg = await fs.readJson(pkgPath);
1468
- pkg.dependencies = pkg.dependencies ?? {};
1469
- pkg.devDependencies = pkg.devDependencies ?? {};
1470
- pkg.dependencies['@sentry/react'] = '^8.45.0';
1471
- pkg.devDependencies['@sentry/vite-plugin'] = '^2.22.7';
1472
- pkg.dependencies = sortObjectKeys(pkg.dependencies);
1473
- pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
1474
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1475
-
1476
- await patchViteConfigForSentry(targetDir);
1477
-
1478
- // .env.example
1479
- const envExamplePath = path.join(targetDir, '.env.example');
1480
- const envBlock = `# Sentry observability (v0.93.0+) — 비워두면 init 자동 skip.
1481
- VITE_SENTRY_DSN=
1482
- VITE_APP_VERSION=
1483
- # Source map upload (vite build 시).
1484
- SENTRY_AUTH_TOKEN=
1485
- SENTRY_ORG=
1486
- SENTRY_PROJECT=
1487
- `;
1488
- if (await fs.pathExists(envExamplePath)) {
1489
- const existing = await fs.readFile(envExamplePath, 'utf-8');
1490
- if (!existing.includes('VITE_SENTRY_DSN')) {
1491
- await fs.writeFile(envExamplePath, existing + '\n' + envBlock);
1492
- }
1493
- } else {
1494
- await fs.writeFile(envExamplePath, envBlock);
1495
- }
1496
- }
1497
-
1498
- async function patchViteConfigForSentry(targetDir) {
1499
- const viteCfgPath = path.join(targetDir, 'vite.config.ts');
1500
- if (!(await fs.pathExists(viteCfgPath))) {
1501
- throw new Error(`vite.config.ts 가 ${targetDir} 에 없습니다.`);
1502
- }
1503
- let cfg = await fs.readFile(viteCfgPath, 'utf-8');
1504
-
1505
- if (!cfg.includes("@sentry/vite-plugin")) {
1506
- cfg = cfg.replace(
1507
- /import { defineConfig } from 'vite';/,
1508
- `import { defineConfig } from 'vite';\nimport { sentryVitePlugin } from '@sentry/vite-plugin';`,
1509
- );
1510
- }
1511
-
1512
- if (!cfg.includes("sentryVitePlugin(")) {
1513
- // i18n 의 viteStaticCopy 처럼 plugins 배열 안에 nested `[]` 가 있을 수 있으므로
1514
- // balanced-bracket helper 로 outer entry append.
1515
- const sentryCall = `sentryVitePlugin({
1516
- org: process.env.SENTRY_ORG,
1517
- project: process.env.SENTRY_PROJECT,
1518
- authToken: process.env.SENTRY_AUTH_TOKEN,
1519
- disable: !process.env.SENTRY_AUTH_TOKEN,
1520
- })`;
1521
- const patched = appendVitePluginEntry(cfg, sentryCall);
1522
- if (patched) cfg = patched;
1523
- }
1524
-
1525
- if (!cfg.includes("sourcemap:")) {
1526
- if (cfg.includes("server: {")) {
1527
- cfg = cfg.replace(/(\n\s*server:\s*\{)/, `\n build: { sourcemap: true },$1`);
1528
- }
1529
- // server 가 없는 변종은 build 삽입 위치가 모호 → 보수적으로 skip (사용자가 수동 추가).
1530
- }
1531
-
1532
- await fs.writeFile(viteCfgPath, cfg);
1533
- }
1534
-
1535
- async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none', appName: appNameOpt = null, port: portOpt = null } = {}) {
1205
+ async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', i18n = 'none', locales = 'ko,en', appName: appNameOpt = null, port: portOpt = null } = {}) {
1536
1206
  await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
1537
1207
 
1538
1208
  // Update root package.json
@@ -1543,7 +1213,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
1543
1213
 
1544
1214
  // CLAUDE.md 의 platform 분기 placeholder 치환. AI 에이전트가 Next.js 가정으로
1545
1215
  // 잘못된 컨벤션을 적용하지 않도록 (v0.94.0+).
1546
- const platformAppDesc = describeAppPlatform(platform, { tauri });
1216
+ const platformAppDesc = describeAppPlatform(platform);
1547
1217
  await replaceInAllFiles(targetDir, '{{PLATFORM_APP_DESCRIPTION}}', platformAppDesc);
1548
1218
 
1549
1219
  // Update turbo.json
@@ -1560,10 +1230,6 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
1560
1230
  turbo.tasks.build.outputs = ['dist/**'];
1561
1231
  // vite 는 클라이언트 노출 env 가 VITE_ 접두사 관례.
1562
1232
  turbo.globalEnv = turbo.globalEnv.map((e) => (e === 'API_URL' ? 'VITE_API_URL' : e));
1563
- // sentry observability 는 플러그인 turboEnvVars 훅을 안 타므로 직접 선언.
1564
- if (observability === 'sentry') {
1565
- turbo.globalEnv.push('MODE', 'SENTRY_ORG', 'SENTRY_PROJECT', 'SENTRY_AUTH_TOKEN');
1566
- }
1567
1233
  }
1568
1234
  turbo.globalEnv = [...new Set(turbo.globalEnv)];
1569
1235
  await fs.writeJson(turboPath, turbo, { spaces: 2 });
@@ -1584,7 +1250,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
1584
1250
 
1585
1251
  const appsDir = path.join(targetDir, 'apps', appName);
1586
1252
  if (platform === 'vite') {
1587
- await generateViteApp(appsDir, appName, port, arch, css, { tauri, i18n, locales, observability });
1253
+ await generateViteApp(appsDir, appName, port, arch, css, { i18n, locales });
1588
1254
  } else {
1589
1255
  await generateApp(appsDir, appName, port, plugins, arch, css);
1590
1256
  }
@@ -1685,7 +1351,7 @@ async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailw
1685
1351
  }
1686
1352
  }
1687
1353
 
1688
- async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { tauri = false, i18n = 'none', locales = 'ko,en', observability = 'none' } = {}) {
1354
+ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { i18n = 'none', locales = 'ko,en' } = {}) {
1689
1355
  // 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 동일 패턴.
1690
1356
  await fs.copy(path.join(TEMPLATES_DIR, 'vite-app'), targetDir, {
1691
1357
  filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
@@ -1745,23 +1411,10 @@ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind',
1745
1411
  await applyCssFrameworkVariant(uiPkgDir, css, { isMonorepo: true, plugins: [], arch, isUiPackage: true });
1746
1412
  }
1747
1413
 
1748
- // tauri 가 켜져 있으면 이 app 안에 src-tauri/ shell 을 떨어뜨린다 (v0.90.0+).
1749
- // standalone 과 달리 monorepo 에서는 app 단위로 별도 dev port (default 3000) 가 있고,
1750
- // tauri 의 devUrl 이 그 port 와 일치해야 한다. frontendDist 는 src-tauri 기준 `../dist`.
1751
- if (tauri) {
1752
- const devPort = Number(port) || 3000;
1753
- await emitTauri(targetDir, appName, { devPort });
1754
- await patchViteForTauri(targetDir, { port: devPort });
1755
- }
1756
-
1757
1414
  if (i18n === 'react-i18next') {
1758
1415
  const localesArr = parseLocales(locales);
1759
1416
  await emitI18n(targetDir, { arch, locales: localesArr });
1760
1417
  }
1761
-
1762
- if (observability === 'sentry') {
1763
- await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
1764
- }
1765
1418
  }
1766
1419
 
1767
1420
  /**
@@ -1957,30 +1610,10 @@ export default function Home() {
1957
1610
  }
1958
1611
  }
1959
1612
 
1960
- // 6) sentry plugin 의 error.tsx 변환 Tailwind 클래스가 박혀있어서 plain/cssmodules 에서 작동 안 함.
1961
- // intl + sentry 면 nextIntl 이 [locale]/error.tsx 를 i18n-aware 로 replace 하므로 그것도 변환.
1962
- const sentryActive = plugins?.some((p) => p.name === 'sentry');
1963
- if (sentryActive) {
1964
- const errorCandidates = intlActive
1965
- ? [path.join(targetDir, 'app/[locale]/error.tsx')]
1966
- : [path.join(targetDir, 'app/error.tsx')];
1967
- for (const errPath of errorCandidates) {
1968
- if (!(await fs.pathExists(errPath))) continue;
1969
- const useI18n = intlActive;
1970
- await fs.writeFile(errPath, buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }));
1971
- if (cssFramework === 'css-modules') {
1972
- await fs.writeFile(
1973
- path.join(path.dirname(errPath), 'error.module.css'),
1974
- buildErrorModuleCss(),
1975
- );
1976
- }
1977
- }
1978
- }
1979
-
1980
- // 7) .prettierrc — tailwind plugin 제거.
1613
+ // 6) .prettierrctailwind plugin 제거.
1981
1614
  await stripTailwindFromPrettier(path.join(targetDir, '.prettierrc'));
1982
1615
 
1983
- // 8) monorepo 인 경우 root .prettierrc 와 root package.json 도 정리 (root 의 prettier-plugin-tailwindcss).
1616
+ // 7) monorepo 인 경우 root .prettierrc 와 root package.json 도 정리 (root 의 prettier-plugin-tailwindcss).
1984
1617
  // applyCssFrameworkVariant 는 apps/web 마다 호출되지만 root 정리는 1회면 충분 — idempotent 라 OK.
1985
1618
  if (isMonorepo && !isUiPackage) {
1986
1619
  const monorepoRoot = path.resolve(targetDir, '..', '..');
@@ -2010,365 +1643,6 @@ async function stripTailwindFromPrettier(prettierPath) {
2010
1643
  await fs.writeJson(prettierPath, c, { spaces: 2 });
2011
1644
  }
2012
1645
 
2013
- /**
2014
- * sentry 의 error.tsx 를 plain/cssmodules 로 변환한 콘텐츠 생성.
2015
- * useI18n=true 면 next-intl 의 useTranslations + Link 사용.
2016
- */
2017
- function buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }) {
2018
- const i18nImports = useI18n
2019
- ? `import { useTranslations } from 'next-intl';\n`
2020
- : '';
2021
- const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
2022
- const linkImport = useI18n
2023
- ? `import { Link } from '${configAlias}/i18n/navigation';\n`
2024
- : `import Link from 'next/link';\n`;
2025
- const tHook = useI18n ? ` const t = useTranslations('error');\n` : '';
2026
- const titleText = useI18n ? `{t('title')}` : `오류가 발생했습니다`;
2027
- const descText = useI18n
2028
- ? `{t('description')}`
2029
- : `예상치 못한 오류가 발생했습니다. 다시 시도해주세요.`;
2030
- const fallback = useI18n ? `t('unexpectedError')` : `'알 수 없는 오류'`;
2031
- const tryAgain = useI18n ? `{t('button.tryAgain')}` : `다시 시도`;
2032
- const goHome = useI18n ? `{t('button.goHome')}` : `홈으로 이동`;
2033
-
2034
- if (cssFramework === 'css-modules') {
2035
- return `'use client';
2036
-
2037
- import * as Sentry from '@sentry/nextjs';
2038
- import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
2039
- ${i18nImports}${linkImport}import { useEffect } from 'react';
2040
-
2041
- import styles from './error.module.css';
2042
-
2043
- export default function Error({
2044
- error,
2045
- reset,
2046
- }: {
2047
- error: Error & { digest?: string };
2048
- reset: () => void;
2049
- }) {
2050
- ${tHook} useEffect(() => {
2051
- Sentry.captureException(error);
2052
- }, [error]);
2053
-
2054
- return (
2055
- <div className={styles.wrapper}>
2056
- <div className={styles.card}>
2057
- <div className={styles.iconRow}>
2058
- <div className={styles.iconCircle}>
2059
- <AlertTriangle className={styles.icon} />
2060
- </div>
2061
- </div>
2062
-
2063
- <h2 className={styles.title}>${titleText}</h2>
2064
- <p className={styles.description}>${descText}</p>
2065
-
2066
- <div className={styles.errorBox}>
2067
- <p className={styles.errorText}>{error.message || ${fallback}}</p>
2068
- </div>
2069
-
2070
- <div className={styles.actions}>
2071
- <button onClick={reset} className={styles.primaryButton}>
2072
- <RefreshCw className={styles.buttonIcon} />
2073
- ${tryAgain}
2074
- </button>
2075
-
2076
- <Link href='/' className={styles.secondaryButton}>
2077
- <Home className={styles.buttonIcon} />
2078
- ${goHome}
2079
- </Link>
2080
- </div>
2081
-
2082
- {process.env.NODE_ENV === 'development' && error.digest && (
2083
- <div className={styles.digest}>
2084
- <p className={styles.digestText}>Error ID: {error.digest}</p>
2085
- </div>
2086
- )}
2087
- </div>
2088
- </div>
2089
- );
2090
- }
2091
- `;
2092
- }
2093
-
2094
- // plain — inline style (토큰 var 활용)
2095
- return `'use client';
2096
-
2097
- import * as Sentry from '@sentry/nextjs';
2098
- import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
2099
- ${i18nImports}${linkImport}import { useEffect } from 'react';
2100
-
2101
- const wrapper: React.CSSProperties = {
2102
- display: 'flex',
2103
- minHeight: '100vh',
2104
- alignItems: 'center',
2105
- justifyContent: 'center',
2106
- padding: '0 16px',
2107
- };
2108
- const card: React.CSSProperties = {
2109
- width: '100%',
2110
- maxWidth: 448,
2111
- borderRadius: 8,
2112
- border: '1px solid var(--border)',
2113
- background: 'var(--background)',
2114
- padding: 24,
2115
- boxShadow: 'var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.15))',
2116
- };
2117
-
2118
- export default function Error({
2119
- error,
2120
- reset,
2121
- }: {
2122
- error: Error & { digest?: string };
2123
- reset: () => void;
2124
- }) {
2125
- ${tHook} useEffect(() => {
2126
- Sentry.captureException(error);
2127
- }, [error]);
2128
-
2129
- return (
2130
- <div style={wrapper}>
2131
- <div style={card}>
2132
- <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
2133
- <div
2134
- style={{
2135
- width: 64,
2136
- height: 64,
2137
- borderRadius: '50%',
2138
- background: 'color-mix(in srgb, var(--danger) 10%, transparent)',
2139
- display: 'flex',
2140
- alignItems: 'center',
2141
- justifyContent: 'center',
2142
- }}
2143
- >
2144
- <AlertTriangle style={{ width: 32, height: 32, color: 'var(--danger)' }} />
2145
- </div>
2146
- </div>
2147
-
2148
- <h2
2149
- style={{
2150
- fontSize: 24,
2151
- fontWeight: 700,
2152
- textAlign: 'center',
2153
- color: 'var(--foreground)',
2154
- margin: '0 0 8px',
2155
- }}
2156
- >
2157
- ${titleText}
2158
- </h2>
2159
- <p
2160
- style={{
2161
- fontSize: 14,
2162
- color: 'var(--foreground-muted)',
2163
- textAlign: 'center',
2164
- margin: '0 0 24px',
2165
- }}
2166
- >
2167
- ${descText}
2168
- </p>
2169
-
2170
- <div
2171
- style={{
2172
- borderRadius: 6,
2173
- border: '1px solid color-mix(in srgb, var(--danger) 30%, transparent)',
2174
- background: 'color-mix(in srgb, var(--danger) 5%, transparent)',
2175
- padding: 12,
2176
- }}
2177
- >
2178
- <p style={{ fontSize: 14, color: 'var(--danger)', margin: 0 }}>
2179
- {error.message || ${fallback}}
2180
- </p>
2181
- </div>
2182
-
2183
- <div style={{ marginTop: 24, display: 'flex', flexDirection: 'column', gap: 12 }}>
2184
- <button
2185
- onClick={reset}
2186
- style={{
2187
- width: '100%',
2188
- display: 'flex',
2189
- alignItems: 'center',
2190
- justifyContent: 'center',
2191
- gap: 8,
2192
- padding: '8px 16px',
2193
- borderRadius: 6,
2194
- border: 'none',
2195
- background: 'var(--primary)',
2196
- color: 'var(--primary-foreground)',
2197
- fontSize: 14,
2198
- fontWeight: 500,
2199
- cursor: 'pointer',
2200
- }}
2201
- >
2202
- <RefreshCw style={{ width: 16, height: 16 }} />
2203
- ${tryAgain}
2204
- </button>
2205
-
2206
- <Link
2207
- href='/'
2208
- style={{
2209
- width: '100%',
2210
- display: 'flex',
2211
- alignItems: 'center',
2212
- justifyContent: 'center',
2213
- gap: 8,
2214
- padding: '8px 16px',
2215
- borderRadius: 6,
2216
- border: '1px solid var(--border)',
2217
- color: 'var(--foreground)',
2218
- fontSize: 14,
2219
- fontWeight: 500,
2220
- textDecoration: 'none',
2221
- }}
2222
- >
2223
- <Home style={{ width: 16, height: 16 }} />
2224
- ${goHome}
2225
- </Link>
2226
- </div>
2227
-
2228
- {process.env.NODE_ENV === 'development' && error.digest && (
2229
- <div
2230
- style={{
2231
- marginTop: 16,
2232
- borderRadius: 6,
2233
- background: 'var(--background-subtle)',
2234
- padding: 12,
2235
- }}
2236
- >
2237
- <p style={{ fontSize: 12, color: 'var(--foreground-subtle)', margin: 0 }}>
2238
- Error ID: {error.digest}
2239
- </p>
2240
- </div>
2241
- )}
2242
- </div>
2243
- </div>
2244
- );
2245
- }
2246
- `;
2247
- }
2248
-
2249
- function buildErrorModuleCss() {
2250
- return `.wrapper {
2251
- display: flex;
2252
- min-height: 100vh;
2253
- align-items: center;
2254
- justify-content: center;
2255
- padding: 0 16px;
2256
- }
2257
-
2258
- .card {
2259
- width: 100%;
2260
- max-width: 448px;
2261
- border-radius: 8px;
2262
- border: 1px solid var(--border);
2263
- background: var(--background);
2264
- padding: 24px;
2265
- box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
2266
- }
2267
-
2268
- .iconRow {
2269
- display: flex;
2270
- justify-content: center;
2271
- margin-bottom: 16px;
2272
- }
2273
-
2274
- .iconCircle {
2275
- width: 64px;
2276
- height: 64px;
2277
- border-radius: 50%;
2278
- background: color-mix(in srgb, var(--danger) 10%, transparent);
2279
- display: flex;
2280
- align-items: center;
2281
- justify-content: center;
2282
- }
2283
-
2284
- .icon {
2285
- width: 32px;
2286
- height: 32px;
2287
- color: var(--danger);
2288
- }
2289
-
2290
- .title {
2291
- font-size: 24px;
2292
- font-weight: 700;
2293
- text-align: center;
2294
- color: var(--foreground);
2295
- margin: 0 0 8px;
2296
- }
2297
-
2298
- .description {
2299
- font-size: 14px;
2300
- color: var(--foreground-muted);
2301
- text-align: center;
2302
- margin: 0 0 24px;
2303
- }
2304
-
2305
- .errorBox {
2306
- border-radius: 6px;
2307
- border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
2308
- background: color-mix(in srgb, var(--danger) 5%, transparent);
2309
- padding: 12px;
2310
- }
2311
-
2312
- .errorText {
2313
- font-size: 14px;
2314
- color: var(--danger);
2315
- margin: 0;
2316
- }
2317
-
2318
- .actions {
2319
- margin-top: 24px;
2320
- display: flex;
2321
- flex-direction: column;
2322
- gap: 12px;
2323
- }
2324
-
2325
- .primaryButton,
2326
- .secondaryButton {
2327
- width: 100%;
2328
- display: flex;
2329
- align-items: center;
2330
- justify-content: center;
2331
- gap: 8px;
2332
- padding: 8px 16px;
2333
- border-radius: 6px;
2334
- font-size: 14px;
2335
- font-weight: 500;
2336
- cursor: pointer;
2337
- text-decoration: none;
2338
- }
2339
-
2340
- .primaryButton {
2341
- border: none;
2342
- background: var(--primary);
2343
- color: var(--primary-foreground);
2344
- }
2345
-
2346
- .secondaryButton {
2347
- border: 1px solid var(--border);
2348
- color: var(--foreground);
2349
- background: transparent;
2350
- }
2351
-
2352
- .buttonIcon {
2353
- width: 16px;
2354
- height: 16px;
2355
- }
2356
-
2357
- .digest {
2358
- margin-top: 16px;
2359
- border-radius: 6px;
2360
- background: var(--background-subtle);
2361
- padding: 12px;
2362
- }
2363
-
2364
- .digestText {
2365
- font-size: 12px;
2366
- color: var(--foreground-subtle);
2367
- margin: 0;
2368
- }
2369
- `;
2370
- }
2371
-
2372
1646
  /**
2373
1647
  * 스캐폴드 마무리 — `gitignore` 파일을 `.gitignore` 로 되돌리고 `git init` 실행.
2374
1648
  *
@@ -2396,12 +1670,9 @@ async function finalizeProject(targetDir, { dryRun = false } = {}) {
2396
1670
  * CLAUDE.md 의 `{{PLATFORM_APP_DESCRIPTION}}` 치환용 문장 — AI 에이전트에게 어떤 플랫폼인지
2397
1671
  * 정확히 전달해서 잘못된 컨벤션 (예: vite 프로젝트에 App Router 가정) 적용 방지.
2398
1672
  */
2399
- function describeAppPlatform(platform, { tauri = false } = {}) {
1673
+ function describeAppPlatform(platform) {
2400
1674
  if (platform === 'vite') {
2401
- const tauriSuffix = tauri
2402
- ? ' Tauri 데스크탑 셸이 동봉되어 있어 `src-tauri/` 가 native 진입점이다.'
2403
- : '';
2404
- return `Vite SPA (React + TypeScript). 라우트 + 비즈니스 로직. RSC / App Router 없음 — 모든 코드가 클라이언트 사이드 실행이다.${tauriSuffix}`;
1675
+ return `Vite SPA (React + TypeScript). 라우트 + 비즈니스 로직. RSC / App Router 없음 — 모든 코드가 클라이언트 사이드 실행이다.`;
2405
1676
  }
2406
1677
  // 디폴트: Next.js. 향후 platform 추가 시 분기 늘릴 것.
2407
1678
  return 'Next.js 앱 (App Router + Server Components). 라우트 + 비즈니스 로직.';
@@ -2528,81 +1799,6 @@ async function writePluginFiles(targetDir, plugins, arch) {
2528
1799
  }
2529
1800
  }
2530
1801
  }
2531
-
2532
- // auth-jwt + next-intl 동시 활성화 시 proxy.ts 병합
2533
- // (각 플러그인이 단독으로 깐 proxy.ts 를 합친 버전으로 덮어쓴다)
2534
- // i18n routing import 는 arch.aliases.config 기준 — FSD 면 @/src/shared/config,
2535
- // flat 이면 @/lib/config 로 해석.
2536
- const names = new Set(plugins.map((p) => p.name));
2537
- if (names.has('auth-jwt') && names.has('next-intl')) {
2538
- const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
2539
- const mergedProxy = `import createIntlMiddleware from 'next-intl/middleware';
2540
- import { NextRequest, NextResponse } from 'next/server';
2541
-
2542
- import { routing } from '${configAlias}/i18n/routing';
2543
-
2544
- const AUTH_ROUTES = ['/sign-in', '/sign-up'];
2545
-
2546
- /**
2547
- * 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
2548
- * \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
2549
- * 인증 가드 위에서 동작하므로 미인증이면 그대로 \`/sign-in\` 으로 빠진다.
2550
- */
2551
- const HOME_REDIRECT = '';
2552
-
2553
- const intl = createIntlMiddleware(routing);
2554
-
2555
- /**
2556
- * 로케일 prefix (/ko, /en) 를 벗겨 인증 라우트 매칭에 사용한다.
2557
- * 예: /ko/sign-in → /sign-in
2558
- */
2559
- const stripLocalePrefix = (pathname: string): string => {
2560
- const locales = routing.locales as readonly string[];
2561
- const segments = pathname.split('/').filter(Boolean);
2562
- if (segments[0] && locales.includes(segments[0])) {
2563
- const rest = segments.slice(1).join('/');
2564
- return \`/\${rest}\`.replace(/\\/$/, '') || '/';
2565
- }
2566
- return pathname;
2567
- };
2568
-
2569
- /**
2570
- * Next 16+ proxy.ts (구 middleware.ts).
2571
- * next-intl 라우팅 + auth-jwt 토큰 존재 체크 합성 버전.
2572
- *
2573
- * - \`/\` + HOME_REDIRECT 설정 → 해당 경로로 리다이렉트 (인증 가드보다 먼저)
2574
- * - intl 이 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
2575
- * - 그 위에 인증 가드 — 토큰 없고 인증 라우트도 아니면 /sign-in 으로 redirect
2576
- * - dev + \`NEXT_PUBLIC_DEV_AUTH_BYPASS=true\` → 가드 전체 우회 (개발용)
2577
- * - AT 만료 검사나 refresh 는 하지 않는다 (BFF 가 처리)
2578
- */
2579
- const DEV_BYPASS =
2580
- process.env.NODE_ENV !== 'production' &&
2581
- process.env.NEXT_PUBLIC_DEV_AUTH_BYPASS === 'true';
2582
-
2583
- export default function proxy(req: NextRequest) {
2584
- const intlRes = intl(req);
2585
- const pathname = stripLocalePrefix(req.nextUrl.pathname);
2586
- const hasToken = !!req.cookies.get('accessToken')?.value;
2587
- const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
2588
-
2589
- if (pathname === '/' && HOME_REDIRECT) {
2590
- return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
2591
- }
2592
-
2593
- if (DEV_BYPASS) return intlRes;
2594
- if (isAuthRoute) return intlRes;
2595
- if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
2596
-
2597
- return intlRes;
2598
- }
2599
-
2600
- export const config = {
2601
- matcher: '/((?!api|_next|_vercel|monitoring|.*\\\\..*).*)',
2602
- };
2603
- `;
2604
- await fs.writeFile(path.join(targetDir, 'proxy.ts'), mergedProxy);
2605
- }
2606
1802
  }
2607
1803
 
2608
1804
  async function composeProviders(targetDir, plugins, arch) {