project-auto-wizard 0.1.7 → 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 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.7 (2026-07-29)
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.8",
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,9 @@ 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도 제거
22
25
  };
23
26
  const args = [...argv];
24
27
  while (args.length > 0) {
@@ -52,6 +55,9 @@ export function parseArgs(argv) {
52
55
  }
53
56
  case "--force": result.force = true; break;
54
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;
55
61
  case "--nexus": result.includeNexus = true; break;
56
62
  case "--no-nexus": result.includeNexus = false; break;
57
63
  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,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
+ }
@@ -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,6 +1,6 @@
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
5
  const MARKER = "<!-- AUTO-VERSION-SECTION";
6
6
  // ## (최신 버전|최신버전|Version|버전) : vX.Y.Z (대소문자 무시)
@@ -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
@@ -19,6 +19,8 @@ import { runFull } from "./commands/full.js";
19
19
  import { runVersion } from "./commands/version.js";
20
20
  import { runWorkflows } from "./commands/workflows.js";
21
21
  import { runRevert } from "./commands/revert.js";
22
+ import { runUninstall, runUninstallFlow } from "./commands/uninstall.js";
23
+ import * as prompts from "./ui/prompts.js";
22
24
  import { runInteractive } from "./commands/interactive.js";
23
25
  import { runStatus, printStatus } from "./commands/status.js";
24
26
  import { runDoctor, printDoctorReport } from "./commands/doctor.js";
@@ -63,11 +65,11 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
63
65
  if (opts.mode === "interactive") {
64
66
  // --dry-run은 대화형 모드에서 조용히 무시되면 안 됨(실제 설치가 진행돼버림) — 명시 에러로 차단.
65
67
  if (opts.dryRun) {
66
- console.error("--dry-run은 --mode <full|version|workflows|revert>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
68
+ console.error("--dry-run은 --mode <full|version|workflows|revert|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
67
69
  return 1;
68
70
  }
69
71
  if (!process.stdout.isTTY) {
70
- console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|revert> 와 --force 를 지정하세요.");
72
+ console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|revert|uninstall> 와 --force 를 지정하세요.");
71
73
  return 1;
72
74
  }
73
75
  return await runInteractive({}, { cwd, payloadRoot: payload, clock });
@@ -89,6 +91,35 @@ export async function run(argv, { cwd = process.cwd(), payloadRoot, clock } = {}
89
91
  console.error("version.yml·README·.gitignore는 보존됩니다 (사용자 데이터).");
90
92
  return 0;
91
93
  }
94
+
95
+ // uninstall 모드 — revert보다 넓게 README·gitignore·version.yml까지 선택적으로 제거.
96
+ if (opts.mode === "uninstall") {
97
+ const safeSelection = {
98
+ workflows: true, scripts: true, coderabbit: true,
99
+ readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
100
+ };
101
+ if (opts.dryRun) {
102
+ printDryRun(planDryRun("uninstall", { uninstallSelection: safeSelection }, payload, cwd));
103
+ return 0;
104
+ }
105
+ if (opts.force) {
106
+ const r = runUninstall({}, payload, cwd, safeSelection);
107
+ const removed = [
108
+ `워크플로우 ${r.workflows.length}개`, `스크립트 ${r.scripts.length}개`,
109
+ r.coderabbit && ".coderabbit.yaml", r.readme && "README 버전 섹션",
110
+ r.gitignore && ".gitignore 자동 추가 항목", r.versionYml && "version.yml",
111
+ ].filter(Boolean).join(", ");
112
+ console.error(`제거됨 — ${removed}`);
113
+ return 0;
114
+ }
115
+ if (!process.stdout.isTTY) {
116
+ console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
117
+ return 1;
118
+ }
119
+ await runUninstallFlow(payload, cwd, prompts);
120
+ return 0;
121
+ }
122
+
92
123
  // status 모드 — 읽기 전용, TTY/--force 무관하게 항상 동작
93
124
  if (opts.mode === "status") {
94
125
  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
  }