project-auto-wizard 0.1.6 → 0.1.8
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.
- package/README.md +80 -2
- package/package.json +2 -2
- package/payload/scripts/changelog_manager.py +102 -4
- package/payload/scripts/version_manager.py +16 -2
- package/payload/version.yml.template +5 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +98 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +47 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -15
- package/src/cli/args.js +11 -0
- package/src/cli/help.js +12 -1
- package/src/commands/doctor.js +73 -0
- package/src/commands/dry-run.js +91 -0
- package/src/commands/full.js +3 -2
- package/src/commands/interactive.js +22 -0
- package/src/commands/revert.js +22 -21
- package/src/commands/status.js +64 -0
- package/src/commands/uninstall.js +129 -0
- package/src/commands/version.js +3 -2
- package/src/context.js +1 -0
- package/src/core/copy/gitignore.js +53 -1
- package/src/core/copy/readme.js +52 -1
- package/src/core/copy/workflows.js +56 -0
- package/src/core/version-yml.js +10 -3
- package/src/index.js +68 -4
- package/src/ui/prompts.js +1 -0
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
# exits cleanly. Version + changelog were already confirmed in the PR.
|
|
14
14
|
#
|
|
15
15
|
# - trunk-based: every direct push to {{MAIN_BRANCH}} is a release.
|
|
16
|
-
# This job bumps the version
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# CHANGELOG.md, and commits everything back with
|
|
16
|
+
# This job bumps the version and syncs it, summarizes the commits since
|
|
17
|
+
# the last tag with the ai-summary engine chain (user API -> GitHub
|
|
18
|
+
# Models -> rule-based fallback, diff-stat-enriched prompt), updates
|
|
19
|
+
# CHANGELOG.json / CHANGELOG.md, and commits everything back with
|
|
20
|
+
# [skip ci]. Same as pr-flow's AUTO-CHANGELOG-CONTROL:
|
|
21
|
+
# - If version.yml metadata.template.options.semver_auto is true
|
|
22
|
+
# (default): classifies commits (feat -> minor, `!` marker -> major,
|
|
23
|
+
# else patch; AI-assisted patch->minor upgrade for unclassified
|
|
24
|
+
# commits) and bumps accordingly.
|
|
25
|
+
# - Otherwise: always patch+1 (legacy behavior).
|
|
20
26
|
#
|
|
21
27
|
# Common tail (both modes): recovery-aware idempotency guard (tag and
|
|
22
28
|
# release checked separately so a half-published release can be finished
|
|
@@ -102,6 +108,19 @@ jobs:
|
|
|
102
108
|
echo "not a release-confirm push in pr-flow mode — skipping (clean exit)"
|
|
103
109
|
fi
|
|
104
110
|
|
|
111
|
+
- name: Read semver_auto option from version.yml
|
|
112
|
+
id: semver_options
|
|
113
|
+
if: >-
|
|
114
|
+
steps.gate.outputs.proceed == 'true' &&
|
|
115
|
+
steps.mode.outputs.mode == 'trunk-based' &&
|
|
116
|
+
github.event_name == 'push'
|
|
117
|
+
run: |
|
|
118
|
+
# scoped to the metadata: -> template: -> options: chain so an
|
|
119
|
+
# unrelated "semver_auto:" key can never hijack the value
|
|
120
|
+
SEMVER_AUTO=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?semver_auto:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
121
|
+
echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
|
|
122
|
+
echo "semver_auto option: $SEMVER_AUTO"
|
|
123
|
+
|
|
105
124
|
- name: Trunk-based version bump + changelog
|
|
106
125
|
if: >-
|
|
107
126
|
steps.gate.outputs.proceed == 'true' &&
|
|
@@ -123,28 +142,48 @@ jobs:
|
|
|
123
142
|
exit 0
|
|
124
143
|
fi
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
fi
|
|
131
|
-
python3 .github/scripts/version_manager.py sync
|
|
132
|
-
echo "release version: $NEW_VERSION"
|
|
133
|
-
|
|
134
|
-
# Commits since the last tag (fall back to full history on first release)
|
|
145
|
+
# Commits (and diff-stat) since the last tag — this is "what's in
|
|
146
|
+
# this release" for trunk-based pushes. Fall back to the full
|
|
147
|
+
# history / a diff against the empty tree on the first-ever
|
|
148
|
+
# release (no previous tag to diff against).
|
|
135
149
|
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
136
150
|
if [ -n "$LAST_TAG" ]; then
|
|
137
151
|
echo "collecting commits since $LAST_TAG"
|
|
138
152
|
git log --pretty=%s "${LAST_TAG}..HEAD" > commits.txt
|
|
153
|
+
DIFF_BASE="$LAST_TAG"
|
|
139
154
|
else
|
|
140
155
|
echo "no previous tag — collecting the full history"
|
|
141
156
|
git log --pretty=%s > commits.txt
|
|
157
|
+
DIFF_BASE="4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git empty tree hash
|
|
142
158
|
fi
|
|
159
|
+
git diff --stat "${DIFF_BASE}..HEAD" > _full_diff_stat.txt
|
|
160
|
+
if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
|
|
161
|
+
{ head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
|
|
162
|
+
else
|
|
163
|
+
cp _full_diff_stat.txt diff_stat.txt
|
|
164
|
+
fi
|
|
165
|
+
rm -f _full_diff_stat.txt
|
|
166
|
+
|
|
167
|
+
if [ "${{ steps.semver_options.outputs.semver_auto }}" = "true" ]; then
|
|
168
|
+
BUMP=$(python3 .github/scripts/changelog_manager.py classify-bump --commits-file commits.txt | tail -n 1)
|
|
169
|
+
else
|
|
170
|
+
BUMP="patch"
|
|
171
|
+
fi
|
|
172
|
+
echo "bump level: $BUMP"
|
|
173
|
+
|
|
174
|
+
NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
|
|
175
|
+
if [ -z "$NEW_VERSION" ]; then
|
|
176
|
+
echo "version bump failed"
|
|
177
|
+
exit 1
|
|
178
|
+
fi
|
|
179
|
+
python3 .github/scripts/version_manager.py sync
|
|
180
|
+
echo "release version: $NEW_VERSION ($BUMP)"
|
|
143
181
|
|
|
144
182
|
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
145
183
|
--commits-file commits.txt \
|
|
146
184
|
--version "$NEW_VERSION" \
|
|
147
|
-
--output summary.md
|
|
185
|
+
--output summary.md \
|
|
186
|
+
--diff-stat-file diff_stat.txt
|
|
148
187
|
|
|
149
188
|
# Same input channel as AUTO-CHANGELOG-CONTROL:
|
|
150
189
|
# update-from-summary reads ./pr_body.md
|
|
@@ -165,7 +204,7 @@ jobs:
|
|
|
165
204
|
python3 .github/scripts/changelog_manager.py update-from-summary
|
|
166
205
|
python3 .github/scripts/changelog_manager.py generate-md
|
|
167
206
|
|
|
168
|
-
rm -f pr_body.md summary.md commits.txt
|
|
207
|
+
rm -f pr_body.md summary.md commits.txt diff_stat.txt
|
|
169
208
|
|
|
170
209
|
git add -A
|
|
171
210
|
if git diff --staged --quiet; then
|
package/src/cli/args.js
CHANGED
|
@@ -11,12 +11,17 @@ export function parseArgs(argv) {
|
|
|
11
11
|
includeNexus: null, // null=미설정
|
|
12
12
|
includeSecretBackup: null,
|
|
13
13
|
includeCodeRabbit: null, // --coderabbit / --no-coderabbit (기본 false — DESIGN-SPEC §4 질문②)
|
|
14
|
+
includeSemverAuto: null, // --semver-auto / --no-semver-auto (기본 true — 미지정 시 다운스트림에서 해석)
|
|
14
15
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
15
16
|
mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
|
|
16
17
|
developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
|
|
17
18
|
force: false,
|
|
18
19
|
help: false,
|
|
19
20
|
showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
|
|
21
|
+
dryRun: false, // --dry-run: 실제 변경 없이 미리보기만 (full/version/workflows/revert 공통)
|
|
22
|
+
purgeReadme: false, // --purge-readme: uninstall --force 시 README 버전 섹션도 제거
|
|
23
|
+
purgeGitignore: false, // --purge-gitignore: uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
24
|
+
purgeVersion: false, // --purge-version: uninstall --force 시 version.yml도 제거
|
|
20
25
|
};
|
|
21
26
|
const args = [...argv];
|
|
22
27
|
while (args.length > 0) {
|
|
@@ -49,12 +54,18 @@ export function parseArgs(argv) {
|
|
|
49
54
|
break;
|
|
50
55
|
}
|
|
51
56
|
case "--force": result.force = true; break;
|
|
57
|
+
case "--dry-run": result.dryRun = true; break;
|
|
58
|
+
case "--purge-readme": result.purgeReadme = true; break;
|
|
59
|
+
case "--purge-gitignore": result.purgeGitignore = true; break;
|
|
60
|
+
case "--purge-version": result.purgeVersion = true; break;
|
|
52
61
|
case "--nexus": result.includeNexus = true; break;
|
|
53
62
|
case "--no-nexus": result.includeNexus = false; break;
|
|
54
63
|
case "--secret-backup": result.includeSecretBackup = true; break;
|
|
55
64
|
case "--no-secret-backup": result.includeSecretBackup = false; break;
|
|
56
65
|
case "--coderabbit": result.includeCodeRabbit = true; break;
|
|
57
66
|
case "--no-coderabbit": result.includeCodeRabbit = false; break;
|
|
67
|
+
case "--semver-auto": result.includeSemverAuto = true; break;
|
|
68
|
+
case "--no-semver-auto": result.includeSemverAuto = false; break;
|
|
58
69
|
case "--paths": result.pathsCsv = args.shift() ?? ""; break;
|
|
59
70
|
case "--main-branch": result.mainBranch = args.shift() ?? ""; break;
|
|
60
71
|
case "--develop-branch": result.developBranch = args.shift() ?? ""; break;
|
package/src/cli/help.js
CHANGED
|
@@ -5,8 +5,10 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
5
5
|
npx project-auto-wizard [옵션]
|
|
6
6
|
|
|
7
7
|
옵션:
|
|
8
|
-
-m, --mode MODE 통합 모드 (full | version | workflows | revert)
|
|
8
|
+
-m, --mode MODE 통합 모드 (full | version | workflows | revert | uninstall | status | doctor)
|
|
9
9
|
기본: interactive (대화형). revert = 설치물 제거(되돌리기)
|
|
10
|
+
uninstall = 완전 삭제(대화형 체크리스트, --force 시 --purge-*로 opt-in)
|
|
11
|
+
status = 설치 상태·드리프트 확인(읽기 전용). doctor = 환경 진단(읽기 전용)
|
|
10
12
|
-t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
|
|
11
13
|
지원: spring flutter next react react-native
|
|
12
14
|
react-native-expo node python basic
|
|
@@ -17,11 +19,20 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
17
19
|
--nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
|
|
18
20
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
19
21
|
--coderabbit / --no-coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로 사용 (기본: 사용 안 함)
|
|
22
|
+
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
20
23
|
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
24
|
+
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/version/workflows/revert/uninstall 전체 지원)
|
|
25
|
+
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
26
|
+
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
27
|
+
--purge-version --mode uninstall --force 시 version.yml도 제거
|
|
21
28
|
-v, --version project-auto-wizard 버전 출력
|
|
22
29
|
-h, --help 이 도움말 표시
|
|
23
30
|
|
|
24
31
|
예시:
|
|
25
32
|
npx project-auto-wizard --mode full --force --type spring,react
|
|
26
33
|
npx project-auto-wizard --mode workflows --type flutter --paths "flutter=app"
|
|
34
|
+
npx project-auto-wizard --mode status
|
|
35
|
+
npx project-auto-wizard --mode doctor
|
|
36
|
+
npx project-auto-wizard --mode full --force --type node --dry-run
|
|
37
|
+
npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version
|
|
27
38
|
`;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// doctor 명령 — 로컬 환경 진단(읽기 전용, 규칙 기반). gh CLI에 위임해 원격 상태를 점검한다.
|
|
2
|
+
// AI 진단은 포함하지 않는다(스펙 §4에서 검토 후 기각 — 복잡도 대비 이득 낮음).
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
8
|
+
|
|
9
|
+
export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
10
|
+
const results = [];
|
|
11
|
+
const add = (name, status, detail) => { results.push({ name, status, detail }); return results; };
|
|
12
|
+
|
|
13
|
+
const installed = existsSync(join(cwd, "version.yml"));
|
|
14
|
+
add("설치 여부", installed ? "OK" : "WARN",
|
|
15
|
+
installed ? "version.yml 발견" : "이 디렉터리에 project-auto-wizard가 설치되어 있지 않습니다 (version.yml 없음)");
|
|
16
|
+
|
|
17
|
+
const ghVersion = exec("gh", ["--version"]);
|
|
18
|
+
if (ghVersion.error || ghVersion.status !== 0) {
|
|
19
|
+
add("gh CLI", "WARN", "gh CLI를 찾을 수 없습니다 — 원격 점검을 건너뜁니다 (https://cli.github.com/ 설치 권장)");
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
add("gh CLI", "OK", (ghVersion.stdout || "").split("\n")[0] || "설치됨");
|
|
23
|
+
|
|
24
|
+
const auth = exec("gh", ["auth", "status"]);
|
|
25
|
+
const authOk = !auth.error && auth.status === 0;
|
|
26
|
+
add("gh 인증", authOk ? "OK" : "FAIL", authOk ? "인증됨" : "`gh auth login`이 필요합니다");
|
|
27
|
+
if (!authOk) return results;
|
|
28
|
+
|
|
29
|
+
const remote = exec("git", ["-C", cwd, "remote", "get-url", "origin"]);
|
|
30
|
+
const url = remote.status === 0 ? (remote.stdout || "").trim() : "";
|
|
31
|
+
const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(\.git)?$/);
|
|
32
|
+
if (!match) {
|
|
33
|
+
add("GitHub 원격", "WARN", "origin 리모트에서 GitHub owner/repo를 확인하지 못했습니다");
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
const [, owner, repo] = match;
|
|
37
|
+
|
|
38
|
+
const perm = exec("gh", ["api", `repos/${owner}/${repo}/actions/permissions/workflow`, "--jq", ".default_workflow_permissions"]);
|
|
39
|
+
const permValue = (perm.stdout || "").trim();
|
|
40
|
+
add("Workflow permissions", perm.status === 0 && permValue === "write" ? "OK" : "WARN",
|
|
41
|
+
perm.status === 0
|
|
42
|
+
? `현재값: ${permValue || "확인불가"} (Settings → Actions → General → Workflow permissions: Read and write 권장)`
|
|
43
|
+
: "조회 실패 — repo 관리자 권한이 필요할 수 있습니다");
|
|
44
|
+
|
|
45
|
+
const secrets = exec("gh", ["secret", "list", "--repo", `${owner}/${repo}`]);
|
|
46
|
+
const hasPat = secrets.status === 0 && (secrets.stdout || "").split("\n").some((l) => l.startsWith("WORKFLOW_PAT"));
|
|
47
|
+
add("WORKFLOW_PAT secret", hasPat ? "OK" : "WARN",
|
|
48
|
+
hasPat
|
|
49
|
+
? "등록됨"
|
|
50
|
+
: "미등록 — automerge 후 후속 워크플로우(tag/Release)가 트리거되지 않을 수 있습니다. Settings → Secrets → Actions에 등록 (scopes: repo, workflow)");
|
|
51
|
+
|
|
52
|
+
const mergeSettings = exec("gh", ["api", `repos/${owner}/${repo}`, "--jq", ".allow_merge_commit"]);
|
|
53
|
+
const automergeOk = mergeSettings.status === 0 && mergeSettings.stdout.trim() === "true";
|
|
54
|
+
add("automerge 호환성(merge commit 허용)", automergeOk ? "OK" : "WARN",
|
|
55
|
+
mergeSettings.status === 0
|
|
56
|
+
? (automergeOk
|
|
57
|
+
? "머지 커밋 허용됨"
|
|
58
|
+
: "이 레포는 merge commit이 비활성화되어 있습니다 — automerge/RELEASE-PUBLISH가 머지 커밋 subject를 감지하는 방식과 충돌할 수 있습니다. Settings → General → Pull Requests → Allow merge commits 활성화 권장")
|
|
59
|
+
: "조회 실패 — repo 관리자 권한이 필요할 수 있습니다");
|
|
60
|
+
|
|
61
|
+
add("GitHub Models 활성화", "INFO",
|
|
62
|
+
"자동 확인 불가 — Settings → Models에서 조직 정책으로 차단되지 않았는지 직접 확인하세요 (차단 시 규칙 기반 fallback으로 자동 전환됩니다)");
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function printDoctorReport(results) {
|
|
68
|
+
const icon = { OK: "✅", WARN: "⚠️ ", FAIL: "❌", INFO: "ℹ️ " };
|
|
69
|
+
const lines = ["", "project-auto-wizard doctor — 환경 진단 결과", ""];
|
|
70
|
+
for (const r of results) lines.push(`${icon[r.status] || " "} [${r.status}] ${r.name} — ${r.detail}`);
|
|
71
|
+
lines.push("");
|
|
72
|
+
console.log(lines.join("\n"));
|
|
73
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// --dry-run 미리보기 — 실제 파일을 쓰지 않고 무엇이 바뀔지 계산한다.
|
|
2
|
+
// full/version/workflows/revert/uninstall 5개 모드 전체 지원.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { PATHS } from "../core/paths.js";
|
|
6
|
+
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
|
+
import { planRevert } from "./revert.js";
|
|
8
|
+
import { planUninstall } from "./uninstall.js";
|
|
9
|
+
import { buildVersionYml } from "../core/version-yml.js";
|
|
10
|
+
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
|
+
import { markerForType } from "../core/detect.js";
|
|
12
|
+
|
|
13
|
+
function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
14
|
+
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
15
|
+
now, today, templateVersion = "unknown",
|
|
16
|
+
includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false,
|
|
17
|
+
includeSemverAuto } = context;
|
|
18
|
+
const pathMarkers = new Map();
|
|
19
|
+
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
20
|
+
const wouldBe = buildVersionYml({
|
|
21
|
+
templateText: readVersionYmlTemplate(payloadRoot),
|
|
22
|
+
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
23
|
+
templateOptions: {
|
|
24
|
+
templateVersion, includeNexus, includeSecretBackup,
|
|
25
|
+
includeCodeRabbit: includeCodeRabbit === true,
|
|
26
|
+
includeSemverAuto: includeSemverAuto !== false,
|
|
27
|
+
optionsDate: today,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
31
|
+
const existing = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
32
|
+
return { existed: existing !== null, changed: existing !== wouldBe };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// mode: "full" | "version" | "workflows" | "revert" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
|
|
36
|
+
export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
37
|
+
if (mode === "revert") return { mode, revert: planRevert(payloadRoot, targetRoot) };
|
|
38
|
+
if (mode === "uninstall") {
|
|
39
|
+
return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = { mode };
|
|
43
|
+
if (mode === "full" || mode === "workflows") {
|
|
44
|
+
result.workflows = planWorkflows(context, payloadRoot, targetRoot);
|
|
45
|
+
}
|
|
46
|
+
if (mode === "full" || mode === "version") {
|
|
47
|
+
result.versionYml = versionYmlPreview(context, payloadRoot, targetRoot);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function printDryRun(plan) {
|
|
53
|
+
const lines = ["", `project-auto-wizard --dry-run (mode: ${plan.mode}) — 미리보기, 실제 파일은 바뀌지 않았습니다`, ""];
|
|
54
|
+
if (plan.mode === "revert") {
|
|
55
|
+
const r = plan.revert;
|
|
56
|
+
lines.push(`제거될 워크플로우 (${r.workflows.length}개):`);
|
|
57
|
+
for (const f of r.workflows) lines.push(` - ${f}`);
|
|
58
|
+
lines.push(`제거될 스크립트 (${r.scripts.length}개):`);
|
|
59
|
+
for (const f of r.scripts) lines.push(` - ${f}`);
|
|
60
|
+
if (r.coderabbit) lines.push("제거될 파일: .coderabbit.yaml");
|
|
61
|
+
} else if (plan.mode === "uninstall") {
|
|
62
|
+
const u = plan.uninstall;
|
|
63
|
+
lines.push(`제거될 워크플로우 (${u.workflows.length}개):`);
|
|
64
|
+
for (const f of u.workflows) lines.push(` - ${f}`);
|
|
65
|
+
lines.push(`제거될 스크립트 (${u.scripts.length}개):`);
|
|
66
|
+
for (const f of u.scripts) lines.push(` - ${f}`);
|
|
67
|
+
if (u.coderabbit) lines.push("제거될 파일: .coderabbit.yaml");
|
|
68
|
+
if (u.readme) lines.push("제거될 항목: README.md 버전 섹션 (AUTO-VERSION-SECTION)");
|
|
69
|
+
if (u.gitignore) lines.push("제거될 항목: .gitignore 자동 추가 항목");
|
|
70
|
+
if (u.versionYml) lines.push("제거될 파일: version.yml");
|
|
71
|
+
} else {
|
|
72
|
+
if (plan.workflows) {
|
|
73
|
+
const w = plan.workflows;
|
|
74
|
+
lines.push(`신규 파일 (${w.newFiles.length}개):`);
|
|
75
|
+
for (const f of w.newFiles) lines.push(` + ${f.filename} [${f.type}]`);
|
|
76
|
+
lines.push(`변경될 파일 (${w.changed.length}개, 기존 설치가 사용자 수정본이면 충돌):`);
|
|
77
|
+
for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
78
|
+
lines.push(`동일한 파일 (${w.unchanged.length}개, 변경 없음)`);
|
|
79
|
+
}
|
|
80
|
+
if (plan.versionYml) {
|
|
81
|
+
lines.push(plan.versionYml.existed
|
|
82
|
+
? (plan.versionYml.changed ? "version.yml: 갱신될 예정" : "version.yml: 변경 없음")
|
|
83
|
+
: "version.yml: 새로 생성될 예정");
|
|
84
|
+
// dry-run은 프롬프트 없이 읽기 전용으로 동작하므로 @wizard ask 배포 설정 값을 계산할 수 없다.
|
|
85
|
+
// spring 등 deploy 블록이 있는 타입은 실제 설치 결과와 미리보기가 다를 수 있음을 안내한다.
|
|
86
|
+
lines.push(" (참고: 배포 설정 질문이 있는 타입(spring 등)은 deploy: 블록이 미리보기에 반영되지 않아 실제 설치와 다르게 보일 수 있습니다.)");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push("");
|
|
90
|
+
console.log(lines.join("\n"));
|
|
91
|
+
}
|
package/src/commands/full.js
CHANGED
|
@@ -19,7 +19,8 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
|
19
19
|
export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
20
20
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
21
21
|
force = true, now, today, templateVersion = "unknown",
|
|
22
|
-
includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false
|
|
22
|
+
includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false,
|
|
23
|
+
includeSemverAuto } = context;
|
|
23
24
|
|
|
24
25
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
25
26
|
const pathMarkers = new Map();
|
|
@@ -36,7 +37,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
36
37
|
templateText: readVersionYmlTemplate(payloadRoot),
|
|
37
38
|
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
38
39
|
deployValues,
|
|
39
|
-
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeCodeRabbit: includeCodeRabbit === true, optionsDate: today },
|
|
40
|
+
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeCodeRabbit: includeCodeRabbit === true, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
|
|
40
41
|
}));
|
|
41
42
|
|
|
42
43
|
// 3. README 버전 섹션
|
|
@@ -18,6 +18,7 @@ import { runFull } from "./full.js";
|
|
|
18
18
|
import { runVersion } from "./version.js";
|
|
19
19
|
import { runWorkflows } from "./workflows.js";
|
|
20
20
|
import { runRevert } from "./revert.js";
|
|
21
|
+
import { runUninstallFlow } from "./uninstall.js";
|
|
21
22
|
import * as prompts from "../ui/prompts.js";
|
|
22
23
|
|
|
23
24
|
const CANCEL = prompts.CANCEL;
|
|
@@ -53,6 +54,14 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
53
54
|
return 0;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// uninstall 모드 — 대화형 체크리스트로 항목별 opt-in 후 삭제. 감지·breaking 게이트 불필요.
|
|
58
|
+
// runUninstallFlow는 취소/항목없음 시 null을 반환한다 — 그때는 완료 outro를 찍지 않는다.
|
|
59
|
+
if (mode === "uninstall") {
|
|
60
|
+
const result = await runUninstallFlow(payload, cwd, io);
|
|
61
|
+
if (result) io.outro?.("완전 삭제를 마쳤습니다.");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
// Breaking Changes 게이트 (.sh execute_integration L4415~4420 — 모든 모드 공통, 대화형은 확인 질문)
|
|
57
66
|
const proceed = await runBreakingCheck({
|
|
58
67
|
cwd, payloadRoot: payload, templateVersion,
|
|
@@ -70,6 +79,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
70
79
|
let includeNexus = existing?.options?.nexus ?? false;
|
|
71
80
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
72
81
|
let includeCodeRabbit = existing?.options?.coderabbit ?? null;
|
|
82
|
+
let includeSemverAuto = existing?.options?.semverAuto ?? null;
|
|
73
83
|
const showOptional = mode === "full" || mode === "workflows";
|
|
74
84
|
const realTty = process.stdout.isTTY === true;
|
|
75
85
|
|
|
@@ -92,8 +102,19 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
92
102
|
const y = await io.askYesNo("CodeRabbit을 사용합니까? (PR AI 리뷰·요약을 릴리스 노트 1순위로 사용)", false);
|
|
93
103
|
includeCodeRabbit = y === true;
|
|
94
104
|
}
|
|
105
|
+
|
|
106
|
+
// 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
|
|
107
|
+
// version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
|
|
108
|
+
if (mode === "full" && includeSemverAuto === null) {
|
|
109
|
+
const y2 = await io.askYesNo("자동 버전 승격을 사용하시겠습니까? (커밋 타입에 따라 major/minor/patch 자동 결정)", true);
|
|
110
|
+
includeSemverAuto = y2 === true;
|
|
111
|
+
}
|
|
95
112
|
}
|
|
96
113
|
includeCodeRabbit = includeCodeRabbit === true;
|
|
114
|
+
// 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로 존중.
|
|
115
|
+
// 질문이 안 나온 경우(version/workflows 모드) — 기존 설치는 안전하게 false로 폴백,
|
|
116
|
+
// 완전 신규 설치만 true(기존 설계) 유지 — CLI 경로(index.js)와 동일한 안전 정책.
|
|
117
|
+
includeSemverAuto = includeSemverAuto === null ? (existing ? false : true) : includeSemverAuto !== false;
|
|
97
118
|
|
|
98
119
|
// 확인/수정 루프 — ESC는 '머무르기' (.sh L1877~1881: 명시적 '아니오'만 종료)
|
|
99
120
|
let paths = new Map();
|
|
@@ -189,6 +210,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
189
210
|
const ctx = createContext({
|
|
190
211
|
mode, force: true, types, version, versionCode, branch, branches, paths,
|
|
191
212
|
includeNexus, includeSecretBackup, includeCodeRabbit,
|
|
213
|
+
includeSemverAuto,
|
|
192
214
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
193
215
|
});
|
|
194
216
|
ctx.templateVersion = templateVersion;
|
package/src/commands/revert.js
CHANGED
|
@@ -18,43 +18,44 @@ function payloadWorkflowNames(payloadRoot) {
|
|
|
18
18
|
return names;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
// payload에 존재하는 파일명과 정확히 일치하는 것만 제거 대상으로 계획한다.
|
|
22
|
+
// 아무것도 지우지 않는 순수 함수 — --dry-run과 status류 기능에서 재사용.
|
|
23
|
+
export function planRevert(payloadRoot, targetRoot = ".") {
|
|
23
24
|
const removedWf = [];
|
|
24
25
|
const removedScripts = [];
|
|
25
|
-
|
|
26
|
-
// 1. 워크플로우 — payload 파일명 일치분 + 마법사가 만든 .template.yaml/.bak 파생본
|
|
27
26
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
28
27
|
const names = payloadWorkflowNames(payloadRoot);
|
|
29
28
|
if (existsSync(wfDir)) {
|
|
30
29
|
for (const name of names) {
|
|
31
30
|
const p = join(wfDir, name);
|
|
32
|
-
if (existsSync(p))
|
|
31
|
+
if (existsSync(p)) removedWf.push(name);
|
|
33
32
|
const templateName = (name.endsWith(".yaml") ? name.slice(0, -".yaml".length) : name) + ".template.yaml";
|
|
34
|
-
|
|
35
|
-
if (existsSync(
|
|
36
|
-
const bp = p + ".bak";
|
|
37
|
-
if (existsSync(bp)) { remove(bp); removedWf.push(name + ".bak"); }
|
|
33
|
+
if (existsSync(join(wfDir, templateName))) removedWf.push(templateName);
|
|
34
|
+
if (existsSync(p + ".bak")) removedWf.push(name + ".bak");
|
|
38
35
|
}
|
|
39
36
|
}
|
|
40
|
-
|
|
41
|
-
// 2. 스크립트 — payload가 설치한 2종만
|
|
42
37
|
for (const s of ["version_manager.py", "changelog_manager.py"]) {
|
|
43
|
-
|
|
44
|
-
if (existsSync(p)) { remove(p); removedScripts.push(s); }
|
|
38
|
+
if (existsSync(join(targetRoot, PATHS.scriptsDir, s))) removedScripts.push(s);
|
|
45
39
|
}
|
|
46
|
-
|
|
47
|
-
// 3. .coderabbit.yaml — payload 원본과 바이트 일치할 때만 제거 (사용자 자체 파일 보호).
|
|
48
|
-
// 설치 시 백업(.bak)이 있으면 복원한다.
|
|
49
40
|
let coderabbit = false;
|
|
50
41
|
const cr = join(targetRoot, ".coderabbit.yaml");
|
|
51
42
|
const crSrc = join(payloadRoot, "coderabbit.yaml");
|
|
52
|
-
if (existsSync(cr) && existsSync(crSrc)
|
|
53
|
-
&& readFileSync(cr, "utf8") === readFileSync(crSrc, "utf8")) {
|
|
54
|
-
remove(cr);
|
|
43
|
+
if (existsSync(cr) && existsSync(crSrc) && readFileSync(cr, "utf8") === readFileSync(crSrc, "utf8")) {
|
|
55
44
|
coderabbit = true;
|
|
56
|
-
if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
|
|
57
45
|
}
|
|
58
|
-
|
|
59
46
|
return { workflows: removedWf, scripts: removedScripts, coderabbit };
|
|
60
47
|
}
|
|
48
|
+
|
|
49
|
+
// 반환: { workflows: [...제거된 파일명], scripts: [...], coderabbit: bool } — planRevert와 동일한 형태.
|
|
50
|
+
export function runRevert(context, payloadRoot, targetRoot = ".") {
|
|
51
|
+
const plan = planRevert(payloadRoot, targetRoot);
|
|
52
|
+
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
53
|
+
for (const name of plan.workflows) remove(join(wfDir, name));
|
|
54
|
+
for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
|
|
55
|
+
if (plan.coderabbit) {
|
|
56
|
+
const cr = join(targetRoot, ".coderabbit.yaml");
|
|
57
|
+
remove(cr);
|
|
58
|
+
if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
|
|
59
|
+
}
|
|
60
|
+
return plan;
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// status 명령 — 읽기 전용 설치 상태 확인. 네트워크 접근 없음(로컬 파일 비교만).
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseExisting } from "../core/version-yml.js";
|
|
5
|
+
import { planWorkflows } from "../core/copy/workflows.js";
|
|
6
|
+
import { makeResolvers, detectRepoName, detectDefaultBranch } from "../core/detect-fs.js";
|
|
7
|
+
import { PATHS } from "../core/paths.js";
|
|
8
|
+
|
|
9
|
+
// payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
|
|
10
|
+
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
11
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
12
|
+
if (!existsSync(vyPath)) return { installed: false };
|
|
13
|
+
|
|
14
|
+
const existing = parseExisting(readFileSync(vyPath, "utf8"));
|
|
15
|
+
const repoName = detectRepoName(targetRoot);
|
|
16
|
+
const resolvers = makeResolvers(targetRoot, repoName, existing.paths);
|
|
17
|
+
// version.yml에 branches 블록이 없으면(신기능 이전 설치·수기 편집) makeSrcText(null)이
|
|
18
|
+
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}를 치환하지 못해 모든 워크플로우가 드리프트로 오탐된다 —
|
|
19
|
+
// 비교용 기본값으로 폴백(실제 저장값은 아니지만 드리프트 비교 목적에는 충분).
|
|
20
|
+
const branchesForCompare = existing.branches || { main: detectDefaultBranch(targetRoot) || "main", develop: "develop", mode: "pr-flow" };
|
|
21
|
+
const context = {
|
|
22
|
+
types: existing.types, paths: existing.paths,
|
|
23
|
+
includeNexus: existing.options.nexus === true,
|
|
24
|
+
includeSecretBackup: existing.options.secretBackup === true,
|
|
25
|
+
repoName, resolvers, branches: branchesForCompare,
|
|
26
|
+
};
|
|
27
|
+
const plan = planWorkflows(context, payloadRoot, targetRoot);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
installed: true,
|
|
31
|
+
version: existing.version,
|
|
32
|
+
templateVersion: existing.templateVersion,
|
|
33
|
+
types: existing.types,
|
|
34
|
+
branches: existing.branches,
|
|
35
|
+
options: existing.options,
|
|
36
|
+
modifiedFiles: plan.changed.map((f) => f.filename),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printStatus(status) {
|
|
41
|
+
const lines = ["", "project-auto-wizard status — 설치 상태", ""];
|
|
42
|
+
if (!status.installed) {
|
|
43
|
+
lines.push("이 디렉터리에 project-auto-wizard가 설치되어 있지 않습니다 (version.yml 없음).", "");
|
|
44
|
+
console.log(lines.join("\n"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
lines.push(`버전 : ${status.version}`);
|
|
48
|
+
lines.push(`템플릿 버전 : ${status.templateVersion}`);
|
|
49
|
+
lines.push(`프로젝트 타입 : ${status.types.join(", ") || "(없음)"}`);
|
|
50
|
+
if (status.branches) {
|
|
51
|
+
lines.push(`브랜치 모드 : ${status.branches.mode} (${status.branches.main} / ${status.branches.develop})`);
|
|
52
|
+
}
|
|
53
|
+
const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
|
|
54
|
+
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
55
|
+
lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} coderabbit=${boolLabel(status.options.coderabbit)} semver_auto=${semverAutoLabel}`);
|
|
56
|
+
if (status.modifiedFiles.length) {
|
|
57
|
+
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
58
|
+
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
59
|
+
} else {
|
|
60
|
+
lines.push("", "모든 워크플로우 파일이 설치 시점 기본값과 동일합니다 (수정 없음).");
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
console.log(lines.join("\n"));
|
|
64
|
+
}
|