diff --git a/README.es.md b/README.es.md index 84cd13e..fcc7139 100644 --- a/README.es.md +++ b/README.es.md @@ -163,6 +163,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +Para desinstalar, ejecuta `npx codex-workflows uninstall` (con `--user` si es una instalación de usuario). Los archivos que hayas modificado localmente no se borran. + --- ## Referencia de flujos diff --git a/README.ja.md b/README.ja.md index 3dd6708..ed3c0fd 100644 --- a/README.ja.md +++ b/README.ja.md @@ -163,6 +163,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +アンインストールは`npx codex-workflows uninstall`で行います(ユーザー単位のインストールでは`--user`を付けます)。ローカルで編集したファイルは削除されません。 + --- ## ワークフローレシピ一覧 diff --git a/README.ko.md b/README.ko.md index 519ffa1..f32893f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -163,6 +163,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +제거하려면 `npx codex-workflows uninstall`을 실행하세요. 사용자 수준 설치라면 `--user`를 붙입니다. 로컬에서 수정한 파일은 삭제되지 않습니다. + --- ## 워크플로 레시피 목록 diff --git a/README.md b/README.md index 227ae8a..4e74c27 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +To uninstall, run `npx codex-workflows uninstall`, adding `--user` for a user-level installation. Files you have modified locally are left in place. + --- ## Workflow Recipe Reference diff --git a/README.pt-BR.md b/README.pt-BR.md index 25ebc15..c87906d 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -163,6 +163,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +Para desinstalar, execute `npx codex-workflows uninstall` (em uma instalação de usuário, acrescente `--user`). Arquivos modificados localmente não são apagados. + --- ## Referência dos fluxos diff --git a/README.zh-CN.md b/README.zh-CN.md index 304de91..95af9fa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -163,6 +163,8 @@ npx codex-workflows status npx codex-workflows status --user ``` +如需卸载,请运行`npx codex-workflows uninstall`(用户级安装需加上`--user`)。你在本地修改过的文件不会被删除。 + --- ## 工作流参考 diff --git a/bin/cli.js b/bin/cli.js index 5ad650f..0bc6f49 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -20,6 +20,7 @@ const COMMAND_OPTIONS = { install: new Set(["--user"]), update: new Set(["--dry-run", "--user"]), status: new Set(["--user"]), + uninstall: new Set(["--dry-run", "--user"]), "--version": new Set(), "-v": new Set(), "--help": new Set(), @@ -717,6 +718,59 @@ function printUpdateSummary(counts, dryRun) { console.log(`\n${dryRun ? "[DRY RUN] " : ""}${parts.join(", ")}.`); } +function uninstall(installation, dryRun) { + const manifest = readManifest(installation); + if (!manifest) throw new CliError("codex-workflows is not installed."); + + const installedHashes = Array.isArray(manifest.files) + ? Object.fromEntries(manifest.files.map(file => [file, null])) + : manifest.files; + const state = readInstalledState(installation, installedHashes); + const prefix = dryRun ? "[DRY RUN] " : ""; + console.log( + `${prefix}Uninstalling codex-workflows v${manifest.version} (${installation.scope})...\n` + ); + + const vacated = new Set(); + const pruneCandidates = []; + let removed = 0; + let kept = 0; + for (const [manifestPath, entry] of state) { + if (!entry.exists) continue; + // Locally modified files stay in place, like dpkg conffiles on remove + if (!entry.baselineHash || entry.currentHash !== entry.baselineHash) { + const reason = entry.baselineHash ? "modified locally" : "no install hash"; + console.log(` ~ ${manifestPath} (${reason}, kept)`); + kept++; + continue; + } + console.log(` - ${manifestPath} (removed)`); + if (!dryRun) fs.unlinkSync(entry.destinationPath); + vacated.add(entry.destinationPath); + pruneCandidates.push({ + dir: path.dirname(entry.destinationPath), + root: installation.targetDir, + }); + removed++; + } + + const prunedDirs = pruneVacatedDirs(pruneCandidates, vacated, new Set(), dryRun); + for (const dir of prunedDirs) { + console.log(` - ${path.relative(installation.targetDir, dir)}/ (removed)`); + } + if (!dryRun) fs.unlinkSync(installation.manifestPath); + + const parts = [`${removed} removed`]; + if (prunedDirs.length > 0) parts.push(`${prunedDirs.length} empty dirs removed`); + if (kept > 0) parts.push(`${kept} kept (local changes)`); + console.log(`\n${prefix}${parts.join(", ")}.`); + if (kept > 0) { + console.log( + "Kept files are still loaded by Codex. Delete them manually if you no longer need them." + ); + } +} + function status(installation) { const manifest = readManifest(installation); if (!manifest) { @@ -741,6 +795,8 @@ Usage: npx codex-workflows update [--user] Update managed files npx codex-workflows update [--user] --dry-run Preview changes without applying npx codex-workflows status [--user] Show installation info + npx codex-workflows uninstall [--user] Remove unmodified managed files + npx codex-workflows uninstall [--user] --dry-run Preview removal without applying npx codex-workflows --version Show version npx codex-workflows --help Show this help @@ -755,6 +811,7 @@ function run({ command, dryRun, installation }) { case "install": install(installation); break; case "update": update(installation, dryRun); break; case "status": status(installation); break; + case "uninstall": uninstall(installation, dryRun); break; case "--version": case "-v": console.log(getVersion(installation.sourceDir)); break; case "--help": case "-h": case undefined: showHelp(); break; default: diff --git a/scripts/tests/cli.test.js b/scripts/tests/cli.test.js index 409d871..7884e4f 100644 --- a/scripts/tests/cli.test.js +++ b/scripts/tests/cli.test.js @@ -527,3 +527,85 @@ test("removes an emptied skill directory after preserving a locally modified fil "locally edited\n" ); }); + +test("uninstall removes unmodified managed files, emptied directories, and the manifest", () => { + const cwd = makeTemporaryDirectory("codex-workflows-project"); + const installResult = runCli(["install"], { cwd }); + assert.equal(installResult.status, 0, installResult.stderr); + fs.writeFileSync(path.join(cwd, ".codex/config.toml"), "model = \"custom\"\n"); + + const result = runCli(["uninstall"], { cwd }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(fs.existsSync(path.join(cwd, ".agents")), false); + assert.equal(fs.existsSync(path.join(cwd, ".codex/agents")), false); + assert.equal(fs.readFileSync(path.join(cwd, ".codex/config.toml"), "utf8"), "model = \"custom\"\n"); + assert.equal(fs.existsSync(path.join(cwd, ".codex-workflows-manifest.json")), false); +}); + +test("uninstall keeps locally modified and user-added files in place", () => { + const cwd = makeTemporaryDirectory("codex-workflows-project"); + const modifiedPath = ".agents/skills/example/SKILL.md"; + const siblingPath = ".agents/skills/example/references/notes.md"; + const userPath = ".agents/skills/personal/SKILL.md"; + writeInstalledFixture({ + cwd, + version: "1.0.0", + files: { [modifiedPath]: "original\n", [siblingPath]: "notes\n" }, + }); + fs.writeFileSync(path.join(cwd, modifiedPath), "locally edited\n"); + fs.mkdirSync(path.dirname(path.join(cwd, userPath)), { recursive: true }); + fs.writeFileSync(path.join(cwd, userPath), "mine\n"); + + const result = runCli(["uninstall"], { cwd }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /modified locally, kept/); + assert.equal(fs.readFileSync(path.join(cwd, modifiedPath), "utf8"), "locally edited\n"); + assert.equal(fs.readFileSync(path.join(cwd, userPath), "utf8"), "mine\n"); + assert.equal(fs.existsSync(path.join(cwd, ".agents/skills/example/references")), false); + assert.equal(fs.existsSync(path.join(cwd, ".codex-workflows-preserved")), false); + assert.equal(fs.existsSync(path.join(cwd, ".codex-workflows-manifest.json")), false); +}); + +test("uninstall --user leaves CODEX_HOME and unrelated user files in place", () => { + const cwd = makeTemporaryDirectory("codex-workflows-project"); + const codexHome = makeTemporaryDirectory("codex-workflows-codex-home"); + const unrelatedFile = path.join(codexHome, "skills/personal/SKILL.md"); + fs.mkdirSync(path.dirname(unrelatedFile), { recursive: true }); + fs.writeFileSync(unrelatedFile, "personal skill\n"); + const installResult = runCli(["install", "--user"], { cwd, codexHome }); + assert.equal(installResult.status, 0, installResult.stderr); + + const result = runCli(["uninstall", "--user"], { cwd, codexHome }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(fs.readFileSync(unrelatedFile, "utf8"), "personal skill\n"); + assert.equal(fs.existsSync(path.join(codexHome, "skills/coding-rules")), false); + assert.equal(fs.existsSync(path.join(codexHome, "agents")), false); + assert.equal(fs.existsSync(path.join(codexHome, ".codex-workflows-manifest.json")), false); + assert.equal(fs.existsSync(codexHome), true); +}); + +test("uninstall dry run reports removals without changing files", () => { + const cwd = makeTemporaryDirectory("codex-workflows-project"); + const managedPath = ".agents/skills/example/SKILL.md"; + writeInstalledFixture({ cwd, version: "1.0.0", files: { [managedPath]: "original\n" } }); + + const result = runCli(["uninstall", "--dry-run"], { cwd }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /\[DRY RUN\]/); + assert.match(result.stdout, /- \.agents\/skills\/example\/ \(removed\)/); + assert.equal(fs.existsSync(path.join(cwd, managedPath)), true); + assert.equal(fs.existsSync(path.join(cwd, ".codex-workflows-manifest.json")), true); +}); + +test("uninstall fails when nothing is installed", () => { + const cwd = makeTemporaryDirectory("codex-workflows-project"); + + const result = runCli(["uninstall"], { cwd }); + + assert.equal(result.status, 1); + assert.match(result.stderr, /not installed/); +});