Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@ This keeps the package small while avoiding live downloads from third-party skil
```
-y, --yes Skip confirmation prompt
--dry-run Show what would be installed without installing
-r, --remove Remove-only mode — skip detection, list installed skills
-h, --help Show help message
```

The default `npx autoskills` picker also supports removal: installed skills start
checked, and unchecking any of them queues a removal that runs after a y/N
confirmation. Use `-r` when you want to skip detection entirely.

## Supported Technologies

Built to work across modern frontend, backend, mobile, cloud, and media stacks.
Expand Down
16 changes: 16 additions & 0 deletions packages/autoskills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ npx autoskills -y
npx autoskills --dry-run
```

### Remove installed skills

Removal works two ways:

- **From the default picker.** Running `npx autoskills` now pre-checks every already-installed skill. Uncheck any of them and confirm to remove them in the same pass as installing new ones. Whenever the selection involves removals, you get a `About to install N and remove M skill(s). Continue? [y/N]` prompt before anything destructive happens.
- **Remove-only mode.** When you just want to clean up, use:

```bash
npx autoskills -r
```

This skips detection entirely, lists every skill currently installed in your project, and lets you pick which ones to remove. Pass `-y` to remove all installed skills without confirmation.

Either way, the canonical `.agents/skills/<name>` directory, the per-agent symlinks (`.claude/skills/<name>`, `.cline/skills/<name>`, etc.), and the matching `skills-lock.json` entry are all cleaned up.

### Claude Code summary

If `claude-code` is auto-detected or passed with `-a`, `autoskills` writes a `CLAUDE.md` file in your project root summarizing the markdown files installed under `.claude/skills`.
Expand All @@ -46,6 +61,7 @@ If `claude-code` is auto-detected or passed with `-a`, `autoskills` writes a `CL
| ----------------- | ----------------------------------------------------- |
| `-y`, `--yes` | Skip confirmation prompt, install all detected skills |
| `--dry-run` | Show detected skills without installing anything |
| `-r`, `--remove` | Remove-only mode — skip detection, list installed |
| `-v`, `--verbose` | Show install trace and error details |
| `-h`, `--help` | Show help message |

Expand Down
80 changes: 80 additions & 0 deletions packages/autoskills/changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createInterface } from "node:readline";

import { bold, dim, yellow } from "./colors.ts";
import { parseSkillPath } from "./lib.ts";
import type { SkillEntry } from "./lib.ts";

// ── Symmetric Selection Helpers ──────────────────────────────
//
// These power the default picker's symmetric checkbox semantics:
// checked = "I want this installed", unchecked installed = "remove it".
// Kept in their own module so tests can import them without executing
// main.ts's top-level CLI bootstrap.

export interface SkillChanges {
/** Not previously installed and currently checked. */
installs: SkillEntry[];
/**
* Bare last-segment names of skills to remove — the same form used as
* keys in `skills-lock.json` and accepted by `uninstallSkill`. Derived
* from `SkillEntry.skill` via `parseSkillPath().skillName`. Empty-string
* names (e.g. raw URL skills with no parseable name) are dropped.
*/
removes: string[];
/** Previously installed and still checked — left alone. */
keeps: SkillEntry[];
/** Not installed and unchecked — ignored. */
skips: SkillEntry[];
}

export function computeChanges(skills: SkillEntry[], selected: boolean[]): SkillChanges {
if (skills.length !== selected.length) {
throw new Error(
`selected length (${selected.length}) must match skills length (${skills.length})`,
);
}
const installs: SkillEntry[] = [];
const removes: string[] = [];
const keeps: SkillEntry[] = [];
const skips: SkillEntry[] = [];
for (let i = 0; i < skills.length; i++) {
const s = skills[i];
const checked = selected[i];
if (s.installed && checked) keeps.push(s);
else if (s.installed && !checked) {
const { skillName } = parseSkillPath(s.skill);
if (skillName) removes.push(skillName);
} else if (!s.installed && checked) installs.push(s);
else skips.push(s);
}
return { installs, removes, keeps, skips };
}

export async function confirmDestructive(
installs: number,
removes: number,
opts: {
autoYes?: boolean;
input?: NodeJS.ReadableStream;
output?: NodeJS.WritableStream;
} = {},
): Promise<boolean> {
if (removes === 0) return true;
if (opts.autoYes) return true;

const out = opts.output ?? process.stdout;
const noun = `skill${removes === 1 ? "" : "s"}`;
out.write("\n");
out.write(yellow(` ⚠ About to install ${installs} and remove ${removes} ${noun}.`) + "\n");
out.write(` ${bold("Continue?")} ${dim("[y/N]")} `);

const input = opts.input ?? process.stdin;
const rl = createInterface({ input, output: out, terminal: false });
return new Promise<boolean>((resolve) => {
rl.once("line", (answer) => {
rl.close();
const trimmed = answer.trim();
resolve(trimmed === "y" || trimmed === "Y");
});
});
}
Loading