Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ npx codex-workflows status
npx codex-workflows status --user
```

アンインストールは`npx codex-workflows uninstall`で行います(ユーザー単位のインストールでは`--user`を付けます)。ローカルで編集したファイルは削除されません。

---

## ワークフローレシピ一覧
Expand Down
2 changes: 2 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ npx codex-workflows status
npx codex-workflows status --user
```

제거하려면 `npx codex-workflows uninstall`을 실행하세요. 사용자 수준 설치라면 `--user`를 붙입니다. 로컬에서 수정한 파일은 삭제되지 않습니다.

---

## 워크플로 레시피 목록
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ npx codex-workflows status
npx codex-workflows status --user
```

如需卸载,请运行`npx codex-workflows uninstall`(用户级安装需加上`--user`)。你在本地修改过的文件不会被删除。

---

## 工作流参考
Expand Down
57 changes: 57 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) {
Expand All @@ -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

Expand All @@ -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:
Expand Down
82 changes: 82 additions & 0 deletions scripts/tests/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Loading