From 744cb36d74575be45b7550412e5627a46c936a93 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 14 Sep 2026 19:53:20 +0900 Subject: [PATCH 1/2] fix: honor --dry-run in update and uninstall --- .changeset/dry-run-update-uninstall.md | 6 +++ .forgejo/workflows/ci.yml | 8 ++++ packages/cli/README.md | 2 +- packages/cli/src/commands/uninstall.ts | 11 ++++-- packages/cli/src/commands/update.ts | 7 ++-- packages/cli/src/index.ts | 7 ++-- packages/cli/test/cli.test.ts | 54 +++++++++++++++++++++++++- packages/core/src/installer.ts | 26 +++++++------ packages/core/test/installer.test.ts | 15 +++++++ 9 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 .changeset/dry-run-update-uninstall.md diff --git a/.changeset/dry-run-update-uninstall.md b/.changeset/dry-run-update-uninstall.md new file mode 100644 index 0000000..975ec1b --- /dev/null +++ b/.changeset/dry-run-update-uninstall.md @@ -0,0 +1,6 @@ +--- +"designpaca": patch +"@designpaca/core": patch +--- + +`update` 와 `uninstall` 이 `--dry-run` 을 무시하고 실제로 파일과 매니페스트를 바꾸던 문제를 고쳤다. 두 명령 모두 계획만 출력하며, CI 스모크에서 `--dry-run` 이 아무것도 쓰지 않는지 검사한다. diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 6dcbade..7880e2e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -83,6 +83,14 @@ jobs: env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca --version env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca install --target claude-code --scope project --yes test -f .claude/skills/designpaca/SKILL.md || { echo "설치 후 스킬이 없다" >&2; exit 1; } + + # --dry-run 은 어느 명령에서도 쓰지 않아야 한다 (v0.12.0 에서 update 가 무시했다) + cp "$state_dir/manifest.json" "$sandbox/manifest.before.json" + env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca update --dry-run --force + cmp -s "$state_dir/manifest.json" "$sandbox/manifest.before.json" || { echo "update --dry-run 이 매니페스트를 바꿨다" >&2; exit 1; } + env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca uninstall --dry-run --yes + test -f .claude/skills/designpaca/SKILL.md || { echo "uninstall --dry-run 이 파일을 지웠다" >&2; exit 1; } + env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca doctor env DESIGNPACA_STATE_DIR="$state_dir" npx --no-install designpaca uninstall --yes test ! -f .claude/skills/designpaca/SKILL.md || { echo "제거 후에도 파일이 남았다" >&2; exit 1; } diff --git a/packages/cli/README.md b/packages/cli/README.md index 6eef93b..e4aa499 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -109,7 +109,7 @@ npx designpaca update --force | `-s, --scope <범위>` | `user` (전역) \| `project` | | `-y, --yes` | 확인 없이 진행 | | `-f, --force` | 직접 수정한 파일도 덮어쓴다 (`.orig` 백업) | -| `--dry-run` | 쓰지 않고 계획만 출력 | +| `--dry-run` | 쓰지 않고 계획만 출력 (install · update · uninstall 공통) | | `--no-update-check` | 새 버전 확인 생략 | ## 요구사항 diff --git a/packages/cli/src/commands/uninstall.ts b/packages/cli/src/commands/uninstall.ts index fbe20c8..03b91cd 100644 --- a/packages/cli/src/commands/uninstall.ts +++ b/packages/cli/src/commands/uninstall.ts @@ -4,6 +4,7 @@ import { dim, heading, ok, warn } from "../ui.ts"; export async function runUninstall(opts: { targets?: TargetId[]; force?: boolean; + dryRun?: boolean; }): Promise { const manifest = await readManifest(); const targets = manifest.installs.filter( @@ -15,13 +16,15 @@ export async function runUninstall(opts: { return 0; } - console.log(heading("제거")); + console.log(heading(opts.dryRun ? "제거 계획" : "제거")); for (const rec of targets) { - const res = await removeInstall(rec, { force: opts.force }); - console.log(ok(`${rec.target} (${rec.scope}) — ${res.removed.length}개 제거 ${dim(tildify(rec.root))}`)); + const res = await removeInstall(rec, { force: opts.force, dryRun: opts.dryRun }); + const verb = opts.dryRun ? "제거할 파일" : "제거"; + console.log(ok(`${rec.target} (${rec.scope}) — ${verb} ${res.removed.length}개 ${dim(tildify(rec.root))}`)); if (res.keptModified.length > 0) { + const tense = opts.dryRun ? "남긴다" : "남겼다"; console.log( - warn(` 직접 수정한 파일 ${res.keptModified.length}개는 남겼다 (--force 로 함께 지운다)`), + warn(` 직접 수정한 파일 ${res.keptModified.length}개는 ${tense} (--force 로 함께 지운다)`), ); for (const p of res.keptModified.slice(0, 5)) console.log(dim(` ${tildify(p)}`)); } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 7f5980b..632b462 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -7,7 +7,7 @@ import { dim, heading, info, ok, warn } from "../ui.ts"; * 설치 기록을 그대로 따라가며 재설치한다. * 사용자가 고친 파일은 기본적으로 보존된다(applyPlan 이 판단) — 강제로 맞추려면 --force. */ -export async function runUpdate(opts: { force?: boolean } = {}): Promise { +export async function runUpdate(opts: { force?: boolean; dryRun?: boolean } = {}): Promise { const version = await getVersion(); const manifest = await readManifest(); @@ -16,7 +16,7 @@ export async function runUpdate(opts: { force?: boolean } = {}): Promise return 1; } - console.log(heading(`업데이트 → v${version}`)); + console.log(heading(opts.dryRun ? `업데이트 계획 → v${version}` : `업데이트 → v${version}`)); const stale = manifest.installs.filter((i) => i.version !== version); if (stale.length === 0) { @@ -48,8 +48,9 @@ export async function runUpdate(opts: { force?: boolean } = {}): Promise targets: [rec.target], scope: rec.scope, force: opts.force, + dryRun: opts.dryRun, }); - printOutcomes(outcomes); + printOutcomes(outcomes, opts.dryRun); } return 0; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7855ce1..937c589 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -78,7 +78,7 @@ ${bold("옵션")} -s, --scope <범위> user | project ${dim("(기본: user)")} -y, --yes 확인 없이 진행합니다 -f, --force 직접 수정한 파일도 덮어씁니다 ${dim("(.orig 로 백업)")} - --dry-run 쓰지 않고 계획만 보여줍니다 + --dry-run 쓰지 않고 계획만 보여줍니다 (install · update · uninstall) --no-update-check 새 버전 확인을 건너뜁니다 -h, --help 이 도움말 -v, --version 버전 @@ -87,6 +87,7 @@ ${bold("예시")} ${dim("npx designpaca install -t claude-code,codex -s user -y")} ${dim("npx designpaca install -t cursor -s project")} ${dim("npx designpaca update --force")} + ${dim("npx designpaca update --dry-run")} ${dim("npx designpaca tools --yes")} `.trimStart(); } @@ -140,11 +141,11 @@ async function main(): Promise { break; } case "update": - code = await runUpdate({ force: args.force }); + code = await runUpdate({ force: args.force, dryRun: args.dryRun }); break; case "uninstall": case "remove": - code = await runUninstall({ targets: args.targets, force: args.force }); + code = await runUninstall({ targets: args.targets, force: args.force, dryRun: args.dryRun }); break; case "doctor": code = await runDoctor(); diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index b0c7f59..092da36 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -22,7 +22,13 @@ after(async () => { function run(args: string[]): string { return execFileSync(process.execPath, [CLI, ...args, "--no-update-check"], { encoding: "utf8", - env: { ...process.env, HOME: home, USERPROFILE: home, FORCE_COLOR: "0" }, + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + FORCE_COLOR: "0", + DESIGNPACA_STATE_DIR: path.join(home, ".designpaca"), + }, }); } @@ -58,6 +64,52 @@ test("알 수 없는 대상은 종료 코드 2 로 거부한다", { skip: !built ); }); +test("update --dry-run 은 파일도 매니페스트도 건드리지 않는다", { skip: !built }, async () => { + run(["install", "-t", "claude-code", "-s", "user", "-y"]); + const manifestFile = path.join(home, ".designpaca", "manifest.json"); + const skillMd = path.join(home, ".claude", "skills", "designpaca", "SKILL.md"); + + // 업데이트 대상이 있는 상태를 만든다 + const manifest = JSON.parse(await fs.readFile(manifestFile, "utf8")); + manifest.installs[0].version = "0.0.1"; + await fs.writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n", "utf8"); + + const manifestBefore = await fs.readFile(manifestFile, "utf8"); + const skillBefore = await fs.readFile(skillMd, "utf8"); + const statBefore = await fs.stat(skillMd); + + const out = run(["update", "--dry-run"]); + assert.ok(out.includes("업데이트 계획")); + assert.ok(out.includes("설치 계획")); + + assert.equal(await fs.readFile(manifestFile, "utf8"), manifestBefore); + assert.equal(await fs.readFile(skillMd, "utf8"), skillBefore); + const statAfter = await fs.stat(skillMd); + assert.equal(statAfter.mtimeMs, statBefore.mtimeMs); + + // 대조군: 실제 update 는 반영돼야 한다 + run(["update"]); + const manifestAfter = JSON.parse(await fs.readFile(manifestFile, "utf8")); + assert.equal(manifestAfter.installs[0].version, run(["--version"]).trim()); + + run(["uninstall", "-y"]); +}); + +test("uninstall --dry-run 은 아무것도 지우지 않는다", { skip: !built }, async () => { + run(["install", "-t", "claude-code", "-s", "user", "-y"]); + const skillMd = path.join(home, ".claude", "skills", "designpaca", "SKILL.md"); + const manifestFile = path.join(home, ".designpaca", "manifest.json"); + + const out = run(["uninstall", "--dry-run"]); + assert.ok(out.includes("제거 계획")); + await fs.access(skillMd); + const manifest = JSON.parse(await fs.readFile(manifestFile, "utf8")); + assert.equal(manifest.installs.length, 1); + + run(["uninstall", "-y"]); + await assert.rejects(() => fs.access(skillMd)); +}); + test("설치 → doctor → 제거 왕복", { skip: !built }, async () => { run(["install", "-t", "claude-code", "-s", "user", "-y"]); const skillMd = path.join(home, ".claude", "skills", "designpaca", "SKILL.md"); diff --git a/packages/core/src/installer.ts b/packages/core/src/installer.ts index 18e4ed0..7538630 100644 --- a/packages/core/src/installer.ts +++ b/packages/core/src/installer.ts @@ -149,7 +149,7 @@ export interface RemoveResult { /** 매니페스트에 기록된 것만 되돌린다. 기록에 없는 파일은 손대지 않는다. */ export async function removeInstall( record: InstallRecord, - opts: { force?: boolean; env?: EnvOptions } = {}, + opts: { force?: boolean; dryRun?: boolean; env?: EnvOptions } = {}, ): Promise { const drift = await inspectDrift(record); const dirty = new Set(drift.modified); @@ -164,21 +164,25 @@ export async function removeInstall( if (f.marker) { const cur = await readIfExists(f.path); if (cur === null) continue; - const next = removeBlock(cur, f.marker); - // 블록만 남아 있던 문서라면 파일째 지운다 - if (next.trim().length === 0) await removeFileAndPrune(f.path, path.dirname(f.path)); - else await writeAtomic(f.path, next); - } else { + if (!opts.dryRun) { + const next = removeBlock(cur, f.marker); + // 블록만 남아 있던 문서라면 파일째 지운다 + if (next.trim().length === 0) await removeFileAndPrune(f.path, path.dirname(f.path)); + else await writeAtomic(f.path, next); + } + } else if (!opts.dryRun) { await removeFileAndPrune(f.path, record.root); } removed.push(f.path); } - if (keptModified.length === 0) { - await dropInstall(record.target, record.scope, record.root, opts.env?.home); - } else { - // 일부만 남았으면 기록도 남은 것만 유지한다 - await upsertInstall({ ...record, files: record.files.filter((f) => dirty.has(f.path)) }, opts.env?.home); + if (!opts.dryRun) { + if (keptModified.length === 0) { + await dropInstall(record.target, record.scope, record.root, opts.env?.home); + } else { + // 일부만 남았으면 기록도 남은 것만 유지한다 + await upsertInstall({ ...record, files: record.files.filter((f) => dirty.has(f.path)) }, opts.env?.home); + } } return { removed, keptModified }; diff --git a/packages/core/test/installer.test.ts b/packages/core/test/installer.test.ts index de9bb59..70447fd 100644 --- a/packages/core/test/installer.test.ts +++ b/packages/core/test/installer.test.ts @@ -56,6 +56,21 @@ test("claude-code: 설치 → 드리프트 없음 → 제거", async () => { await assert.rejects(() => fs.access(plan.root)); }); +test("removeInstall dryRun 은 파일과 매니페스트를 건드리지 않는다", async () => { + const plan = await planInstall("claude-code", "user", skill, env); + const res = await applyPlan(plan, skill.version, { env }); + + const dry = await removeInstall(res.record, { env, dryRun: true }); + assert.equal(dry.removed.length, 3); + assert.equal(dry.keptModified.length, 0); + await fs.access(path.join(plan.root, "SKILL.md")); + assert.equal((await readManifest(home)).installs.length, 1); + + const real = await removeInstall(res.record, { env }); + assert.equal(real.removed.length, 3); + assert.equal((await readManifest(home)).installs.length, 0); +}); + test("사용자가 고친 파일은 update 가 건너뛴다", async () => { const plan = await planInstall("claude-code", "user", skill, env); const first = await applyPlan(plan, skill.version, { env }); From 2a7017717b708b9959d4de73f8618ed7b26bf77c Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 14 Sep 2026 19:53:40 +0900 Subject: [PATCH 2/2] release: v0.12.1 --- .changeset/dry-run-update-uninstall.md | 6 ------ packages/cli/CHANGELOG.md | 6 ++++++ packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 6 ++++++ packages/core/package.json | 2 +- packages/skill/CHANGELOG.md | 2 ++ packages/skill/package.json | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) delete mode 100644 .changeset/dry-run-update-uninstall.md diff --git a/.changeset/dry-run-update-uninstall.md b/.changeset/dry-run-update-uninstall.md deleted file mode 100644 index 975ec1b..0000000 --- a/.changeset/dry-run-update-uninstall.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"designpaca": patch -"@designpaca/core": patch ---- - -`update` 와 `uninstall` 이 `--dry-run` 을 무시하고 실제로 파일과 매니페스트를 바꾸던 문제를 고쳤다. 두 명령 모두 계획만 출력하며, CI 스모크에서 `--dry-run` 이 아무것도 쓰지 않는지 검사한다. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 644482c..344c77f 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # designpaca +## 0.12.1 + +### Patch Changes + +- 744cb36: `update` 와 `uninstall` 이 `--dry-run` 을 무시하고 실제로 파일과 매니페스트를 바꾸던 문제를 고쳤다. 두 명령 모두 계획만 출력하며, CI 스모크에서 `--dry-run` 이 아무것도 쓰지 않는지 검사한다. + ## 0.12.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index fcafe17..bf80938 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "designpaca", - "version": "0.12.0", + "version": "0.12.1", "description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다", "keywords": [ "design", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 984db74..465c38b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @designpaca/core +## 0.12.1 + +### Patch Changes + +- 744cb36: `update` 와 `uninstall` 이 `--dry-run` 을 무시하고 실제로 파일과 매니페스트를 바꾸던 문제를 고쳤다. 두 명령 모두 계획만 출력하며, CI 스모크에서 `--dry-run` 이 아무것도 쓰지 않는지 검사한다. + ## 0.12.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index f19526f..b6f7225 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/core", - "version": "0.12.0", + "version": "0.12.1", "private": true, "description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지", "type": "module", diff --git a/packages/skill/CHANGELOG.md b/packages/skill/CHANGELOG.md index 052521e..43634b3 100644 --- a/packages/skill/CHANGELOG.md +++ b/packages/skill/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG +## 0.12.1 + ## 0.12.0 ### Minor Changes diff --git a/packages/skill/package.json b/packages/skill/package.json index 09ab188..9ae442e 100644 --- a/packages/skill/package.json +++ b/packages/skill/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/skill", - "version": "0.12.0", + "version": "0.12.1", "private": true, "description": "designpaca 스킬 원본 — SKILL.md 와 참조 문서", "scripts": {