project-auto-wizard 0.1.32 → 0.1.34

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
@@ -17,7 +17,7 @@ npx project-auto-wizard
17
17
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
18
18
 
19
19
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
20
- ## 최신 버전 : v0.1.30 (2026-08-08)
20
+ ## 최신 버전 : v0.1.33 (2026-08-10)
21
21
 
22
22
  [전체 버전 기록 보기](CHANGELOG.md)
23
23
 
@@ -72,13 +72,11 @@ flutter.APP_ARTIFACT_NAME:
72
72
  - **react/next**: CI와 CI+CD 분리 구성
73
73
  - **python**: CI / PR 프리뷰 / SimpleCICD
74
74
 
75
- ### 되돌리기(`--mode revert`)
76
-
77
- `npx project-auto-wizard --mode revert --force`는 payload가 설치한 파일명과 **정확히 일치하는 것만** 제거합니다. 사용자가 직접 만든 워크플로우, `version.yml`, `README.md`, `.gitignore`는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
78
-
79
75
  ### 완전 삭제(`--mode uninstall`)
80
76
 
81
- `npx project-auto-wizard --mode uninstall`은 `revert`보다 넓게 제거합니다 — 워크플로우·스크립트는 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
77
+ `npx project-auto-wizard --mode uninstall`은 마법사가 설치한 것을 제거합니다 — 워크플로우·스크립트는 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
78
+
79
+ 제거 대상은 payload가 설치한 파일명과 정확히 일치하는 것, 그리고 마법사 관리 마커를 가진 파일뿐입니다. 사용자가 직접 만든 워크플로우는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
82
80
 
83
81
  - **대화형(TTY)**: 실제로 설치된 항목만 체크리스트로 보여줍니다. 워크플로우·스크립트는 기본 체크, README·`.gitignore`·`version.yml`은 opt-in입니다. 선택 후 최종 확인(기본 "아니오")을 거쳐야 실제로 삭제됩니다.
84
82
  - **비대화형(`--force`)**: 워크플로우·스크립트만 기본 삭제합니다. README·`.gitignore`·`version.yml`까지 지우려면 `--purge-readme`/`--purge-gitignore`/`--purge-version`을 함께 지정하세요.
@@ -129,7 +127,7 @@ flowchart LR
129
127
  ```
130
128
  npx project-auto-wizard [옵션]
131
129
 
132
- -m, --mode MODE full | version | workflows | revert | uninstall | status | doctor (기본: 대화형)
130
+ -m, --mode MODE full | uninstall | status | doctor (기본: 대화형)
133
131
  -t, --type CSV spring,react,... (미지정 시 자동 감지)
134
132
  --project-version V 초기 버전 (미지정 시 자동 감지)
135
133
  --paths "t=p,..." 모노레포 타입별 경로
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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",
@@ -105,10 +105,41 @@ jobs:
105
105
  echo "proceed=$PROCEED" >> $GITHUB_OUTPUT
106
106
  if [ "$PROCEED" = "true" ]; then
107
107
  echo "release gate open (mode=$MODE, event=$EVENT)"
108
- else
109
- echo "not a release-confirm push in pr-flow mode — skipping (clean exit)"
108
+ exit 0
109
+ fi
110
+
111
+ echo "not a release-confirm push in pr-flow mode — skipping"
112
+
113
+ # Drift guard (issue #61): skipping is normal, but a version.yml that
114
+ # has run ahead of the newest tag is not — it means the version was
115
+ # bumped without a release, so npm silently stays behind. Six versions
116
+ # (0.1.26~0.1.31) went missing this way with every workflow green.
117
+ FILE_VERSION=$(grep -m1 '^version:' version.yml | sed 's/version:[ \t]*"\{0,1\}\([0-9][0-9.]*\)"\{0,1\}.*/\1/')
118
+ LATEST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
119
+ LATEST_VERSION="${LATEST_TAG#v}"
120
+
121
+ if [ -z "$FILE_VERSION" ] || [ -z "$LATEST_VERSION" ]; then
122
+ echo "drift guard skipped (no version.yml version or no tag yet)"
123
+ exit 0
110
124
  fi
111
125
 
126
+ HIGHER=$(printf '%s\n%s\n' "$FILE_VERSION" "$LATEST_VERSION" | sort -V | tail -n 1)
127
+ if [ "$FILE_VERSION" != "$LATEST_VERSION" ] && [ "$HIGHER" = "$FILE_VERSION" ]; then
128
+ echo "::error::version.yml($FILE_VERSION) is ahead of the newest tag($LATEST_TAG) but no release was published — this version will never reach npm. Recover with: Actions → PROJECT-RELEASE-PUBLISH → Run workflow (workflow_dispatch bypasses this gate)."
129
+ {
130
+ echo "### ⚠️ 릴리스 누락 감지"
131
+ echo ""
132
+ echo "- version.yml: \`$FILE_VERSION\`"
133
+ echo "- 최신 태그: \`$LATEST_TAG\`"
134
+ echo ""
135
+ echo "릴리스 확정 없이 버전만 올라갔습니다. 이 버전은 npm에 배포되지 않습니다."
136
+ echo "복구: Actions → PROJECT-RELEASE-PUBLISH → Run workflow"
137
+ } >> "$GITHUB_STEP_SUMMARY"
138
+ exit 1
139
+ fi
140
+
141
+ echo "drift guard ok (version.yml=$FILE_VERSION, latest tag=$LATEST_TAG)"
142
+
112
143
  - name: Read semver_auto option from version.yml
113
144
  id: semver_options
114
145
  if: >-
package/src/cli/args.js CHANGED
@@ -17,7 +17,7 @@ export function parseArgs(argv) {
17
17
  force: false,
18
18
  help: false,
19
19
  showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
20
- dryRun: false, // --dry-run: 실제 변경 없이 미리보기만 (full/version/workflows/revert 공통)
20
+ dryRun: false, // --dry-run: 실제 변경 없이 미리보기만 (full/uninstall)
21
21
  purgeReadme: false, // --purge-readme: uninstall --force 시 README 버전 섹션도 제거
22
22
  purgeGitignore: false, // --purge-gitignore: uninstall --force 시 .gitignore 자동 추가 항목도 제거
23
23
  purgeVersion: false, // --purge-version: uninstall --force 시 version.yml도 제거
@@ -111,7 +111,7 @@ export function parseArgs(argv) {
111
111
  }
112
112
  if (!VALID_MODES.includes(result.mode)) {
113
113
  throw new CliError(
114
- `지원하지 않는 모드: '${result.mode}'\n지원 모드: interactive full version workflows revert uninstall status doctor`
114
+ `지원하지 않는 모드: '${result.mode}'\n지원 모드: interactive full uninstall status doctor`
115
115
  );
116
116
  }
117
117
  return result;
package/src/cli/help.js CHANGED
@@ -5,8 +5,8 @@ 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 | uninstall | status | doctor)
9
- 기본: interactive (대화형). revert = 설치물 제거(되돌리기)
8
+ -m, --mode MODE 통합 모드 (full | uninstall | status | doctor)
9
+ 기본: interactive (대화형). full = 설치 및 업데이트
10
10
  uninstall = 완전 삭제(대화형 체크리스트, --force 시 --purge-*로 opt-in)
11
11
  status = 설치 상태·드리프트 확인(읽기 전용). doctor = 환경 진단(읽기 전용)
12
12
  -t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
@@ -19,8 +19,8 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
19
19
  --nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
20
20
  --secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
21
21
  --semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
22
- --force full/version/workflows/revert 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
23
- --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/version/workflows/revert/uninstall 전체 지원)
22
+ --force full 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
23
+ --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/uninstall 지원)
24
24
  --purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
25
25
  --purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
26
26
  --purge-version --mode uninstall --force 시 version.yml도 제거
@@ -1,10 +1,9 @@
1
1
  // --dry-run 미리보기 — 실제 파일을 쓰지 않고 무엇이 바뀔지 계산한다.
2
- // full/version/workflows/revert/uninstall 5개 모드 전체 지원.
2
+ // full/uninstall 모드 지원 (issue #70 — 부분 설치·되돌리기 모드 제거).
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
- import { planRevert } from "./revert.js";
8
7
  import { planUninstall } from "./uninstall.js";
9
8
  import { buildVersionYml, parseExisting } from "../core/version-yml.js";
10
9
  import { readVersionYmlTemplate } from "../core/assets.js";
@@ -35,32 +34,21 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
35
34
  return { existed: existingRaw !== null, changed: existingRaw !== wouldBe };
36
35
  }
37
36
 
38
- // mode: "full" | "version" | "workflows" | "revert" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
37
+ // mode: "full" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
39
38
  export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
40
- if (mode === "revert") return { mode, revert: planRevert(payloadRoot, targetRoot) };
41
39
  if (mode === "uninstall") {
42
40
  return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
43
41
  }
44
-
45
- const result = { mode };
46
- if (mode === "full" || mode === "workflows") {
47
- result.workflows = planWorkflows(context, payloadRoot, targetRoot);
48
- }
49
- if (mode === "full" || mode === "version") {
50
- result.versionYml = versionYmlPreview(context, payloadRoot, targetRoot);
51
- }
52
- return result;
42
+ return {
43
+ mode,
44
+ workflows: planWorkflows(context, payloadRoot, targetRoot),
45
+ versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
46
+ };
53
47
  }
54
48
 
55
49
  export function printDryRun(plan) {
56
50
  const lines = ["", `project-auto-wizard --dry-run (mode: ${plan.mode}) — 미리보기, 실제 파일은 바뀌지 않았습니다`, ""];
57
- if (plan.mode === "revert") {
58
- const r = plan.revert;
59
- lines.push(`제거될 워크플로우 (${r.workflows.length}개):`);
60
- for (const f of r.workflows) lines.push(` - ${f}`);
61
- lines.push(`제거될 스크립트 (${r.scripts.length}개):`);
62
- for (const f of r.scripts) lines.push(` - ${f}`);
63
- } else if (plan.mode === "uninstall") {
51
+ if (plan.mode === "uninstall") {
64
52
  const u = plan.uninstall;
65
53
  lines.push(`제거될 워크플로우 (${u.workflows.length}개):`);
66
54
  for (const f of u.workflows) lines.push(` - ${f}`);
@@ -15,9 +15,6 @@ import { promptEnvPlan } from "../ui/env-plan.js";
15
15
  import { listWorkflowConflicts } from "../core/copy/workflows.js";
16
16
  import { createContext, VALID_TYPES } from "../context.js";
17
17
  import { runFull } from "./full.js";
18
- import { runVersion } from "./version.js";
19
- import { runWorkflows } from "./workflows.js";
20
- import { runRevert } from "./revert.js";
21
18
  import { runUninstallFlow } from "./uninstall.js";
22
19
  import * as prompts from "../ui/prompts.js";
23
20
  import { runStatus, printStatus } from "./status.js";
@@ -55,16 +52,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
55
52
  break;
56
53
  }
57
54
 
58
- // revert 모드 — 확인 질문(기본 아니오) 후 payload 유래 파일 제거. 감지·breaking 게이트 불필요.
59
- if (mode === "revert") {
60
- const ok = await io.askYesNo("마법사가 설치한 워크플로우·스크립트를 제거할까요? (version.yml·README는 보존)", false);
61
- if (ok !== true) { io.cancelMessage?.("되돌리기를 취소했습니다."); return 0; }
62
- const r = runRevert({}, payload, cwd);
63
- io.note?.(`워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개 제거`, "되돌리기 완료");
64
- io.outro?.("되돌리기를 마쳤습니다.");
65
- return 0;
66
- }
67
-
68
55
  // uninstall 모드 — 대화형 체크리스트로 항목별 opt-in 후 삭제. 감지·breaking 게이트 불필요.
69
56
  // runUninstallFlow는 취소/항목없음 시 null을 반환한다 — 그때는 완료 outro를 찍지 않는다.
70
57
  if (mode === "uninstall") {
@@ -89,7 +76,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
89
76
  let includeNexus = existing?.options?.nexus ?? false;
90
77
  let includeSecretBackup = existing?.options?.secretBackup ?? false;
91
78
  let includeSemverAuto = existing?.options?.semverAuto ?? null;
92
- const showOptional = mode === "full" || mode === "workflows";
79
+ const showOptional = mode === "full";
93
80
  const realTty = process.stdout.isTTY === true;
94
81
 
95
82
  // 층2 — 감지 로그 (#446)
@@ -188,8 +175,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
188
175
  }
189
176
  }
190
177
 
191
- // 경로 확정 (.sh resolve_project_paths L1362~1589 — full/version만. 저장값·후보 스캔·질문)
192
- if (mode === "full" || mode === "version") {
178
+ // 경로 확정 (.sh resolve_project_paths L1362~1589 — 저장값·후보 스캔·질문)
179
+ if (mode === "full") {
193
180
  paths = await resolveProjectPaths({
194
181
  root: cwd, types, paths, existingPaths: existing?.paths ?? new Map(),
195
182
  force: false, tty: realTty, io: io.engineIo ?? {},
@@ -244,10 +231,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
244
231
  }
245
232
  }
246
233
 
247
- let result = null;
248
- if (mode === "full") result = runFull(ctx, payload, cwd, hooks);
249
- else if (mode === "version") result = runVersion(ctx, payload, cwd);
250
- else if (mode === "workflows") result = runWorkflows(ctx, payload, cwd, hooks);
234
+ const result = runFull(ctx, payload, cwd, hooks);
251
235
 
252
236
  // 완료 요약 (.sh print_summary L5438)
253
237
  io.summary?.({
@@ -1,4 +1,4 @@
1
- // purge 모드 — revert가 지우는 전부(워크플로우·스크립트) + version.yml·README
1
+ // purge 모드 — planRemoval이 판별하는 전부(워크플로우·스크립트) + version.yml·README
2
2
  // AUTO-VERSION-SECTION 블록·CHANGELOG를 추가로 제거해 설치 이전 상태로 완전히 되돌린다.
3
3
  // 개발·테스트 전용 숨김 모드 — DESIGN-SPEC purge #6.
4
4
  // develop 브랜치 삭제는 파일 삭제와 성격이 달라(실행 시점 git 상태 판단 필요) 여기 plan에는
@@ -7,7 +7,7 @@ import { join } from "node:path";
7
7
  import { existsSync } from "node:fs";
8
8
  import { PATHS } from "../core/paths.js";
9
9
  import { remove } from "../core/fsutil.js";
10
- import { planRevert } from "./revert.js";
10
+ import { planRemoval } from "../core/removal-plan.js";
11
11
  import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/readme.js";
12
12
 
13
13
  const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
@@ -15,10 +15,10 @@ const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
15
15
  // keepFlags: { versionYml, readme, changelog, workflows, scripts } — true인 카테고리는 후보에서 제외.
16
16
  // 반환: { workflows, scripts, versionYml, readmeSection, changelog } — 아무것도 지우지 않는 순수 함수.
17
17
  export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
18
- const revertPlan = planRevert(payloadRoot, targetRoot);
18
+ const removalPlan = planRemoval(payloadRoot, targetRoot);
19
19
  return {
20
- workflows: keepFlags.workflows ? [] : revertPlan.workflows,
21
- scripts: keepFlags.scripts ? [] : revertPlan.scripts,
20
+ workflows: keepFlags.workflows ? [] : removalPlan.workflows,
21
+ scripts: keepFlags.scripts ? [] : removalPlan.scripts,
22
22
  versionYml: !keepFlags.versionYml && existsSync(join(targetRoot, PATHS.versionFile)),
23
23
  readmeSection: !keepFlags.readme && hasVersionSection(targetRoot),
24
24
  changelog: keepFlags.changelog ? [] : CHANGELOG_FILES.filter((f) => existsSync(join(targetRoot, f))),
@@ -44,7 +44,7 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
44
44
  }
45
45
 
46
46
  // 실제 삭제 수행 — planPurge()와 동일 shape을 반환하되 실제로 제거된 항목을 반영한다.
47
- // runRevert()를 통째로 호출하지 않는 이유: runRevert는 항상 전체를 지우므로
47
+ // planRemoval 결과를 그대로 지우지 않는 이유: 목록은 항상 전체라서
48
48
  // --keep-* 로 선택적 카테고리만 보존하는 요구사항과 맞지 않는다.
49
49
  // H3 (Fable 검토): readmeSection은 plan의 판정을 그대로 되돌려주지 않고
50
50
  // removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 — 스펙 §6이
@@ -1,10 +1,11 @@
1
- // uninstall 모드 — revert(payload 파일명 일치분)보다 넓게, README/.gitignore/version.yml까지 선택적으로 제거.
2
- // 대화형 체크리스트 또는 --force + --purge-* 로 항목별 opt-in. revert.js는 건드리지 않고 읽기 전용으로만 재사용한다.
1
+ // uninstall 모드 — 마법사가 설치한 파일(planRemoval 판별분) 더해 README/.gitignore/version.yml까지
2
+ // 선택적으로 제거한다. 대화형 체크리스트 또는 --force + --purge-* 로 항목별 opt-in.
3
+ // core/removal-plan.js는 읽기 전용으로만 재사용한다(아무것도 지우지 않는 순수 함수).
3
4
  import { join } from "node:path";
4
5
  import { existsSync } from "node:fs";
5
6
  import { PATHS } from "../core/paths.js";
6
7
  import { remove } from "../core/fsutil.js";
7
- import { planRevert } from "./revert.js";
8
+ import { planRemoval } from "../core/removal-plan.js";
8
9
  import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/readme.js";
9
10
  import { removeAutoAddedEntriesFromGitignore, hasAutoAddedEntries } from "../core/copy/gitignore.js";
10
11
  import { CANCEL } from "../ui/prompts.js";
@@ -12,10 +13,10 @@ import { CANCEL } from "../ui/prompts.js";
12
13
  // selection: { workflows, scripts, readme, gitignore, versionYml } (모두 boolean).
13
14
  // 반환: 위와 동일한 키의 boolean/배열 — 실제로 제거 "대상"인지 여부(순수 함수, 아무것도 지우지 않음).
14
15
  export function planUninstall(payloadRoot, targetRoot, selection) {
15
- const revertPlan = planRevert(payloadRoot, targetRoot);
16
+ const removalPlan = planRemoval(payloadRoot, targetRoot);
16
17
  return {
17
- workflows: selection.workflows ? revertPlan.workflows : [],
18
- scripts: selection.scripts ? revertPlan.scripts : [],
18
+ workflows: selection.workflows ? removalPlan.workflows : [],
19
+ scripts: selection.scripts ? removalPlan.scripts : [],
19
20
  readme: selection.readme ? hasVersionSection(targetRoot) : false,
20
21
  gitignore: selection.gitignore ? hasAutoAddedEntries(targetRoot) : false,
21
22
  versionYml: selection.versionYml ? existsSync(join(targetRoot, PATHS.versionFile)) : false,
@@ -52,10 +53,10 @@ const ITEM_DEFS = [
52
53
  export const SAFE_ITEMS = ["workflows", "scripts"];
53
54
 
54
55
  function detectAvailableItems(payloadRoot, targetRoot) {
55
- const revertPlan = planRevert(payloadRoot, targetRoot);
56
+ const removalPlan = planRemoval(payloadRoot, targetRoot);
56
57
  const presence = {
57
- workflows: revertPlan.workflows.length > 0,
58
- scripts: revertPlan.scripts.length > 0,
58
+ workflows: removalPlan.workflows.length > 0,
59
+ scripts: removalPlan.scripts.length > 0,
59
60
  readme: hasVersionSection(targetRoot),
60
61
  gitignore: hasAutoAddedEntries(targetRoot),
61
62
  versionYml: existsSync(join(targetRoot, PATHS.versionFile)),
package/src/context.js CHANGED
@@ -6,9 +6,11 @@ export const VALID_TYPES = [
6
6
 
7
7
  // --mode 화이트리스트 (issue #19) — 알 수 없는 값은 부수효과(브랜치 조회 등) 이전에 즉시 거부해야 한다.
8
8
  // purge는 --help/대화형 메뉴에 노출하지 않는 숨김 모드(issue #6)이지만 검증 대상에는 포함한다.
9
+ // version/workflows(부분 설치)와 revert는 제거됐다 (issue #70) — 부분 설치는 설치 시점 baseline을
10
+ // 반쪽만 갱신해 업데이트 판정을 흐리고, revert는 uninstall의 부분집합이었다.
9
11
  export const VALID_MODES = [
10
- "interactive", "full", "version", "workflows",
11
- "revert", "uninstall", "status", "doctor", "purge",
12
+ "interactive", "full",
13
+ "uninstall", "status", "doctor", "purge",
12
14
  ];
13
15
 
14
16
  export const DEFAULT_VERSION = "0.0.0"; // 패키지 버전 읽기 실패 시 폴백 (배너용 — breaking 비교엔 안 씀)
@@ -175,7 +175,7 @@ export function listWorkflowConflicts(context, payloadRoot, targetRoot = ".") {
175
175
 
176
176
  // 대화형 진입점 (async) — 충돌마다 onConflict(filename, type)를 await해 결정 Map을 만든 뒤
177
177
  // 동기 엔진에 위임한다. WHY 분리: copyWorkflows를 async로 바꾸면 await 없이 호출하는
178
- // 기존 호출부(runFull/runWorkflows)가 깨진다 — 시그니처 무변경 원칙.
178
+ // 기존 호출부(runFull)가 깨진다 — 시그니처 무변경 원칙.
179
179
  // onConflict 반환값: 'template' | 'skip' | 'backup' (그 외/미지정 → 'skip').
180
180
  export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot = ".", { onConflict } = {}) {
181
181
  const decisions = new Map();
@@ -1,4 +1,6 @@
1
- // revert 모드payload 유래 파일 + 마커로 식별되는 파일을 제거 (DESIGN-SPEC §4 되돌리기).
1
+ // 제거 대상 판별기 마법사가 설치한 파일이 무엇인지 가려낸다.
2
+ // uninstall(항목 체크리스트)과 purge(숨김 개발 모드)의 공통 기반이며, 아무것도 지우지 않는다.
3
+ //
2
4
  // 원칙: (a) 현재 payload에 존재하는 파일명과 정확히 일치하는 것, (b) 설치된 워크플로우 파일 중
3
5
  // 마법사 관리 마커(MANAGED_WORKFLOW_MARKER)로 시작하는 것 — 이 둘의 합집합을 제거 대상으로 삼는다.
4
6
  // (a)만으로는 과거 버전에서 설치된 뒤 이후 릴리스에서 payload 파일명이 바뀌거나 삭제된 파일을
@@ -6,12 +8,14 @@
6
8
  // 없는(그러나 파일명은 여전히 현재 payload와 일치하는) 기존 설치 전체를 인식하지 못하는 회귀가
7
9
  // 생긴다 — 그래서 두 방식을 합집합으로 병행한다. 마커는 이 수정 이후 배포되는 payload 템플릿부터
8
10
  // 포함되므로, (b) 경로가 실제로 새로 잡아내는 것은 "이름이 바뀌거나 삭제된, 마커가 있는" 파일뿐이다.
9
- // 사용자가 직접 만든 워크플로우·version.yml·README·.gitignore는 건드리지 않는다
11
+ // 사용자가 직접 만든 워크플로우·version.yml·README·.gitignore는 대상이 아니다
10
12
  // (version.yml은 사용자 버전 데이터 — 제거 대상이 아니라 산출물이다).
13
+ //
14
+ // 이 파일은 원래 src/commands/revert.js였다. revert 모드는 uninstall의 부분집합이라 제거됐고
15
+ // (issue #70), 판별 로직만 남아 commands가 아닌 core로 옮겨졌다.
11
16
  import { join } from "node:path";
12
17
  import { existsSync, readFileSync, readdirSync } from "node:fs";
13
- import { PATHS, PAYLOAD } from "../core/paths.js";
14
- import { remove } from "../core/fsutil.js";
18
+ import { PATHS, PAYLOAD } from "./paths.js";
15
19
 
16
20
  // payload/workflows/**/*.yaml 첫 줄에 심어둔 고정 마커 — 이 값이 바뀌면 과거 설치분과의 매칭이 끊긴다.
17
21
  export const MANAGED_WORKFLOW_MARKER = "# project-auto-wizard:managed-workflow";
@@ -41,13 +45,13 @@ function markedWorkflowNames(wfDir) {
41
45
  return names;
42
46
  }
43
47
 
44
- // 아무것도 지우지 않는 순수 함수 — --dry-run status류 기능에서 재사용.
45
- export function planRevert(payloadRoot, targetRoot = ".") {
48
+ // 아무것도 지우지 않는 순수 함수 — uninstall/purge/--dry-run 공유한다.
49
+ export function planRemoval(payloadRoot, targetRoot = ".") {
46
50
  const removedWf = new Set();
47
51
  const removedScripts = [];
48
52
  const wfDir = join(targetRoot, PATHS.workflowsDir);
49
53
  if (existsSync(wfDir)) {
50
- // (a) 현재 payload와 파일명이 일치하는 것 — 기존 동작 그대로(마커 유무 무관, 기존 설치 회귀 방지).
54
+ // (a) 현재 payload와 파일명이 일치하는 것 — 마커 유무 무관(기존 설치 회귀 방지).
51
55
  for (const name of payloadWorkflowNames(payloadRoot)) {
52
56
  const p = join(wfDir, name);
53
57
  if (existsSync(p)) removedWf.add(name);
@@ -63,12 +67,3 @@ export function planRevert(payloadRoot, targetRoot = ".") {
63
67
  }
64
68
  return { workflows: [...removedWf], scripts: removedScripts };
65
69
  }
66
-
67
- // 반환: { workflows: [...제거된 파일명], scripts: [...] } — planRevert와 동일한 형태.
68
- export function runRevert(context, payloadRoot, targetRoot = ".") {
69
- const plan = planRevert(payloadRoot, targetRoot);
70
- const wfDir = join(targetRoot, PATHS.workflowsDir);
71
- for (const name of plan.workflows) remove(join(wfDir, name));
72
- for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
73
- return plan;
74
- }
package/src/index.js CHANGED
@@ -17,9 +17,6 @@ import { resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, default
17
17
  import { printBannerCompact } from "./ui/banner.js";
18
18
  import { printSummary } from "./ui/summary.js";
19
19
  import { runFull } from "./commands/full.js";
20
- import { runVersion } from "./commands/version.js";
21
- import { runWorkflows } from "./commands/workflows.js";
22
- import { runRevert } from "./commands/revert.js";
23
20
  import { runUninstall, runUninstallFlow } from "./commands/uninstall.js";
24
21
  import * as prompts from "./ui/prompts.js";
25
22
  import { runInteractive } from "./commands/interactive.js";
@@ -81,33 +78,16 @@ export async function run(argv, {
81
78
  if (opts.mode === "interactive") {
82
79
  // --dry-run은 대화형 모드에서 조용히 무시되면 안 됨(실제 설치가 진행돼버림) — 명시 에러로 차단.
83
80
  if (opts.dryRun) {
84
- console.error("--dry-run은 --mode <full|version|workflows|revert|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
81
+ console.error("--dry-run은 --mode <full|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
85
82
  return 1;
86
83
  }
87
84
  if (!process.stdout.isTTY) {
88
- console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|revert|uninstall> 와 --force 를 지정하세요.");
85
+ console.error("대화형 입력이 불가능한 환경입니다. --mode <full|uninstall> 와 --force 를 지정하세요.");
89
86
  return 1;
90
87
  }
91
88
  return await runInteractive({}, { cwd, payloadRoot: payload, clock });
92
89
  }
93
90
 
94
- // revert 모드 — payload 유래 파일 제거 (감지·질문 불필요, --force 게이트만)
95
- if (opts.mode === "revert") {
96
- // TTY 여부와 무관하게 --force가 없으면 거부한다 (issue #19 — TTY에서 확인 없이 즉시 실행되던 결함 수정).
97
- // --dry-run은 파일을 쓰지 않으므로 --force 게이트를 우회한다 (status/doctor와 동일한 안전성).
98
- if (!opts.force && !opts.dryRun) {
99
- console.error("revert 모드는 --force 없이 실행할 수 없습니다 (확인 절차가 없습니다).");
100
- return 1;
101
- }
102
- if (opts.dryRun) {
103
- printDryRun(planDryRun("revert", {}, payload, cwd));
104
- return 0;
105
- }
106
- const r = runRevert({}, payload, cwd);
107
- console.error(`제거됨 — 워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개`);
108
- console.error("version.yml·README·.gitignore는 보존됩니다 (사용자 데이터).");
109
- return 0;
110
- }
111
91
  // purge 모드 — 마법사가 만든 모든 산출물을 지워 설치 이전 상태로 완전히 되돌린다.
112
92
  // 개발·테스트 전용 숨김 모드 — --help/대화형 메뉴에 노출하지 않는다 (issue #6).
113
93
  if (opts.mode === "purge") {
@@ -183,7 +163,7 @@ export async function run(argv, {
183
163
  return 0;
184
164
  }
185
165
 
186
- // uninstall 모드 — revert보다 넓게 README·gitignore·version.yml까지 선택적으로 제거.
166
+ // uninstall 모드 — 설치물에 더해 README·gitignore·version.yml까지 선택적으로 제거.
187
167
  if (opts.mode === "uninstall") {
188
168
  const safeSelection = {
189
169
  workflows: true, scripts: true,
@@ -303,14 +283,10 @@ export async function run(argv, {
303
283
  return 0;
304
284
  }
305
285
 
306
- // opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/revert/purge/uninstall/status/doctor는
307
- // 전부 위에서 조기 반환했으므로 이 시점엔 full/version/workflows 하나로 보장된다(issue #19 — default 분기 제거).
308
- let result = null;
309
- switch (opts.mode) {
310
- case "full": result = runFull(context, payload, cwd); break;
311
- case "version": result = runVersion(context, payload, cwd); break;
312
- case "workflows": result = runWorkflows(context, payload, cwd); break;
313
- }
286
+ // opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/purge/uninstall/status/doctor는
287
+ // 전부 위에서 조기 반환했으므로 이 시점엔 full 하나로 보장된다 (issue #19 — default 분기 제거,
288
+ // issue #70 — 부분 설치 모드 제거로 분기 자체가 사라졌다).
289
+ const result = runFull(context, payload, cwd);
314
290
 
315
291
  // 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
316
292
  printSummary({
package/src/ui/prompts.js CHANGED
@@ -12,10 +12,7 @@ export async function selectMode({ again = false } = {}) {
12
12
  return engine.select({
13
13
  message: again ? "다음으로 무엇을 할까요?" : "무엇을 설치할까요?",
14
14
  options: [
15
- { value: "full", label: "전체 설치 — 버전관리 + 자동화 워크플로우 (처음이라면 추천)" },
16
- { value: "version", label: "버전 관리만 — 버전 자동 증가·동기화 시스템만 설치" },
17
- { value: "workflows", label: "워크플로우만 — 빌드·배포 GitHub Actions만 설치" },
18
- { value: "revert", label: "되돌리기 — 마법사가 설치한 워크플로우·스크립트 제거" },
15
+ { value: "full", label: "설치 / 업데이트 — 버전관리 + 자동화 워크플로우 (처음이라면 추천)" },
19
16
  { value: "uninstall", label: "완전 삭제 — 마법사가 설치·수정한 모든 항목 제거(확인 후, README·gitignore·version.yml 포함)" },
20
17
  { value: "status", label: "설치 상태 확인 — 읽기 전용, 버전·타입·드리프트 확인" },
21
18
  { value: "doctor", label: "환경 진단 — 읽기 전용, gh CLI·권한·secret 설정 점검" },
@@ -1,37 +0,0 @@
1
- // version 모드 (.sh execute_integration version case 등가).
2
- // 순서: version.yml → readme → scripts.
3
- // (워크플로우를 복사하지 않으므로 충돌 백업 부산물이 생길 수 없다 — gitignore 갱신 대상 없음, issue #7.
4
- // util·issue·setup-guide는 스코프 제외.)
5
- import { join } from "node:path";
6
- import { existsSync, readFileSync } from "node:fs";
7
- import { writeText } from "../core/fsutil.js";
8
- import { PATHS } from "../core/paths.js";
9
- import { buildVersionYml, parseExisting } from "../core/version-yml.js";
10
- import { readVersionYmlTemplate } from "../core/assets.js";
11
- import { markerForType } from "../core/detect.js";
12
- import { addVersionSectionToReadme } from "../core/copy/readme.js";
13
- import { copyScripts } from "../core/copy/simple.js";
14
-
15
- export function runVersion(context, payloadRoot, targetRoot = ".") {
16
- const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
17
- now, today, templateVersion = "unknown",
18
- includeNexus = false, includeSecretBackup = false,
19
- includeSemverAuto } = context;
20
-
21
- const pathMarkers = new Map();
22
- for (const [t] of paths) pathMarkers.set(t, markerForType(t));
23
-
24
- // 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다 (issue #20 M8).
25
- const vyPath = join(targetRoot, PATHS.versionFile);
26
- const extraTopLevel = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")).extraTopLevel : [];
27
-
28
- writeText(join(targetRoot, PATHS.versionFile),
29
- buildVersionYml({
30
- templateText: readVersionYmlTemplate(payloadRoot),
31
- version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
32
- extraTopLevel,
33
- templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
34
- }));
35
- addVersionSectionToReadme(version, targetRoot);
36
- copyScripts(payloadRoot, targetRoot);
37
- }
@@ -1,52 +0,0 @@
1
- // workflows 모드 (.sh execute_integration workflows case 등가).
2
- // 순서: copy_workflows → update_version_yml_deploy(version.yml 있을 때만) → scripts.
3
- // (version.yml 생성 안 함 — 기존 version.yml이 있을 때만 deploy 블록 추가.
4
- // util/config/setup-guide 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
5
- import { join } from "node:path";
6
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
- import { PATHS } from "../core/paths.js";
8
- import { copyWorkflows } from "../core/copy/workflows.js";
9
- import { copyScripts } from "../core/copy/simple.js";
10
- import { escapeYamlDoubleQuoted } from "../core/wizard-env.js";
11
-
12
- export function runWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
13
- const wf = copyWorkflows(context, payloadRoot, targetRoot, hooks);
14
-
15
- // update_version_yml_deploy: 기존 version.yml이 있고 ask 값이 있을 때만 deploy 블록 갱신
16
- const vy = join(targetRoot, PATHS.versionFile);
17
- if (existsSync(vy) && wf.deployValues && wf.deployValues.size) {
18
- writeFileSync(vy, upsertDeployBlock(readFileSync(vy, "utf8"), wf.deployValues));
19
- }
20
-
21
- copyScripts(payloadRoot, targetRoot);
22
- return { workflows: wf };
23
- }
24
-
25
- // 기존 version.yml에서 deploy: 블록을 제거하고 새로 append (.sh update_version_yml_deploy 멱등).
26
- export function upsertDeployBlock(content, deployValues) {
27
- // 기존 deploy: 블록 제거 (deploy: 라인 ~ 다음 최상위 키 전까지)
28
- const lines = content.split(/\r?\n/);
29
- const out = [];
30
- let inDeploy = false;
31
- for (const line of lines) {
32
- if (/^deploy:/.test(line)) { inDeploy = true; continue; }
33
- if (inDeploy) {
34
- if (/^\s/.test(line) || line === "") continue; // 들여쓰기/빈줄 = deploy 내부
35
- inDeploy = false;
36
- }
37
- out.push(line);
38
- }
39
- let text = out.join("\n").replace(/\n+$/, "\n");
40
- // 새 deploy 블록 append
41
- const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t)?.size);
42
- if (deployTypes.length) {
43
- text += `\ndeploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
44
- for (const t of deployTypes) {
45
- text += ` ${t}:\n`;
46
- // 동일한 이스케이프를 재사용 — deploy 값도 @wizard ask 값과 같은 경로로 들어오므로
47
- // 큰따옴표가 섞이면 YAML이 깨진다 (issue #20 L9, 세 번째 지점).
48
- for (const [k, v] of deployValues.get(t)) text += ` ${k}: "${escapeYamlDoubleQuoted(v)}"\n`;
49
- }
50
- }
51
- return text;
52
- }