From 1d3baa450a4220f2928a66da014a7e80f52a2c29 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:23:31 +0100 Subject: [PATCH] fix: point --no-skills projects upstream instead of at a missing .agents/ The generated AGENTS.md and README always referenced .agents/skills/tool-design/, even when the project was scaffolded with --no-skills and that directory does not exist. The skill is now copied before any template is rendered, and the docs are rendered with withSkills set to whether the copy actually happened. Without it, both link to the skill on GitHub and give `npx skills add agentailor/skills --skill tool-design`. A failed copy is covered by the same path. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 11 ++++++ src/project-generator.ts | 41 ++++++++++++----------- src/skills.ts | 11 ++++++ src/templates/common/agents.md.test.ts | 11 ++++++ src/templates/common/agents.md.ts | 23 +++++++++++-- src/templates/common/skill-readme.test.ts | 28 ++++++++++++++++ src/templates/common/skill-readme.ts | 37 ++++++++++++++++++++ src/templates/common/types.ts | 5 +++ src/templates/sdk/http/readme.ts | 15 ++------- src/templates/sdk/stdio/readme.ts | 15 ++------- 10 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 src/templates/common/skill-readme.test.ts create mode 100644 src/templates/common/skill-readme.ts diff --git a/AGENTS.md b/AGENTS.md index a9e3dd8..cddc12a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -211,6 +211,17 @@ writes nothing — so check the files landed rather than trusting the exit code. `.agents/` is deliberately **not** in the generated `.gitignore`: the skill is meant to be committed so the whole team gets the same copy. +### When the skill is absent + +The generated README and `AGENTS.md` both have a skill section, and it must +describe what is on disk. `project-generator.ts` therefore copies the skill +**before** rendering any template and passes `withSkills: skillCopied` — the +copy's actual outcome, not the flag, so a failed copy is covered too. Without +the skill, both docs link upstream (`SKILL_URL`) and give `SKILLS_ADD_COMMAND` +instead of pointing at `.agents/`. That command omits `-a universal` on purpose: +whoever opted out gets to pick the agent. The README section lives in +`src/templates/common/skill-readme.ts`, shared by both transports. + ## Examples `examples/` holds complete servers built with the CLI, one folder per example, indexed in `examples/README.md`. They are not part of the published package (`files` is `dist` only) and the root `lint`, `format`, `tsc` and `test` scripts do not touch them. diff --git a/src/project-generator.ts b/src/project-generator.ts index 7394668..cb37dd6 100644 --- a/src/project-generator.ts +++ b/src/project-generator.ts @@ -49,18 +49,34 @@ export interface ProjectConfig { export async function generateProject(config: ProjectConfig): Promise { const { projectName, packageManager, transport, withOAuth, withGitInit, withSkills } = config; - const templateOptions: CommonTemplateOptions = { - withOAuth: transport === 'http' ? withOAuth : false, - packageManager, - transport, - }; - const projectPath = join(process.cwd(), projectName); const srcPath = join(projectPath, 'src'); // Create directories await mkdir(srcPath, { recursive: true }); + // The skill is vendored, so this is a file copy. It never fails the scaffold: + // the project is complete without it. It runs before the templates so the + // README and AGENTS.md describe what is actually on disk — a project without + // the skill must not point at a directory that does not exist. + let skillCopied = false; + if (withSkills) { + const result = await copySkill(projectPath); + skillCopied = result.copied; + + if (!result.copied) { + console.log(`\n Could not add the tool-design skill (${result.reason}).`); + console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`); + } + } + + const templateOptions: CommonTemplateOptions = { + withOAuth: transport === 'http' ? withOAuth : false, + packageManager, + transport, + withSkills: skillCopied, + }; + // Build list of files to write const filesToWrite: Promise[] = []; @@ -132,19 +148,6 @@ export async function generateProject(config: ProjectConfig): Promise { } } - // The skill is vendored, so this is a file copy. It never fails the scaffold: - // the project is complete without it. - let skillCopied = false; - if (withSkills) { - const result = await copySkill(projectPath); - skillCopied = result.copied; - - if (!result.copied) { - console.log(`\n Could not add the tool-design skill (${result.reason}).`); - console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`); - } - } - const commands = packageManagerCommands[packageManager]; console.log(`\nCreated ${projectName} with MCP SDK (${transport}) at ${projectPath}`); console.log(`\nNext steps:`); diff --git a/src/skills.ts b/src/skills.ts index bfbe227..4d5141b 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -32,6 +32,17 @@ export const SKILLS_UPDATE_COMMAND = 'npx skills update -p -y'; */ export const SKILLS_INSTALL_HINT = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME} -a universal`; +/** Where the skill lives upstream, for projects scaffolded without it. */ +export const SKILL_URL = `https://github.com/${SKILL_REPO}/tree/main/${SKILL_NAME}`; + +/** + * Offered to projects scaffolded with `--no-skills`. + * + * No `-a universal` here: whoever opted out of the vendored copy gets to pick + * which agent the skills CLI installs for. + */ +export const SKILLS_ADD_COMMAND = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME}`; + /** * The vendored skill, copied verbatim from `agentailor/skills`. * diff --git a/src/templates/common/agents.md.test.ts b/src/templates/common/agents.md.test.ts index 1c07008..85e7dab 100644 --- a/src/templates/common/agents.md.test.ts +++ b/src/templates/common/agents.md.test.ts @@ -136,6 +136,17 @@ describe('getAgentsMdTemplate', () => { expect(template).toContain('Commit `.agents/`'); }); + // With --no-skills there is no .agents/ to point at; send the reader upstream. + it('should link upstream and give the install command when the skill is absent', () => { + for (const options of [sdkHttp, sdkStdio]) { + const template = getAgentsMdTemplate(projectName, { ...options, withSkills: false }); + expect(template).not.toContain('.agents/'); + expect(template).not.toContain('npx skills update'); + expect(template).toContain('https://github.com/agentailor/skills/tree/main/tool-design'); + expect(template).toContain('npx skills add agentailor/skills --skill tool-design'); + } + }); + // The copy is vendored, so updating is the user's choice, not automatic. it("should frame the update as the reader's choice", () => { const template = getAgentsMdTemplate(projectName, sdkHttp); diff --git a/src/templates/common/agents.md.ts b/src/templates/common/agents.md.ts index 375fa4a..e3c86e7 100644 --- a/src/templates/common/agents.md.ts +++ b/src/templates/common/agents.md.ts @@ -1,5 +1,11 @@ import type { CommonTemplateOptions } from './types.js'; -import { SKILLS_UPDATE_COMMAND, SKILLS_INSTALL_HINT, SKILLS_DIR } from '../../skills.js'; +import { + SKILLS_UPDATE_COMMAND, + SKILLS_INSTALL_HINT, + SKILLS_DIR, + SKILLS_ADD_COMMAND, + SKILL_URL, +} from '../../skills.js'; /** * AGENTS.md for the generated project. @@ -12,6 +18,7 @@ export function getAgentsMdTemplate(projectName: string, options?: CommonTemplat const transport = options?.transport ?? 'http'; const withOAuth = options?.withOAuth ?? false; const packageManager = options?.packageManager ?? 'npm'; + const withSkills = options?.withSkills ?? true; const isStdio = transport === 'stdio'; @@ -152,7 +159,8 @@ root, and domain logic stays free of MCP imports so it remains testable on its own. `; - const skillSection = ` + const skillSection = withSkills + ? ` ## The tool-design skill \`${SKILLS_DIR}/skills/tool-design/SKILL.md\` is the long form of the guidance above, @@ -172,6 +180,17 @@ That is yours to run, not something this project does for you. If the directory is missing entirely, add it with \`${SKILLS_INSTALL_HINT}\`. Commit \`${SKILLS_DIR}/\` so everyone working on this project gets the same copy. +` + : ` +## The tool-design skill + +The guidance above is the short form of Agentailor's +[tool-design](${SKILL_URL}) skill, which has the long form with worked +examples. It is not installed in this project. To add it: + +\`\`\`bash +${SKILLS_ADD_COMMAND} +\`\`\` `; const conventions = ` diff --git a/src/templates/common/skill-readme.test.ts b/src/templates/common/skill-readme.test.ts new file mode 100644 index 0000000..0495d48 --- /dev/null +++ b/src/templates/common/skill-readme.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { getSkillReadmeSection } from './skill-readme.js'; +import { getReadmeTemplate as getHttpReadme } from '../sdk/http/index.js'; +import { getReadmeTemplate as getStdioReadme } from '../sdk/stdio/index.js'; + +describe('getSkillReadmeSection', () => { + it('should point at the installed skill and how to update it', () => { + const section = getSkillReadmeSection(true); + expect(section).toContain('.agents/skills/tool-design/'); + expect(section).toContain('npx skills update -p -y'); + }); + + // A --no-skills project has no .agents/; send the reader upstream instead. + it('should link upstream and give the install command when the skill is absent', () => { + const section = getSkillReadmeSection(false); + expect(section).not.toContain('.agents/'); + expect(section).not.toContain('npx skills update'); + expect(section).toContain('https://github.com/agentailor/skills/tree/main/tool-design'); + expect(section).toContain('npx skills add agentailor/skills --skill tool-design'); + }); + + it('should follow withSkills in both transport READMEs', () => { + for (const getReadme of [getHttpReadme, getStdioReadme]) { + expect(getReadme('p')).toContain('.agents/skills/tool-design/'); + expect(getReadme('p', { withSkills: false })).not.toContain('.agents/'); + } + }); +}); diff --git a/src/templates/common/skill-readme.ts b/src/templates/common/skill-readme.ts new file mode 100644 index 0000000..ff315d8 --- /dev/null +++ b/src/templates/common/skill-readme.ts @@ -0,0 +1,37 @@ +import { SKILLS_ADD_COMMAND, SKILLS_DIR, SKILLS_UPDATE_COMMAND, SKILL_URL } from '../../skills.js'; + +/** + * The README's "Tool design skill" section, shared by every transport. + * + * Branches on whether the skill is actually in the project: pointing at + * `.agents/skills/tool-design/` in a project scaffolded with `--no-skills` sends + * the reader to a directory that does not exist. + */ +export function getSkillReadmeSection(withSkills: boolean): string { + if (!withSkills) { + return `## Tool design skill + +Agentailor's [tool-design](${SKILL_URL}) skill gives a coding agent working here +detailed guidance on designing MCP tools. It is not installed in this project. +To add it: + +\`\`\`bash +${SKILLS_ADD_COMMAND} +\`\`\` +`; + } + + return `## Tool design skill + +Agentailor's [tool-design](${SKILL_URL}) skill is +included at \`${SKILLS_DIR}/skills/tool-design/\`, so a coding agent working here has +the guidance on hand. Commit it so the whole team gets the same copy. + +It is the version that shipped with the scaffolding CLI. To check for a newer +one — your call, not automatic: + +\`\`\`bash +${SKILLS_UPDATE_COMMAND} +\`\`\` +`; +} diff --git a/src/templates/common/types.ts b/src/templates/common/types.ts index a0b70ea..eb9e533 100644 --- a/src/templates/common/types.ts +++ b/src/templates/common/types.ts @@ -6,6 +6,11 @@ export type TransportType = 'http' | 'stdio'; */ export interface BaseTemplateOptions { packageManager?: PackageManager; + /** + * Whether the tool-design skill is present in the project. Defaults to true. + * The generator sets it from the copy's actual outcome, not just the flag. + */ + withSkills?: boolean; } /** diff --git a/src/templates/sdk/http/readme.ts b/src/templates/sdk/http/readme.ts index 5084889..455e1a8 100644 --- a/src/templates/sdk/http/readme.ts +++ b/src/templates/sdk/http/readme.ts @@ -1,4 +1,5 @@ import type { TemplateOptions } from './index.js'; +import { getSkillReadmeSection } from '../../common/skill-readme.js'; /** * Shared README template, used by both SDK HTTP template types. @@ -245,19 +246,7 @@ docker run -p 3000:3000 ${projectName} instance is discarded between calls. - Modify the HTTP handler and Express configuration in \`src/index.ts\`${customizationOAuthNote} -## Tool design skill - -Agentailor's [tool-design](https://github.com/agentailor/skills) skill is -included at \`.agents/skills/tool-design/\`, so a coding agent working here has -the guidance on hand. Commit it so the whole team gets the same copy. - -It is the version that shipped with the scaffolding CLI. To check for a newer -one — your call, not automatic: - -\`\`\`bash -npx skills update -p -y -\`\`\` - +${getSkillReadmeSection(options?.withSkills ?? true)} ## Learn More - [Model Context Protocol](https://modelcontextprotocol.io/) diff --git a/src/templates/sdk/stdio/readme.ts b/src/templates/sdk/stdio/readme.ts index e7b4b69..c579a48 100644 --- a/src/templates/sdk/stdio/readme.ts +++ b/src/templates/sdk/stdio/readme.ts @@ -1,4 +1,5 @@ import type { TemplateOptions } from './index.js'; +import { getSkillReadmeSection } from '../../common/skill-readme.js'; export function getReadmeTemplate(projectName: string, options?: TemplateOptions): string { const packageManager = options?.packageManager ?? 'npm'; @@ -155,19 +156,7 @@ ${projectName}/ imports so it stays testable on its own. - Modify transport configuration in \`src/index.ts\` -## Tool design skill - -Agentailor's [tool-design](https://github.com/agentailor/skills) skill is -included at \`.agents/skills/tool-design/\`, so a coding agent working here has -the guidance on hand. Commit it so the whole team gets the same copy. - -It is the version that shipped with the scaffolding CLI. To check for a newer -one — your call, not automatic: - -\`\`\`bash -npx skills update -p -y -\`\`\` - +${getSkillReadmeSection(options?.withSkills ?? true)} ## Learn More - [Model Context Protocol](https://modelcontextprotocol.io/)