project-auto-wizard 0.9.1 → 0.11.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 (54) hide show
  1. package/README.md +83 -4
  2. package/package.json +1 -1
  3. package/payload/config/breaking-changes.json +24 -1
  4. package/payload/flutter-app/android/fastlane/Fastfile.playstore +86 -0
  5. package/payload/flutter-app/ios/ExportOptions.plist +31 -0
  6. package/payload/flutter-app/ios/fastlane/Fastfile +117 -0
  7. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  8. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  9. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  10. package/payload/scripts/changelog_manager.py +2 -2
  11. package/payload/scripts/version_manager.py +2 -2
  12. package/payload/version.yml.template +1 -0
  13. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +12 -12
  14. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +48 -20
  15. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +44 -19
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +28 -36
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +33 -46
  18. package/payload/workflows/flutter/{PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml → PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml} +38 -38
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +57 -42
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
  22. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
  23. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +40 -40
  24. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
  25. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
  26. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +40 -40
  27. package/payload/workflows/react/PROJECT-REACT-CI.yaml +47 -0
  28. package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
  29. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +47 -0
  30. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
  31. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +39 -39
  32. package/src/cli/args.js +37 -0
  33. package/src/cli/help.js +4 -0
  34. package/src/commands/doctor.js +55 -2
  35. package/src/commands/dry-run.js +10 -0
  36. package/src/commands/full.js +28 -3
  37. package/src/commands/interactive-flutter.js +95 -0
  38. package/src/commands/interactive.js +51 -5
  39. package/src/commands/status.js +20 -2
  40. package/src/context.js +6 -0
  41. package/src/core/breaking.js +7 -3
  42. package/src/core/copy/flutter-app.js +42 -0
  43. package/src/core/copy/workflows.js +21 -9
  44. package/src/core/detect-fs.js +9 -1
  45. package/src/core/flutter-options.js +118 -0
  46. package/src/core/installed-stores.js +12 -0
  47. package/src/core/verify.js +2 -2
  48. package/src/core/version-yml.js +40 -5
  49. package/src/core/wizard-env.js +26 -4
  50. package/src/index.js +32 -1
  51. package/src/ui/env-plan.js +8 -5
  52. package/src/ui/prompts.js +88 -3
  53. package/src/ui/status-cards.js +13 -1
  54. package/src/ui/summary.js +24 -5
@@ -35,6 +35,7 @@ on:
35
35
  branches: ["{{MAIN_BRANCH}}"]
36
36
  tags:
37
37
  - 'v*.*.*'
38
+ # @wizard paths-anchor (모노레포일 때 integrator가 paths 필터를 여기 주입)
38
39
  workflow_dispatch:
39
40
 
40
41
  concurrency:
@@ -26,11 +26,39 @@ permissions:
26
26
 
27
27
  env:
28
28
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
29
+ # 모노레포에서 이 프로젝트가 있는 하위 경로 ('.'이면 항상 검증)
30
+ PROJECT_PATH: "." # @wizard auto:project-path
29
31
 
30
32
  jobs:
33
+ # ===================================================================
34
+ # 변경 감지 (모노레포에서 이 프로젝트 경로가 바뀌었는지 판별)
35
+ # ===================================================================
36
+ changes:
37
+ name: 변경 감지
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ contents: read
41
+ pull-requests: read
42
+ outputs:
43
+ project: ${{ steps.filter.outputs.project }}
44
+ steps:
45
+ - name: Checkout repository
46
+ uses: actions/checkout@v7
47
+
48
+ - name: 프로젝트 경로 변경 여부 판별
49
+ id: filter
50
+ if: ${{ github.event_name != 'workflow_dispatch' }}
51
+ uses: dorny/paths-filter@v4
52
+ with:
53
+ filters: |
54
+ project:
55
+ - '${{ env.PROJECT_PATH == '.' && '**' || format('{0}/**', env.PROJECT_PATH) }}'
56
+
31
57
  build-check:
32
58
  name: Build Verification
33
59
  runs-on: ubuntu-latest
60
+ needs: changes
61
+ if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true' }}
34
62
 
35
63
  steps:
36
64
  - name: Checkout Repository
@@ -318,3 +346,22 @@ jobs:
318
346
  run: |
319
347
  echo "❌ 빌드가 실패했습니다. 위의 에러 메시지를 확인해주세요."
320
348
  exit 1
349
+
350
+ # ===================================================================
351
+ # CI Gate (required check로 등록할 단일 job)
352
+ # ===================================================================
353
+ ci-gate:
354
+ name: CI Gate
355
+ if: ${{ always() }}
356
+ needs: [changes, build-check]
357
+ runs-on: ubuntu-latest
358
+ steps:
359
+ - name: 결과 집계
360
+ env:
361
+ RESULTS: ${{ toJSON(needs.*.result) }}
362
+ run: |
363
+ echo "needs 결과: $RESULTS"
364
+ if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
365
+ echo "실패하거나 취소된 job이 있습니다."
366
+ exit 1
367
+ fi
@@ -15,6 +15,7 @@ on:
15
15
  branches: ["{{MAIN_BRANCH}}"]
16
16
  tags:
17
17
  - 'v*.*.*'
18
+ # @wizard paths-anchor (모노레포일 때 integrator가 paths 필터를 여기 주입)
18
19
 
19
20
  concurrency:
20
21
  group: nexus-publish-${{ github.ref }}
@@ -15,9 +15,9 @@
15
15
  # ===================================================================
16
16
  #
17
17
  # 🚀 사용법:
18
- # @suh-lab server build - PR/Issue 빌드 및 배포
19
- # @suh-lab server destroy - Preview 환경 삭제
20
- # @suh-lab server status - 현재 상태 확인
18
+ # /wizard server build - PR/Issue 빌드 및 배포
19
+ # /wizard server destroy - Preview 환경 삭제
20
+ # /wizard server status - 현재 상태 확인
21
21
  #
22
22
  # 📊 리소스 네이밍 규칙:
23
23
  # - 컨테이너: {PROJECT_NAME}-pr-{PR번호}
@@ -65,7 +65,7 @@ env:
65
65
 
66
66
  # Issue Helper 댓글 마커 (프로젝트별 수정 필요)
67
67
  # Issue 댓글에서 브랜치명을 추출할 때 사용
68
- ISSUE_HELPER_MARKER: 'Guide by SUH-LAB'
68
+ ISSUE_HELPER_MARKER: '<!-- project-auto-wizard issue helper -->'
69
69
 
70
70
  # Health Check 설정 (프로젝트별 수정 필요)
71
71
  # - HEALTH_CHECK_PATH: HTTP Health Check 경로 (빈값이면 HTTP 체크 건너뜀)
@@ -115,7 +115,7 @@ jobs:
115
115
  is_custom_branch: ${{ steps.parse.outputs.is_custom_branch }}
116
116
  steps:
117
117
  - name: 댓글에 👀 리액션 추가
118
- if: contains(github.event.comment.body, '@suh-lab') && contains(github.event.comment.body, 'server')
118
+ if: contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server')
119
119
  uses: actions/github-script@v9
120
120
  with:
121
121
  github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -143,8 +143,8 @@ jobs:
143
143
  fi
144
144
 
145
145
  # 명령어 파싱 (브랜치 파라미터 지원)
146
- # 형식: @suh-lab server <command> [브랜치명]
147
- if [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
146
+ # 형식: /wizard server <command> [브랜치명]
147
+ if [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
148
148
  echo "command=build" >> $GITHUB_OUTPUT
149
149
  echo "is_valid=true" >> $GITHUB_OUTPUT
150
150
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
@@ -155,7 +155,7 @@ jobs:
155
155
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
156
156
  echo "✅ 명령어 감지: build"
157
157
  fi
158
- elif [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
158
+ elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
159
159
  echo "command=destroy" >> $GITHUB_OUTPUT
160
160
  echo "is_valid=true" >> $GITHUB_OUTPUT
161
161
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
@@ -166,7 +166,7 @@ jobs:
166
166
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
167
167
  echo "✅ 명령어 감지: destroy"
168
168
  fi
169
- elif [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
169
+ elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
170
170
  echo "command=status" >> $GITHUB_OUTPUT
171
171
  echo "is_valid=true" >> $GITHUB_OUTPUT
172
172
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
@@ -179,7 +179,7 @@ jobs:
179
179
  fi
180
180
  else
181
181
  echo "is_valid=false" >> $GITHUB_OUTPUT
182
- echo "ℹ️ @suh-lab server 명령어가 아님"
182
+ echo "ℹ️ /wizard server 명령어가 아님"
183
183
  fi
184
184
 
185
185
  # -----------------------------------------------------------------
@@ -292,13 +292,13 @@ jobs:
292
292
  # env:
293
293
  # VERTEX_SA_KEY: ${{ secrets.VERTEX_SA_KEY }}
294
294
  # run: |
295
- # echo "$VERTEX_SA_KEY" | sed 's/\\n/\n/g' > ./Suh-Web/src/main/resources/vertex-ai-key.json
295
+ # echo "$VERTEX_SA_KEY" | sed 's/\\n/\n/g' > ./<모듈명>/src/main/resources/vertex-ai-key.json
296
296
 
297
297
  # - name: "[선택] Firebase Admin SDK 생성"
298
298
  # env:
299
299
  # FIREBASE_KEY_JSON: ${{ secrets.FIREBASE_KEY_JSON }}
300
300
  # run: |
301
- # echo "$FIREBASE_KEY_JSON" | sed 's/\\n/\n/g' > ./Suh-Web/src/main/resources/firebase-admin-sdk.json
301
+ # echo "$FIREBASE_KEY_JSON" | sed 's/\\n/\n/g' > ./<모듈명>/src/main/resources/firebase-admin-sdk.json
302
302
 
303
303
  # =================================================================
304
304
  # ⚠️ [영역 2 끝] 프로젝트별 Secret 파일 생성 끝
@@ -606,17 +606,17 @@ jobs:
606
606
  '### 📋 명령어',
607
607
  '| 명령어 | 설명 |',
608
608
  '|--------|------|',
609
- '| `@suh-lab server build` | 최신 커밋으로 재배포 |',
610
- '| `@suh-lab server destroy` | Preview 환경 삭제 |',
611
- '| `@suh-lab server status` | 현재 상태 확인 |',
609
+ '| `/wizard server build` | 최신 커밋으로 재배포 |',
610
+ '| `/wizard server destroy` | Preview 환경 삭제 |',
611
+ '| `/wizard server status` | 현재 상태 확인 |',
612
612
  '',
613
613
  '<details>',
614
614
  '<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
615
615
  '',
616
616
  '```',
617
- `@suh-lab server build ${branchName}`,
618
- `@suh-lab server destroy ${branchName}`,
619
- `@suh-lab server status ${branchName}`,
617
+ `/wizard server build ${branchName}`,
618
+ `/wizard server destroy ${branchName}`,
619
+ `/wizard server status ${branchName}`,
620
620
  '```',
621
621
  '</details>',
622
622
  '',
@@ -693,7 +693,7 @@ jobs:
693
693
  '',
694
694
  '### 💡 다음 단계',
695
695
  '1. 위 링크에서 빌드/배포 로그를 확인하세요',
696
- '2. 문제를 수정한 후 다시 시도하세요: `@suh-lab server build`',
696
+ '2. 문제를 수정한 후 다시 시도하세요: `/wizard server build`',
697
697
  '',
698
698
  '---',
699
699
  '*🤖 이 댓글은 PR Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -1278,17 +1278,17 @@ jobs:
1278
1278
  '### 📋 명령어',
1279
1279
  '| 명령어 | 설명 |',
1280
1280
  '|--------|------|',
1281
- '| `@suh-lab server build` | 최신 커밋으로 재배포 |',
1282
- '| `@suh-lab server destroy` | Preview 환경 삭제 |',
1283
- '| `@suh-lab server status` | 현재 상태 확인 |',
1281
+ '| `/wizard server build` | 최신 커밋으로 재배포 |',
1282
+ '| `/wizard server destroy` | Preview 환경 삭제 |',
1283
+ '| `/wizard server status` | 현재 상태 확인 |',
1284
1284
  '',
1285
1285
  '<details>',
1286
1286
  '<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
1287
1287
  '',
1288
1288
  '```',
1289
- `@suh-lab server build ${branchName}`,
1290
- `@suh-lab server destroy ${branchName}`,
1291
- `@suh-lab server status ${branchName}`,
1289
+ `/wizard server build ${branchName}`,
1290
+ `/wizard server destroy ${branchName}`,
1291
+ `/wizard server status ${branchName}`,
1292
1292
  '```',
1293
1293
  '</details>',
1294
1294
  '',
@@ -1362,7 +1362,7 @@ jobs:
1362
1362
  '',
1363
1363
  '### 💡 다음 단계',
1364
1364
  '1. 위 링크에서 빌드/배포 로그를 확인하세요',
1365
- '2. 문제를 수정한 후 다시 시도하세요: `@suh-lab server build`',
1365
+ '2. 문제를 수정한 후 다시 시도하세요: `/wizard server build`',
1366
1366
  '',
1367
1367
  '---',
1368
1368
  '*🤖 이 댓글은 Issue Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -1393,7 +1393,7 @@ jobs:
1393
1393
  if: |
1394
1394
  (github.event_name == 'pull_request' && github.event.action == 'closed') ||
1395
1395
  (github.event_name == 'issues' && github.event.action == 'closed') ||
1396
- (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@suh-lab') && contains(github.event.comment.body, 'server destroy'))
1396
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server destroy'))
1397
1397
  runs-on: ubuntu-latest
1398
1398
  steps:
1399
1399
  - name: PR/Issue 번호 가져오기
@@ -1474,7 +1474,7 @@ jobs:
1474
1474
  `| **컨테이너** | \`${projectName}-pr-${number}\` |`,
1475
1475
  '| **상태** | 삭제됨 |',
1476
1476
  '',
1477
- '다시 배포하려면: `@suh-lab server build`',
1477
+ '다시 배포하려면: `/wizard server build`',
1478
1478
  '',
1479
1479
  '---',
1480
1480
  '*🤖 이 댓글은 PR/Issue Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -1573,8 +1573,8 @@ jobs:
1573
1573
  '### 📋 명령어',
1574
1574
  '| 명령어 | 설명 |',
1575
1575
  '|--------|------|',
1576
- '| `@suh-lab server build` | 최신 커밋으로 재배포 |',
1577
- '| `@suh-lab server destroy` | Preview 환경 삭제 |',
1576
+ '| `/wizard server build` | 최신 커밋으로 재배포 |',
1577
+ '| `/wizard server destroy` | Preview 환경 삭제 |',
1578
1578
  '',
1579
1579
  '---',
1580
1580
  `*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
@@ -1588,7 +1588,7 @@ jobs:
1588
1588
  `| **컨테이너** | \`${projectName}-pr-${number}\` |`,
1589
1589
  '| **상태** | 🔴 Not Found |',
1590
1590
  '',
1591
- '배포하려면: `@suh-lab server build`',
1591
+ '배포하려면: `/wizard server build`',
1592
1592
  '',
1593
1593
  '---',
1594
1594
  `*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
@@ -1605,7 +1605,7 @@ jobs:
1605
1605
  # ============================================================
1606
1606
  # Custom Branch Preview Jobs
1607
1607
  # 특정 브랜치를 지정하여 빌드/삭제/상태 확인하는 기능
1608
- # 명령어: @suh-lab server build <브랜치명>
1608
+ # 명령어: /wizard server build <브랜치명>
1609
1609
  # ============================================================
1610
1610
 
1611
1611
  build-preview-custom-branch:
@@ -1984,9 +1984,9 @@ jobs:
1984
1984
  '### 📋 명령어',
1985
1985
  '| 명령어 | 설명 |',
1986
1986
  '|--------|------|',
1987
- `| \`@suh-lab server build ${branchName}\` | 최신 커밋으로 재배포 |`,
1988
- `| \`@suh-lab server destroy ${branchName}\` | Preview 환경 삭제 |`,
1989
- `| \`@suh-lab server status ${branchName}\` | 현재 상태 확인 |`,
1987
+ `| \`/wizard server build ${branchName}\` | 최신 커밋으로 재배포 |`,
1988
+ `| \`/wizard server destroy ${branchName}\` | Preview 환경 삭제 |`,
1989
+ `| \`/wizard server status ${branchName}\` | 현재 상태 확인 |`,
1990
1990
  '',
1991
1991
  '---',
1992
1992
  '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -2154,7 +2154,7 @@ jobs:
2154
2154
  `| **브랜치** | \`${branchName}\` |`,
2155
2155
  `| **컨테이너** | \`${containerName}\` |`,
2156
2156
  '',
2157
- `다시 배포하려면: \`@suh-lab server build ${branchName}\``,
2157
+ `다시 배포하려면: \`/wizard server build ${branchName}\``,
2158
2158
  '',
2159
2159
  '---',
2160
2160
  '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -2247,8 +2247,8 @@ jobs:
2247
2247
  '### 📋 명령어',
2248
2248
  '| 명령어 | 설명 |',
2249
2249
  '|--------|------|',
2250
- `| \`@suh-lab server build ${branchName}\` | 최신 커밋으로 재배포 |`,
2251
- `| \`@suh-lab server destroy ${branchName}\` | Preview 환경 삭제 |`,
2250
+ `| \`/wizard server build ${branchName}\` | 최신 커밋으로 재배포 |`,
2251
+ `| \`/wizard server destroy ${branchName}\` | Preview 환경 삭제 |`,
2252
2252
  '',
2253
2253
  '---',
2254
2254
  '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
@@ -2263,7 +2263,7 @@ jobs:
2263
2263
  `| **컨테이너** | \`${containerName}\` |`,
2264
2264
  '| **상태** | 🔴 Not Found |',
2265
2265
  '',
2266
- `배포하려면: \`@suh-lab server build ${branchName}\``,
2266
+ `배포하려면: \`/wizard server build ${branchName}\``,
2267
2267
  '',
2268
2268
  '---',
2269
2269
  '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
package/src/cli/args.js CHANGED
@@ -1,6 +1,9 @@
1
1
  // CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
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 {
5
+ ENV_MODES, DEPLOY_MODES, STORE_PLATFORMS, NO_STORE, isEnvMode, isDeployMode, parseStoreList,
6
+ } from "../core/flutter-options.js";
4
7
 
5
8
  // argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
6
9
  export function parseArgs(argv) {
@@ -16,6 +19,10 @@ export function parseArgs(argv) {
16
19
  mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
17
20
  developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
18
21
  deployStyle: "", // 서버 배포 방식 (--deploy-style). 빈값=version.yml 저장값 → simple
22
+ flutterEnvMode: "", // Flutter 환경변수 방식 (--flutter-env-mode). 빈값=저장값 → 신규 dart-define/기존 dotenv
23
+ flutterStore: null, // Flutter 스토어 배포 대상 string[] (--flutter-store). null=미지정, none → []
24
+ androidDeployMode: "", // Play Store 배포 모드 (--android-deploy-mode). 빈값=저장값 → store_only
25
+ iosDeployMode: "", // iOS 배포 모드 (--ios-deploy-mode). 빈값=저장값 → store_only
19
26
  force: false,
20
27
  help: false,
21
28
  showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
@@ -84,6 +91,36 @@ export function parseArgs(argv) {
84
91
  }
85
92
  result.deployStyle = v; break;
86
93
  }
94
+ case "--flutter-env-mode": {
95
+ const v = args.shift();
96
+ if (!isEnvMode(v)) {
97
+ throw new CliError(`--flutter-env-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${ENV_MODES.join(" | ")})`);
98
+ }
99
+ result.flutterEnvMode = v; break;
100
+ }
101
+ case "--flutter-store": {
102
+ const v = args.shift();
103
+ // 빈 문자열은 parseStoreList가 []로 보지만, 스토어를 안 고르겠다는 뜻은 명시적인 none으로만 받는다.
104
+ const stores = v ? parseStoreList(v) : null;
105
+ if (stores === null) {
106
+ throw new CliError(`--flutter-store 값이 올바르지 않습니다: ${v || "(없음)"} (${[STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ")})`);
107
+ }
108
+ result.flutterStore = stores; break;
109
+ }
110
+ case "--android-deploy-mode": {
111
+ const v = args.shift();
112
+ if (!isDeployMode(v)) {
113
+ throw new CliError(`--android-deploy-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_MODES.join(" | ")})`);
114
+ }
115
+ result.androidDeployMode = v; break;
116
+ }
117
+ case "--ios-deploy-mode": {
118
+ const v = args.shift();
119
+ if (!isDeployMode(v)) {
120
+ throw new CliError(`--ios-deploy-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_MODES.join(" | ")})`);
121
+ }
122
+ result.iosDeployMode = v; break;
123
+ }
87
124
  case "--nexus":
88
125
  if (seenFlags.has("--no-nexus")) throw new CliError("--nexus와 --no-nexus는 동시에 지정할 수 없습니다");
89
126
  seenFlags.add("--nexus"); result.includeNexus = true; break;
package/src/cli/help.js CHANGED
@@ -17,6 +17,10 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
17
17
  --main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
18
18
  --develop-branch B 개발 브랜치 (기본: develop). 릴리스 브랜치와 같으면 trunk-based 모드
19
19
  --deploy-style STYLE 서버 배포 방식: simple | nginx | traefik | none (기본: simple)
20
+ --flutter-env-mode MODE Flutter 환경변수 방식: dart-define | dotenv (기본: 신규 설치 dart-define, 기존 설치는 저장값·dotenv 유지)
21
+ --flutter-store CSV Flutter 스토어 배포 대상: android,ios | android | ios | none (기본: 둘 다 설치)
22
+ --android-deploy-mode MODE Play Store 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
23
+ --ios-deploy-mode MODE iOS 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
20
24
  --nexus / --no-nexus 라이브러리 publish 워크플로우 포함/제외 (Nexus + GitHub Packages)
21
25
  --secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
22
26
  --semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
@@ -11,9 +11,13 @@
11
11
  // ④ GitHub 설정 화면에 실제로 표시되는 문자열("Read and write permissions" 등)은 번역하지
12
12
  // 않는다. 번역하면 설명은 읽히지만 정작 화면에서 그 항목을 찾지 못한다.
13
13
  import { spawnSync } from "node:child_process";
14
- import { existsSync } from "node:fs";
15
- import { join } from "node:path";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join, posix } from "node:path";
16
16
  import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
17
+ import { PATHS } from "../core/paths.js";
18
+ import { parseExisting } from "../core/version-yml.js";
19
+ import { STORE_PLATFORMS, STORE_APP_FILES, parseStoreList, storeAppFilesFor } from "../core/flutter-options.js";
20
+ import { inferInstalledStores } from "../core/installed-stores.js";
17
21
 
18
22
  const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
19
23
 
@@ -23,6 +27,7 @@ const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
23
27
  const REPO_URL = "https://github.com/Twin-Fang/project-auto-wizard";
24
28
  export const DOC = {
25
29
  postInstall: `${REPO_URL}#post-install`,
30
+ flutterStore: `${REPO_URL}#flutter-store`,
26
31
  };
27
32
 
28
33
  export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
@@ -39,6 +44,9 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
39
44
  note: ["이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다."],
40
45
  });
41
46
 
47
+ // Flutter 스토어 배포 파일 점검 (이슈 #131) — 로컬 파일만 보므로 gh 조회 결과와 무관하게 수행한다.
48
+ if (installed) for (const item of flutterStoreChecks(cwd)) add(item);
49
+
42
50
  const ghVersion = exec("gh", ["--version"]);
43
51
  if (ghVersion.error || ghVersion.status !== 0) {
44
52
  add({
@@ -163,6 +171,51 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
163
171
  return results;
164
172
  }
165
173
 
174
+ const PLATFORM_ROW_NAME = { android: "Flutter Android 배포 파일", ios: "Flutter iOS 배포 파일" };
175
+ const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // 감지 규칙은 ExportOptions.plist 템플릿·IOS-TESTFLIGHT 검증과 동일
176
+ const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
177
+
178
+ // Flutter 스토어 배포 진단 — 선택한 플랫폼의 필수 파일과 ExportOptions.plist 플레이스홀더 (이슈 #131).
179
+ // 스토어 시크릿 등록 여부는 이번 범위 밖이다. 반환: doctor 결과 행 배열.
180
+ function flutterStoreChecks(cwd) {
181
+ const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
182
+ if (!existing.types.includes("flutter")) return [];
183
+ const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
184
+ // 저장값 없는 기존 설치는 설치된 스토어 워크플로우로 추론한다 (interactive와 같은 규칙).
185
+ const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
186
+ const flutterRoot = existing.paths.get("flutter") || ".";
187
+ const rows = [];
188
+
189
+ for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
190
+ const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
191
+ const missing = files.filter((file) => !existsSync(join(cwd, file)));
192
+ const head = { name: PLATFORM_ROW_NAME[platform], purpose: "fastlane 스토어 업로드에 필요한 파일" };
193
+ rows.push(missing.length
194
+ ? {
195
+ ...head, status: "WARN", value: `없는 파일: ${missing.join(", ")}`,
196
+ impact: ["이 파일이 없으면 스토어 배포 워크플로우가 fastlane 단계에서 실패합니다."],
197
+ actions: ["마법사를 다시 실행하면 없는 파일만 새로 만들어 줍니다 (이미 있는 파일은 덮어쓰지 않음)"],
198
+ doc: DOC.flutterStore,
199
+ }
200
+ : { ...head, status: "OK", value: `${files.length}개 있음` });
201
+ }
202
+
203
+ const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
204
+ if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
205
+ const head = { name: "ExportOptions.plist", purpose: "iOS 서명·내보내기 설정" };
206
+ const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
207
+ rows.push(left.length
208
+ ? {
209
+ ...head, status: "WARN", value: `채워지지 않은 값: ${left.join(", ")}`,
210
+ impact: ["채우지 않으면 IOS-TESTFLIGHT 워크플로우가 ExportOptions.plist 검증 단계에서 중단됩니다."],
211
+ actions: [`${plistPath} 의 플레이스홀더를 실제 값(Team ID · 번들 ID · 프로비저닝 프로파일 이름)으로 바꾸세요`],
212
+ doc: DOC.flutterStore,
213
+ }
214
+ : { ...head, status: "OK", value: "플레이스홀더 없음" });
215
+ }
216
+ return rows;
217
+ }
218
+
166
219
  const asLines = (v) => (Array.isArray(v) ? v : v ? [String(v)] : []);
167
220
  const headOf = (r) => `${r.label || r.name}${r.purpose ? ` — ${r.purpose}` : ""}`;
168
221
 
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { PATHS } from "../core/paths.js";
6
6
  import { planWorkflows } from "../core/copy/workflows.js";
7
+ import { planFlutterAppFiles } from "../core/copy/flutter-app.js";
7
8
  import { planUninstall } from "./uninstall.js";
8
9
  import { renderVersionYml, parseExisting } from "../core/version-yml.js";
9
10
  import { readVersionYmlTemplate } from "../core/assets.js";
@@ -33,6 +34,8 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
33
34
  return {
34
35
  mode,
35
36
  workflows: planWorkflows(context, payloadRoot, targetRoot),
37
+ // Flutter 스토어 배포 파일(Fastfile·ExportOptions.plist) — 이미 있는 파일은 덮어쓰지 않고 유지한다.
38
+ flutterApp: planFlutterAppFiles(context, payloadRoot, targetRoot),
36
39
  versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
37
40
  };
38
41
  }
@@ -57,6 +60,13 @@ export function printDryRun(plan) {
57
60
  for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
58
61
  lines.push(`동일한 파일 (${w.unchanged.length}개, 변경 없음)`);
59
62
  }
63
+ if (plan.flutterApp && (plan.flutterApp.created.length || plan.flutterApp.kept.length)) {
64
+ const { created, kept } = plan.flutterApp;
65
+ lines.push(`Flutter 스토어 배포 파일 — 신규 (${created.length}개):`);
66
+ for (const f of created) lines.push(` + ${f}`);
67
+ lines.push(`Flutter 스토어 배포 파일 — 기존 파일 유지 (${kept.length}개, 덮어쓰지 않음):`);
68
+ for (const f of kept) lines.push(` = ${f} (기존 파일 유지)`);
69
+ }
60
70
  if (plan.versionYml) {
61
71
  lines.push(plan.versionYml.existed
62
72
  ? (plan.versionYml.changed ? "version.yml: 갱신될 예정" : "version.yml: 변경 없음")
@@ -1,5 +1,5 @@
1
1
  // full 모드 오케스트레이터 (.sh execute_integration full case 등가).
2
- // 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → gitignore(조건부)
2
+ // 복사 순서: workflows(+env 치환) → flutter 앱 파일 → version.yml → readme → scripts → gitignore(조건부)
3
3
  // gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
4
4
  // (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
5
5
  import { join } from "node:path";
@@ -12,10 +12,12 @@ import { existingMarkerInDir } from "../core/paths-resolve.js";
12
12
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
13
13
  import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
14
14
  import { copyScripts } from "../core/copy/simple.js";
15
+ import { copyFlutterAppFiles } from "../core/copy/flutter-app.js";
15
16
  import { ensureGitignore } from "../core/copy/gitignore.js";
16
17
  import { readBaseline, writeBaseline } from "../core/baseline.js";
17
18
  import { scanUnsubstituted, collectRequiredSecrets, narrowSecretsBySshAuth } from "../core/verify.js";
18
19
  import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
20
+ import { cleanupDeselectedStoreWorkflows } from "../core/flutter-options.js";
19
21
  import { log, maskValue } from "../core/logger.js";
20
22
 
21
23
  // context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
@@ -47,6 +49,12 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
47
49
  const wfCounters = copyWorkflows(context, payloadRoot, targetRoot, hooks);
48
50
  const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
49
51
 
52
+ // 1-1. Flutter 앱 파일(fastlane·ExportOptions) — 사용자가 값을 채워 쓰는 파일이라 없을 때만 만든다.
53
+ // 워크플로우 복사가 끝난 뒤에 실행한다(워크플로우가 이 파일들을 전제로 돈다).
54
+ const flutterApp = copyFlutterAppFiles(context, payloadRoot, targetRoot);
55
+ for (const f of flutterApp.created) log.info("flutter-app", "create", f);
56
+ for (const f of flutterApp.kept) log.info("flutter-app", "keep", `${f} (기존 파일 유지)`);
57
+
50
58
  // 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다 (issue #20 M8).
51
59
  const vyPath = join(targetRoot, PATHS.versionFile);
52
60
  const extraTopLevel = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")).extraTopLevel : [];
@@ -80,7 +88,24 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
80
88
 
81
89
  for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (이전 배포 방식 정리)`);
82
90
  for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
83
- const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0;
91
+
92
+ // 6-1. 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 남아
93
+ // main push마다 계속 도는 것을 막는다. 규칙은 6과 같다(미수정 삭제, 수정본 .bak).
94
+ // 선택이 미결정(null)이거나 Flutter가 없으면 현행 동작 그대로 아무것도 지우지 않는다.
95
+ // Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
96
+ const workflowsDir = join(targetRoot, PATHS.workflowsDir);
97
+ const storeCleanup = Array.isArray(context.flutterStore) && types.includes("flutter")
98
+ ? cleanupDeselectedStoreWorkflows(
99
+ workflowsDir,
100
+ existsSync(workflowsDir) ? readdirSync(workflowsDir) : [],
101
+ context.flutterStore,
102
+ previousBaseline)
103
+ : { removed: [], backedUp: [] };
104
+ for (const f of [...storeCleanup.removed, ...storeCleanup.backedUp]) delete previousBaseline?.files?.[f];
105
+ for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (선택 해제된 스토어 워크플로우 정리)`);
106
+ for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
107
+
108
+ const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0 || storeCleanup.backedUp.length > 0;
84
109
  if (gitignoreUpdated) ensureGitignore(targetRoot);
85
110
 
86
111
  // 7. baseline 기록 (issue #69) — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
@@ -122,7 +147,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
122
147
  ["결과", unresolved.length ? `주의 (미치환 ${unresolved.length}건)` : "OK"],
123
148
  ]);
124
149
 
125
- return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup };
150
+ return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, flutterApp };
126
151
  }
127
152
 
128
153
  // deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.