project-auto-wizard 0.2.0 → 0.3.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 (43) hide show
  1. package/README.md +15 -4
  2. package/package.json +1 -1
  3. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  4. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  5. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  6. package/payload/scripts/issue_helper.py +334 -0
  7. package/payload/scripts/truncate_release_notes.py +89 -0
  8. package/payload/version.yml.template +1 -0
  9. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +47 -0
  10. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +4 -4
  11. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +1 -1
  12. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +1 -1
  13. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +1 -1
  14. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +6 -6
  15. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +1 -1
  16. package/payload/workflows/spring/{PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml → nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml} +8 -2
  17. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +8 -6
  18. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +2 -2
  19. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +8 -8
  20. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +2 -2
  21. package/src/cli/args.js +9 -0
  22. package/src/cli/help.js +2 -1
  23. package/src/commands/dry-run.js +7 -16
  24. package/src/commands/full.js +75 -16
  25. package/src/commands/interactive.js +47 -6
  26. package/src/commands/uninstall.js +3 -1
  27. package/src/core/copy/simple.js +2 -2
  28. package/src/core/copy/workflows.js +32 -29
  29. package/src/core/deploy-style.js +90 -0
  30. package/src/core/detect-fs.js +36 -7
  31. package/src/core/detect.js +68 -3
  32. package/src/core/install-log.js +182 -0
  33. package/src/core/options-ask.js +5 -2
  34. package/src/core/paths-resolve.js +4 -8
  35. package/src/core/removal-plan.js +1 -1
  36. package/src/core/verify.js +86 -0
  37. package/src/core/version-yml.js +31 -4
  38. package/src/core/wizard-env.js +6 -3
  39. package/src/index.js +15 -2
  40. package/src/ui/env-plan.js +63 -33
  41. package/src/ui/prompts.js +41 -2
  42. package/src/ui/status-cards.js +7 -3
  43. package/src/ui/summary.js +56 -6
@@ -86,7 +86,7 @@ env:
86
86
 
87
87
  # ☁️ Spring Boot 설정
88
88
  SPRING_PROFILE: "prod"
89
- JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
89
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
90
90
  GRADLE_OPTS: "-Dspring.profiles.active=prod"
91
91
 
92
92
  # 📁 경로 설정
@@ -94,13 +94,15 @@ env:
94
94
  DOCKERFILE_PATH: "./Dockerfile"
95
95
 
96
96
  # 🌐 포트 및 컨테이너 설정
97
+ # BLUE/GREEN은 서버에서 번갈아 뜨는 두 컨테이너의 호스트 포트다 — 다른 서비스와 겹치면 배포가 실패한다.
97
98
  CONTAINER_INTERNAL_PORT: "8080"
98
- BLUE_PORT: "8080"
99
- GREEN_PORT: "8081"
99
+ BLUE_PORT: "__BLUE_PORT__" # @wizard ask:8080
100
+ GREEN_PORT: "__GREEN_PORT__" # @wizard ask:8081
100
101
 
101
102
  # 🔧 Nginx 설정
102
- NGINX_RP_CONF: "/etc/nginx/sites-enabled/example.conf"
103
- NGINX_BACKUP_DIR: "/volume1/project/nginx/backups"
103
+ # NGINX_RP_CONF 경로가 틀리면 무중단 전환(proxy_pass 포트 토글) 자체가 동작하지 않는다.
104
+ NGINX_RP_CONF: "__NGINX_RP_CONF__" # @wizard ask:/etc/nginx/sites-enabled/__PROJECT_NAME__.conf
105
+ NGINX_BACKUP_DIR: "__NGINX_BACKUP_DIR__" # @wizard ask:/volume1/projects/nginx/backups
104
106
  NGINX_BACKUP_KEEP: "10"
105
107
 
106
108
  # 📦 볼륨 마운트 설정
@@ -114,7 +116,7 @@ env:
114
116
  SSH_AUTH_METHOD: "password" # @wizard ask:password
115
117
 
116
118
  # 🔌 SSH 연결 설정
117
- SSH_PORT: "2022"
119
+ SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
118
120
  SSH_COMMAND_TIMEOUT: "600s"
119
121
  SSH_CONNECTION_TIMEOUT: "60s"
120
122
 
@@ -82,7 +82,7 @@ env:
82
82
 
83
83
  # ☁️ Spring Boot 설정
84
84
  SPRING_PROFILE: "prod"
85
- JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
85
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
86
86
  GRADLE_OPTS: "-Dspring.profiles.active=prod"
87
87
 
88
88
  # 📁 경로 설정
@@ -109,7 +109,7 @@ env:
109
109
  SSH_AUTH_METHOD: "password" # @wizard ask:password
110
110
 
111
111
  # 🔌 SSH 연결 설정
112
- SSH_PORT: "2022"
112
+ SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
113
113
  SSH_COMMAND_TIMEOUT: "600s"
114
114
  SSH_CONNECTION_TIMEOUT: "60s"
115
115
 
@@ -22,7 +22,7 @@
22
22
  # 📊 리소스 네이밍 규칙:
23
23
  # - 컨테이너: {PROJECT_NAME}-pr-{PR번호}
24
24
  # - 이미지: {DOCKERHUB_USERNAME}/{PROJECT_NAME}:pr-{PR번호}
25
- # - 도메인: {PROJECT_NAME}-pr-{PR번호}.pr.suhsaechan.kr
25
+ # - 도메인: {PROJECT_NAME}-pr-{PR번호}.pr.example.com
26
26
  # - Preview URL: http://{도메인}:8079
27
27
  #
28
28
  # ===================================================================
@@ -37,9 +37,9 @@ env:
37
37
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
38
38
 
39
39
  # Spring Boot 빌드 설정 (프로젝트별 수정 필요)
40
- JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
40
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
41
41
  GRADLE_BUILD_CMD: './gradlew clean build -x test -Dspring.profiles.active=prod'
42
- JAR_PATH: 'Suh-Web/build/libs/*.jar' # 멀티모듈이면 빌드 산출물 경로를 직접 수정하세요
42
+ JAR_PATH: "__JAR_PATH__" # @wizard ask:build/libs/*.jar
43
43
  APPLICATION_YML_PATH: "__APPLICATION_YML_PATH__" # @wizard auto:spring-app-yml-path
44
44
 
45
45
  # Docker 설정 (프로젝트별 수정 필요)
@@ -52,7 +52,7 @@ env:
52
52
 
53
53
  # Traefik & Preview 도메인 설정 (환경 구축 후 수정 금지)
54
54
  TRAEFIK_NETWORK: traefik-network
55
- PREVIEW_DOMAIN_SUFFIX: pr.suhsaechan.kr
55
+ PREVIEW_DOMAIN_SUFFIX: "__PREVIEW_DOMAIN_SUFFIX__" # @wizard ask:pr.example.com
56
56
  PREVIEW_PORT: '8079'
57
57
 
58
58
  # 🔐 SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
@@ -61,7 +61,7 @@ env:
61
61
  SSH_AUTH_METHOD: "password" # @wizard ask:password
62
62
 
63
63
  # SSH 포트 (서버 포트 환경 구축 후 수정 금지)
64
- SSH_PORT: '2022'
64
+ SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
65
65
 
66
66
  # Issue Helper 댓글 마커 (프로젝트별 수정 필요)
67
67
  # Issue 댓글에서 브랜치명을 추출할 때 사용
@@ -1814,8 +1814,8 @@ jobs:
1814
1814
  if: steps.check_branch.outputs.exists == 'true'
1815
1815
  uses: docker/login-action@v3
1816
1816
  with:
1817
- username: ${{ secrets.DOCKER_USERNAME }}
1818
- password: ${{ secrets.DOCKER_PASSWORD }}
1817
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
1818
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
1819
1819
 
1820
1820
  - name: Docker 이미지 빌드 & 푸시
1821
1821
  if: steps.check_branch.outputs.exists == 'true'
@@ -1823,7 +1823,7 @@ jobs:
1823
1823
  run: |
1824
1824
  DOCKER_START=$(date +%s)
1825
1825
  CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
1826
- IMAGE_TAG="${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:${CONTAINER_SUFFIX}"
1826
+ IMAGE_TAG="${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${CONTAINER_SUFFIX}"
1827
1827
  docker build -t $IMAGE_TAG .
1828
1828
  docker push $IMAGE_TAG
1829
1829
  DOCKER_END=$(date +%s)
@@ -79,7 +79,7 @@ env:
79
79
 
80
80
  # ☁️ Spring Boot 설정
81
81
  SPRING_PROFILE: "prod"
82
- JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
82
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
83
83
  GRADLE_OPTS: "-Dspring.profiles.active=prod"
84
84
 
85
85
  # 📁 경로 설정
@@ -108,7 +108,7 @@ env:
108
108
  SSH_AUTH_METHOD: "password" # @wizard ask:password
109
109
 
110
110
  # 🔌 SSH 연결 설정
111
- SSH_PORT: "2022" # SSH 포트 (Synology 기본: 2022)
111
+ SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
112
112
  SSH_COMMAND_TIMEOUT: "600s" # SSH 명령 실행 타임아웃
113
113
  SSH_CONNECTION_TIMEOUT: "60s" # SSH 연결 타임아웃
114
114
 
package/src/cli/args.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
2
2
  import { VALID_TYPES, VALID_MODES } from "../context.js";
3
+ import { DEPLOY_STYLES, isDeployStyle } from "../core/deploy-style.js";
3
4
 
4
5
  // argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
5
6
  export function parseArgs(argv) {
@@ -14,6 +15,7 @@ export function parseArgs(argv) {
14
15
  pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
15
16
  mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
16
17
  developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
18
+ deployStyle: "", // 서버 배포 방식 (--deploy-style). 빈값=version.yml 저장값 → simple
17
19
  force: false,
18
20
  help: false,
19
21
  showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
@@ -75,6 +77,13 @@ export function parseArgs(argv) {
75
77
  case "--keep-changelog": result.keepChangelog = true; break;
76
78
  case "--keep-workflows": result.keepWorkflows = true; break;
77
79
  case "--keep-scripts": result.keepScripts = true; break;
80
+ case "--deploy-style": {
81
+ const v = args.shift();
82
+ if (!isDeployStyle(v)) {
83
+ throw new CliError(`--deploy-style 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_STYLES.map((s) => s.value).join(" | ")})`);
84
+ }
85
+ result.deployStyle = v; break;
86
+ }
78
87
  case "--nexus":
79
88
  if (seenFlags.has("--no-nexus")) throw new CliError("--nexus와 --no-nexus는 동시에 지정할 수 없습니다");
80
89
  seenFlags.add("--nexus"); result.includeNexus = true; break;
package/src/cli/help.js CHANGED
@@ -16,7 +16,8 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
16
16
  --paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
17
17
  --main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
18
18
  --develop-branch B 개발 브랜치 (기본: develop). 릴리스 브랜치와 같으면 trunk-based 모드
19
- --nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
19
+ --deploy-style STYLE 서버 배포 방식: simple | nginx | traefik (기본: simple)
20
+ --nexus / --no-nexus 라이브러리 publish 워크플로우 포함/제외 (Nexus + GitHub Packages)
20
21
  --secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
21
22
  --semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
22
23
  --force full 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
@@ -5,31 +5,22 @@ import { join } from "node:path";
5
5
  import { PATHS } from "../core/paths.js";
6
6
  import { planWorkflows } from "../core/copy/workflows.js";
7
7
  import { planUninstall } from "./uninstall.js";
8
- import { buildVersionYml, parseExisting } from "../core/version-yml.js";
8
+ import { renderVersionYml, parseExisting } from "../core/version-yml.js";
9
9
  import { readVersionYmlTemplate } from "../core/assets.js";
10
- import { markerForType } from "../core/detect.js";
10
+ import { existingMarkerInDir } from "../core/paths-resolve.js";
11
11
 
12
12
  function versionYmlPreview(context, payloadRoot, targetRoot) {
13
- const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
14
- now, today, templateVersion = "unknown",
15
- includeNexus = false, includeSecretBackup = false,
16
- includeSemverAuto } = context;
13
+ const { paths = new Map() } = context;
17
14
  const pathMarkers = new Map();
18
- for (const [t] of paths) pathMarkers.set(t, markerForType(t));
15
+ for (const [t, p] of paths) pathMarkers.set(t, existingMarkerInDir(t, join(targetRoot, p || ".")));
19
16
 
20
17
  const vyPath = join(targetRoot, PATHS.versionFile);
21
18
  const existingRaw = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
22
19
  const extraTopLevel = existingRaw !== null ? parseExisting(existingRaw).extraTopLevel : [];
23
20
 
24
- const wouldBe = buildVersionYml({
25
- templateText: readVersionYmlTemplate(payloadRoot),
26
- version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
27
- extraTopLevel,
28
- templateOptions: {
29
- templateVersion, includeNexus, includeSecretBackup,
30
- includeSemverAuto: includeSemverAuto !== false,
31
- optionsDate: today,
32
- },
21
+ // 실제 설치와 같은 렌더 함수를 쓴다 — 각자 조립하면 옵션이 늘 때마다 미리보기가 어긋난다.
22
+ const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
23
+ pathMarkers, extraTopLevel,
33
24
  });
34
25
  return { existed: existingRaw !== null, changed: existingRaw !== wouldBe };
35
26
  }
@@ -3,17 +3,20 @@
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";
6
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { writeText } from "../core/fsutil.js";
8
8
  import { PATHS } from "../core/paths.js";
9
- import { buildVersionYml, parseExisting } from "../core/version-yml.js";
9
+ import { renderVersionYml, parseExisting } from "../core/version-yml.js";
10
10
  import { readVersionYmlTemplate } from "../core/assets.js";
11
- import { markerForType } from "../core/detect.js";
11
+ 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
15
  import { ensureGitignore } from "../core/copy/gitignore.js";
16
16
  import { readBaseline, writeBaseline } from "../core/baseline.js";
17
+ import { scanUnsubstituted, collectRequiredSecrets, narrowSecretsBySshAuth } from "../core/verify.js";
18
+ import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
19
+ import { writeInstallLog } from "../core/install-log.js";
17
20
 
18
21
  // context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
19
22
  // force, repoName, resolvers, now, today }
@@ -24,9 +27,11 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
24
27
  includeNexus = false, includeSecretBackup = false,
25
28
  includeSemverAuto } = context;
26
29
 
27
- // project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
30
+ // project_paths 마커 계산 (.sh existing_marker_in_dir 등가).
31
+ // 대표 마커명이 아니라 그 폴더에 실제로 있는 파일을 쓴다 — build.gradle.kts만 있는 레포의
32
+ // version.yml에 "# build.gradle"이라고 적히면 감지 로그와 같은 종류의 거짓말이 된다 (이슈 #77).
28
33
  const pathMarkers = new Map();
29
- for (const [t] of paths) pathMarkers.set(t, markerForType(t));
34
+ for (const [t, p] of paths) pathMarkers.set(t, existingMarkerInDir(t, join(targetRoot, p || ".")));
30
35
 
31
36
  // 1. 워크플로우 복사 (+ env 치환) — deploy 블록에 쓸 ask 값을 수집한다.
32
37
  // hooks.decisions: 대화형 충돌 3지선 결정 Map (미지정=skip — 현행 force 동작)
@@ -39,12 +44,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
39
44
 
40
45
  // 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성 전략 D4)
41
46
  writeText(join(targetRoot, PATHS.versionFile),
42
- buildVersionYml({
43
- templateText: readVersionYmlTemplate(payloadRoot),
44
- version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
45
- deployValues, extraTopLevel,
46
- templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
47
- }));
47
+ renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel }));
48
48
 
49
49
  // 3. README 버전 섹션
50
50
  addVersionSectionToReadme(version, targetRoot);
@@ -54,10 +54,23 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
54
54
 
55
55
  // 5. gitignore — 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
56
56
  // 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다 — issue #7.
57
- const gitignoreUpdated = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
57
+ const gitignoreUpdated0 = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
58
+
59
+ // 6. 이전 배포 방식 정리 (이슈 #80) — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
60
+ // 옛 baseline이 살아 있는 지금이 "사용자가 손댔는가"를 판정할 수 있는 유일한 시점이다.
61
+ const previousBaseline = readBaseline(targetRoot);
62
+ const cleanup = cleanupOtherDeployWorkflows(
63
+ join(targetRoot, PATHS.workflowsDir),
64
+ existsSync(join(targetRoot, PATHS.workflowsDir)) ? readdirSync(join(targetRoot, PATHS.workflowsDir)) : [],
65
+ context.deployStyle || DEFAULT_DEPLOY_STYLE,
66
+ previousBaseline);
67
+ // 지운 파일의 기준점은 baseline에서도 빼야 다음 실행에서 "사용자가 지웠다"로 오인하지 않는다.
68
+ for (const f of [...cleanup.removed, ...cleanup.backedUp]) delete previousBaseline?.files?.[f];
69
+
70
+ const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0;
58
71
  if (gitignoreUpdated) ensureGitignore(targetRoot);
59
72
 
60
- // 6. baseline 기록 (issue #69) — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
73
+ // 7. baseline 기록 (issue #69) — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
61
74
  // env 치환까지 전부 끝난 뒤에 해시해야 디스크 내용이 최종형이다. 그래서 copyWorkflows 안이
62
75
  // 아니라 여기서 기록한다.
63
76
  // 기존 baseline은 병합 대상 — 이번에 건드리지 않은 파일의 기준점을 잃지 않는다.
@@ -67,9 +80,55 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
67
80
  entries: computeBaselineEntries(
68
81
  wfCounters.baselineTargets || new Map(),
69
82
  join(targetRoot, PATHS.workflowsDir),
70
- makeSrcText(context.branches || null)),
71
- previous: readBaseline(targetRoot),
83
+ makeSrcText(context.branches || null, context.deployStyle || "")),
84
+ previous: previousBaseline,
85
+ });
86
+
87
+ // 8. 설치 후 검증 (이슈 #81, #80) — 디스크에 실제로 쓰인 내용을 다시 읽어 확인한다.
88
+ // 미치환 플레이스홀더가 남았는지, 어떤 Secret이 있어야 워크플로우가 도는지.
89
+ // 설치를 실패시키지는 않는다 — 사실을 알려주는 것이 목적이고, 판단은 사용자 몫이다.
90
+ const wfDir = join(targetRoot, PATHS.workflowsDir);
91
+ const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
92
+ const unresolved = scanUnsubstituted(wfDir, managed);
93
+ const secrets = narrowSecretsBySshAuth(
94
+ collectRequiredSecrets(wfDir, managed),
95
+ firstDeployValue(deployValues, "SSH_AUTH_METHOD"),
96
+ );
97
+
98
+ // 9. 설치 로그 (이슈 #79) — 이 실행에서 무엇을 어떤 값으로 설치했는지 레포에 남긴다.
99
+ // 실패해도 설치는 성공으로 끝난다.
100
+ const installLog = writeInstallLog(targetRoot, {
101
+ action: context.previousTemplateVersion ? "update" : "install",
102
+ at: now || today || "",
103
+ templateVersion,
104
+ previousTemplateVersion: context.previousTemplateVersion || "",
105
+ mode: context.mode || "full",
106
+ types, markers: context.markers || new Map(), version,
107
+ versionSource: context.versionSource || "",
108
+ branch, branches: context.branches, paths,
109
+ options: {
110
+ nexus: includeNexus,
111
+ secretBackup: includeSecretBackup,
112
+ semverAuto: includeSemverAuto !== false,
113
+ deployStyle: context.deployStyle || "",
114
+ },
115
+ answers: context.envAnswers || [],
116
+ warnings: context.detectWarnings || [],
117
+ result: {
118
+ copiedFiles: wfCounters.copiedFiles || [],
119
+ gitignoreUpdated,
120
+ },
121
+ unresolved, secrets, cleanup,
72
122
  });
73
123
 
74
- return { workflows: wfCounters, gitignoreUpdated };
124
+ return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, installLog, cleanup };
125
+ }
126
+
127
+ // deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
128
+ function firstDeployValue(deployValues, key) {
129
+ for (const [, asks] of deployValues) {
130
+ const v = asks.get(key);
131
+ if (v) return v;
132
+ }
133
+ return "";
75
134
  }
@@ -5,7 +5,7 @@
5
5
  import { join } from "node:path";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
8
- import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber } from "../core/detect-fs.js";
8
+ import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "../core/detect-fs.js";
9
9
  import { parseExisting } from "../core/version-yml.js";
10
10
  import { runBreakingCheck } from "../core/breaking-check.js";
11
11
  import { resolveProjectPaths } from "../core/paths-resolve.js";
@@ -14,6 +14,7 @@ import { askAllOptionalWorkflows } from "../core/options-ask.js";
14
14
  import { promptEnvPlan } from "../ui/env-plan.js";
15
15
  import { surveyWorkflows } from "../core/copy/workflows.js";
16
16
  import { createContext, VALID_TYPES } from "../context.js";
17
+ import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
17
18
  import { runFull } from "./full.js";
18
19
  import { runUninstallFlow } from "./uninstall.js";
19
20
  import * as prompts from "../ui/prompts.js";
@@ -68,19 +69,43 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
68
69
  if (!proceed) { io.cancelMessage?.("통합을 안전하게 취소했습니다."); return 0; }
69
70
 
70
71
  // full/version/workflows — 감지 (version은 기존 version.yml 최우선)
72
+ // 감지 경고는 즉시 찍지 않고 모았다가 감지 박스 안에서 출력한다 — 종전에는 경고가 박스보다
73
+ // 먼저 나와 앞선 질문에 대한 경고처럼 보였다 (이슈 #80). 안내 문구도 대화형용으로 바꾼다.
74
+ const detectWarnings = [];
71
75
  let types = detectTypes(cwd);
72
- let version = (existing?.version) || detectVersion(cwd);
76
+ let version = (existing?.version) || detectVersion(cwd, {
77
+ warn: (m) => detectWarnings.push(m),
78
+ hint: "다음 화면의 '수정하기 > 버전'에서 바로 고칠 수 있습니다.",
79
+ });
73
80
  let branch = detectDefaultBranch(cwd);
74
81
  const repoName = detectRepoName(cwd);
75
82
  // 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
76
83
  let includeNexus = existing?.options?.nexus ?? false;
77
84
  let includeSecretBackup = existing?.options?.secretBackup ?? false;
78
85
  let includeSemverAuto = existing?.options?.semverAuto ?? null;
86
+ // 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (nexus/secret_backup과 같은 규약).
87
+ let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
79
88
  const showOptional = mode === "full";
80
89
  const realTty = process.stdout.isTTY === true;
81
90
 
82
- // 층2 — 감지 로그 (#446)
83
- io.detectionLog?.({ types, version, branch });
91
+ // 층2 — 감지 로그 (#446). markers = 실제로 존재를 확인한 파일 (이슈 #77).
92
+ let markers = detectMarkers(cwd, types);
93
+ io.detectionLog?.({ types, version, branch, markers, warnings: detectWarnings });
94
+
95
+ // 타입 확정 (이슈 #78) — 감지는 추정이므로 다른 질문보다 먼저 확인받는다. 종전에는 확정 UI가
96
+ // '수정하기 > 프로젝트 타입' 두 단계 뒤에 숨어 있어, 타입이 틀린 채로 설치가 끝나는 일이 많았다.
97
+ // 타입이 뒤에 나올 질문(선택 워크플로우·경로·env)의 범위를 정하므로 순서상 여기가 맞다.
98
+ // 저장값이 있는 업데이트 설치와 비대화형에서는 묻지 않는다 — 기존 동작 그대로.
99
+ if (showOptional && !existing?.types?.length) {
100
+ const picked = await io.confirmTypes({ types, markers });
101
+ if (!isCancel(picked) && Array.isArray(picked) && picked.length) {
102
+ const next = picked.filter((x) => VALID_TYPES.includes(x));
103
+ if (next.length) {
104
+ types = next;
105
+ markers = detectMarkers(cwd, types);
106
+ }
107
+ }
108
+ }
84
109
 
85
110
  // 선택 워크플로우(Nexus/Secret) 질문 (.sh ask_all_optional_workflows L2707 — full/workflows만)
86
111
  if (showOptional) {
@@ -93,6 +118,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
93
118
  includeNexus = r.nexus;
94
119
  includeSecretBackup = r.secretBackup;
95
120
 
121
+ // 서버 배포 방식 (이슈 #80). Nexus(라이브러리)를 고르면 server-deploy가 통째로 빠지므로 묻지 않는다.
122
+ if (!includeNexus && !isDeployStyle(deployStyle)) {
123
+ const picked = await io.selectDeployStyle();
124
+ deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
125
+ }
126
+
96
127
  // 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
97
128
  // version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
98
129
  if (mode === "full" && includeSemverAuto === null) {
@@ -187,14 +218,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
187
218
 
188
219
  // @wizard env 계획 질문 (.sh wf_prompt_env_plan L3220 — full/workflows만)
189
220
  const resolvers = makeResolvers(cwd, repoName, paths);
190
- let envValues = new Map(), envUseDefaults = true;
221
+ let envValues = new Map(), envUseDefaults = true, envAnswers = [];
191
222
  if (showOptional) {
192
223
  const plan = await promptEnvPlan({
193
224
  payloadRoot: payload, types, io: io.engineIo ?? null, force: false,
194
- resolvers, includeNexus, targetRoot: cwd, repoName,
225
+ resolvers, includeNexus, includeSecretBackup, deployStyle, targetRoot: cwd, repoName,
195
226
  });
196
227
  envValues = plan.values;
197
228
  envUseDefaults = plan.useDefaults;
229
+ envAnswers = plan.answers || []; // 완료 요약·설치 로그가 같은 답변 데이터를 쓴다 (#79, #80)
198
230
  }
199
231
 
200
232
  const { now, today } = clock || utcNow();
@@ -203,6 +235,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
203
235
  includeNexus, includeSecretBackup,
204
236
  includeSemverAuto,
205
237
  repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
238
+ // 설치 로그(#79)·완료 요약(#80)이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
239
+ markers, envAnswers, detectWarnings,
240
+ deployStyle: deployStyle || DEFAULT_DEPLOY_STYLE,
241
+ previousTemplateVersion: existing?.templateVersion || "",
206
242
  });
207
243
  ctx.templateVersion = templateVersion;
208
244
 
@@ -258,6 +294,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
258
294
  mode, types, version, versionCode, branches,
259
295
  copiedFiles: result?.workflows?.copiedFiles ?? [],
260
296
  gitignoreUpdated: result?.gitignoreUpdated === true,
297
+ answers: envAnswers,
298
+ unresolved: result?.unresolved ?? [],
299
+ secrets: result?.secrets ?? new Map(),
300
+ installLogPath: result?.installLog?.path ?? "",
301
+ cleanup: result?.cleanup ?? null,
261
302
  });
262
303
  io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
263
304
  return 0;
@@ -44,7 +44,9 @@ export function runUninstall(context, payloadRoot, targetRoot, selection) {
44
44
 
45
45
  // ── 대화형 체크리스트 흐름 ────────────────────────────────────────────
46
46
  const ITEM_DEFS = [
47
- { key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml)" },
47
+ // .github/.wizard 에는 baseline.json과 설치 로그(.wizard/logs) 함께 들어 있다 —
48
+ // 워크플로우를 지우면 함께 사라지므로 라벨에 명시한다 (이슈 #79).
49
+ { key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml) + 설치 기록 (.github/.wizard)" },
48
50
  { key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
49
51
  { key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
50
52
  { key: "gitignore", label: ".gitignore 자동 추가 항목" },
@@ -6,9 +6,9 @@ import { chmodSync } from "node:fs";
6
6
  import { PATHS, PAYLOAD } from "../paths.js";
7
7
  import { exists, copyFileSync } from "../fsutil.js";
8
8
 
9
- // version_manager.py, changelog_manager.py 무조건 덮어쓰기 (+chmod — Windows에선 무의미하나 무해).
9
+ // version_manager.py, changelog_manager.py, truncate_release_notes.py 무조건 덮어쓰기 (+chmod — Windows에선 무의미하나 무해).
10
10
  export function copyScripts(payloadRoot, targetRoot = ".") {
11
- const scripts = ["version_manager.py", "changelog_manager.py"];
11
+ const scripts = ["version_manager.py", "changelog_manager.py", "truncate_release_notes.py", "issue_helper.py"];
12
12
  let copied = 0;
13
13
  for (const s of scripts) {
14
14
  const src = join(payloadRoot, PAYLOAD.scriptsDir, s);