project-auto-wizard 0.12.2 → 0.13.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 (86) hide show
  1. package/README.ja.md +165 -0
  2. package/README.ko.md +165 -0
  3. package/README.md +107 -286
  4. package/README.zh-CN.md +165 -0
  5. package/bin/project-auto-wizard.js +1 -2
  6. package/package.json +7 -3
  7. package/payload/config/breaking-changes.json +24 -0
  8. package/payload/config/wizard-prompts.yml +45 -0
  9. package/payload/scripts/changelog_manager.py +141 -46
  10. package/payload/scripts/issue_helper.py +34 -5
  11. package/payload/scripts/version_manager.py +397 -97
  12. package/payload/version.yml.template +4 -2
  13. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
  14. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
  15. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
  16. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
  17. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
  25. package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
  26. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
  27. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
  28. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
  29. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
  30. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
  31. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
  32. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
  33. package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
  34. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
  35. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
  38. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
  39. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
  40. package/src/cli/args.js +33 -24
  41. package/src/cli/help.js +16 -4
  42. package/src/commands/doctor.js +17 -10
  43. package/src/commands/dry-run.js +54 -1
  44. package/src/commands/full.js +127 -50
  45. package/src/commands/interactive-flutter.js +1 -1
  46. package/src/commands/interactive.js +124 -56
  47. package/src/commands/purge.js +59 -17
  48. package/src/commands/status.js +16 -1
  49. package/src/commands/uninstall.js +58 -13
  50. package/src/context.js +5 -5
  51. package/src/core/baseline.js +20 -5
  52. package/src/core/branches.js +19 -1
  53. package/src/core/branding.js +1 -1
  54. package/src/core/breaking-check.js +14 -19
  55. package/src/core/breaking.js +7 -3
  56. package/src/core/copy/gitignore.js +20 -12
  57. package/src/core/copy/readme.js +24 -5
  58. package/src/core/copy/simple.js +52 -13
  59. package/src/core/copy/workflows.js +90 -47
  60. package/src/core/deploy-style.js +83 -11
  61. package/src/core/detect-fs.js +127 -24
  62. package/src/core/detect.js +131 -47
  63. package/src/core/errors.js +3 -0
  64. package/src/core/flutter-options.js +4 -3
  65. package/src/core/fsutil.js +23 -6
  66. package/src/core/logger.js +55 -19
  67. package/src/core/paths-resolve.js +50 -47
  68. package/src/core/paths.js +26 -1
  69. package/src/core/release-options.js +23 -0
  70. package/src/core/removal-plan.js +75 -19
  71. package/src/core/types.js +113 -0
  72. package/src/core/verify.js +85 -19
  73. package/src/core/version-yml.js +42 -8
  74. package/src/core/wizard-env.js +21 -14
  75. package/src/core/wizard-labels.js +34 -25
  76. package/src/index.js +122 -49
  77. package/src/ui/ansi.js +3 -1
  78. package/src/ui/banner.js +12 -8
  79. package/src/ui/env-plan.js +68 -44
  80. package/src/ui/prompts.js +22 -12
  81. package/src/ui/readline-engine.js +109 -22
  82. package/src/ui/status-cards.js +10 -5
  83. package/src/ui/summary.js +71 -33
  84. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  85. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  86. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
@@ -27,6 +27,10 @@ name: PROJECT-SPRING-PR-PREVIEW
27
27
  env:
28
28
  # 프로젝트 고유 식별자 (컨테이너명, 이미지명, 도메인에 사용) (프로젝트별 수정 필요)
29
29
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
30
+ # 모노레포(--paths)에서 이 프로젝트가 있는 하위 경로. 빌드·Docker context의 기준 ('.'이면 레포 루트)
31
+ PROJECT_PATH: "." # @wizard auto:project-path
32
+ # 모노레포(PROJECT_PATH가 '.'이 아님)에서는 이미지·컨테이너·도메인 이름에 '-spring'를 붙여
33
+ # 같은 레포의 다른 타입 서비스와 이름이 겹치지 않게 한다 (예: my-app-spring)
30
34
 
31
35
  # Spring Boot 빌드 설정 (프로젝트별 수정 필요)
32
36
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
@@ -35,7 +39,7 @@ env:
35
39
  APPLICATION_YML_PATH: "__APPLICATION_YML_PATH__" # @wizard auto:spring-app-yml-path
36
40
 
37
41
  # Docker 설정 (프로젝트별 수정 필요)
38
- DOCKERFILE_PATH: './Dockerfile'
42
+ DOCKERFILE_PATH: './Dockerfile' # PROJECT_PATH 기준 상대 경로
39
43
  INTERNAL_PORT: '8080'
40
44
 
41
45
  # 볼륨 마운트 설정 (프로젝트별 수정 필요, 빈값이면 마운트 안함)
@@ -231,6 +235,42 @@ jobs:
231
235
  with:
232
236
  ref: ${{ steps.pr.outputs.ref }}
233
237
 
238
+ # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
239
+ - name: 배포 사전 점검 (Secret·Dockerfile)
240
+ working-directory: ${{ env.PROJECT_PATH }}
241
+ env:
242
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
243
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
244
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
245
+ SERVER_USER: ${{ secrets.SERVER_USER }}
246
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
247
+ SSH_KEY: ${{ secrets.SSH_KEY }}
248
+ DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
249
+ run: |
250
+ MISSING=""
251
+ for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
252
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
253
+ done
254
+ # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
255
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
256
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
257
+ else
258
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
259
+ fi
260
+ FAILED=false
261
+ if [ -n "$MISSING" ]; then
262
+ echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
263
+ FAILED=true
264
+ fi
265
+ if [ ! -f "$DOCKERFILE" ]; then
266
+ echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
267
+ FAILED=true
268
+ fi
269
+ if [ "$FAILED" = "true" ]; then
270
+ exit 1
271
+ fi
272
+ echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
273
+
234
274
  - name: JDK 설정
235
275
  uses: actions/setup-java@v5
236
276
  with:
@@ -239,6 +279,7 @@ jobs:
239
279
  cache: 'gradle'
240
280
 
241
281
  - name: Gradle 권한 설정
282
+ working-directory: ${{ env.PROJECT_PATH }}
242
283
  run: chmod +x gradlew
243
284
 
244
285
  # [영역 2] 프로젝트별 Secret 파일 생성
@@ -280,6 +321,7 @@ jobs:
280
321
  # [영역 2 끝] 프로젝트별 Secret 파일 생성 끝
281
322
 
282
323
  - name: Gradle 빌드
324
+ working-directory: ${{ env.PROJECT_PATH }}
283
325
  run: ${{ env.GRADLE_BUILD_CMD }}
284
326
 
285
327
  - name: 진행 상황 - 빌드 완료
@@ -333,10 +375,10 @@ jobs:
333
375
  - name: Docker 이미지 빌드 & Push
334
376
  uses: docker/build-push-action@v5
335
377
  with:
336
- context: .
337
- file: ${{ env.DOCKERFILE_PATH }}
378
+ context: ${{ env.PROJECT_PATH }}
379
+ file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
338
380
  push: true
339
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:pr-${{ github.event.issue.number }}
381
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}:pr-${{ github.event.issue.number }}
340
382
  cache-from: type=gha
341
383
  cache-to: type=gha,mode=max
342
384
 
@@ -406,7 +448,7 @@ jobs:
406
448
  echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
407
449
 
408
450
  PR_NUMBER=${{ github.event.issue.number }}
409
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
451
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
410
452
  CONTAINER_NAME="${PROJECT_NAME}-pr-${PR_NUMBER}"
411
453
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
412
454
  DOMAIN="${PROJECT_NAME}-pr-${PR_NUMBER}.${{ env.PREVIEW_DOMAIN_SUFFIX }}"
@@ -535,7 +577,7 @@ jobs:
535
577
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
536
578
  const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
537
579
 
538
- const projectName = '${{ env.PROJECT_NAME }}';
580
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
539
581
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
540
582
  const previewPort = '${{ env.PREVIEW_PORT }}';
541
583
  const apiDocsPath = '${{ env.API_DOCS_PATH }}';
@@ -604,7 +646,7 @@ jobs:
604
646
  with:
605
647
  script: |
606
648
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
607
- const projectName = '${{ env.PROJECT_NAME }}';
649
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
608
650
  const prNumber = context.issue.number;
609
651
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
610
652
 
@@ -653,6 +695,7 @@ jobs:
653
695
  `**[📋 빌드/배포 로그 확인](${runUrl})**`,
654
696
  '',
655
697
  '### 🔍 가능한 원인',
698
+ '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
656
699
  '- Gradle 빌드 실패',
657
700
  '- Docker 이미지 빌드 실패',
658
701
  '- 컨테이너 시작 실패 (Spring Boot 기동 오류)',
@@ -894,6 +937,43 @@ jobs:
894
937
  with:
895
938
  ref: ${{ needs.get-branch-from-issue.outputs.branch_name }}
896
939
 
940
+ # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
941
+ - name: 배포 사전 점검 (Secret·Dockerfile)
942
+ if: steps.check_branch.outputs.exists == 'true'
943
+ working-directory: ${{ env.PROJECT_PATH }}
944
+ env:
945
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
946
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
947
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
948
+ SERVER_USER: ${{ secrets.SERVER_USER }}
949
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
950
+ SSH_KEY: ${{ secrets.SSH_KEY }}
951
+ DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
952
+ run: |
953
+ MISSING=""
954
+ for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
955
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
956
+ done
957
+ # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
958
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
959
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
960
+ else
961
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
962
+ fi
963
+ FAILED=false
964
+ if [ -n "$MISSING" ]; then
965
+ echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
966
+ FAILED=true
967
+ fi
968
+ if [ ! -f "$DOCKERFILE" ]; then
969
+ echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
970
+ FAILED=true
971
+ fi
972
+ if [ "$FAILED" = "true" ]; then
973
+ exit 1
974
+ fi
975
+ echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
976
+
897
977
  - name: JDK 설정
898
978
  if: steps.check_branch.outputs.exists == 'true'
899
979
  uses: actions/setup-java@v5
@@ -903,6 +983,7 @@ jobs:
903
983
  cache: 'gradle'
904
984
 
905
985
  - name: Gradle 권한 설정
986
+ working-directory: ${{ env.PROJECT_PATH }}
906
987
  if: steps.check_branch.outputs.exists == 'true'
907
988
  run: chmod +x gradlew
908
989
 
@@ -917,6 +998,7 @@ jobs:
917
998
  # [영역 2 끝] 프로젝트별 Secret 파일 생성 끝
918
999
 
919
1000
  - name: Gradle 빌드
1001
+ working-directory: ${{ env.PROJECT_PATH }}
920
1002
  if: steps.check_branch.outputs.exists == 'true'
921
1003
  run: ${{ env.GRADLE_BUILD_CMD }}
922
1004
 
@@ -978,10 +1060,10 @@ jobs:
978
1060
  if: steps.check_branch.outputs.exists == 'true'
979
1061
  uses: docker/build-push-action@v5
980
1062
  with:
981
- context: .
982
- file: ${{ env.DOCKERFILE_PATH }}
1063
+ context: ${{ env.PROJECT_PATH }}
1064
+ file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
983
1065
  push: true
984
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:pr-${{ needs.get-branch-from-issue.outputs.issue_number }}
1066
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}:pr-${{ needs.get-branch-from-issue.outputs.issue_number }}
985
1067
  cache-from: type=gha
986
1068
  cache-to: type=gha,mode=max
987
1069
 
@@ -1056,7 +1138,7 @@ jobs:
1056
1138
  echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
1057
1139
 
1058
1140
  ISSUE_NUMBER=${{ needs.get-branch-from-issue.outputs.issue_number }}
1059
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
1141
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1060
1142
  CONTAINER_NAME="${PROJECT_NAME}-pr-${ISSUE_NUMBER}"
1061
1143
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${ISSUE_NUMBER}"
1062
1144
  DOMAIN="${PROJECT_NAME}-pr-${ISSUE_NUMBER}.${{ env.PREVIEW_DOMAIN_SUFFIX }}"
@@ -1187,7 +1269,7 @@ jobs:
1187
1269
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
1188
1270
  const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
1189
1271
 
1190
- const projectName = '${{ env.PROJECT_NAME }}';
1272
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1191
1273
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
1192
1274
  const previewPort = '${{ env.PREVIEW_PORT }}';
1193
1275
  const apiDocsPath = '${{ env.API_DOCS_PATH }}';
@@ -1257,7 +1339,7 @@ jobs:
1257
1339
  with:
1258
1340
  script: |
1259
1341
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1260
- const projectName = '${{ env.PROJECT_NAME }}';
1342
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1261
1343
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
1262
1344
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1263
1345
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
@@ -1303,6 +1385,7 @@ jobs:
1303
1385
  `**[📋 빌드/배포 로그 확인](${runUrl})**`,
1304
1386
  '',
1305
1387
  '### 🔍 가능한 원인',
1388
+ '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
1306
1389
  '- Gradle 빌드 실패',
1307
1390
  '- Docker 이미지 빌드 실패',
1308
1391
  '- 컨테이너 시작 실패 (Spring Boot 기동 오류)',
@@ -1379,7 +1462,7 @@ jobs:
1379
1462
  echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
1380
1463
 
1381
1464
  PR_NUMBER=${{ steps.pr_number.outputs.number }}
1382
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
1465
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1383
1466
  CONTAINER_NAME="${PROJECT_NAME}-pr-${PR_NUMBER}"
1384
1467
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
1385
1468
 
@@ -1407,7 +1490,7 @@ jobs:
1407
1490
  with:
1408
1491
  script: |
1409
1492
  const number = ${{ steps.pr_number.outputs.number }};
1410
- const projectName = '${{ env.PROJECT_NAME }}';
1493
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1411
1494
 
1412
1495
  const body = [
1413
1496
  '## 🗑️ Preview 환경 삭제 완료!',
@@ -1450,7 +1533,7 @@ jobs:
1450
1533
  export PATH=$PATH:/usr/local/bin
1451
1534
 
1452
1535
  NUMBER=${{ github.event.issue.number }}
1453
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
1536
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1454
1537
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1455
1538
 
1456
1539
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1480,7 +1563,7 @@ jobs:
1480
1563
  export PATH=$PATH:/usr/local/bin
1481
1564
 
1482
1565
  NUMBER=${{ github.event.issue.number }}
1483
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
1566
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1484
1567
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1485
1568
  docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
1486
1569
 
@@ -1489,7 +1572,7 @@ jobs:
1489
1572
  with:
1490
1573
  script: |
1491
1574
  const number = context.issue.number;
1492
- const projectName = '${{ env.PROJECT_NAME }}';
1575
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1493
1576
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
1494
1577
  const previewPort = '${{ env.PREVIEW_PORT }}';
1495
1578
  const domain = `${projectName}-pr-${number}.${domainSuffix}`;
@@ -1640,6 +1723,43 @@ jobs:
1640
1723
  with:
1641
1724
  ref: ${{ steps.extract.outputs.branch_name }}
1642
1725
 
1726
+ # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
1727
+ - name: 배포 사전 점검 (Secret·Dockerfile)
1728
+ if: steps.check_branch.outputs.exists == 'true'
1729
+ working-directory: ${{ env.PROJECT_PATH }}
1730
+ env:
1731
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
1732
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
1733
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
1734
+ SERVER_USER: ${{ secrets.SERVER_USER }}
1735
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
1736
+ SSH_KEY: ${{ secrets.SSH_KEY }}
1737
+ DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
1738
+ run: |
1739
+ MISSING=""
1740
+ for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
1741
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
1742
+ done
1743
+ # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
1744
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
1745
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
1746
+ else
1747
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
1748
+ fi
1749
+ FAILED=false
1750
+ if [ -n "$MISSING" ]; then
1751
+ echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
1752
+ FAILED=true
1753
+ fi
1754
+ if [ ! -f "$DOCKERFILE" ]; then
1755
+ echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
1756
+ FAILED=true
1757
+ fi
1758
+ if [ "$FAILED" = "true" ]; then
1759
+ exit 1
1760
+ fi
1761
+ echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
1762
+
1643
1763
  - name: 최신 커밋 정보 가져오기
1644
1764
  if: steps.check_branch.outputs.exists == 'true'
1645
1765
  id: commit
@@ -1651,7 +1771,7 @@ jobs:
1651
1771
  uses: actions/github-script@v9
1652
1772
  with:
1653
1773
  script: |
1654
- const projectName = '${{ env.PROJECT_NAME }}';
1774
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1655
1775
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
1656
1776
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1657
1777
  const sha = '${{ steps.commit.outputs.sha }}';
@@ -1700,6 +1820,7 @@ jobs:
1700
1820
  ${{ runner.os }}-gradle-
1701
1821
 
1702
1822
  - name: Gradle 빌드
1823
+ working-directory: ${{ env.PROJECT_PATH }}
1703
1824
  if: steps.check_branch.outputs.exists == 'true'
1704
1825
  id: build
1705
1826
  run: |
@@ -1717,7 +1838,7 @@ jobs:
1717
1838
  with:
1718
1839
  script: |
1719
1840
  const commentId = ${{ steps.progress.outputs.comment_id }};
1720
- const projectName = '${{ env.PROJECT_NAME }}';
1841
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1721
1842
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1722
1843
  const sha = '${{ steps.commit.outputs.sha }}';
1723
1844
  const buildDuration = '${{ steps.build.outputs.duration }}';
@@ -1755,13 +1876,14 @@ jobs:
1755
1876
  password: ${{ secrets.DOCKERHUB_TOKEN }}
1756
1877
 
1757
1878
  - name: Docker 이미지 빌드 & 푸시
1879
+ working-directory: ${{ env.PROJECT_PATH }}
1758
1880
  if: steps.check_branch.outputs.exists == 'true'
1759
1881
  id: docker
1760
1882
  run: |
1761
1883
  DOCKER_START=$(date +%s)
1762
1884
  CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
1763
- IMAGE_TAG="${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${CONTAINER_SUFFIX}"
1764
- docker build -t $IMAGE_TAG .
1885
+ IMAGE_TAG="${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}:${CONTAINER_SUFFIX}"
1886
+ docker build -t $IMAGE_TAG -f "${{ env.DOCKERFILE_PATH }}" .
1765
1887
  docker push $IMAGE_TAG
1766
1888
  DOCKER_END=$(date +%s)
1767
1889
  DOCKER_DURATION=$((DOCKER_END - DOCKER_START))
@@ -1775,7 +1897,7 @@ jobs:
1775
1897
  with:
1776
1898
  script: |
1777
1899
  const commentId = ${{ steps.progress.outputs.comment_id }};
1778
- const projectName = '${{ env.PROJECT_NAME }}';
1900
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1779
1901
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1780
1902
  const sha = '${{ steps.commit.outputs.sha }}';
1781
1903
  const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
@@ -1819,7 +1941,7 @@ jobs:
1819
1941
  script: |
1820
1942
  export PATH=$PATH:/usr/local/bin
1821
1943
 
1822
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
1944
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1823
1945
  CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
1824
1946
  CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_SUFFIX}"
1825
1947
  IMAGE_TAG="${{ steps.docker.outputs.image_tag }}"
@@ -1877,7 +1999,7 @@ jobs:
1877
1999
  with:
1878
2000
  script: |
1879
2001
  const commentId = ${{ steps.progress.outputs.comment_id }};
1880
- const projectName = '${{ env.PROJECT_NAME }}';
2002
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1881
2003
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
1882
2004
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1883
2005
  const sha = '${{ steps.commit.outputs.sha }}';
@@ -1942,7 +2064,7 @@ jobs:
1942
2064
  with:
1943
2065
  script: |
1944
2066
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1945
- const projectName = '${{ env.PROJECT_NAME }}';
2067
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1946
2068
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1947
2069
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1948
2070
 
@@ -1987,6 +2109,7 @@ jobs:
1987
2109
  `**[📋 빌드/배포 로그 확인](${runUrl})**`,
1988
2110
  '',
1989
2111
  '### 🔍 가능한 원인',
2112
+ '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
1990
2113
  '- Gradle 빌드 실패',
1991
2114
  '- Docker 이미지 빌드 실패',
1992
2115
  '- 서버 연결 문제',
@@ -2056,7 +2179,7 @@ jobs:
2056
2179
  script: |
2057
2180
  export PATH=$PATH:/usr/local/bin
2058
2181
 
2059
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
2182
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
2060
2183
  CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
2061
2184
  CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_SUFFIX}"
2062
2185
 
@@ -2078,7 +2201,7 @@ jobs:
2078
2201
  uses: actions/github-script@v9
2079
2202
  with:
2080
2203
  script: |
2081
- const projectName = '${{ env.PROJECT_NAME }}';
2204
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2082
2205
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2083
2206
  const branchName = '${{ steps.extract.outputs.branch_name }}';
2084
2207
  const containerName = `${projectName}-${containerSuffix}`;
@@ -2149,7 +2272,7 @@ jobs:
2149
2272
  script: |
2150
2273
  export PATH=$PATH:/usr/local/bin
2151
2274
 
2152
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
2275
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
2153
2276
  CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
2154
2277
  CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_SUFFIX}"
2155
2278
 
@@ -2159,7 +2282,7 @@ jobs:
2159
2282
  uses: actions/github-script@v9
2160
2283
  with:
2161
2284
  script: |
2162
- const projectName = '${{ env.PROJECT_NAME }}';
2285
+ const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2163
2286
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2164
2287
  const branchName = '${{ steps.extract.outputs.branch_name }}';
2165
2288
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -63,6 +63,10 @@ on:
63
63
  env:
64
64
  # 프로젝트 설정 - 실제 프로젝트명으로 변경하세요
65
65
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
66
+ # 모노레포(--paths)에서 이 프로젝트가 있는 하위 경로. 빌드·Docker context의 기준 ('.'이면 레포 루트)
67
+ PROJECT_PATH: "." # @wizard auto:project-path
68
+ # 모노레포(PROJECT_PATH가 '.'이 아님)에서는 이미지·컨테이너·도메인 이름에 '-spring'를 붙여
69
+ # 같은 레포의 다른 타입 서비스와 이름이 겹치지 않게 한다 (예: my-app-spring)
66
70
 
67
71
  # Spring Boot 설정
68
72
  SPRING_PROFILE: "prod"
@@ -71,7 +75,7 @@ env:
71
75
 
72
76
  # 경로 설정
73
77
  APPLICATION_YML_DIR: "__APPLICATION_YML_DIR__" # @wizard auto:spring-app-yml-dir
74
- DOCKERFILE_PATH: "./Dockerfile"
78
+ DOCKERFILE_PATH: "./Dockerfile" # PROJECT_PATH 기준 상대 경로
75
79
 
76
80
  # 포트 설정
77
81
  CONTAINER_INTERNAL_PORT: "8080" # Spring Boot 내부 포트 (고정값)
@@ -116,6 +120,42 @@ jobs:
116
120
  - name: 코드 체크아웃
117
121
  uses: actions/checkout@v7
118
122
 
123
+ # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
124
+ - name: 배포 사전 점검 (Secret·Dockerfile)
125
+ working-directory: ${{ env.PROJECT_PATH }}
126
+ env:
127
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
128
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
129
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
130
+ SERVER_USER: ${{ secrets.SERVER_USER }}
131
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
132
+ SSH_KEY: ${{ secrets.SSH_KEY }}
133
+ DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
134
+ run: |
135
+ MISSING=""
136
+ for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
137
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
138
+ done
139
+ # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
140
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
141
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
142
+ else
143
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
144
+ fi
145
+ FAILED=false
146
+ if [ -n "$MISSING" ]; then
147
+ echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
148
+ FAILED=true
149
+ fi
150
+ if [ ! -f "$DOCKERFILE" ]; then
151
+ echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
152
+ FAILED=true
153
+ fi
154
+ if [ "$FAILED" = "true" ]; then
155
+ exit 1
156
+ fi
157
+ echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
158
+
119
159
  - name: Java 설정
120
160
  uses: actions/setup-java@v5
121
161
  with:
@@ -124,6 +164,7 @@ jobs:
124
164
  cache: 'gradle'
125
165
 
126
166
  - name: Gradle Wrapper 실행권한 부여
167
+ working-directory: ${{ env.PROJECT_PATH }}
127
168
  run: chmod +x gradlew
128
169
 
129
170
  # GitHub Secrets에서 APPLICATION_PROD_YML 값을 읽어와서
@@ -140,6 +181,7 @@ jobs:
140
181
 
141
182
  # 테스트는 제외하고 운영 프로파일로 빌드
142
183
  - name: Build with Gradle
184
+ working-directory: ${{ env.PROJECT_PATH }}
143
185
  run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
144
186
 
145
187
  - name: Docker 빌드환경 설정
@@ -163,10 +205,10 @@ jobs:
163
205
  - name: Docker 이미지 빌드 및 푸시
164
206
  uses: docker/build-push-action@v5
165
207
  with:
166
- context: .
167
- file: ${{ env.DOCKERFILE_PATH }}
208
+ context: ${{ env.PROJECT_PATH }}
209
+ file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
168
210
  push: true
169
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.ref_name }}
211
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}:${{ github.ref_name }}
170
212
  cache-from: type=local,src=/tmp/.buildx-cache
171
213
  cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
172
214
 
@@ -214,7 +256,7 @@ jobs:
214
256
 
215
257
  BRANCH=${{ github.ref_name }}
216
258
 
217
- PROJECT_NAME="${{ env.PROJECT_NAME }}"
259
+ PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
218
260
 
219
261
  # 포트 및 컨테이너명 설정
220
262
  PORT=${{ env.DEPLOY_PORT }}
package/src/cli/args.js CHANGED
@@ -1,9 +1,15 @@
1
- // CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
1
+ // CLI 인자 파싱.
2
2
  import { VALID_TYPES, VALID_MODES } from "../context.js";
3
3
  import { DEPLOY_STYLES, isDeployStyle, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
4
+ import { isValidBranchName } from "../core/branches.js";
4
5
  import {
5
6
  ENV_MODES, DEPLOY_MODES, STORE_PLATFORMS, NO_STORE, isEnvMode, isDeployMode, parseStoreList,
6
7
  } from "../core/flutter-options.js";
8
+ import { CliError } from "../core/errors.js";
9
+ import { normalizePath, isRepoRelativePath } from "../core/paths.js";
10
+
11
+ // 기존 import 경로(cli/args.js) 호환 — 정의는 core에 있다.
12
+ export { CliError, normalizePath, isRepoRelativePath };
7
13
 
8
14
  // argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
9
15
  export function parseArgs(argv) {
@@ -49,8 +55,15 @@ export function parseArgs(argv) {
49
55
  case "-v": case "--version":
50
56
  // npm 관례: -v/--version 은 패키지 버전 출력. (초기 버전 지정은 --project-version)
51
57
  result.showVersion = true; break;
52
- case "--project-version":
53
- result.version = args.shift() ?? ""; break;
58
+ case "--project-version": {
59
+ // 릴리스 워크플로우(version_manager)는 x.y.z만 올릴 수 있다 — 흔한 v 접두사는 떼고 받는다.
60
+ const raw = (args.shift() ?? "").trim();
61
+ const v = raw.replace(/^v/i, "");
62
+ if (!/^\d+\.\d+\.\d+$/.test(v)) {
63
+ throw new CliError(`--project-version 값이 올바르지 않습니다: '${raw}' (x.y.z 형식, 예: 1.0.0)`);
64
+ }
65
+ result.version = v; break;
66
+ }
54
67
  case "-t": case "--type": {
55
68
  const csv = args.shift() ?? "";
56
69
  const seen = new Set();
@@ -132,16 +145,19 @@ export function parseArgs(argv) {
132
145
  case "--no-copilot":
133
146
  if (seenFlags.has("--copilot")) throw new CliError("--copilot과 --no-copilot은 동시에 지정할 수 없습니다");
134
147
  seenFlags.add("--no-copilot"); result.includeCopilotAi = false; break;
135
- case "--paths": result.pathsCsv = args.shift() ?? ""; break;
136
- case "--main-branch": {
137
- const v = args.shift();
138
- if (!v) throw new CliError("--main-branch에 빈 값을 지정할 수 없습니다");
139
- result.mainBranch = v; break;
148
+ case "--paths": {
149
+ // 값이 없는데 조용히 자동 감지로 넘어가면 사용자가 지정했다고 믿은 경로와 다르게 설치된다.
150
+ const v = (args.shift() ?? "").trim();
151
+ if (!v) throw new CliError("--paths 인자가 비어 있습니다 (예: --paths flutter=app,react=client)");
152
+ result.pathsCsv = v; break;
140
153
  }
141
- case "--develop-branch": {
142
- const v = args.shift();
143
- if (!v) throw new CliError("--develop-branch에 빈 값을 지정할 수 없습니다");
144
- result.developBranch = v; break;
154
+ case "--main-branch": case "--develop-branch": {
155
+ const v = (args.shift() ?? "").trim();
156
+ if (!v) throw new CliError(`${a}에 빈 값을 지정할 수 없습니다`);
157
+ // 워크플로우 트리거·셸 명령에 그대로 치환되는 값이라 쓸 수 없는 이름은 설치 전에 거부한다.
158
+ if (!isValidBranchName(v)) throw new CliError(`${a} 값이 브랜치 이름으로 올바르지 않습니다: '${v}'`);
159
+ if (a === "--main-branch") result.mainBranch = v; else result.developBranch = v;
160
+ break;
145
161
  }
146
162
  case "-h": case "--help": result.help = true; break;
147
163
  default:
@@ -156,17 +172,6 @@ export function parseArgs(argv) {
156
172
  return result;
157
173
  }
158
174
 
159
- export class CliError extends Error {}
160
-
161
- // 경로 정규화 (.sh resolve_project_paths §3.4): 앞뒤 공백·\→/·끝 /·앞 ./ 제거, 빈값→"."
162
- export function normalizePath(p) {
163
- let s = String(p).trim();
164
- s = s.replace(/\\/g, "/");
165
- s = s.replace(/\/+$/, ""); // 끝 /
166
- s = s.replace(/^\.\//, ""); // 앞 ./
167
- return s === "" ? "." : s;
168
- }
169
-
170
175
  // "flutter=app,react=client" → Map<type, normalizedPath>. 타입 검증(무효 → throw).
171
176
  export function parsePathsCsv(csv) {
172
177
  const map = new Map();
@@ -179,7 +184,11 @@ export function parsePathsCsv(csv) {
179
184
  if (!VALID_TYPES.includes(type)) {
180
185
  throw new CliError(`--paths에 지원하지 않는 타입: '${type}'`);
181
186
  }
182
- map.set(type, normalizePath(rawPath));
187
+ const path = normalizePath(rawPath);
188
+ if (!isRepoRelativePath(path)) {
189
+ throw new CliError(`--paths는 레포 안의 상대경로만 지정할 수 있습니다: '${type}=${rawPath.trim()}'`);
190
+ }
191
+ map.set(type, path);
183
192
  }
184
193
  return map;
185
194
  }