project-auto-wizard 0.1.7 → 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.6 (2026-07-27)
19
+ ## 최신 버전 : v0.1.9 (2026-08-02)
20
20
 
21
21
  [전체 버전 기록 보기](CHANGELOG.md)
22
22
 
@@ -73,6 +73,20 @@ flutter.APP_ARTIFACT_NAME:
73
73
 
74
74
  `npx project-auto-wizard --mode revert`는 payload가 설치한 파일명과 **정확히 일치하는 것만** 제거합니다. 사용자가 직접 만든 워크플로우, `version.yml`, `README.md`, `.gitignore`는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
75
75
 
76
+ ### 완전 삭제(`--mode uninstall`)
77
+
78
+ `npx project-auto-wizard --mode uninstall`은 `revert`보다 넓게 제거합니다 — 워크플로우·스크립트·`.coderabbit.yaml`은 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
79
+
80
+ - **대화형(TTY)**: 실제로 설치된 항목만 체크리스트로 보여줍니다. 워크플로우/스크립트/`.coderabbit.yaml`은 기본 체크, README·`.gitignore`·`version.yml`은 opt-in입니다. 선택 후 최종 확인(기본 "아니오")을 거쳐야 실제로 삭제됩니다.
81
+ - **비대화형(`--force`)**: 워크플로우·스크립트·`.coderabbit.yaml`만 기본 삭제합니다. README·`.gitignore`·`version.yml`까지 지우려면 `--purge-readme`/`--purge-gitignore`/`--purge-version`을 함께 지정하세요.
82
+ - `--dry-run`과 함께 쓰면 무엇이 지워질지 미리 볼 수 있습니다.
83
+
84
+ ```bash
85
+ npx project-auto-wizard --mode uninstall # 대화형 체크리스트
86
+ npx project-auto-wizard --mode uninstall --force # 워크플로우·스크립트·coderabbit만 안전 삭제
87
+ npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version # 완전 삭제
88
+ ```
89
+
76
90
  ## API 키 0개 AI — 요약 엔진 체인
77
91
 
78
92
  릴리스 노트는 4단 엔진 체인으로 생성됩니다. **어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
@@ -111,7 +125,7 @@ flowchart LR
111
125
  ```
112
126
  npx project-auto-wizard [옵션]
113
127
 
114
- -m, --mode MODE full | version | workflows | revert | status | doctor (기본: 대화형)
128
+ -m, --mode MODE full | version | workflows | revert | uninstall | status | doctor (기본: 대화형)
115
129
  -t, --type CSV spring,react,... (미지정 시 자동 감지)
116
130
  --project-version V 초기 버전 (미지정 시 자동 감지)
117
131
  --paths "t=p,..." 모노레포 타입별 경로
@@ -122,6 +136,9 @@ npx project-auto-wizard [옵션]
122
136
  --coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로
123
137
  --semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함, --no-semver-auto로 끔)
124
138
  --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌
139
+ --purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
140
+ --purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
141
+ --purge-version --mode uninstall --force 시 version.yml도 제거
125
142
  --force 전 질문 생략 (CI용)
126
143
  ```
127
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.7",
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
@@ -19,6 +19,19 @@ export function parseArgs(argv) {
19
19
  help: false,
20
20
  showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
21
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도 제거
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,
22
35
  };
23
36
  const args = [...argv];
24
37
  while (args.length > 0) {
@@ -52,6 +65,18 @@ export function parseArgs(argv) {
52
65
  }
53
66
  case "--force": result.force = true; break;
54
67
  case "--dry-run": result.dryRun = true; break;
68
+ case "--purge-readme": result.purgeReadme = true; break;
69
+ case "--purge-gitignore": result.purgeGitignore = true; break;
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;
55
80
  case "--nexus": result.includeNexus = true; break;
56
81
  case "--no-nexus": result.includeNexus = false; break;
57
82
  case "--secret-backup": result.includeSecretBackup = true; break;
package/src/cli/help.js CHANGED
@@ -5,8 +5,9 @@ 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 | status | doctor)
8
+ -m, --mode MODE 통합 모드 (full | version | workflows | revert | uninstall | status | doctor)
9
9
  기본: interactive (대화형). revert = 설치물 제거(되돌리기)
10
+ uninstall = 완전 삭제(대화형 체크리스트, --force 시 --purge-*로 opt-in)
10
11
  status = 설치 상태·드리프트 확인(읽기 전용). doctor = 환경 진단(읽기 전용)
11
12
  -t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
12
13
  지원: spring flutter next react react-native
@@ -20,7 +21,10 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
20
21
  --coderabbit / --no-coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로 사용 (기본: 사용 안 함)
21
22
  --semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
22
23
  --force 모든 확인 생략, 비대화형 기본값 사용
23
- --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/version/workflows/revert 전체 지원)
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도 제거
24
28
  -v, --version project-auto-wizard 버전 출력
25
29
  -h, --help 이 도움말 표시
26
30
 
@@ -30,4 +34,5 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
30
34
  npx project-auto-wizard --mode status
31
35
  npx project-auto-wizard --mode doctor
32
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
33
38
  `;
@@ -1,10 +1,11 @@
1
1
  // --dry-run 미리보기 — 실제 파일을 쓰지 않고 무엇이 바뀔지 계산한다.
2
- // full/version/workflows/revert 4개 모드 전체 지원.
2
+ // full/version/workflows/revert/uninstall 5개 모드 전체 지원.
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { PATHS } from "../core/paths.js";
6
6
  import { planWorkflows } from "../core/copy/workflows.js";
7
7
  import { planRevert } from "./revert.js";
8
+ import { planUninstall } from "./uninstall.js";
8
9
  import { buildVersionYml } from "../core/version-yml.js";
9
10
  import { readVersionYmlTemplate } from "../core/assets.js";
10
11
  import { markerForType } from "../core/detect.js";
@@ -31,9 +32,12 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
31
32
  return { existed: existing !== null, changed: existing !== wouldBe };
32
33
  }
33
34
 
34
- // mode: "full" | "version" | "workflows" | "revert". 읽기 전용 — 아무 파일도 쓰지 않는다.
35
+ // mode: "full" | "version" | "workflows" | "revert" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
35
36
  export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
36
37
  if (mode === "revert") return { mode, revert: planRevert(payloadRoot, targetRoot) };
38
+ if (mode === "uninstall") {
39
+ return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
40
+ }
37
41
 
38
42
  const result = { mode };
39
43
  if (mode === "full" || mode === "workflows") {
@@ -54,6 +58,16 @@ export function printDryRun(plan) {
54
58
  lines.push(`제거될 스크립트 (${r.scripts.length}개):`);
55
59
  for (const f of r.scripts) lines.push(` - ${f}`);
56
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");
57
71
  } else {
58
72
  if (plan.workflows) {
59
73
  const w = plan.workflows;
@@ -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,
@@ -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
+ }
@@ -0,0 +1,129 @@
1
+ // uninstall 모드 — revert(payload 파일명 일치분)보다 넓게, README/.gitignore/version.yml까지 선택적으로 제거.
2
+ // 대화형 체크리스트 또는 --force + --purge-* 로 항목별 opt-in. revert.js는 건드리지 않고 읽기 전용으로만 재사용한다.
3
+ import { join } from "node:path";
4
+ import { existsSync, renameSync } from "node:fs";
5
+ import { PATHS } from "../core/paths.js";
6
+ import { remove } from "../core/fsutil.js";
7
+ import { planRevert } from "./revert.js";
8
+ import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/readme.js";
9
+ import { removeAutoAddedEntriesFromGitignore, hasAutoAddedEntries } from "../core/copy/gitignore.js";
10
+ import { CANCEL } from "../ui/prompts.js";
11
+
12
+ // selection: { workflows, scripts, coderabbit, readme, gitignore, versionYml } (모두 boolean).
13
+ // 반환: 위와 동일한 키의 boolean/배열 — 실제로 제거 "대상"인지 여부(순수 함수, 아무것도 지우지 않음).
14
+ export function planUninstall(payloadRoot, targetRoot, selection) {
15
+ const revertPlan = planRevert(payloadRoot, targetRoot);
16
+ return {
17
+ workflows: selection.workflows ? revertPlan.workflows : [],
18
+ scripts: selection.scripts ? revertPlan.scripts : [],
19
+ coderabbit: selection.coderabbit ? revertPlan.coderabbit : false,
20
+ readme: selection.readme ? hasVersionSection(targetRoot) : false,
21
+ gitignore: selection.gitignore ? hasAutoAddedEntries(targetRoot) : false,
22
+ versionYml: selection.versionYml ? existsSync(join(targetRoot, PATHS.versionFile)) : false,
23
+ };
24
+ }
25
+
26
+ // 반환: planUninstall과 동일한 형태 — 실제로 제거된 항목.
27
+ export function runUninstall(context, payloadRoot, targetRoot, selection) {
28
+ const plan = planUninstall(payloadRoot, targetRoot, selection);
29
+ const wfDir = join(targetRoot, PATHS.workflowsDir);
30
+ for (const name of plan.workflows) remove(join(wfDir, name));
31
+ for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
32
+ if (plan.coderabbit) {
33
+ const cr = join(targetRoot, ".coderabbit.yaml");
34
+ remove(cr);
35
+ if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
36
+ }
37
+ // removeVersionSectionFromReadme/removeAutoAddedEntriesFromGitignore는 plan이 "제거 대상"으로
38
+ // 판단했더라도 실제로는 안전하게 포기(skip-*)할 수 있다 — 반환 상태를 그대로 신뢰하지 않고
39
+ // 실제 결과로 plan을 덮어써서 호출부(CLI/대화형 요약)가 거짓 성공을 보고하지 않게 한다.
40
+ const readmeRemoved = plan.readme && removeVersionSectionFromReadme(targetRoot) === "removed";
41
+ const gitignoreStatus = plan.gitignore ? removeAutoAddedEntriesFromGitignore(targetRoot) : null;
42
+ const gitignoreRemoved = gitignoreStatus === "removed" || gitignoreStatus === "file-deleted";
43
+ if (plan.versionYml) remove(join(targetRoot, PATHS.versionFile));
44
+ return { ...plan, readme: readmeRemoved, gitignore: gitignoreRemoved };
45
+ }
46
+
47
+ // ── 대화형 체크리스트 흐름 ────────────────────────────────────────────
48
+ const ITEM_DEFS = [
49
+ { key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml)" },
50
+ { key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
51
+ { key: "coderabbit", label: ".coderabbit.yaml" },
52
+ { key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
53
+ { key: "gitignore", label: ".gitignore 자동 추가 항목" },
54
+ { key: "versionYml", label: "version.yml (버전/브랜치 설정 전체)" },
55
+ ];
56
+
57
+ // 기본 체크 상태 — 설치 시 옵션(nexus/secret-backup/coderabbit)이 opt-in인 것과 대칭으로,
58
+ // 여기서는 "안전 삭제" 3종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
59
+ export const SAFE_ITEMS = ["workflows", "scripts", "coderabbit"];
60
+
61
+ function detectAvailableItems(payloadRoot, targetRoot) {
62
+ const revertPlan = planRevert(payloadRoot, targetRoot);
63
+ const presence = {
64
+ workflows: revertPlan.workflows.length > 0,
65
+ scripts: revertPlan.scripts.length > 0,
66
+ coderabbit: revertPlan.coderabbit,
67
+ readme: hasVersionSection(targetRoot),
68
+ gitignore: hasAutoAddedEntries(targetRoot),
69
+ versionYml: existsSync(join(targetRoot, PATHS.versionFile)),
70
+ };
71
+ return ITEM_DEFS.filter((d) => presence[d.key]).map((d) => ({ value: d.key, label: d.label }));
72
+ }
73
+
74
+ function toSelection(checkedKeys) {
75
+ const set = new Set(checkedKeys);
76
+ return {
77
+ workflows: set.has("workflows"), scripts: set.has("scripts"), coderabbit: set.has("coderabbit"),
78
+ readme: set.has("readme"), gitignore: set.has("gitignore"), versionYml: set.has("versionYml"),
79
+ };
80
+ }
81
+
82
+ function summarizeSelection(selection) {
83
+ const labelOf = Object.fromEntries(ITEM_DEFS.map((d) => [d.key, d.label]));
84
+ const chosen = Object.keys(selection).filter((k) => selection[k]).map((k) => `- ${labelOf[k]}`);
85
+ return chosen.length ? chosen.join("\n") : "(선택된 항목 없음)";
86
+ }
87
+
88
+ function summarizeResult(result) {
89
+ const lines = [];
90
+ if (result.workflows.length) lines.push(`워크플로우 ${result.workflows.length}개 제거`);
91
+ if (result.scripts.length) lines.push(`스크립트 ${result.scripts.length}개 제거`);
92
+ if (result.coderabbit) lines.push(".coderabbit.yaml 제거");
93
+ if (result.readme) lines.push("README.md 버전 섹션 제거");
94
+ if (result.gitignore) lines.push(".gitignore 자동 추가 항목 제거");
95
+ if (result.versionYml) lines.push("version.yml 제거");
96
+ return lines.length ? lines.join("\n") : "제거된 항목이 없습니다.";
97
+ }
98
+
99
+ // io 계약: engineIo.multiselect({message,options,initialValues}), askYesNo(msg,def),
100
+ // note(text,title)?, cancelMessage(text)? — src/ui/prompts.js가 실물, 테스트는 스텁 주입.
101
+ export async function runUninstallFlow(payloadRoot, targetRoot, io) {
102
+ const available = detectAvailableItems(payloadRoot, targetRoot);
103
+ if (available.length === 0) {
104
+ io.note?.("제거할 항목이 없습니다.", "완전 삭제");
105
+ return null;
106
+ }
107
+
108
+ const checked = await io.engineIo.multiselect({
109
+ message: "삭제할 항목을 선택하세요 (Space 토글, Enter 확정)",
110
+ options: available,
111
+ initialValues: available.map((o) => o.value).filter((v) => SAFE_ITEMS.includes(v)),
112
+ });
113
+ if (checked === CANCEL || !Array.isArray(checked) || checked.length === 0) {
114
+ io.cancelMessage?.("완전 삭제를 취소했습니다.");
115
+ return null;
116
+ }
117
+
118
+ const selection = toSelection(checked);
119
+ io.note?.(summarizeSelection(selection), "삭제 예정 항목");
120
+ const ok = await io.askYesNo("정말 삭제할까요? 되돌릴 수 없습니다.", false);
121
+ if (ok !== true) {
122
+ io.cancelMessage?.("완전 삭제를 취소했습니다.");
123
+ return null;
124
+ }
125
+
126
+ const result = runUninstall({}, payloadRoot, targetRoot, selection);
127
+ io.note?.(summarizeResult(result), "완전 삭제 완료");
128
+ return result;
129
+ }
@@ -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,6 +1,6 @@
1
1
  // .gitignore 보장 (.sh ensure_gitignore + normalize/check 등가) — template_integrator.sh 3996~4111.
2
2
  import { join } from "node:path";
3
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
4
4
 
5
5
  const REQUIRED_ENTRIES = ["/.idea", "/.claude/settings.local.json"];
6
6
 
@@ -53,3 +53,55 @@ export function ensureGitignore(targetRoot = ".") {
53
53
  writeFileSync(p, content);
54
54
  return { created: false, added: toAdd };
55
55
  }
56
+
57
+ // ensureGitignore가 기존 파일에 배너 블록을 추가할 때 항상 이 정확한 시퀀스로 시작한다
58
+ // (빈 줄 하나 + 3줄 배너). 배너 직후, REQUIRED_ENTRIES와 일치하는 라인이 연속되는 동안만
59
+ // "마법사가 추가한 항목"이다 — 그 뒤에 사용자가 나중에 추가한 항목은 절대 건드리지 않는다.
60
+ const BANNER =
61
+ "\n" +
62
+ "# ====================================================================\n" +
63
+ "# project-auto-wizard: Auto-added entries\n" +
64
+ "# ====================================================================\n";
65
+
66
+ // .gitignore에 마법사가 추가한 항목이 있는지 확인 (체크리스트 노출 판단용).
67
+ // 두 케이스: (1) 원래 없던 파일을 통째로(또는 그 뒤에 사용자가 이어 쓴 형태로) 새로 만든 경우
68
+ // (2) 기존 파일에 배너 블록을 붙인 경우.
69
+ export function hasAutoAddedEntries(targetRoot = ".") {
70
+ const p = join(targetRoot, ".gitignore");
71
+ if (!existsSync(p)) return false;
72
+ const content = readFileSync(p, "utf8");
73
+ return content.startsWith(NEW_FILE_CONTENT) || content.includes(BANNER);
74
+ }
75
+
76
+ // 반환: 'removed' | 'file-deleted' | 'skip-no-gitignore' | 'skip-not-found'
77
+ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
78
+ const p = join(targetRoot, ".gitignore");
79
+ if (!existsSync(p)) return "skip-no-gitignore";
80
+ const content = readFileSync(p, "utf8");
81
+
82
+ // 원래 파일이 없었는데 마법사가 통째로 만든 경우 — 그 뒤에 사용자가 이어서 추가한 내용만 보존.
83
+ if (content.startsWith(NEW_FILE_CONTENT)) {
84
+ const remainder = content.slice(NEW_FILE_CONTENT.length);
85
+ if (remainder === "") {
86
+ rmSync(p);
87
+ return "file-deleted";
88
+ }
89
+ writeFileSync(p, remainder);
90
+ return "removed";
91
+ }
92
+
93
+ // 기존 파일에 배너 블록이 붙은 경우 — 배너 직후 REQUIRED_ENTRIES와 일치하는 라인이 연속되는
94
+ // 동안만 제거하고, 그 뒤(사용자가 나중에 추가한 항목)는 그대로 둔다.
95
+ const idx = content.indexOf(BANNER);
96
+ if (idx === -1) return "skip-not-found";
97
+ const afterBanner = idx + BANNER.length;
98
+ const lines = content.slice(afterBanner).split("\n");
99
+ let consumed = 0;
100
+ for (const line of lines) {
101
+ const isKnownEntry = REQUIRED_ENTRIES.some((e) => normalizeGitignoreEntry(line) === normalizeGitignoreEntry(e));
102
+ if (!isKnownEntry) break;
103
+ consumed += line.length + 1; // +1: split이 삼킨 "\n"
104
+ }
105
+ writeFileSync(p, content.slice(0, idx) + content.slice(afterBanner + consumed));
106
+ return "removed";
107
+ }
@@ -1,8 +1,8 @@
1
1
  // README 버전 섹션 추가 (.sh add_version_section_to_readme 등가) — template_integrator.sh 2145~2181.
2
2
  import { join } from "node:path";
3
- import { existsSync, readFileSync, appendFileSync } from "node:fs";
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
 
@@ -28,3 +28,54 @@ export function addVersionSectionToReadme(version, targetRoot = ".") {
28
28
  appendFileSync(p, section);
29
29
  return "added";
30
30
  }
31
+
32
+ // addVersionSectionToReadme가 항상 파일 맨 끝에 붙이는 접두/접미 시퀀스.
33
+ // 접두(SECTION_PREFIX)가 있으면 마법사가 append한 "블록"이다 — 이 블록은 항상 SECTION_TAIL
34
+ // 라인으로 끝나므로, 그 지점까지만 잘라내야 그 뒤에 사용자가 나중에 덧붙인 내용(라이선스 절 등)을
35
+ // 지우지 않는다("파일 끝까지" 자르면 사용자 콘텐츠가 소실된다).
36
+ const SECTION_PREFIX = "\n---\n\n" + MARKER;
37
+ const SECTION_TAIL = "[전체 버전 기록 보기](CHANGELOG.md)\n";
38
+ // PROJECT-COMMON-README-VERSION-UPDATE.yaml(설치되는 CI)은 설치 시점에 이미 사용자가 자기 버전
39
+ // 라인을 갖고 있던 README(addVersionSectionToReadme가 'skip-version-line'으로 건너뛴 경우)에는
40
+ // '---' 구분자 없이 마커 주석 한 줄만 그 버전 라인 위에 끼워넣는다. 이 경우 버전 라인 자체는
41
+ // 사용자 소유이므로 지우지 않고 마커 주석 한 줄만 제거한다.
42
+ const MARKER_LINE = "<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n";
43
+
44
+ // README.md에 마법사(또는 설치된 CI)가 추가한 흔적이 있는지 확인 (체크리스트 노출 판단용).
45
+ export function hasVersionSection(targetRoot = ".") {
46
+ const p = join(targetRoot, "README.md");
47
+ if (!existsSync(p)) return false;
48
+ const content = readFileSync(p, "utf8");
49
+ return content.includes(SECTION_PREFIX) || content.includes(MARKER_LINE);
50
+ }
51
+
52
+ // 반환: 'removed' | 'skip-no-readme' | 'skip-no-marker' | 'skip-unexpected-format'
53
+ export function removeVersionSectionFromReadme(targetRoot = ".") {
54
+ const p = join(targetRoot, "README.md");
55
+ if (!existsSync(p)) return "skip-no-readme";
56
+ const content = readFileSync(p, "utf8");
57
+
58
+ const idx = content.indexOf(SECTION_PREFIX);
59
+ if (idx !== -1) {
60
+ // 마법사가 append한 전체 블록 케이스 — SECTION_TAIL 라인까지만 잘라내고 그 이후는 보존한다.
61
+ // tail을 못 찾거나(사용자가 그 줄을 지웠다면), 위저드 블록치고 너무 먼 곳에서 발견되면
62
+ // (사용자가 같은 문구를 자기 문서 어딘가로 옮겨 적은 경우) 그 사이 사용자 콘텐츠까지
63
+ // 지워버릴 수 있으므로 어디까지가 "마법사 구간"인지 확신할 수 없어 안전하게 포기한다.
64
+ // 위저드 블록은 버전 문자열이 길어져도 수백 바이트를 넘지 않는다.
65
+ const MAX_SECTION_LENGTH = 300;
66
+ const tailIdx = content.indexOf(SECTION_TAIL, idx);
67
+ if (tailIdx === -1 || tailIdx > idx + MAX_SECTION_LENGTH) return "skip-unexpected-format";
68
+ const cutEnd = tailIdx + SECTION_TAIL.length;
69
+ writeFileSync(p, content.slice(0, idx) + content.slice(cutEnd));
70
+ return "removed";
71
+ }
72
+
73
+ const markerIdx = content.indexOf(MARKER_LINE);
74
+ if (markerIdx !== -1) {
75
+ // CI가 사용자의 기존 버전 라인 위에 마커만 끼워넣은 케이스 — 버전 라인은 건드리지 않는다.
76
+ writeFileSync(p, content.slice(0, markerIdx) + content.slice(markerIdx + MARKER_LINE.length));
77
+ return "removed";
78
+ }
79
+
80
+ return "skip-no-marker";
81
+ }
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,17 +13,20 @@ 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";
19
20
  import { runVersion } from "./commands/version.js";
20
21
  import { runWorkflows } from "./commands/workflows.js";
21
22
  import { runRevert } from "./commands/revert.js";
23
+ import { runUninstall, runUninstallFlow } from "./commands/uninstall.js";
24
+ import * as prompts from "./ui/prompts.js";
22
25
  import { runInteractive } from "./commands/interactive.js";
23
26
  import { runStatus, printStatus } from "./commands/status.js";
24
27
  import { runDoctor, printDoctorReport } from "./commands/doctor.js";
25
28
  import { planDryRun, printDryRun } from "./commands/dry-run.js";
29
+ import { planPurge, executePurge, printPurgePlan, printPurgeResult } from "./commands/purge.js";
26
30
 
27
31
  // 패키지 버전 읽기 (-v/--version 출력용). src/../package.json.
28
32
  function readPkgVersion() {
@@ -43,10 +47,24 @@ function utcNow(date = new Date()) {
43
47
  return { now: `${d} ${t}`, today: d };
44
48
  }
45
49
 
46
- // 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? }
47
61
  // payloadRoot: 테스트 픽스처 주입점 (기본: 패키지 동봉 payload/)
48
62
  // clock: {now, today} 주입 (기본 현재 UTC).
49
- 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
+ } = {}) {
50
68
  let opts;
51
69
  try {
52
70
  opts = parseArgs(argv);
@@ -63,11 +81,11 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
63
81
  if (opts.mode === "interactive") {
64
82
  // --dry-run은 대화형 모드에서 조용히 무시되면 안 됨(실제 설치가 진행돼버림) — 명시 에러로 차단.
65
83
  if (opts.dryRun) {
66
- console.error("--dry-run은 --mode <full|version|workflows|revert>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
84
+ console.error("--dry-run은 --mode <full|version|workflows|revert|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
67
85
  return 1;
68
86
  }
69
87
  if (!process.stdout.isTTY) {
70
- console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|revert> 와 --force 를 지정하세요.");
88
+ console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|revert|uninstall> 와 --force 를 지정하세요.");
71
89
  return 1;
72
90
  }
73
91
  return await runInteractive({}, { cwd, payloadRoot: payload, clock });
@@ -89,6 +107,109 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
89
107
  console.error("version.yml·README·.gitignore는 보존됩니다 (사용자 데이터).");
90
108
  return 0;
91
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
+ }
184
+
185
+ // uninstall 모드 — revert보다 넓게 README·gitignore·version.yml까지 선택적으로 제거.
186
+ if (opts.mode === "uninstall") {
187
+ const safeSelection = {
188
+ workflows: true, scripts: true, coderabbit: true,
189
+ readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
190
+ };
191
+ if (opts.dryRun) {
192
+ printDryRun(planDryRun("uninstall", { uninstallSelection: safeSelection }, payload, cwd));
193
+ return 0;
194
+ }
195
+ if (opts.force) {
196
+ const r = runUninstall({}, payload, cwd, safeSelection);
197
+ const removed = [
198
+ `워크플로우 ${r.workflows.length}개`, `스크립트 ${r.scripts.length}개`,
199
+ r.coderabbit && ".coderabbit.yaml", r.readme && "README 버전 섹션",
200
+ r.gitignore && ".gitignore 자동 추가 항목", r.versionYml && "version.yml",
201
+ ].filter(Boolean).join(", ");
202
+ console.error(`제거됨 — ${removed}`);
203
+ return 0;
204
+ }
205
+ if (!process.stdout.isTTY) {
206
+ console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
207
+ return 1;
208
+ }
209
+ await runUninstallFlow(payload, cwd, prompts);
210
+ return 0;
211
+ }
212
+
92
213
  // status 모드 — 읽기 전용, TTY/--force 무관하게 항상 동작
93
214
  if (opts.mode === "status") {
94
215
  printStatus(runStatus(payload, cwd));
package/src/ui/prompts.js CHANGED
@@ -14,6 +14,7 @@ export async function selectMode() {
14
14
  { value: "version", label: "버전 관리만 — 버전 자동 증가·동기화 시스템만 설치" },
15
15
  { value: "workflows", label: "워크플로우만 — 빌드·배포 GitHub Actions만 설치" },
16
16
  { value: "revert", label: "되돌리기 — 마법사가 설치한 워크플로우·스크립트 제거" },
17
+ { value: "uninstall", label: "완전 삭제 — 마법사가 설치·수정한 모든 항목 제거(확인 후, README·gitignore·version.yml 포함)" },
17
18
  ],
18
19
  });
19
20
  }