fix: honor --dry-run in update and uninstall

This commit is contained in:
Yun Chan 2026-09-14 19:53:20 +09:00
parent 6229538d29
commit 744cb36d74
9 changed files with 113 additions and 23 deletions

View file

@ -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` | 새 버전 확인 생략 |
## 요구사항

View file

@ -4,6 +4,7 @@ import { dim, heading, ok, warn } from "../ui.ts";
export async function runUninstall(opts: {
targets?: TargetId[];
force?: boolean;
dryRun?: boolean;
}): Promise<number> {
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)}`));
}

View file

@ -7,7 +7,7 @@ import { dim, heading, info, ok, warn } from "../ui.ts";
* 설치 기록을 그대로 따라가며 재설치한다.
* 사용자가 고친 파일은 기본적으로 보존된다(applyPlan 이 판단) — 강제로 맞추려면 --force.
*/
export async function runUpdate(opts: { force?: boolean } = {}): Promise<number> {
export async function runUpdate(opts: { force?: boolean; dryRun?: boolean } = {}): Promise<number> {
const version = await getVersion();
const manifest = await readManifest();
@ -16,7 +16,7 @@ export async function runUpdate(opts: { force?: boolean } = {}): Promise<number>
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<number>
targets: [rec.target],
scope: rec.scope,
force: opts.force,
dryRun: opts.dryRun,
});
printOutcomes(outcomes);
printOutcomes(outcomes, opts.dryRun);
}
return 0;
}

View file

@ -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<number> {
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();

View file

@ -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");