project-auto-wizard 0.1.8 → 0.1.10
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 +1 -1
- package/package.json +1 -1
- package/src/cli/args.js +19 -0
- package/src/commands/full.js +10 -5
- package/src/commands/interactive.js +1 -0
- package/src/commands/purge.js +86 -0
- package/src/commands/version.js +3 -4
- package/src/core/branches.js +1 -1
- package/src/core/copy/gitignore.js +14 -7
- package/src/core/copy/readme.js +1 -1
- package/src/core/copy/workflows.js +4 -3
- package/src/index.js +94 -3
- package/src/ui/summary.js +2 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx project-auto-wizard
|
|
|
16
16
|
[](package.json)
|
|
17
17
|
|
|
18
18
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
19
|
-
## 최신 버전 : v0.1.
|
|
19
|
+
## 최신 버전 : v0.1.10 (2026-08-02)
|
|
20
20
|
|
|
21
21
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
22
22
|
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -22,6 +22,16 @@ export function parseArgs(argv) {
|
|
|
22
22
|
purgeReadme: false, // --purge-readme: uninstall --force 시 README 버전 섹션도 제거
|
|
23
23
|
purgeGitignore: false, // --purge-gitignore: uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
24
24
|
purgeVersion: false, // --purge-version: uninstall --force 시 version.yml도 제거
|
|
25
|
+
// purge 전용 플래그 (숨김 모드 — HELP_TEXT에는 노출하지 않는다).
|
|
26
|
+
yes: false, // --yes: purge 실행 확인 (필수, --force로 대체 불가)
|
|
27
|
+
allowDirty: false, // --allow-dirty: git 작업트리 dirty 상태에서도 강행
|
|
28
|
+
deleteDevelopBranch: false, // --delete-develop-branch: 로컬 develop 브랜치까지 삭제
|
|
29
|
+
keepVersionYml: false,
|
|
30
|
+
keepReadme: false,
|
|
31
|
+
keepChangelog: false,
|
|
32
|
+
keepWorkflows: false,
|
|
33
|
+
keepScripts: false,
|
|
34
|
+
keepCoderabbit: false,
|
|
25
35
|
};
|
|
26
36
|
const args = [...argv];
|
|
27
37
|
while (args.length > 0) {
|
|
@@ -58,6 +68,15 @@ export function parseArgs(argv) {
|
|
|
58
68
|
case "--purge-readme": result.purgeReadme = true; break;
|
|
59
69
|
case "--purge-gitignore": result.purgeGitignore = true; break;
|
|
60
70
|
case "--purge-version": result.purgeVersion = true; break;
|
|
71
|
+
case "--yes": result.yes = true; break;
|
|
72
|
+
case "--allow-dirty": result.allowDirty = true; break;
|
|
73
|
+
case "--delete-develop-branch": result.deleteDevelopBranch = true; break;
|
|
74
|
+
case "--keep-version-yml": result.keepVersionYml = true; break;
|
|
75
|
+
case "--keep-readme": result.keepReadme = true; break;
|
|
76
|
+
case "--keep-changelog": result.keepChangelog = true; break;
|
|
77
|
+
case "--keep-workflows": result.keepWorkflows = true; break;
|
|
78
|
+
case "--keep-scripts": result.keepScripts = true; break;
|
|
79
|
+
case "--keep-coderabbit": result.keepCoderabbit = true; break;
|
|
61
80
|
case "--nexus": result.includeNexus = true; break;
|
|
62
81
|
case "--no-nexus": result.includeNexus = false; break;
|
|
63
82
|
case "--secret-backup": result.includeSecretBackup = true; break;
|
package/src/commands/full.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// full 모드 오케스트레이터 (.sh execute_integration full case 등가).
|
|
2
|
-
// 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → coderabbit → gitignore
|
|
2
|
+
// 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → coderabbit → gitignore(조건부)
|
|
3
|
+
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
|
|
3
4
|
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { writeText } from "../core/fsutil.js";
|
|
@@ -46,9 +47,13 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
46
47
|
// 4. scripts (payload/scripts/*.py → .github/scripts/)
|
|
47
48
|
copyScripts(payloadRoot, targetRoot);
|
|
48
49
|
|
|
49
|
-
// 5. coderabbit (opt-in true일 때만 — DESIGN-SPEC §4 질문②)
|
|
50
|
-
|
|
51
|
-
ensureGitignore(targetRoot);
|
|
50
|
+
// 5. coderabbit (opt-in true일 때만 — DESIGN-SPEC §4 질문②)
|
|
51
|
+
const coderabbitResult = includeCodeRabbit === true ? copyCoderabbit(payloadRoot, { force }, targetRoot) : null;
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// 6. gitignore — 워크플로우/coderabbit 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
|
|
54
|
+
// 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다 — issue #7.
|
|
55
|
+
const gitignoreUpdated = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0 || coderabbitResult === "overwritten-backup";
|
|
56
|
+
if (gitignoreUpdated) ensureGitignore(targetRoot);
|
|
57
|
+
|
|
58
|
+
return { workflows: wfCounters, gitignoreUpdated };
|
|
54
59
|
}
|
|
@@ -249,6 +249,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
249
249
|
io.summary?.({
|
|
250
250
|
mode, types, version, branches, includeCodeRabbit,
|
|
251
251
|
counters: { workflows: result?.workflows?.copied ?? 0 },
|
|
252
|
+
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
252
253
|
}, cwd);
|
|
253
254
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
254
255
|
return 0;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// purge 모드 — revert가 지우는 전부(워크플로우·스크립트·coderabbit) + version.yml·README
|
|
2
|
+
// AUTO-VERSION-SECTION 블록·CHANGELOG를 추가로 제거해 설치 이전 상태로 완전히 되돌린다.
|
|
3
|
+
// 개발·테스트 전용 숨김 모드 — DESIGN-SPEC purge #6.
|
|
4
|
+
// develop 브랜치 삭제는 파일 삭제와 성격이 달라(실행 시점 git 상태 판단 필요) 여기 plan에는
|
|
5
|
+
// 포함하지 않고 index.js의 purge 분기에서 직접 처리한다.
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { existsSync, renameSync } from "node:fs";
|
|
8
|
+
import { PATHS } from "../core/paths.js";
|
|
9
|
+
import { remove } from "../core/fsutil.js";
|
|
10
|
+
import { planRevert } from "./revert.js";
|
|
11
|
+
import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/readme.js";
|
|
12
|
+
|
|
13
|
+
const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
|
|
14
|
+
|
|
15
|
+
// keepFlags: { versionYml, readme, changelog, workflows, scripts, coderabbit } — true인 카테고리는 후보에서 제외.
|
|
16
|
+
// 반환: { workflows, scripts, coderabbit, versionYml, readmeSection, changelog } — 아무것도 지우지 않는 순수 함수.
|
|
17
|
+
export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
18
|
+
const revertPlan = planRevert(payloadRoot, targetRoot);
|
|
19
|
+
return {
|
|
20
|
+
workflows: keepFlags.workflows ? [] : revertPlan.workflows,
|
|
21
|
+
scripts: keepFlags.scripts ? [] : revertPlan.scripts,
|
|
22
|
+
coderabbit: keepFlags.coderabbit ? false : revertPlan.coderabbit,
|
|
23
|
+
versionYml: !keepFlags.versionYml && existsSync(join(targetRoot, PATHS.versionFile)),
|
|
24
|
+
readmeSection: !keepFlags.readme && hasVersionSection(targetRoot),
|
|
25
|
+
changelog: keepFlags.changelog ? [] : CHANGELOG_FILES.filter((f) => existsSync(join(targetRoot, f))),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 삭제 전 요약 출력 — dry-run 미리보기와 실제 실행 전 요약 양쪽에서 재사용한다.
|
|
30
|
+
export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
31
|
+
const lines = ["",
|
|
32
|
+
dryRun
|
|
33
|
+
? "project-auto-wizard --mode purge --dry-run — 미리보기, 실제 파일은 바뀌지 않았습니다"
|
|
34
|
+
: "project-auto-wizard --mode purge — 아래 항목을 제거합니다",
|
|
35
|
+
""];
|
|
36
|
+
lines.push(`워크플로우 (${plan.workflows.length}개):`);
|
|
37
|
+
for (const f of plan.workflows) lines.push(` - ${f}`);
|
|
38
|
+
lines.push(`스크립트 (${plan.scripts.length}개):`);
|
|
39
|
+
for (const f of plan.scripts) lines.push(` - ${f}`);
|
|
40
|
+
if (plan.coderabbit) lines.push("파일: .coderabbit.yaml");
|
|
41
|
+
if (plan.versionYml) lines.push("파일: version.yml");
|
|
42
|
+
if (plan.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
|
|
43
|
+
for (const f of plan.changelog) lines.push(`파일: ${f}`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
console.log(lines.join("\n"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 실제 삭제 수행 — planPurge()와 동일 shape을 반환하되 실제로 제거된 항목을 반영한다.
|
|
49
|
+
// runRevert()를 통째로 호출하지 않는 이유: runRevert는 항상 전체를 지우므로
|
|
50
|
+
// --keep-* 로 선택적 카테고리만 보존하는 요구사항과 맞지 않는다.
|
|
51
|
+
// H3 (Fable 검토): readmeSection은 plan의 판정을 그대로 되돌려주지 않고
|
|
52
|
+
// removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 — 스펙 §6이
|
|
53
|
+
// "반환값은 실제 삭제 결과를 반영"하라고 명시하기 때문에, plan과 실제 제거 조건이
|
|
54
|
+
// 이론상 어긋나는 경우에도 printPurgeResult가 거짓으로 "제거됨"을 보고하지 않는다.
|
|
55
|
+
export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
56
|
+
const plan = planPurge(payloadRoot, targetRoot, keepFlags);
|
|
57
|
+
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
58
|
+
for (const name of plan.workflows) remove(join(wfDir, name));
|
|
59
|
+
for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
|
|
60
|
+
if (plan.coderabbit) {
|
|
61
|
+
const cr = join(targetRoot, ".coderabbit.yaml");
|
|
62
|
+
remove(cr);
|
|
63
|
+
if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
|
|
64
|
+
}
|
|
65
|
+
if (plan.versionYml) remove(join(targetRoot, PATHS.versionFile));
|
|
66
|
+
const readmeSection = plan.readmeSection && removeVersionSectionFromReadme(targetRoot) === "removed";
|
|
67
|
+
for (const f of plan.changelog) remove(join(targetRoot, f));
|
|
68
|
+
return { ...plan, readmeSection };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 삭제 후 실제 제거된 목록 출력 — printPurgePlan과 완전히 동일한 형태(파일명 나열)로
|
|
72
|
+
// 맞춘다 (M3, Fable 검토: 개수만 출력하면 스펙 §5-6의 "제거된 목록 재출력" 요구를 충족하지 못함).
|
|
73
|
+
export function printPurgeResult(result) {
|
|
74
|
+
const lines = ["", "제거됨:", ""];
|
|
75
|
+
lines.push(`워크플로우 (${result.workflows.length}개):`);
|
|
76
|
+
for (const f of result.workflows) lines.push(` - ${f}`);
|
|
77
|
+
lines.push(`스크립트 (${result.scripts.length}개):`);
|
|
78
|
+
for (const f of result.scripts) lines.push(` - ${f}`);
|
|
79
|
+
if (result.coderabbit) lines.push("파일: .coderabbit.yaml");
|
|
80
|
+
if (result.versionYml) lines.push("파일: version.yml");
|
|
81
|
+
if (result.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
|
|
82
|
+
for (const f of result.changelog) lines.push(`파일: ${f}`);
|
|
83
|
+
lines.push("(.gitignore에 추가된 백업 파일 제외 항목(*.bak/*.template.yaml)은 purge 대상에서 제외되어 그대로 보존됩니다)");
|
|
84
|
+
lines.push("");
|
|
85
|
+
console.log(lines.join("\n"));
|
|
86
|
+
}
|
package/src/commands/version.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// version 모드 (.sh execute_integration version case 등가).
|
|
2
|
-
// 순서: version.yml → readme → scripts
|
|
3
|
-
// (
|
|
2
|
+
// 순서: version.yml → readme → scripts.
|
|
3
|
+
// (워크플로우/coderabbit을 복사하지 않으므로 충돌 백업 부산물이 생길 수 없다 — gitignore 갱신 대상 없음, issue #7.
|
|
4
|
+
// util·issue·coderabbit·setup-guide는 스코프 제외.)
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { writeText } from "../core/fsutil.js";
|
|
6
7
|
import { PATHS } from "../core/paths.js";
|
|
@@ -9,7 +10,6 @@ import { readVersionYmlTemplate } from "../core/assets.js";
|
|
|
9
10
|
import { markerForType } from "../core/detect.js";
|
|
10
11
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
11
12
|
import { copyScripts } from "../core/copy/simple.js";
|
|
12
|
-
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
13
13
|
|
|
14
14
|
export function runVersion(context, payloadRoot, targetRoot = ".") {
|
|
15
15
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
@@ -28,5 +28,4 @@ export function runVersion(context, payloadRoot, targetRoot = ".") {
|
|
|
28
28
|
}));
|
|
29
29
|
addVersionSectionToReadme(version, targetRoot);
|
|
30
30
|
copyScripts(payloadRoot, targetRoot);
|
|
31
|
-
ensureGitignore(targetRoot);
|
|
32
31
|
}
|
package/src/core/branches.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// 기본 exec — git 명령 실행. 반환 {code, stdout, stderr}. 테스트는 mock 주입.
|
|
8
|
-
function defaultExec(cmd, args, { cwd } = {}) {
|
|
8
|
+
export function defaultExec(cmd, args, { cwd } = {}) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
10
|
execFile(cmd, args, { cwd, windowsHide: true }, (err, stdout, stderr) => {
|
|
11
11
|
resolve({ code: err ? (err.code ?? 1) : 0, stdout: String(stdout), stderr: String(stderr) });
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
// .gitignore 보장
|
|
1
|
+
// .gitignore 보장 — 마법사 자신이 만드는 충돌 백업 부산물(*.bak, *.template.yaml)만 대상으로 한다.
|
|
2
|
+
// 마법사가 설치하는 것과 무관한 개인 개발환경 설정(IDE 등)은 마법사 책임 범위 밖 — issue #7.
|
|
3
|
+
// 주의(의도된 트레이드오프): 이 변경 이전 버전으로 설치해 /.idea·/.claude/settings.local.json이 이미
|
|
4
|
+
// 배너 블록에 남아있는 레포에서 removeAutoAddedEntriesFromGitignore()를 실행하면, 배너는 제거되지만
|
|
5
|
+
// REQUIRED_ENTRIES가 더 이상 옛 항목과 일치하지 않아 그 두 줄은 지워지지 않고 배너 표시 없이 남는다.
|
|
6
|
+
// 이슈 #7이 "이미 설치된 레포의 .gitignore는 소급 처리하지 않고 사용자 판단에 맡긴다"고 명시하므로
|
|
7
|
+
// 별도 마이그레이션 로직을 추가하지 않는다 — 남은 항목은 무해하며 필요하면 사용자가 직접 지운다.
|
|
2
8
|
import { join } from "node:path";
|
|
3
9
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
// issue #7: 마법사가 설치하는 것과 무관한 개인 개발환경 항목(/.idea 등)은 마법사 책임 밖이므로 제거.
|
|
12
|
+
// 대신 마법사 자신의 충돌 처리(workflows.js backup/template 결정, coderabbit.js 덮어쓰기 백업)가
|
|
13
|
+
// 실제로 만들어내는 부산물만 gitignore 대상으로 삼는다.
|
|
14
|
+
const REQUIRED_ENTRIES = ["*.bak", "*.template.yaml"];
|
|
6
15
|
|
|
7
16
|
// .sh normalize_gitignore_entry: 주석 제거·트림·앞 / 제거·앞 ./ 제거·뒤 / 제거. 빈값이면 원본.
|
|
8
17
|
export function normalizeGitignoreEntry(entry) {
|
|
@@ -26,11 +35,9 @@ function entryExists(target, content) {
|
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
const NEW_FILE_CONTENT =
|
|
29
|
-
"#
|
|
30
|
-
"
|
|
31
|
-
"\n"
|
|
32
|
-
"# Claude AI Settings\n" +
|
|
33
|
-
"/.claude/settings.local.json\n";
|
|
38
|
+
"# project-auto-wizard: 충돌 처리 시 생성되는 백업 파일 (안전하게 무시해도 됩니다)\n" +
|
|
39
|
+
"*.bak\n" +
|
|
40
|
+
"*.template.yaml\n";
|
|
34
41
|
|
|
35
42
|
// 반환: {created, added:[...]}
|
|
36
43
|
export function ensureGitignore(targetRoot = ".") {
|
package/src/core/copy/readme.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
|
|
5
|
-
const MARKER = "<!-- AUTO-VERSION-SECTION";
|
|
5
|
+
export const MARKER = "<!-- AUTO-VERSION-SECTION";
|
|
6
6
|
// ## (최신 버전|최신버전|Version|버전) : vX.Y.Z (대소문자 무시)
|
|
7
7
|
const VERSION_LINE_RE = /##\s*(최신\s*버전|최신버전|Version|버전)\s*:\s*v[0-9]+\.[0-9]+\.[0-9]+/i;
|
|
8
8
|
|
|
@@ -58,7 +58,7 @@ function classify(srcDir, workflowsDir, envOpts, srcText) {
|
|
|
58
58
|
// envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
|
|
59
59
|
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
|
|
60
60
|
// 미지정 파일은 'skip'(현행 force 동작 100% 유지). 대화형 수집은 copyWorkflowsInteractive 참조.
|
|
61
|
-
// 반환: {copied, skipped, templateAdded, optionalCopied}
|
|
61
|
+
// 반환: {copied, skipped, templateAdded, optionalCopied, backupAdded}
|
|
62
62
|
export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
63
63
|
const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true } = context;
|
|
64
64
|
const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
|
|
@@ -66,7 +66,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
66
66
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
67
67
|
if (!exists(projectTypesDir)) throw new Error("패키지 구조 오류 — payload/workflows 폴더를 찾지 못했습니다.");
|
|
68
68
|
|
|
69
|
-
const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0 };
|
|
69
|
+
const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0, backupAdded: 0 };
|
|
70
70
|
const deployValues = new Map(); // Map<type, Map<key,value>> — deploy 블록용 ask 값
|
|
71
71
|
counters.deployValues = deployValues;
|
|
72
72
|
const srcText = makeSrcText(context.branches || null);
|
|
@@ -124,6 +124,7 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
124
124
|
renameSync(dst, dst + ".bak");
|
|
125
125
|
writeText(dst, srcText(src));
|
|
126
126
|
counters.copied++;
|
|
127
|
+
counters.backupAdded++;
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
if (decision === "template") {
|
|
@@ -213,7 +214,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
213
214
|
counters.skipped++;
|
|
214
215
|
continue;
|
|
215
216
|
}
|
|
216
|
-
if (existsSync(dst)) renameSync(dst, dst + ".bak");
|
|
217
|
+
if (existsSync(dst)) { renameSync(dst, dst + ".bak"); counters.backupAdded++; }
|
|
217
218
|
writeText(dst, body);
|
|
218
219
|
counters.optionalCopied++;
|
|
219
220
|
counters.copied++;
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
|
|
8
9
|
import { HELP_TEXT } from "./cli/help.js";
|
|
9
10
|
import { createContext } from "./context.js";
|
|
@@ -12,7 +13,7 @@ import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeRe
|
|
|
12
13
|
import { parseExisting } from "./core/version-yml.js";
|
|
13
14
|
import { runBreakingCheck } from "./core/breaking-check.js";
|
|
14
15
|
import { resolveProjectPaths } from "./core/paths-resolve.js";
|
|
15
|
-
import { resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch } from "./core/branches.js";
|
|
16
|
+
import { resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, defaultExec } from "./core/branches.js";
|
|
16
17
|
import { printBannerCompact } from "./ui/banner.js";
|
|
17
18
|
import { printSummary } from "./ui/summary.js";
|
|
18
19
|
import { runFull } from "./commands/full.js";
|
|
@@ -25,6 +26,7 @@ import { runInteractive } from "./commands/interactive.js";
|
|
|
25
26
|
import { runStatus, printStatus } from "./commands/status.js";
|
|
26
27
|
import { runDoctor, printDoctorReport } from "./commands/doctor.js";
|
|
27
28
|
import { planDryRun, printDryRun } from "./commands/dry-run.js";
|
|
29
|
+
import { planPurge, executePurge, printPurgePlan, printPurgeResult } from "./commands/purge.js";
|
|
28
30
|
|
|
29
31
|
// 패키지 버전 읽기 (-v/--version 출력용). src/../package.json.
|
|
30
32
|
function readPkgVersion() {
|
|
@@ -45,10 +47,24 @@ function utcNow(date = new Date()) {
|
|
|
45
47
|
return { now: `${d} ${t}`, today: d };
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
//
|
|
50
|
+
// purge TTY 확인 — 실제 stdin에서 한 줄 입력을 받는다 (테스트는 promptRepoName 주입으로 대체).
|
|
51
|
+
async function defaultPromptRepoName(repoName) {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
try {
|
|
54
|
+
return await rl.question(`purge를 실행하려면 정확히 이 레포명을 입력하세요: ${repoName}\n> `);
|
|
55
|
+
} finally {
|
|
56
|
+
rl.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// run(argv, opts) → exitCode. opts: { cwd, payloadRoot?, clock?, exec?, promptRepoName? }
|
|
49
61
|
// payloadRoot: 테스트 픽스처 주입점 (기본: 패키지 동봉 payload/)
|
|
50
62
|
// clock: {now, today} 주입 (기본 현재 UTC).
|
|
51
|
-
|
|
63
|
+
// exec/promptRepoName: purge 모드 안전장치 게이트용 주입점 (기본 실제 구현, 테스트는 mock 주입).
|
|
64
|
+
export async function run(argv, {
|
|
65
|
+
cwd = process.cwd(), payloadRoot, clock,
|
|
66
|
+
exec = defaultExec, promptRepoName = defaultPromptRepoName,
|
|
67
|
+
} = {}) {
|
|
52
68
|
let opts;
|
|
53
69
|
try {
|
|
54
70
|
opts = parseArgs(argv);
|
|
@@ -91,6 +107,80 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
|
|
|
91
107
|
console.error("version.yml·README·.gitignore는 보존됩니다 (사용자 데이터).");
|
|
92
108
|
return 0;
|
|
93
109
|
}
|
|
110
|
+
// purge 모드 — 마법사가 만든 모든 산출물을 지워 설치 이전 상태로 완전히 되돌린다.
|
|
111
|
+
// 개발·테스트 전용 숨김 모드 — --help/대화형 메뉴에 노출하지 않는다 (issue #6).
|
|
112
|
+
if (opts.mode === "purge") {
|
|
113
|
+
if (!existsSync(join(cwd, ".git"))) {
|
|
114
|
+
console.error("git 레포가 아닙니다(.git 없음) — purge는 git 레포 안에서만 실행할 수 있습니다.");
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
const keepFlags = {
|
|
118
|
+
versionYml: opts.keepVersionYml, readme: opts.keepReadme, changelog: opts.keepChangelog,
|
|
119
|
+
workflows: opts.keepWorkflows, scripts: opts.keepScripts, coderabbit: opts.keepCoderabbit,
|
|
120
|
+
};
|
|
121
|
+
// version.yml은 여기서 미리 읽어둔다 — (a) executePurge()가 version.yml 자체를 지울 수 있어
|
|
122
|
+
// 실행 이후에는 읽을 수 없고, (b) 아래 dry-run 예고 문구도 trunk-based 여부(develop === main)를
|
|
123
|
+
// 알아야 실제 실행 시 삭제를 건너뛸지 미리 알릴 수 있기 때문에, 두 지점보다 앞서 읽어야 한다.
|
|
124
|
+
const vyPath = join(cwd, "version.yml");
|
|
125
|
+
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
126
|
+
if (opts.dryRun) {
|
|
127
|
+
printPurgePlan(planPurge(payload, cwd, keepFlags), { dryRun: true });
|
|
128
|
+
// M4 (Fable 검토): develop 브랜치 삭제는 plan에 포함되지 않으므로(§6 — git 상태는 실행 시점에만
|
|
129
|
+
// 판단 가능) 별도로 예고하지 않으면 dry-run 미리보기가 유일한 파괴적 동작을 사용자에게 숨기게 된다.
|
|
130
|
+
if (opts.deleteDevelopBranch) {
|
|
131
|
+
const developBranch = existing?.branches?.develop || "develop";
|
|
132
|
+
if (existing?.branches?.main && developBranch === existing.branches.main) {
|
|
133
|
+
console.log("(--delete-develop-branch 지정됨: trunk-based 구성(develop === main)이라 실제 실행 시에도 삭제를 건너뜁니다)");
|
|
134
|
+
} else {
|
|
135
|
+
console.log("(--delete-develop-branch 지정됨: 실제 실행 시 로컬 develop 브랜치도 삭제를 시도합니다)");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (!opts.yes) {
|
|
141
|
+
console.error("--yes 없이는 purge를 실행할 수 없습니다 (--force로 대체할 수 없습니다).");
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const st = await exec("git", ["status", "--porcelain"], { cwd });
|
|
145
|
+
if (st.code !== 0) {
|
|
146
|
+
console.error("git 상태를 확인할 수 없습니다 — 안전을 위해 purge를 중단합니다.");
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
if (!opts.allowDirty && st.stdout.trim() !== "") {
|
|
150
|
+
console.error("작업트리에 커밋되지 않은 변경 사항이 있습니다 — purge 후 복구할 수 없습니다. 커밋하거나 --allow-dirty를 사용하세요.");
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
if (!opts.force) {
|
|
154
|
+
if (!process.stdout.isTTY) {
|
|
155
|
+
console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const repoName = detectRepoName(cwd);
|
|
159
|
+
const typed = await promptRepoName(repoName);
|
|
160
|
+
if (typed !== repoName) {
|
|
161
|
+
console.error("입력한 레포명이 일치하지 않습니다 — purge를 중단합니다.");
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const plan = planPurge(payload, cwd, keepFlags);
|
|
166
|
+
printPurgePlan(plan, { dryRun: false });
|
|
167
|
+
const result = executePurge(payload, cwd, keepFlags);
|
|
168
|
+
printPurgeResult(result);
|
|
169
|
+
if (opts.deleteDevelopBranch) {
|
|
170
|
+
const developBranch = existing?.branches?.develop || "develop";
|
|
171
|
+
if (existing?.branches?.main && developBranch === existing.branches.main) {
|
|
172
|
+
console.error("trunk-based 구성(develop === main)입니다 — 릴리스 브랜치 삭제는 건너뜁니다.");
|
|
173
|
+
} else {
|
|
174
|
+
const br = await exec("git", ["branch", "-d", developBranch], { cwd });
|
|
175
|
+
if (br.code !== 0) {
|
|
176
|
+
console.error(`⚠️ 로컬 '${developBranch}' 브랜치 삭제 실패 (${(br.stderr || "").trim() || "이유 확인 불가"}) — 수동으로 확인하세요.`);
|
|
177
|
+
} else {
|
|
178
|
+
console.error(`로컬 '${developBranch}' 브랜치를 삭제했습니다.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
94
184
|
|
|
95
185
|
// uninstall 모드 — revert보다 넓게 README·gitignore·version.yml까지 선택적으로 제거.
|
|
96
186
|
if (opts.mode === "uninstall") {
|
|
@@ -221,6 +311,7 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
|
|
|
221
311
|
mode: opts.mode, types, version, branches,
|
|
222
312
|
includeCodeRabbit: context.includeCodeRabbit === true,
|
|
223
313
|
counters: { workflows: result?.workflows?.copied ?? 0 },
|
|
314
|
+
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
224
315
|
}, cwd);
|
|
225
316
|
return 0;
|
|
226
317
|
}
|
package/src/ui/summary.js
CHANGED
|
@@ -8,7 +8,7 @@ import { listYamlFiles } from "../core/fsutil.js";
|
|
|
8
8
|
const SEPARATOR = "────────────────────────────────────────";
|
|
9
9
|
|
|
10
10
|
export function printSummary(ctx, targetRoot = ".") {
|
|
11
|
-
const { mode, types = [], version = "", counters = {}, branches = null, includeCodeRabbit = false } = ctx || {};
|
|
11
|
+
const { mode, types = [], version = "", counters = {}, branches = null, includeCodeRabbit = false, gitignoreUpdated = false } = ctx || {};
|
|
12
12
|
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
13
13
|
// 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
|
|
14
14
|
const isTty = !!process.stderr.isTTY;
|
|
@@ -32,12 +32,11 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
32
32
|
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
33
33
|
err(" ✅ README.md 자동 버전 업데이트");
|
|
34
34
|
err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
|
|
35
|
-
err(" ✅ .gitignore
|
|
35
|
+
if (gitignoreUpdated) err(" ✅ .gitignore 백업 파일 제외 항목 (*.bak/*.template.yaml)");
|
|
36
36
|
break;
|
|
37
37
|
case "version":
|
|
38
38
|
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
39
39
|
err(" ✅ README.md 자동 버전 업데이트");
|
|
40
|
-
err(" ✅ .gitignore 필수 항목");
|
|
41
40
|
break;
|
|
42
41
|
case "workflows":
|
|
43
42
|
err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
|