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
5 changes: 5 additions & 0 deletions packages/create-react-router/.changes/minor.agent-skills.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions packages/create-react-router/__tests__/create-react-router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +166,10 @@ describe("create-react-router CLI", () => {
question: /install dependencies/i,
type: ["n"],
},
{
question: /agent skill/i,
type: ["y"],
},
],
});

Expand All @@ -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 () => {
Expand Down Expand Up @@ -534,6 +563,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -568,6 +598,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -602,6 +633,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -635,6 +667,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -668,6 +701,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -701,6 +735,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -734,6 +769,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
]);

stdoutMock.mockReset();
Expand Down Expand Up @@ -767,6 +803,7 @@ describe("create-react-router CLI", () => {
path.join(__dirname, "fixtures", "blank"),
"--no-git-init",
"--yes",
"--no-agent-skills",
"--package-manager",
"pnpm",
]);
Expand Down Expand Up @@ -1177,6 +1214,22 @@ async function execCreateReactRouter({
cwd?: string;
}) {
let cliPath = await ensureBuiltCli();
let controlsAgentSkills = args.some((arg) =>
[
"--agent-skills",
"--with-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",
[
Expand Down Expand Up @@ -1219,6 +1272,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;
});
}
Expand Down
69 changes: 69 additions & 0 deletions packages/create-react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand All @@ -48,6 +56,8 @@ async function createReactRouter(argv: string[]) {
copyTempDirToAppDirStep,
gitInitQuestionStep,
installDependenciesQuestionStep,
agentSkillsQuestionStep,
copyAgentSkillsToAppDirStep,
installDependenciesStep,
gitInitStep,
doneStep,
Expand Down Expand Up @@ -79,6 +89,9 @@ async function getContext(argv: string[]): Promise<Context> {
"--no-install": Boolean,
"--package-manager": String,
"--show-install-output": Boolean,
"--agent-skills": Boolean,
"--with-agent-skills": "--agent-skills",
"--no-agent-skills": Boolean,
"--git-init": Boolean,
"--no-git-init": Boolean,
"--help": Boolean,
Expand All @@ -102,6 +115,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,
Expand Down Expand Up @@ -144,6 +159,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),
Expand Down Expand Up @@ -171,6 +187,7 @@ interface Context {
cwd: string;
interactive: boolean;
debug: boolean;
agentSkills?: boolean;
git?: boolean;
help: boolean;
install?: boolean;
Expand Down Expand Up @@ -299,6 +316,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("agents"),
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);

Expand Down Expand Up @@ -647,6 +715,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`)}
Expand Down
1 change: 1 addition & 0 deletions packages/create-react-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"scripts": {
"build": "wireit",
"prepack": "node ./scripts/copy-agent-skills.mjs",
"typecheck": "tsc"
},
"wireit": {
Expand Down
42 changes: 42 additions & 0 deletions packages/create-react-router/scripts/copy-agent-skills.mjs
Original file line number Diff line number Diff line change
@@ -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}/`);