diff --git a/.agents/skills/plan-github-issue/SKILL.md b/.agents/skills/plan-github-issue/SKILL.md new file mode 100644 index 0000000..eec641c --- /dev/null +++ b/.agents/skills/plan-github-issue/SKILL.md @@ -0,0 +1,155 @@ +--- +name: plan-github-issue +description: >- + Plan implementation work from a GitHub issue. Fetches issue details via gh + CLI, explores the codebase, produces an implementation plan, and asks the + developer follow-up questions. Use when the user wants to plan, scope, or + start work on a GitHub issue, or provides an issue link or issue number. +--- + +# Plan GitHub Issue + +Turn a GitHub issue into an implementation plan before writing code. + +## Required Input + +The developer must provide **at least one** of: + +- Issue number (e.g. `86`) +- Issue URL (e.g. `https://github.com/r-webdev/webdev-bot/issues/86`) + +Both may be provided. When both are given, verify they refer to the same issue. + +If neither is provided, ask for one before continuing. + +## Workflow + +Copy this checklist and track progress: + +``` +Planning Progress: +- [ ] Step 1: Fetch issue details +- [ ] Step 2: Read and summarize the issue +- [ ] Step 3: Explore the codebase +- [ ] Step 4: Draft implementation plan +- [ ] Step 5: Ask follow-up questions +- [ ] Step 6: Wait for developer answers before implementing +``` + +### Step 1: Fetch Issue Details + +Run the fetch script from the repository root: + +```bash +.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh [issue-url-or-number] +``` + +Examples: + +```bash +.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh 86 +.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh https://github.com/r-webdev/webdev-bot/issues/86 +.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh 86 https://github.com/r-webdev/webdev-bot/issues/86 +``` + +Read the full script output. Do not guess issue content. + +If the script fails: + +- **`gh` not installed** — tell the developer to install the [GitHub CLI](https://cli.github.com/). +- **Not authenticated** — tell the developer to run `gh auth login`. +- **Issue not found** — confirm the number or URL with the developer. + +### Step 2: Read and Summarize the Issue + +Extract from the fetched issue: + +- **Goal** — what problem is being solved? +- **Acceptance criteria** — explicit requirements, checklists, or "done when" statements +- **Constraints** — labels, comments, assignees, or notes that limit scope +- **Open ambiguities** — anything unclear or underspecified + +### Step 3: Explore the Codebase + +Before planning, inspect relevant areas of the repository: + +- Search for related commands, features, tests, and config +- Read [AGENTS.md](../../../AGENTS.md) for repository conventions +- Identify existing patterns to reuse (do not invent parallel approaches) + +Keep exploration focused on what the issue touches. + +### Step 4: Draft Implementation Plan + +Present the plan using this template: + +```markdown +# Plan: Issue # + +## Issue Summary +<One short paragraph> + +## Acceptance Criteria +- [ ] <criterion from issue> +- [ ] <criterion from issue> + +## Proposed Approach +<High-level strategy> + +## Files to Change +| File | Change | +|------|--------| +| `path/to/file` | <what and why> | + +## Testing Plan +- <what to test> +- Commands: `pnpm test`, `pnpm lint`, `pnpm fmt:check`, etc. + +## Branch and PR +- Branch: `<type>/<issue-number>/<short-description>` (see AGENTS.md) +- PR title includes `(#<issue-number>)` +- PR body includes `Closes #<issue-number>` + +## Risks and Unknowns +- <anything that could block or expand scope> +``` + +Adjust sections if the issue is docs-only, infra-only, or otherwise atypical. + +### Step 5: Ask Follow-Up Questions + +Always ask the developer clarifying questions before implementing. Aim for **3–8 targeted questions** based on gaps in the issue. + +Ask about: + +- **Ambiguous requirements** — multiple valid interpretations +- **Scope boundaries** — what is explicitly out of scope +- **Design choices** — UX, naming, error handling, backwards compatibility +- **Dependencies** — blocked on other PRs, secrets, external setup, or maintainer decisions +- **Verification** — how the developer wants to validate the change + +Format questions as a numbered list. Make each question specific and actionable. + +Example: + +```markdown +## Follow-Up Questions + +1. The issue mentions "standard expected from agents" — should the file be named `AGENTS.md` or `AGENT.md`? +2. Should tool-specific config stay separate from the shared agent file, or be merged? +3. Is full test coverage required for any helper scripts added as part of this issue? +``` + +### Step 6: Wait Before Implementing + +**Do not start coding** until the developer answers the follow-up questions, unless they explicitly say to proceed with stated assumptions. + +When they answer, update the plan if needed and confirm the final approach before making changes. + +## Rules + +- Follow [AGENTS.md](../../../AGENTS.md) for all repository conventions. +- Fetch the issue with the script — do not rely on memory or partial quotes. +- Prefer reusing existing code and config over adding packages or new tooling. +- Everything that can be tested should be tested. +- Do not create commits or pull requests unless the developer asks. diff --git a/.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh b/.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh new file mode 100755 index 0000000..e1f6d3b --- /dev/null +++ b/.agents/skills/plan-github-issue/scripts/fetch-github-issue.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Fetch a GitHub issue from the current repository using the GitHub CLI. + +Usage: + fetch-github-issue.sh <issue-number|issue-url> [issue-url|issue-number] + +Arguments: + First argument Issue number (e.g. 86) or GitHub issue URL + Second argument Optional cross-check: the other form (URL or number) + +Examples: + fetch-github-issue.sh 86 + fetch-github-issue.sh https://github.com/r-webdev/webdev-bot/issues/86 + fetch-github-issue.sh 86 https://github.com/r-webdev/webdev-bot/issues/86 + +Requires: gh CLI authenticated for this repository (run `gh auth status`), and Node.js. +EOF +} + +extract_issue_number() { + local input="$1" + + if [[ "$input" =~ ^[0-9]+$ ]]; then + echo "$input" + return + fi + + if [[ "$input" =~ github\.com/[^/]+/[^/]+/issues/([0-9]+) ]]; then + echo "${BASH_REMATCH[1]}" + return + fi + + echo "error: cannot parse issue number from: $input" >&2 + exit 1 +} + +# Captured once per script run. All obfuscation in a run uses this value so names +# stay stable while formatting; a new run (even milliseconds later) may differ. +OBFUSCATION_DATETIME="" +issue_json="" + +capture_obfuscation_datetime() { + if date +%3N >/dev/null 2>&1; then + OBFUSCATION_DATETIME=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) + return + fi + + OBFUSCATION_DATETIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) +} + +sha256_hex() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum | awk '{print $1}' + return + fi + + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 | awk '{print $1}' + return + fi + + echo "error: sha256sum or shasum is required to obfuscate author names" >&2 + exit 1 +} + +# Deterministic pseudonym from name + run datetime. Same name and run datetime +# always produce the same output; a new script run uses a new datetime. +obfuscate_name() { + local name="$1" + local hash seed adjective_index noun_index + local -a adjectives=( + amber bold calm coral crisp dusk ember flat gloss hazy indigo jade keen lime + muted neon olive plum quiet rapid sage teal vivid warm zinc + ) + local -a nouns=( + arch beacon canyon delta echo flint grove harbor inlet jetty knoll lagoon mesa + narrows oracle prism quill ridge summit tundra vertex willow + ) + + hash=$(printf '%s\x1f%s' "$name" "$OBFUSCATION_DATETIME" | sha256_hex) + seed=$((16#${hash:0:8})) + adjective_index=$((seed % ${#adjectives[@]})) + noun_index=$(((seed / ${#adjectives[@]}) % ${#nouns[@]})) + + echo "${adjectives[adjective_index]}-${nouns[noun_index]}" +} + +# Parse a field from the fetched issue JSON using Node (no external jq required). +issue_json_read() { + local node_expression="$1" + + ISSUE_JSON="$issue_json" node -e " + const issue = JSON.parse(process.env.ISSUE_JSON); + const value = ${node_expression}; + if (value !== undefined && value !== null) { + if (typeof value === 'object') { + process.stdout.write(JSON.stringify(value)); + } else { + process.stdout.write(String(value)); + } + } + " +} + +format_labels() { + local labels + + labels=$(issue_json_read 'issue.labels.map((label) => label.name).join(", ")') + if [[ -z "$labels" ]]; then + echo "_none_" + else + echo "$labels" + fi +} + +format_assignees() { + local assignee_count index login obfuscated assignee_lines="" + + assignee_count=$(issue_json_read 'issue.assignees.length') + if [[ "$assignee_count" -eq 0 ]]; then + echo "_none_" + return + fi + + for ((index = 0; index < assignee_count; index++)); do + login=$(issue_json_read "issue.assignees[${index}].login") + obfuscated=$(obfuscate_name "$login") + if [[ -n "$assignee_lines" ]]; then + assignee_lines+=", " + fi + assignee_lines+="$obfuscated" + done + + echo "$assignee_lines" +} + +format_comments() { + local comment_count index login created_at body obfuscated comment_lines="" + + comment_count=$(issue_json_read 'issue.comments.length') + if [[ "$comment_count" -eq 0 ]]; then + echo "_No comments yet._" + return + fi + + for ((index = 0; index < comment_count; index++)); do + login=$(issue_json_read "issue.comments[${index}].author.login") + created_at=$(issue_json_read "issue.comments[${index}].createdAt") + body=$(issue_json_read "issue.comments[${index}].body") + obfuscated=$(obfuscate_name "$login") + + if [[ -n "$comment_lines" ]]; then + comment_lines+=$'\n\n---\n\n' + fi + comment_lines+="### ${obfuscated} (${created_at})"$'\n\n'"${body}" + done + + echo "$comment_lines" +} + +print_issue() { + local issue_number title state url milestone body comment_count labels assignees comments + + issue_number=$(issue_json_read 'issue.number') + title=$(issue_json_read 'issue.title') + state=$(issue_json_read 'issue.state') + url=$(issue_json_read 'issue.url') + milestone=$(issue_json_read 'issue.milestone?.title ?? ""') + body=$(issue_json_read 'issue.body') + comment_count=$(issue_json_read 'issue.comments.length') + labels=$(format_labels) + assignees=$(format_assignees) + comments=$(format_comments) + + echo "# Issue #${issue_number}: ${title}" + echo + echo "**URL:** ${url}" + echo "**State:** ${state}" + if [[ -n "$milestone" ]]; then + echo "**Milestone:** ${milestone}" + fi + echo + echo "## Labels" + echo "$labels" + echo + echo "## Assignees" + echo "$assignees" + echo + echo "## Body" + echo + echo "$body" + echo + echo "## Comments (${comment_count})" + echo + echo "$comments" +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ $# -lt 1 || $# -gt 2 ]]; then + usage >&2 + exit 1 +fi + +if ! command -v gh >/dev/null 2>&1; then + echo "error: gh CLI is not installed. Install it from https://cli.github.com/" >&2 + exit 1 +fi + +if ! command -v node >/dev/null 2>&1; then + echo "error: node is required to parse issue JSON" >&2 + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + echo "error: gh CLI is not authenticated. Run \`gh auth login\`." >&2 + exit 1 +fi + +first_number="$(extract_issue_number "$1")" +issue_number="$first_number" + +if [[ $# -eq 2 ]]; then + second_number="$(extract_issue_number "$2")" + if [[ "$first_number" != "$second_number" ]]; then + echo "error: issue number $first_number does not match $second_number" >&2 + exit 1 + fi + issue_number="$second_number" +fi + +if ! gh issue view "$issue_number" --json number >/dev/null 2>&1; then + echo "error: issue #$issue_number not found in this repository" >&2 + exit 1 +fi + +capture_obfuscation_datetime + +issue_json="$( + gh issue view "$issue_number" \ + --json number,title,body,state,url,labels,assignees,comments,milestone +)" + +print_issue diff --git a/.cursor/rules/env-access.mdc b/.cursor/rules/env-access.mdc deleted file mode 100644 index c024e92..0000000 --- a/.cursor/rules/env-access.mdc +++ /dev/null @@ -1,16 +0,0 @@ ---- -alwaysApply: true -description: Disallow accessing .env* files; use env.ts as the single source of truth for environment configuration ---- - -# Environment access policy - -- Do not open, read, or reference any files matching: - - `.env` - - `.env.*` - - `.env*` -- Treat `src/env.ts` as the only allowed source of environment configuration. Use it whenever environment info is required. -- If a needed value appears only in a `.env*` file, stop and ask to add it to `src/env.ts` instead of reading the `.env*` file. - -Reference: [env.ts](mdc:src/env.ts) - diff --git a/.gitignore b/.gitignore index 9de0913..c633aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,12 @@ advent-of-code-tracker.json # Docker docker-compose.yml +# Agent-specific skill symlinks (canonical source: .agents/skills/) +.claude/ +.cursor/ +.github/skills/ +.codex/ + # terraform **/.terraform/ *.tfstate diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..db26307 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,232 @@ +# Agent Instructions + +This file provides repository-wide guidance for any automated or AI-assisted contributor. Follow these conventions unless a human maintainer explicitly overrides them. + +## Project Overview + +**webdev-bot** is a Discord bot for the Web Dev & Design server. It is a TypeScript Node.js application built with `pnpm`, bundled via `tsup`, and tested with the Node.js built-in test runner. + +## Branch Naming + +Branches use [Conventional Commits](https://www.conventionalcommits.org/) type prefixes: + +``` +<type>/<short-description> +<type>/<issue-number>/<short-description> +``` + +**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, and other conventional commit types as appropriate. + +**Examples:** + +``` +feat/add-baseline-command +feat/86/agent-file +fix/42/handle-timeout-error +docs/update-contributing-guide +``` + +Use lowercase, hyphen-separated descriptions. When work relates to an existing GitHub issue, include the issue number in the branch name. + +## Pull Requests + +When a change addresses an existing issue: + +1. **Branch** — include the issue number (see above). +2. **Title** — include the issue number, e.g. `feat(#86): add baseline command`. +3. **Body** — include a closing keyword so GitHub links and auto-closes the issue: + + ``` + Closes #86 + ``` + +Use `Closes`, `Fixes`, or `Resolves` as appropriate. One issue per PR when possible. + +For changes with no related issue, omit the issue number from the branch and title. + +## Keep the Codebase Small + +- **Do not add new npm packages** unless a maintainer explicitly approves. Prefer built-in Node.js APIs and existing dependencies. +- **Minimize scope** — change only what is needed for the task. Avoid drive-by refactors or unrelated cleanup. +- **Reuse existing patterns** — read surrounding code and match its style, abstractions, and conventions before writing something new. + +## Coding Style + +### Comments + +Code should be self-descriptive. Do not add comments that restate what the code already says. + +Add a comment only when the **business logic** is not obvious from the code itself — for example, a non-standard algorithm, a Discord API quirk, or a constraint that would surprise a reader. + +```typescript +// ❌ BAD — narrates the obvious +// Get the user from the interaction +const user = interaction.user; + +// ✅ GOOD — explains non-obvious business logic +// Discord allows bots to timeout members only if the bot's highest role +// is above the target member's highest role. +if (botRole.position <= targetMember.roles.highest.position) { + return; +} +``` + +### Naming + +Always use full words for variables, parameters, and functions. Do not abbreviate. + +```typescript +// ❌ BAD +const msg = interaction.options.getString('query'); +const cfg = getConfig(); + +// ✅ GOOD +const query = interaction.options.getString('query'); +const configuration = getConfiguration(); +``` + +### TypeScript Types + +- **Do not use `interface`.** Use `type` aliases instead. + + ```typescript + // ❌ BAD + interface CommandOptions { + name: string; + description: string; + } + + // ✅ GOOD + type CommandOptions = { + name: string; + description: string; + }; + ``` + +- **Do not use `enum`.** Use string literal unions or `as const` objects instead. + + ```typescript + // ❌ BAD + enum CommandStatus { + Pending = 'pending', + Ready = 'ready', + } + + // ✅ GOOD + type CommandStatus = 'pending' | 'ready'; + + // ✅ GOOD — when you need a runtime value map + const CommandStatus = { + Pending: 'pending', + Ready: 'ready', + } as const; + ``` + +## Configuration Is the Source of Truth + +Root-level config files define how this project is built, linted, formatted, and run. **Do not invent parallel config** (no new ESLint/Prettier/Biome/Jest/Vitest configs, no duplicate tsconfig variants, no ad-hoc tooling files). + +Treat these as authoritative: + +| File | Purpose | +|------|---------| +| `package.json` | Scripts, dependencies, lint-staged hooks | +| `pnpm-lock.yaml` | Locked dependency versions | +| `tsconfig.json` | TypeScript compiler options | +| `tsup.config.ts` | Build/bundle configuration | +| `oxlint.config.ts` | Linter rules | +| `oxfmt.config.ts` | Formatter rules | +| `docker-compose.yml` | Local Docker services | +| `Dockerfile` | Container image | +| `.nvmrc` | Node.js version | +| `.gitignore` | Ignored paths | +| `.dockerignore` | Docker build exclusions | +| `src/env.ts` | Environment variable schema and access (do not read `.env*` files directly) | + +If something seems missing from config, ask a maintainer rather than adding a new config file. + +### Environment Variables + +- Do **not** open, read, or reference any files matching: + - `.env` + - `.env.*` + - `.env*` +- Treat `src/env.ts` as the only allowed source of environment configuration. Use it whenever environment info is required. +- If a needed value appears only in a `.env*` file, stop and ask to add it to `src/env.ts` instead of reading the `.env*` file. + +## Code Quality + +### Linting and Formatting + +Before considering work complete, run lint and format checks on changed code: + +```bash +pnpm lint # check for lint errors +pnpm lint:fix # auto-fix lint issues where possible +pnpm fmt:check # verify formatting +pnpm fmt # apply formatting +pnpm typecheck # TypeScript type checking +``` + +CI runs lint, format check, build, and tests on every pull request. Fix any failures before submitting. + +### Unit Tests + +**Everything that can be tested should be tested.** When adding or changing code, write unit tests for the new or modified behavior. + +- Test files live alongside source code as `*.test.ts`. +- Use the Node.js built-in test runner (`node:test` / `node:assert`). +- Run tests locally: + + ```bash + pnpm test # run tests via tsx (development) + pnpm test:ci # run compiled tests (matches CI) + pnpm build # required before test:ci + ``` + +Match the style of existing tests (see `src/**/*.test.ts`). + +## Development Commands + +```bash +pnpm install # install dependencies +pnpm dev # start with hot reload +pnpm build # compile for production +pnpm start # run compiled output +pnpm deploy # deploy Discord slash commands +``` + +Package manager is **pnpm** (see `packageManager` field in `package.json`). Do not use npm or yarn. + +## Skills + +Project skills live in `.agents/skills/`. Each skill is a `SKILL.md` file with step-by-step workflow instructions. These are tool-agnostic — any agent should read and follow them when relevant. + +After cloning, prepare the repository for your agent: + +```bash +pnpm agent-ready claude +pnpm agent-ready cursor +pnpm agent-ready copilot +pnpm agent-ready codex +``` + +Pass `--skills` to link skills only, skipping other setup steps: + +```bash +pnpm agent-ready claude --skills # .claude/skills -> .agents/skills +``` + +Agent-specific skill directories are gitignored; `.agents/skills/` is the canonical source committed to the repository. Cursor and Codex also read `.agents/skills/` directly — linking for those agents is optional and the script will ask for confirmation. + +| Skill | Use when | +|-------|----------| +| [plan-github-issue](.agents/skills/plan-github-issue/SKILL.md) | Planning work from a GitHub issue (provide issue number or URL) | + +## General Guidelines + +- Read existing code in the area you are changing before writing new code. +- Prefer small, focused diffs over large rewrites. +- Do not commit secrets, credentials, or `.env` files. +- Do not create git commits or open pull requests unless explicitly asked by the human working with you. +- When unsure about a convention, check existing branches, pull requests, and config files before guessing. diff --git a/package.json b/package.json index ae49473..d3c86d4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "prepare": "husky", "pre-commit": "lint-staged", "sync-guides": "tsx src/scripts/sync-guides.ts", - "sync-guides:init": "tsx src/scripts/sync-guides.ts --initialize" + "sync-guides:init": "tsx src/scripts/sync-guides.ts --initialize", + "agent-ready": "bash scripts/agent-ready.sh", + "link-agent-skills": "bash scripts/link-agent-skills.sh" }, "dependencies": { "discord.js": "^14.26.4", diff --git a/scripts/agent-ready.sh b/scripts/agent-ready.sh new file mode 100755 index 0000000..3991777 --- /dev/null +++ b/scripts/agent-ready.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat <<'EOF' +Prepare the repository for AI-assisted development. + +Usage: + agent-ready.sh <agent> [--skills] + +Agents: + claude Claude Code + cursor Cursor + copilot GitHub Copilot + codex OpenAI Codex + other Generic agent (no agent-specific setup) + +Flags: + --skills Link skills only (skip other setup steps) + +With no flags, all setup steps run for the chosen agent. + +Examples: + pnpm agent-ready claude + pnpm agent-ready claude --skills + +Run from the repository root, or via: pnpm agent-ready <agent> [--skills] +EOF +} + +agent="" +skills_only=false +manual_nvm_use_required=false + +for argument in "$@"; do + case "$argument" in + --skills) + skills_only=true + ;; + -h | --help | help) + usage + exit 0 + ;; + -*) + echo "error: unknown flag '$argument'" >&2 + usage >&2 + exit 1 + ;; + *) + if [[ -n "$agent" ]]; then + echo "error: unexpected argument '$argument'" >&2 + usage >&2 + exit 1 + fi + agent="${argument,,}" + ;; + esac +done + +if [[ -z "$agent" ]]; then + usage >&2 + exit 1 +fi + +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$root" + +step() { + echo + echo "==> $1" +} + +load_nvm() { + if [[ -n "${NVM_DIR:-}" && -s "${NVM_DIR}/nvm.sh" ]]; then + # shellcheck source=/dev/null + . "${NVM_DIR}/nvm.sh" + return 0 + fi + + for nvm_script in \ + "${HOME}/.nvm/nvm.sh" \ + /usr/local/opt/nvm/nvm.sh \ + /opt/homebrew/opt/nvm/nvm.sh; do + if [[ -s "$nvm_script" ]]; then + # shellcheck source=/dev/null + . "$nvm_script" + return 0 + fi + done + + return 1 +} + +node_major_minor() { + node -p "process.versions.node.split('.').slice(0, 2).join('.')" 2>/dev/null || true +} + +check_node_version() { + step "Checking Node.js version" + + if [[ ! -f .nvmrc ]]; then + echo "warning: .nvmrc not found; skipping version check" + return 0 + fi + + expected_version="$(tr -d '[:space:]' < .nvmrc)" + expected_major_minor="$(echo "${expected_version#v}" | cut -d. -f1-2)" + current_version="$(node_major_minor)" + + if [[ -n "$current_version" && "$current_version" == "$expected_major_minor" ]]; then + echo "ok: Node.js ${current_version} matches .nvmrc (${expected_version})" + return 0 + fi + + if [[ -n "$current_version" ]]; then + echo "Node.js ${current_version} does not match .nvmrc (${expected_version})" + else + echo "Node.js is not installed or not on PATH" + fi + + if ! load_nvm; then + echo "warning: nvm not found; install Node ${expected_version} manually" + echo " See https://github.com/nvm-sh/nvm" + return 0 + fi + + echo "running: nvm install ${expected_version}" + nvm install "$expected_version" + + echo "running: nvm use ${expected_version}" + nvm use "$expected_version" + + current_version="$(node_major_minor)" + if [[ "$current_version" == "$expected_major_minor" ]]; then + echo "ok: switched to Node.js ${current_version} for this script" + manual_nvm_use_required=true + return 0 + fi + + echo "warning: still on Node.js ${current_version:-unknown} after nvm use" +} + +print_manual_steps() { + if [[ "$manual_nvm_use_required" != true ]]; then + return 0 + fi + + echo + echo "┌─────────────────────────────────────────────────────────────────────┐" + echo "│ Manual step required │" + echo "├─────────────────────────────────────────────────────────────────────┤" + echo "│ This script switched Node.js only inside its own process. │" + echo "│ Your terminal is still using the previous version. │" + echo "│ │" + echo "│ In this terminal, run: │" + echo "│ │" + echo "│ nvm use │" + echo "│ │" + echo "│ Then confirm with: │" + echo "│ │" + echo "│ node -v │" + echo "└─────────────────────────────────────────────────────────────────────┘" +} + +install_dependencies() { + step "Installing dependencies" + + if ! command -v pnpm >/dev/null 2>&1; then + echo "warning: pnpm is not installed; run pnpm install manually" + return 0 + fi + + if [[ -d node_modules ]]; then + echo "ok: node_modules already present" + return 0 + fi + + pnpm install +} + +scaffold_env_file() { + step "Scaffolding .env file" + + if [[ -f .env ]]; then + echo "ok: .env already exists" + return 0 + fi + + if [[ ! -f .env.example ]]; then + echo "warning: .env.example not found; create .env manually" + return 0 + fi + + cp .env.example .env + echo "created: .env from .env.example" + echo " Fill in required values before running the bot." +} + +check_github_cli() { + step "Checking GitHub CLI" + + if command -v gh >/dev/null 2>&1; then + echo "ok: gh is installed" + return 0 + fi + + echo "warning: gh is not installed" + echo " Install it for the plan-github-issue skill: https://cli.github.com/" +} + +link_agent_skills() { + step "Linking agent skills" + bash "$script_directory/link-agent-skills.sh" "$agent" +} + +run_all_steps() { + check_node_version + install_dependencies + scaffold_env_file + check_github_cli + link_agent_skills + + echo + echo "Agent setup complete for ${agent}." + print_manual_steps +} + +if [[ "$skills_only" == true ]]; then + link_agent_skills +else + run_all_steps +fi diff --git a/scripts/link-agent-skills.sh b/scripts/link-agent-skills.sh new file mode 100755 index 0000000..e93ef98 --- /dev/null +++ b/scripts/link-agent-skills.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Link project skills from .agents/skills/ to an agent-specific directory. + +Usage: + link-agent-skills.sh <agent> + +Agents: + claude Symlink .claude/skills -> .agents/skills + cursor Symlink .cursor/skills -> .agents/skills + copilot Symlink .github/skills -> .agents/skills + codex Symlink .codex/skills -> .agents/skills + other No symlink; print where skills live + +Run from the repository root, or via: pnpm agent-ready <agent> --skills +EOF +} + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 1 +fi + +agent="${1,,}" + +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$root" + +source_skills=".agents/skills" + +confirm_optional_symlink() { + local agent_name="$1" + local target_path="$2" + + cat <<EOF + +${agent_name^} already discovers skills from .agents/skills/ directly. +A symlink at ${target_path} is optional and usually not needed. + +EOF + read -r -p "Create symlink anyway? [y/N] " reply + case "$reply" in + y | Y | yes | Yes) + return 0 + ;; + *) + echo "skipped: no symlink created" + exit 0 + ;; + esac +} + +if [[ ! -d "$source_skills" ]]; then + echo "error: $source_skills not found in repository root" >&2 + exit 1 +fi + +case "$agent" in + claude) + link_path=".claude/skills" + ;; + cursor) + link_path=".cursor/skills" + ;; + copilot) + link_path=".github/skills" + ;; + codex) + link_path=".codex/skills" + ;; + other) + cat <<EOF +No agent-specific symlink is created for "$agent". + +Project skills live in .agents/skills/. Read the relevant SKILL.md when a task matches. + +To link skills for a supported agent, run: + pnpm agent-ready claude --skills + pnpm agent-ready cursor --skills + pnpm agent-ready copilot --skills + pnpm agent-ready codex --skills +EOF + exit 0 + ;; + -h | --help | help) + usage + exit 0 + ;; + *) + echo "error: unknown agent '$1'" >&2 + usage >&2 + exit 1 + ;; +esac + +if [[ -L "$link_path" ]]; then + current_target="$(readlink "$link_path")" + if [[ "$current_target" == "$source_skills" || "$current_target" == "../agents/skills" ]]; then + echo "already linked: $link_path -> $source_skills" + exit 0 + fi + rm "$link_path" +elif [[ -e "$link_path" ]]; then + echo "error: $link_path exists and is not a symlink; remove it manually first" >&2 + exit 1 +elif [[ "$agent" == "cursor" || "$agent" == "codex" ]]; then + confirm_optional_symlink "$agent" "$link_path" +fi + +mkdir -p "$(dirname "$link_path")" +ln -s "$source_skills" "$link_path" +echo "linked: $link_path -> $source_skills"