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
29 changes: 10 additions & 19 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,16 @@ tests/

### How `skills check` and `skills update` Work

1. Read `~/.agents/.skill-lock.json` for installed skills
2. For each skill, get `skillFolderHash` from lock file
3. POST to `https://add-skill.vercel.sh/check-updates` with:
```json
{
"skills": [{ "name": "...", "source": "...", "skillFolderHash": "..." }],
"forceRefresh": true
}
```
4. API fetches fresh content from GitHub, computes hash, compares
5. Returns list of skills with different hashes (updates available)

### Why `forceRefresh: true`?

Both `check` and `update` always send `forceRefresh: true`. This ensures the API fetches fresh content from GitHub rather than using its Redis cache.

**Without forceRefresh:** Users saw phantom "updates available" due to stale cached hashes. The fix was to always fetch fresh.

**Tradeoff:** Slightly slower (GitHub API call per skill), but always accurate.
1. Read lock files from project and/or global scope:
- Project: `<cwd>/.agents/.skill-lock.json`
- Global: `~/.agents/.skill-lock.json`
2. For each GitHub skill, compare stored `skillFolderHash` to current GitHub tree SHA via `fetchSkillFolderHash`.
3. `skills check` reports available updates.
4. `skills update` reinstalls changed skills via `npx skills add ... --rename <installed-name>` to preserve local rename aliases.
5. Scope flags:
- `--project` / `-p`: project only
- `--global` / `-g`: global only
- default: both scopes

### Lock File Compatibility

Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ npx skills add ./my-local-skills
| `-g, --global` | Install to user directory instead of project |
| `-a, --agent <agents...>` | <!-- agent-names:start -->Target specific agents (e.g., `claude-code`, `codex`). See [Available Agents](#available-agents)<!-- agent-names:end --> |
| `-s, --skill <skills...>` | Install specific skills by name (use `'*'` for all skills) |
| `--rename <name>` | Install the selected skill under a different local name (updates folder + `SKILL.md` frontmatter `name`) |
| `-l, --list` | List available skills without installing |
| `--copy` | Copy files instead of symlinking to agent directories |
| `-y, --yes` | Skip all confirmation prompts |
Expand Down Expand Up @@ -72,6 +73,9 @@ npx skills add vercel-labs/agent-skills --skill '*' -a claude-code

# Install specific skills to all agents
npx skills add vercel-labs/agent-skills --agent '*' --skill frontend-design

# Install a skill under a different local name
npx skills add vercel-labs/agent-skills --skill review --rename team-review
```

### Installation Scope
Expand Down Expand Up @@ -131,13 +135,21 @@ npx skills find typescript
### `skills check` / `skills update`

```bash
# Check if any installed skills have updates
# Check for updates in both project + global lock files (default)
npx skills check

# Update all skills to latest versions
# Check only project-scoped installs
npx skills check --project

# Update all tracked skills (project + global)
npx skills update

# Update only global installs
npx skills update --global
```

`skills update` preserves local install names (including `--rename`) during upgrades.

### `skills init`

```bash
Expand Down
76 changes: 75 additions & 1 deletion src/add.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, rmSync, mkdirSync, writeFileSync } from 'fs';
import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { runCli } from './test-utils.ts';
Expand Down Expand Up @@ -158,6 +158,68 @@ description: Test
expect(result.stdout).toContain('No project skills found in skills-lock.json');
});

it('should install a skill with --rename and rewrite SKILL.md name', () => {
const sourceSkillDir = join(testDir, 'source', 'my-skill');
mkdirSync(sourceSkillDir, { recursive: true });
writeFileSync(
join(sourceSkillDir, 'SKILL.md'),
`---
name: my-skill
description: Original name skill
---
# My Skill
`
);

const projectDir = join(testDir, 'project');
mkdirSync(projectDir, { recursive: true });

const result = runCli(
['add', join(testDir, 'source'), '-y', '--agent', 'amp', '--rename', 'renamed-skill'],
projectDir
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('renamed-skill');

const installedPath = join(projectDir, '.agents', 'skills', 'renamed-skill', 'SKILL.md');
expect(existsSync(installedPath)).toBe(true);
const installed = readFileSync(installedPath, 'utf-8');
expect(installed).toContain('name: renamed-skill');
});

it('should reject --rename when multiple skills are selected', () => {
const skillOneDir = join(testDir, 'multi', 'skill-one');
const skillTwoDir = join(testDir, 'multi', 'skill-two');
mkdirSync(skillOneDir, { recursive: true });
mkdirSync(skillTwoDir, { recursive: true });

writeFileSync(
join(skillOneDir, 'SKILL.md'),
`---
name: skill-one
description: First skill
---
# Skill One
`
);
writeFileSync(
join(skillTwoDir, 'SKILL.md'),
`---
name: skill-two
description: Second skill
---
# Skill Two
`
);

const result = runCli(
['add', join(testDir, 'multi'), '-y', '--agent', 'amp', '--rename', 'renamed'],
testDir
);
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain('--rename requires exactly one selected skill');
});

describe('internal skills', () => {
it('should skip internal skills by default', () => {
// Create an internal skill
Expand Down Expand Up @@ -389,6 +451,18 @@ describe('parseAddOptions', () => {
expect(result.options.list).toBe(true);
expect(result.options.global).toBe(true);
});

it('should parse --rename with a value', () => {
const result = parseAddOptions(['source', '--rename', 'renamed-skill']);
expect(result.source).toEqual(['source']);
expect(result.options.rename).toBe('renamed-skill');
});

it('should mark --rename as invalid when value is missing', () => {
const result = parseAddOptions(['source', '--rename']);
expect(result.source).toEqual(['source']);
expect(result.options.rename).toBe('');
});
});

describe('find-skills prompt with -y flag', () => {
Expand Down
Loading