Coding guidelines for AI agents working on this project.
skills-manager is a CLI tool that backs up and restores AI agent skills to GitHub. It is a companion to vercel-labs/skills — it reads the lock file that vercel-labs/skills owns but never modifies it.
src/
├── index.ts # CLI entry point (Commander.js)
├── errors.ts # CliError class for structured error handling
├── agents.ts # Agent registry (46 agents) + path resolution
├── auth.ts # GitHub token resolution (gh CLI → env vars → interactive)
├── config.ts # XDG-compliant config persistence (~/.config/skills-manager/)
├── lockfile.ts # .skill-lock.json reader (READ ONLY)
├── linker.ts # Symlink creation (relative) + project copy/link (absolute)
├── git-ops.ts # Git push/pull via simple-git + repo init/remote setup
└── commands/
├── push.ts # Push handler (auto git-init + remote prompt)
├── pull.ts # Pull handler (auto-runs link)
└── link.ts # Link handler (interactive multiselect, remembers selection, --project mode)
export const AGENTS_DIR = join(homedir(), '.agents'); // git repo root
export const CANONICAL_SKILLS_DIR = join(homedir(), '.agents', 'skills'); // symlink target
export const SKILL_LOCK_PATH = join(homedir(), '.agents', '.skill-lock.json');push: ~/.agents/ → git add → git commit → git push origin <branch>
pull: git pull --rebase → auto-run link
link: read .skill-lock.json → [--agents or multiselect] → create relative symlinks
link --project: read .skill-lock.json → [--skills or select skills] → [--mode or select copy/symlink] → [--agents or select agents] → group by projectPath → copy/link to CWD
The lock file at ~/.agents/.skill-lock.json is owned by vercel-labs/skills. This project must NEVER create, modify, or delete it. Only read lastSelectedAgents from it.
The entire ~/.agents/ directory is the git repository — not ~/.agents/skills/. This ensures both .skill-lock.json and the skills/ directory are versioned together.
Global mode (link) uses relative symlinks via computeRelativeSymlinkTarget() from linker.ts, matching the convention set by vercel-labs/skills. Project mode (link --project) uses absolute symlinks via createProjectSymlinks() (or direct copy via copySkills()) because CWD and ~/.agents/skills/ are in unrelated directory trees.
The 46-agent registry in agents.ts must stay synchronized with the upstream agent list. When adding agents, follow the existing pattern of universal vs non-universal classification.
- Language: TypeScript (strict mode, ESM)
- Target: ES2022, Node16 module resolution
- Runtime: Node.js ≥ 20
- CLI: Commander.js
- Git: simple-git (named import:
import { simpleGit } from 'simple-git') - Prompts: @clack/prompts
- Testing: Vitest + @vitest/coverage-v8
- Linting: ESLint (flat config, typescript-eslint)
- Hooks: Husky (pre-commit: lint-staged, pre-push: test:coverage)
This project uses ESM ("type": "module" in package.json). All imports must use .js extensions:
import { foo } from './bar.js'; // correct
import { foo } from './bar'; // wrong — will fail at runtime// Named imports for simple-git (default import causes TS errors with Node16 resolution)
import { simpleGit } from 'simple-git';
// Namespace import for @clack/prompts
import * as p from '@clack/prompts';- Strict mode enabled — no
as any,@ts-ignore, or@ts-expect-error - Use the
AgentIdunion type for agent identifiers - Use
p.multiselect<string>({...})to fix union type distribution issues with @clack/prompts
Commands follow this pattern:
- Validate prerequisites (auth, paths, lock file)
- Show spinner during async operations
- Catch errors →
p.cancel()→process.exit(1)
Use CliError from errors.ts for user-facing errors with structured exit codes.
- TDD: write failing test first, implement, verify pass
- Tests use Vitest with globals enabled
- Mock filesystem operations — don't touch real
~/.agents/ - Test files are colocated:
foo.ts→foo.test.ts - Use
vi.resetAllMocks()(notvi.clearAllMocks()) to resetmockReturnValueimplementations - When mocking
node:child_process, place it invi.hoisted()block - Run tests:
npm test - Run coverage:
npm run test:coverage
Commit messages follow Conventional Commits:
feat:— new featurefix:— bug fixchore:— tooling, dependenciesdocs:— documentation onlytest:— test additions or fixes
| Variable | Purpose | Default |
|---|---|---|
GITHUB_TOKEN |
Auth fallback | — |
GH_TOKEN |
Auth fallback | — |
XDG_CONFIG_HOME |
Path resolution for opencode, amp, kimi-cli, replit, universal, goose, crush | ~/.config |
CODEX_HOME |
Path resolution for codex | ~/.codex |
CLAUDE_CONFIG_DIR |
Path resolution for claude-code | ~/.claude |
- CI (
ci.yml): Runs on push/PR to main/master → lint → test:coverage → build (Node 22) - Release (
publish.yml): Runs on push to main/master → lint → test → build →semantic-release(auto version bump + npm publish via OIDC + GitHub Release + CHANGELOG.md) - Package:
@tc9011/skills-manageron npm, published with provenance via Trusted Publishers
- Add ID to
AgentIdunion inagents.ts - Add entry to
agentRegistrywith correctglobalPath,projectPath, anduniversalflag - If it uses an env var, add resolution logic in
getAgentGlobalPath() - Add test in
agents.test.ts
- Create
src/commands/<name>.tswith an async handler function - Wire it up in
src/index.tsviaprogram.command() - Add tests in
src/commands/<name>.test.ts
npx tsx src/index.ts push # run directly with tsx
npm run dev -- push # same via npm script
npm link && skills-manager push # simulate global installPublishing is fully automated via semantic-release:
- Use Conventional Commits (
feat:,fix:,chore:, etc.) - Push to
main(or merge a PR) - GitHub Actions runs
semantic-releasewhich:- Analyzes commit messages since last release
- Determines version bump (patch/minor/major)
- Updates
package.jsonversion +CHANGELOG.md - Publishes to npm via OIDC Trusted Publishers
- Creates a GitHub Release with auto-generated notes
- Commits version bump back to the repo