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
75 changes: 75 additions & 0 deletions docs/guide/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Use with Claude (Skill)

`@tsops/skill` is a [Claude Skill](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) that teaches Claude how to operate tsops correctly. Once installed, Claude Code, the Agent SDK, and any other tool implementing the [Agent Skills standard](https://agentskills.io/specification) load it automatically when relevant.

## Why

The docs explain what tsops _is_. The Skill tells Claude how to _use_ it — specifically, the hard rules that an agent will violate by default if it has only ever seen YAML deploys before:

- Don't put internal service URLs in env vars — use `config.url('api', 'service')`.
- Always run `tsops plan` before `tsops deploy`.
- After editing `tsops.config.ts`, run `tsc --noEmit` so renames propagate.
- Never `--no-verify` past secret validation.

Without the Skill, an agent reaches for `BACKEND_URL=http://api:3000` because that pattern is everywhere else on the public internet. With the Skill, it reaches for `config.url`.

## Install

::: code-group

```bash [user-scope]
# Install once for your account → ~/.claude/skills/tsops
npx @tsops/skill install
```

```bash [project-scope]
# Commit the Skill to your repo → ./.claude/skills/tsops
# Every contributor's agent picks it up automatically.
npx @tsops/skill install --project
```

:::

After install, restart Claude Code (or your Agent SDK session). Verify with:

```bash
claude /skills
# "tsops" should appear in the list
```

## What's inside

```
~/.claude/skills/tsops/
├── SKILL.md # entry point — frontmatter + hard rules
├── reference/
│ ├── commands.md # CLI commands and flags
│ ├── runtime-helpers.md # config.url, config.env, config.dns
│ ├── secrets.md # secret validation
│ └── preview-overlays.md # overlay namespace lifecycle
└── examples/
├── add-app.md
├── rename-app.md
└── add-secret.md
```

The Skill is small on purpose. The entry-point `SKILL.md` covers the mental model and the hard rules. References load on demand — Claude pulls in `reference/secrets.md` only when secret work is in scope.

## Updating

```bash
npx @tsops/skill@latest install --force
```

The Skill is versioned independently of tsops core. When the CLI surface changes in a way that affects how an agent should operate, the Skill gets a release with updated instructions.

## Uninstall

```bash
npx @tsops/skill uninstall # remove from ~/.claude/skills/tsops
npx @tsops/skill uninstall --project # remove from ./.claude/skills/tsops
```

## Source

The Skill content lives at [`skills/tsops/`](https://github.com/Pom4H/tsops/tree/main/skills/tsops) in the main tsops repo. Edits go through PR review, the same as any other code change. Changes to the Skill should explain — in the PR description — what failure mode the change prevents.
69 changes: 69 additions & 0 deletions packages/skill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# @tsops/skill

A [Claude Skill](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) that teaches Claude how to use [tsops](https://github.com/Pom4H/tsops) correctly.

When installed, Claude Code, the Agent SDK, or any other tool that implements the [Agent Skills standard](https://agentskills.io/specification) will load this skill automatically when:

- the project contains a `tsops.config.ts` file, or
- the user mentions tsops, `tsops plan`, `tsops deploy`, `tsops up`, preview namespaces, or asks to add/rename/remove apps, secrets, namespaces, or routes.

## Install

```bash
# Run once — copies the skill into ~/.claude/skills/tsops
npx @tsops/skill install
```

Or commit the skill to your repo so every contributor's agent picks it up:

```bash
npx @tsops/skill install --project
```

After install, restart Claude Code (or your Agent SDK session). Verify:

```bash
claude /skills
# tsops should be listed
```

## What's inside

```
~/.claude/skills/tsops/
├── SKILL.md # entry point — frontmatter + tactical rules
├── reference/
│ ├── commands.md # tsops plan / build / deploy / up / down
│ ├── runtime-helpers.md # config.url, config.env, config.dns
│ ├── secrets.md # secret validation, cluster fallback
│ └── preview-overlays.md # PR-style preview namespaces
└── examples/
├── add-app.md # recipe: add a new app
├── rename-app.md # recipe: rename safely (compiler-driven)
└── add-secret.md # recipe: add a secret
```

The skill is small on purpose — references load on demand, only the file relevant to the current task.

## Why a skill, not just docs

The tsops docs explain what tsops is. This skill teaches Claude **how to operate it correctly** — the hard rules (no internal URLs in env vars, never bypass `tsops plan`, always run `tsc --noEmit` after a rename), the canonical workflow, and the specific failure modes that cost the most time.

Without the skill, an LLM agent will reach for `BACKEND_URL=http://api:3000` because that's the pattern it has seen everywhere else. With the skill, it reaches for `config.url('api', 'service')`.

## Uninstall

```bash
npx @tsops/skill uninstall
npx @tsops/skill uninstall --project
```

## Versioning

This package is versioned independently of the tsops core. The skill text is content-addressable — pinning a version pins the wording.

When tsops's CLI surface changes in a way that affects how an agent should use it, this package gets a release with the updated instructions.

## License

MIT
129 changes: 129 additions & 0 deletions packages/skill/bin/install.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env node
/**
* `tsops-skill install` — copy the bundled tsops Claude Skill into
* either `~/.claude/skills/tsops` (user scope, default) or
* `<cwd>/.claude/skills/tsops` (project scope, with `--project`).
*
* Idempotent: re-running overwrites existing files. Refuses to delete
* unrelated content under the target directory.
*/
import { argv, exit, cwd } from 'node:process'
import { homedir } from 'node:os'
import { mkdir, cp, stat, readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { dirname, join, resolve } from 'node:path'

const __dirname = dirname(fileURLToPath(import.meta.url))
const PKG_DIR = resolve(__dirname, '..')
const SKILL_SRC = join(PKG_DIR, 'skill')
const SKILL_NAME = 'tsops'

function parseArgs(args) {
const out = { command: 'install', scope: 'user', force: false, help: false }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg === 'install' || arg === 'uninstall' || arg === 'where') {
out.command = arg
} else if (arg === '--project' || arg === '-p') {
out.scope = 'project'
} else if (arg === '--user' || arg === '-u') {
out.scope = 'user'
} else if (arg === '--force' || arg === '-f') {
out.force = true
} else if (arg === '--help' || arg === '-h') {
out.help = true
} else {
console.error(`Unknown argument: ${arg}`)
out.help = true
}
}
return out
}

function targetDir(scope) {
const base = scope === 'project' ? cwd() : homedir()
return join(base, '.claude', 'skills', SKILL_NAME)
}

function help() {
console.log(`tsops-skill — install the tsops Claude Skill

Usage:
tsops-skill install [--user | --project] [--force]
tsops-skill uninstall [--user | --project]
tsops-skill where [--user | --project]

Options:
--user, -u Install into ~/.claude/skills/tsops (default)
--project, -p Install into ./.claude/skills/tsops (commit to repo)
--force, -f Overwrite without prompting
--help, -h Show this help

After install, restart Claude Code (or any Agent SDK session) so the skill
is picked up. Verify with: claude /skills
`)
}

async function exists(p) {
try { await stat(p); return true } catch { return false }
}

async function readSkillVersion() {
const pkgJson = JSON.parse(await readFile(join(PKG_DIR, 'package.json'), 'utf8'))
return pkgJson.version
}

async function install({ scope, force }) {
const dst = targetDir(scope)
const version = await readSkillVersion()

if (!(await exists(SKILL_SRC))) {
console.error(`Bundled skill source missing: ${SKILL_SRC}`)
console.error(`This package is broken — please file an issue.`)
exit(2)
}

if (await exists(dst) && !force) {
console.log(`Skill already present at ${dst}`)
console.log(`Re-run with --force to overwrite.`)
exit(0)
}

await mkdir(dst, { recursive: true })
await cp(SKILL_SRC, dst, { recursive: true, force: true })

console.log(`✅ Installed @tsops/skill@${version} → ${dst}`)
console.log(``)
console.log(`Next: restart Claude Code so the skill is picked up.`)
console.log(`Verify: run "claude /skills" and confirm "tsops" appears.`)
}

async function uninstall({ scope }) {
const dst = targetDir(scope)
if (!(await exists(dst))) {
console.log(`Nothing to uninstall — ${dst} does not exist.`)
exit(0)
}
// Conservative: only remove files we'd write. Use rm with force.
const { rm } = await import('node:fs/promises')
await rm(dst, { recursive: true, force: true })
console.log(`Removed ${dst}`)
}

async function where({ scope }) {
const dst = targetDir(scope)
console.log(dst)
console.log((await exists(dst)) ? '(installed)' : '(not installed)')
}

const args = parseArgs(argv.slice(2))
if (args.help) { help(); exit(0) }

try {
if (args.command === 'install') await install(args)
if (args.command === 'uninstall') await uninstall(args)
if (args.command === 'where') await where(args)
} catch (err) {
console.error(err.message ?? err)
exit(1)
}
40 changes: 40 additions & 0 deletions packages/skill/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@tsops/skill",
"version": "0.1.0",
"description": "Claude Skill for tsops — installs into ~/.claude/skills or .claude/skills",
"type": "module",
"bin": {
"tsops-skill": "./bin/install.mjs"
},
"files": [
"bin",
"skill",
"README.md"
],
"scripts": {
"build": "node ./scripts/sync-skill.mjs",
"lint": "pnpm -w exec eslint ."
},
"keywords": [
"tsops",
"claude",
"claude-code",
"agent-skill",
"skill",
"anthropic"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Pom4H/tsops.git",
"directory": "packages/skill"
},
"license": "MIT",
"author": "Roman Popov",
"engines": {
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
34 changes: 34 additions & 0 deletions packages/skill/scripts/sync-skill.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node
/**
* Build step for @tsops/skill.
*
* The canonical source for the Skill lives at the repo root in
* `skills/tsops/`. This script syncs it into `packages/skill/skill/`
* so that the npm tarball ships the files. We don't symlink because
* npm's tarball does not preserve symlinks reliably across all
* package managers.
*/
import { cp, rm, stat } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const PKG_DIR = resolve(__dirname, '..')
const REPO_ROOT = resolve(PKG_DIR, '..', '..')

const SRC = join(REPO_ROOT, 'skills', 'tsops')
const DST = join(PKG_DIR, 'skill')

async function exists(p) { try { await stat(p); return true } catch { return false } }

if (!(await exists(SRC))) {
console.error(`Source skill missing: ${SRC}`)
process.exit(1)
}

if (await exists(DST)) {
await rm(DST, { recursive: true, force: true })
}

await cp(SRC, DST, { recursive: true })
console.log(`Synced ${SRC} → ${DST}`)
Loading
Loading