diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md new file mode 100644 index 0000000000..a0fb11004a --- /dev/null +++ b/.agents/skills/create-pr/SKILL.md @@ -0,0 +1,167 @@ +--- +name: create-pr +description: Create and package React Router pull requests. Use when the user asks to create, open, prepare, or finish a PR for this repository, including branch/commit/push handoff, draft PR creation, PR body writing, and applying GitHub labels such as pkg:*, feat:*, docs, github-actions, or dependencies. +--- + +# Create React Router PR + +Create the pull request handoff for completed React Router work. Default to a draft PR targeting `main` unless the user explicitly asks for a ready PR or a different base branch. + +## Preconditions + +- Inspect `git status --short --branch` and `git branch --show-current`. +- Do not include unrelated dirty files. If unrelated changes are present, leave them unstaged and mention them. +- If the worktree is detached, create a branch from the current `HEAD` before committing. Use the branch name requested by the user, an existing repository convention, or `/` when no stronger convention is available. +- If already on a suitable named branch, use it. +- Confirm appropriate automated coverage exists or was added. If tests were skipped, failed, or left to CI, say that in the PR body. +- If user-facing functionality is being updated, check that the appropriate package has a change file under `packages//.changes/`. This is not necessary for docs-only, GitHub Actions/workflow-only, or dependency-maintenance PRs unless the dependency change itself has user-facing impact. + +## Context + +- Capture what changed, why it changed, and who it affects. +- Find related issues, discussions, or PRs and include links when relevant. +- Prefer `git diff --stat` plus focused `git diff` over broad repo archaeology when the change is small. +- If the user supplies a report, issue, discussion, or related PR, treat that as the primary context source. +- For new feature work, include a concise usage snippet. If the feature replaces or improves an older approach, include before/after examples when they help reviewers. + +## Commit and Push + +1. Review the diff with `git diff --stat` and focused `git diff` as needed. +2. Stage only the intended files. +3. Commit with a concise imperative subject. +4. Push the branch before creating the PR. + +## PR Creation + +Save the PR body to a temporary file, then use `gh pr create` with: + +```sh +gh pr create --draft --base main --head --title "" --body-file <file> +``` + +- Omit `--draft` only when the user explicitly asks for a ready PR. +- Change `--base` only when the user explicitly requests a different base branch. +- Keep shell quoting simple. Prefer a body file if the body contains backticks, quotes, or multiple paragraphs. +- Include issue/discussion links when known. Use `Closes #NNNN` for bug fixes the PR should close; use `Implements #NNNN` or a plain link for RFCs/discussions when closing semantics are not appropriate. +- Include testing notes. Be explicit about skipped verification, failures, or checks intentionally left to CI. +- If `gh pr create` fails, leave the branch pushed when possible and give the user a ready-to-open compare URL plus the prepared title and body. + +Recommended PR body shape: + +````markdown +This change ... + +- Optional extra detail when useful. + +```tsx +// Optional feature usage example +``` +```` + +```tsx +// Optional before/after example +``` + +**Testing** + +- ... + +```` + +- Do not use a `## Summary` heading. Start with one or two short sentences explaining what the change accomplishes. +- Add bullets after the opening only when more detail is useful. +- Include usage examples for new features, and before/after examples for replacements or improvements, when they help reviewers understand the change. +- Add `**Testing**` with bullets below the description when testing notes are needed. Omit it only for trivial PRs where there is genuinely nothing useful to say. + +## Testing Notes + +Prefer automated testing notes over manual testing instructions: + +- If appropriate unit, integration, or E2E tests were added or updated, list those commands or say that CI will run the relevant coverage. +- If no local verification was run because CI will cover it, say so plainly. +- Do not add manual testing instructions by default. +- Add manual testing instructions only when necessary, such as visual/UI behavior that needs human review, environment-specific behavior not covered by CI, release/publish dry-run steps, external service integration, or a reproduction that cannot be expressed reliably in automated tests. +- When manual testing is necessary, keep the instructions minimal and directly tied to the uncovered risk. + +## Labels + +Apply labels after the PR exists. Rely on the stable labels listed in this skill for normal PRs. + +Apply labels with: + +```sh +gh pr edit <number-or-url> --add-label "<label>" +```` + +If `gh pr edit --add-label` fails because the specified label is invalid or missing, run: + +```sh +gh label list --limit 200 +``` + +Then choose the correct label from the live list and update this skill in place so the stable label guidance stays current. Use real labels only. If the right label does not exist, do not invent one; mention the missing label. + +### Package Labels + +Add every applicable `pkg:*` label based on touched package paths: + +| Touched path | Label | +| ---------------------------------------------------- | ----------------------------------------------- | +| `packages/react-router/` | `pkg:react-router` | +| `packages/react-router-dev/` | `pkg:@react-router/dev` | +| `packages/create-react-router/` | `pkg:create-react-router` | +| `packages/react-router-architect/` | `pkg:@react-router/architect` | +| `packages/react-router-cloudflare/` | `pkg:@react-router/cloudflare` | +| `packages/react-router-node/` | `pkg:@react-router/node` | +| `packages/react-router-serve/` | `pkg:@react-router/serve` | +| `packages/react-router-express/` | `pkg:@react-router/express` | +| `packages/react-router-fs-routes/` | `pkg:@react-router/fs-routes` | +| `packages/react-router-remix-routes-option-adapter/` | `pkg:@react-router/remix-routes-option-adapter` | + +If a package path is unclear, inspect its `package.json` `name` and use `pkg:<name>` when that label exists. If a change touches generated artifacts or integration tests only, infer the package label from the runtime/tooling area being tested. For example, Vite plugin or prerender integration coverage usually maps to `pkg:@react-router/dev`. + +### Feature Labels + +Add applicable `feat:*` labels for the behavior area being changed. Common labels include: + +| Behavior area | Label | +| --------------------------------------------------------------------------------- | --------------------------- | +| Core navigation, loaders/actions, fetchers, redirects, matching, and router state | `feat:router` | +| Route config APIs and `routes.ts` | `feat:routes.ts` | +| Vite plugin and build pipeline behavior | `feat:vite` | +| SPA mode | `feat:spa-mode` | +| Prerendering | `feat:prerender` | +| Lazy route discovery | `feat:lazy-route-discovery` | +| Hydration and hydration fallback behavior | `feat:hydration` | +| View transition APIs | `feat:view-transitions` | +| Middleware behavior | `feat:middleware` | +| Split route module behavior | `feat:split-route-modules` | +| Streaming behavior | `feat:streaming` | +| CSS handling | `feat:css` | +| Windows-specific fixes | `feat:windows` | +| RSC Data or RSC Framework behavior | `feat:rsc` | +| Path matching semantics | `feat:path-matching` | +| Single fetch behavior | `feat:single-fetch` | +| Types, typegen, and TypeScript behavior | `feat:typescript` | + +Multiple feature labels are fine when the diff truly spans multiple areas. Prefer the most specific label that exists. + +### Non-Package Labels + +Some PRs do not need package or feature labels: + +- Add `docs` for documentation-only changes. +- Add `github-actions` for `.github/workflows/` or Actions infrastructure changes. +- Add `dependencies` for dependency or lockfile-only maintenance. +- Add version labels such as `v6`, `v7`, or `v8` only when the PR is intentionally scoped to that release line or the user asks for it. + +## Final Report + +Report: + +- Branch name. +- Commit hash. +- PR URL and whether it is draft or ready. +- Base branch. +- Labels applied. +- Verification performed or skipped. diff --git a/packages/create-react-router/.changes/minor.agent-skills.md b/packages/create-react-router/.changes/minor.agent-skills.md new file mode 100644 index 0000000000..dbccc8b34f --- /dev/null +++ b/packages/create-react-router/.changes/minor.agent-skills.md @@ -0,0 +1,5 @@ +Add a default-on CLI option to include the official React Router agent skill in generated projects. + +- New projects include `.agents/skills/react-router` by default when running with `--yes` or in non-interactive shells. +- Interactive runs prompt to include the skill, defaulting to yes. +- Use `--no-agent-skills` to skip copying the skill. diff --git a/packages/create-react-router/__tests__/create-react-router-test.ts b/packages/create-react-router/__tests__/create-react-router-test.ts index 196a3e9ba7..2359a2f74a 100644 --- a/packages/create-react-router/__tests__/create-react-router-test.ts +++ b/packages/create-react-router/__tests__/create-react-router-test.ts @@ -108,6 +108,7 @@ describe("create-react-router CLI", () => { --[no-]install Whether or not to install dependencies after creation --package-manager The package manager to use --show-install-output Whether to show the output of the install process + --[no-]agent-skills Whether or not to include the React Router agent skill --[no-]git-init Whether or not to initialize a Git repository --yes, -y Skip all option prompts and run setup --react-router-version, -v The version of React Router to use @@ -165,6 +166,10 @@ describe("create-react-router CLI", () => { question: /install dependencies/i, type: ["n"], }, + { + question: /agent skill/i, + type: ["y"], + }, ], }); @@ -185,6 +190,30 @@ describe("create-react-router CLI", () => { expect(status).toBe(0); expect(existsSync(path.join(projectDir, "package.json"))).toBeTruthy(); expect(existsSync(path.join(projectDir, "app/root.tsx"))).toBeTruthy(); + expect( + existsSync(path.join(projectDir, ".agents/skills/react-router/SKILL.md")), + ).toBeTruthy(); + }); + + it("supports the --no-agent-skills flag", async () => { + let projectDir = getProjectDir("no-agent-skills"); + + let { status, stderr } = await execCreateReactRouter({ + args: [ + projectDir, + "--yes", + "--no-agent-skills", + "--no-git-init", + "--no-install", + ], + }); + + expect(stderr.trim()).toBeFalsy(); + expect(status).toBe(0); + expect(existsSync(path.join(projectDir, "package.json"))).toBeTruthy(); + expect( + existsSync(path.join(projectDir, ".agents/skills/react-router/SKILL.md")), + ).toBeFalsy(); }); it("errors when project directory isn't provided when shell isn't interactive", async () => { @@ -534,6 +563,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -568,6 +598,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -602,6 +633,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -635,6 +667,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -668,6 +701,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -701,6 +735,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -734,6 +769,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", ]); stdoutMock.mockReset(); @@ -767,6 +803,7 @@ describe("create-react-router CLI", () => { path.join(__dirname, "fixtures", "blank"), "--no-git-init", "--yes", + "--no-agent-skills", "--package-manager", "pnpm", ]); @@ -1177,6 +1214,17 @@ async function execCreateReactRouter({ cwd?: string; }) { let cliPath = await ensureBuiltCli(); + let controlsAgentSkills = args.some((arg) => + ["--agent-skills", "--no-agent-skills", "--yes"].includes(arg), + ); + let answersAgentSkillsPrompt = interactions.some(({ question }) => + question.test("agent skill"), + ); + + if (interactive && !controlsAgentSkills && !answersAgentSkillsPrompt) { + args = [...args, "--no-agent-skills"]; + } + let proc = spawn( "node", [ @@ -1219,6 +1267,15 @@ async function ensureBuiltCli() { env: { ...process.env, NO_COLOR: "1" }, stdio: "pipe", }); + execFileSync( + pnpm, + ["run", "--filter", "create-react-router", "prepack"], + { + cwd: REPO_ROOT, + env: { ...process.env, NO_COLOR: "1" }, + stdio: "pipe", + }, + ); return BUILT_CLI; }); } diff --git a/packages/create-react-router/index.ts b/packages/create-react-router/index.ts index ebcc6ce836..6fa255d521 100644 --- a/packages/create-react-router/index.ts +++ b/packages/create-react-router/index.ts @@ -3,6 +3,7 @@ import { existsSync } from "node:fs"; import { cp, readFile, realpath, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import stripAnsi from "strip-ansi"; import { execa } from "execa"; import arg from "arg"; @@ -30,6 +31,13 @@ import { renderLoadingIndicator } from "./loading-indicator"; import { copyTemplate, CopyTemplateError } from "./copy-template"; import pkgJson from "./package.json" with { type: "json" }; +const currentFileDir = path.dirname(fileURLToPath(import.meta.url)); +const packageDir = + path.basename(currentFileDir) === "dist" + ? path.dirname(currentFileDir) + : currentFileDir; +const agentSkillPath = path.join(packageDir, "dist/agent-skills/react-router"); + async function createReactRouter(argv: string[]) { let ctx = await getContext(argv); if (ctx.help) { @@ -48,6 +56,8 @@ async function createReactRouter(argv: string[]) { copyTempDirToAppDirStep, gitInitQuestionStep, installDependenciesQuestionStep, + agentSkillsQuestionStep, + copyAgentSkillsToAppDirStep, installDependenciesStep, gitInitStep, doneStep, @@ -79,6 +89,8 @@ async function getContext(argv: string[]): Promise<Context> { "--no-install": Boolean, "--package-manager": String, "--show-install-output": Boolean, + "--agent-skills": Boolean, + "--no-agent-skills": Boolean, "--git-init": Boolean, "--no-git-init": Boolean, "--help": Boolean, @@ -102,6 +114,8 @@ async function getContext(argv: string[]): Promise<Context> { "--no-install": noInstall, "--package-manager": pkgManager, "--show-install-output": showInstallOutput = false, + "--agent-skills": agentSkills, + "--no-agent-skills": noAgentSkills, "--git-init": git, "--no-git-init": noGit, "--no-motion": noMotion, @@ -144,6 +158,7 @@ async function getContext(argv: string[]): Promise<Context> { overwrite, interactive, debug, + agentSkills: agentSkills ?? (noAgentSkills ? false : yes), git: git ?? (noGit ? false : yes), help, install: install ?? (noInstall ? false : yes), @@ -171,6 +186,7 @@ interface Context { cwd: string; interactive: boolean; debug: boolean; + agentSkills?: boolean; git?: boolean; help: boolean; install?: boolean; @@ -299,6 +315,57 @@ async function copyTemplateToTempDirStep(ctx: Context) { }); } +async function agentSkillsQuestionStep(ctx: Context) { + if (ctx.agentSkills === undefined) { + let { agentSkills = true } = await ctx.prompt({ + name: "agentSkills", + type: "confirm", + label: title("skill"), + message: "Include the React Router agent skill?", + hint: "recommended", + initial: true, + }); + ctx.agentSkills = agentSkills; + } +} + +async function copyAgentSkillsToAppDirStep(ctx: Context) { + if (!ctx.agentSkills) { + await sleep(100); + info("Skipping agent skill.", [ + "You can add it later from ", + color.reset( + "https://github.com/remix-run/react-router/tree/main/.agents/skills/react-router", + ), + ".", + ]); + return; + } + + if (!existsSync(path.join(agentSkillPath, "SKILL.md"))) { + error( + "Oh no!", + "React Router agent skill files were not found in this package.", + ); + throw new Error("React Router agent skill files were not found"); + } + + let destPath = path.join(ctx.cwd, ".agents", "skills", "react-router"); + + if (existsSync(destPath)) { + info("Agent skill:", "React Router agent skill already included"); + return; + } + + await ensureDirectory(path.dirname(destPath)); + await cp(agentSkillPath, destPath, { + errorOnExist: true, + force: false, + recursive: true, + }); + info("Agent skill:", "Included React Router agent skill"); +} + async function copyTempDirToAppDirStep(ctx: Context) { await ensureDirectory(ctx.cwd); @@ -647,6 +714,7 @@ ${color.arg("--template <name>")} ${color.dim(`The project template to use`)} ${color.arg("--[no-]install")} ${color.dim(`Whether or not to install dependencies after creation`)} ${color.arg("--package-manager")} ${color.dim(`The package manager to use`)} ${color.arg("--show-install-output")} ${color.dim(`Whether to show the output of the install process`)} +${color.arg("--[no-]agent-skills")} ${color.dim(`Whether or not to include the React Router agent skill`)} ${color.arg("--[no-]git-init")} ${color.dim(`Whether or not to initialize a Git repository`)} ${color.arg("--yes, -y")} ${color.dim(`Skip all option prompts and run setup`)} ${color.arg("--react-router-version, -v")} ${color.dim(`The version of React Router to use`)} diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index 69c5420a47..cf3cb266d4 100644 --- a/packages/create-react-router/package.json +++ b/packages/create-react-router/package.json @@ -21,6 +21,7 @@ }, "scripts": { "build": "wireit", + "prepack": "node ./scripts/copy-agent-skills.mjs", "typecheck": "tsc" }, "wireit": { diff --git a/packages/create-react-router/scripts/copy-agent-skills.mjs b/packages/create-react-router/scripts/copy-agent-skills.mjs new file mode 100644 index 0000000000..42f8f2a75f --- /dev/null +++ b/packages/create-react-router/scripts/copy-agent-skills.mjs @@ -0,0 +1,42 @@ +// Copies the canonical React Router agent skill into dist/ so it ships with the +// create-react-router package without duplicating source files in this package. +// +// This runs before packing/publishing (see `prepack` in package.json). + +/* eslint-disable import/no-nodejs-modules -- This package lifecycle script runs in Node. */ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const PACKAGE_DIR = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +const SOURCE_SKILL_DIR = path.resolve( + PACKAGE_DIR, + "../../.agents/skills/react-router", +); +const TARGET_SKILL_DIR = path.resolve( + PACKAGE_DIR, + "dist/agent-skills/react-router", +); +const BUILT_CLI_PATH = path.resolve(PACKAGE_DIR, "dist/cli.js"); + +if (!fs.existsSync(BUILT_CLI_PATH)) { + throw new Error( + "Could not find dist/cli.js. Run `pnpm run --filter create-react-router build` before packing or publishing.", + ); +} + +if (!fs.existsSync(path.join(SOURCE_SKILL_DIR, "SKILL.md"))) { + throw new Error( + `Could not find React Router agent skill at ${SOURCE_SKILL_DIR}`, + ); +} + +fs.rmSync(TARGET_SKILL_DIR, { recursive: true, force: true }); +fs.mkdirSync(path.dirname(TARGET_SKILL_DIR), { recursive: true }); +fs.cpSync(SOURCE_SKILL_DIR, TARGET_SKILL_DIR, { recursive: true }); + +let relativeTargetSkillDir = path.relative(PACKAGE_DIR, TARGET_SKILL_DIR); +console.log(`Copied React Router agent skill to ${relativeTargetSkillDir}/`);