project-auto-wizard 0.1.8 → 0.1.9

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 CHANGED
@@ -16,7 +16,7 @@ npx project-auto-wizard
16
16
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
17
17
 
18
18
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
19
- ## 최신 버전 : v0.1.7 (2026-07-29)
19
+ ## 최신 버전 : v0.1.9 (2026-08-02)
20
20
 
21
21
  [전체 버전 기록 보기](CHANGELOG.md)
22
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
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;
@@ -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 자동 추가 블록은 보존됩니다 — 별도 이슈에서 다룸)");
84
+ lines.push("");
85
+ console.log(lines.join("\n"));
86
+ }
@@ -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) });
@@ -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
 
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
- // run(argv, opts) exitCode. opts: { cwd, payloadRoot?, clock? }
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
- export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}) {
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") {