Skip to content
Closed
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
3 changes: 2 additions & 1 deletion code/core/src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export type EventType =
| 'doctor'
| 'share'
| 'ghost-stories'
| 'ai-prepare';
| 'ai-prepare'
| 'ai-install-skills';
export interface Dependency {
version: string | undefined;
versionSpecifier?: string;
Expand Down
13 changes: 13 additions & 0 deletions code/lib/cli-storybook/src/bin/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { doctor } from '../doctor/index.ts';
import { link } from '../link.ts';
import { migrate } from '../migrate.ts';
import { sandbox } from '../sandbox.ts';
import { installAgentSkills } from '../../../create-storybook/src/install-agent-skills.ts';
import { aiPrepare } from '../ai/index.ts';
import { type UpgradeOptions, upgrade } from '../upgrade.ts';

Expand Down Expand Up @@ -328,6 +329,18 @@ aiCommand
}).catch(handleCommandFailure(mergedOptions.logfile));
});

aiCommand
.command('install-skills')
.description(
'Install Storybook agent skills for AI coding agents (Claude, Cursor, Copilot, etc.)'
)
.action(async (_options, cmd) => {
const parentOptions = cmd.parent?.opts() ?? {};
await withTelemetry('ai-install-skills', { cliOptions: parentOptions }, async () => {
await installAgentSkills();
}).catch(handleCommandFailure(parentOptions.logfile));
});

// Show available subcommands when `storybook ai` is run without arguments
aiCommand.action(() => {
aiCommand.outputHelp();
Expand Down
1 change: 1 addition & 0 deletions code/lib/create-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"bin/**/*",
"dist/**/*",
"rendererAssets/**/*",
"skills/**/*",
"templates/**/*",
"README.md",
"!src/**/*"
Expand Down
37 changes: 37 additions & 0 deletions code/lib/create-storybook/skills/setup-storybook/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: setup-storybook
description: >
Use when setting up Storybook in a project: writing the initial preview
configuration, decorators, and example stories for existing components.
Trigger this skill after `storybook init` has installed Storybook, or
whenever the user asks to "set up Storybook", "configure Storybook",
or "write stories for my components".
---

# Storybook Setup

> **Managed by Storybook.** This file is installed and updated by `@storybook/cli`. It is symlinked from `node_modules/@storybook/cli/skills/setup-storybook/SKILL.md`, so it refreshes automatically when you upgrade Storybook. Edit the symlink target only if you know what you're doing — your changes will be overwritten on the next install.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Correct the symlink source package path.

Line 13 says the file is symlinked from node_modules/@storybook/cli/..., but the installer resolves skills from create-storybook/skills. This can send contributors to the wrong source file.

📝 Proposed fix
-> **Managed by Storybook.** This file is installed and updated by `@storybook/cli`. It is symlinked from `node_modules/@storybook/cli/skills/setup-storybook/SKILL.md`, so it refreshes automatically when you upgrade Storybook. Edit the symlink target only if you know what you're doing — your changes will be overwritten on the next install.
+> **Managed by Storybook.** This file is installed and updated by Storybook tooling. It is symlinked from `node_modules/create-storybook/skills/setup-storybook/SKILL.md`, so it refreshes automatically when you upgrade Storybook. Edit the symlink target only if you know what you're doing — your changes will be overwritten on the next install.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> **Managed by Storybook.** This file is installed and updated by `@storybook/cli`. It is symlinked from `node_modules/@storybook/cli/skills/setup-storybook/SKILL.md`, so it refreshes automatically when you upgrade Storybook. Edit the symlink target only if you know what you're doing — your changes will be overwritten on the next install.
> **Managed by Storybook.** This file is installed and updated by Storybook tooling. It is symlinked from `node_modules/create-storybook/skills/setup-storybook/SKILL.md`, so it refreshes automatically when you upgrade Storybook. Edit the symlink target only if you know what you're doing — your changes will be overwritten on the next install.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/skills/setup-storybook/SKILL.md` at line 13, Update
the explanatory path in SKILL.md: change the symlink source reference from
node_modules/@storybook/cli/skills/setup-storybook/SKILL.md to the actual
installer source create-storybook/skills/setup-storybook/SKILL.md so
contributors are pointed to the correct package; edit the sentence in the file
SKILL.md that currently references `node_modules/@storybook/cli/...` to instead
reference `create-storybook/skills/...` and keep the rest of the note about
symlink behavior intact.


This skill helps you finish setting up Storybook in a project that already has it installed. It does **not** install Storybook from scratch — for that, run `npx storybook init` first.

## How to use this skill

Run the following command and follow its output exactly:

```bash
npx storybook ai prepare
```

This command inspects the project's Storybook configuration and prints a tailored markdown prompt with:

- A project info table (version, renderer, framework, builder, addons, CSF format)
- Renderer-specific documentation links
- Step-by-step instructions for analyzing the codebase, configuring `preview.ts` or `preview.tsx` with the right decorators, writing example stories, and verifying them with Vitest

**Treat the output of `storybook ai prepare` as your authoritative instructions.** Do not improvise setup steps from memory — the command's output is generated from the project's actual configuration and stays in sync with the installed Storybook version.

## When NOT to use this skill

- The user wants to add Storybook to a fresh project → run `npx storybook init` instead
- The user is asking general questions about Storybook → fetch https://storybook.js.org/llms.txt or append `.md` to any docs URL
- The user wants to upgrade Storybook → run `npx storybook upgrade`
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class UserPreferencesCommand {

return prompt.confirm({
message: dedent`Would you like to improve your Storybook setup with AI?
We will provide you with a prompt that you can use with your LLM to fully set up Storybook with best practices, tailored to your project.`,
We will install Storybook skills for your AI coding agent (Claude, Cursor, Copilot, etc.) and provide you with a prompt to fully set up Storybook with best practices, tailored to your project.`,
Comment on lines 195 to +196

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't promise the tailored prompt is generated during init.

In the non-agent flow, doInitiate() only installs the skills and then tells the user to run npx storybook ai prepare later. This copy reads like the tailored prompt is produced immediately after consent, which is not what the current flow does.

✏️ Suggested wording
-      We will install Storybook skills for your AI coding agent (Claude, Cursor, Copilot, etc.) and provide you with a prompt to fully set up Storybook with best practices, tailored to your project.`,
+      We will install Storybook skills for your AI coding agent (Claude, Cursor, Copilot, etc.). Then you can run \`storybook ai prepare\` to generate a tailored prompt for finishing your setup with project-specific best practices.`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/commands/UserPreferencesCommand.ts` around
lines 195 - 196, Update the user-facing message in UserPreferencesCommand (the
message string used in doInitiate()) so it does not promise a tailored prompt
will be generated during init; instead say we will install Storybook AI skills
now and instruct the user to run "npx storybook ai prepare" later to generate
the tailored prompt and setup instructions. Locate the message constant or
property inside UserPreferencesCommand (used by doInitiate()/prompt flow) and
change the copy accordingly to avoid implying the prompt is produced
immediately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we say something like "We'll install skills and tell you which skill to call to set up Storybook"? As the skill would be providing the prompt rather than us adding skills + printing a prompt?

});
}

Expand Down
1 change: 1 addition & 0 deletions code/lib/create-storybook/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { doInitiate as initiate } from './initiate.ts';
export { installAgentSkills } from './install-agent-skills.ts';
23 changes: 11 additions & 12 deletions code/lib/create-storybook/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,23 @@ export async function doInitiate(options: CommandOptions): Promise<
options,
});

if (selectedFeatures.has(Feature.AI)) {
logger.step('Setting up Storybook AI skills file');
const { installAgentSkills } = await import('./install-agent-skills.ts');
await installAgentSkills();

logger.log(
CLI_COLORS.storybook(dedent`Done. To finalize setting up with AI, either use the /setup-storybook skill or paste the following prompt to your AI agent:
Run \`npx storybook ai prepare\` and follow its instructions precisely.`)
);
Comment on lines +113 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't advertise /setup-storybook after a best-effort install fails.

installAgentSkills() swallows failures and only logs a warning, so this unconditional “Done” message can still imply the skill is available when it was never installed. Please gate the success wording on the helper's result, or fall back to only the manual storybook ai prepare path on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/initiate.ts` around lines 113 - 119, The
post-install "Done" message is emitted unconditionally even though
installAgentSkills() may have failed silently; update the flow to await and
inspect the helper's outcome (e.g., const installed = await
installAgentSkills(); or wrap call in try/catch if it throws) and only emit the
CLI_COLORS.storybook "Done" message when installed is truthy/successful;
otherwise log a fallback message that omits the /setup-storybook suggestion and
only shows the manual "Run `npx storybook ai prepare`" instructions via
logger.log.

Comment on lines +116 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve configDir in the printed ai prepare command.

configDir is already known here, but Line 118 always tells users to run npx storybook ai prepare. If init wrote the config somewhere other than .storybook, that follow-up step loses the resolved path and can target the wrong config.

💡 Proposed fix
+    const prepareCommand =
+      configDir && configDir !== '.storybook'
+        ? `npx storybook ai prepare --config-dir ${configDir}`
+        : 'npx storybook ai prepare';
+
     logger.log(
       CLI_COLORS.storybook(dedent`Done. To finalize setting up with AI, either use the /setup-storybook skill or paste the following prompt to your AI agent:
-      Run \`npx storybook ai prepare\` and follow its instructions precisely.`)
+      Run \`${prepareCommand}\` and follow its instructions precisely.`)
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/initiate.ts` around lines 116 - 119, The
printed follow-up command always omits the resolved configDir; update the
message built in the logger.log call (the CLI_COLORS.storybook template) to
include the configDir variable so users run the exact command used by init
(e.g., `npx storybook ai prepare --config-dir <configDir>` or similar quoting) —
reference the logger.log invocation that wraps CLI_COLORS.storybook and the
configDir variable in this module and interpolate/format configDir into the
output string so the displayed command targets the actual written config
location.

}

// Step 8: Print final summary
await executeFinalization({
logfile: options.logfile,
storybookCommand,
});

if (selectedFeatures.has(Feature.AI)) {
if (options.agent) {
const { aiPrepare } = await import('../../cli-storybook/src/ai/index.ts');
await aiPrepare({ configDir: configDir ?? '.storybook' });
} else {
logger.step(
CLI_COLORS.storybook(dedent`To finalize setting up with AI, paste this prompt to your AI agent:
Run \`npx storybook ai prepare\` and follow its instructions precisely.`)
);
}
}

// Step 9: Track telemetry
await telemetryService.trackInitWithContext(projectType, selectedFeatures, newUser);

Expand Down
48 changes: 48 additions & 0 deletions code/lib/create-storybook/src/install-agent-skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';

import { executeCommand } from 'storybook/internal/common';
import { logger } from 'storybook/internal/node-logger';

/**
* Installs all Storybook agent skills using the Vercel skills CLI.
*
* Points `npx skills add` at the bundled `skills/` directory, which auto-discovers
* every `SKILL.md` underneath. Adding a new skill is as simple as creating a new
* `skills/<skill-name>/SKILL.md` file — no changes to this command needed.
*
* Uses symlink mode (default) so that updating Storybook automatically refreshes
* the skill content via the symlink target in node_modules. The skills CLI handles
* agent detection and file placement.
*
* The `skills/` directory ships with the `create-storybook` package. We resolve
* its location via `require.resolve` so the path is correct regardless of which
* package bundled this code — this file is inlined into both `create-storybook`
* and `@storybook/cli` builds by the bundler.
*/
export async function installAgentSkills(): Promise<void> {
const require = createRequire(import.meta.url);
const createStorybookPkg = require.resolve('create-storybook/package.json');
const skillsDir = join(dirname(createStorybookPkg), 'skills');
Comment on lines +24 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard package resolution inside the non-critical error boundary.

Line 25 can throw before the try, which bypasses the fallback warning path and can fail init despite this step being documented as non-critical.

🛠️ Proposed fix
 export async function installAgentSkills(): Promise<void> {
-  const require = createRequire(import.meta.url);
-  const createStorybookPkg = require.resolve('create-storybook/package.json');
-  const skillsDir = join(dirname(createStorybookPkg), 'skills');
-
   try {
+    const require = createRequire(import.meta.url);
+    const createStorybookPkg = require.resolve('create-storybook/package.json');
+    const skillsDir = join(dirname(createStorybookPkg), 'skills');
     await executeCommand({
       command: 'npx',
       args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
       stdio: 'inherit',
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const require = createRequire(import.meta.url);
const createStorybookPkg = require.resolve('create-storybook/package.json');
const skillsDir = join(dirname(createStorybookPkg), 'skills');
export async function installAgentSkills(): Promise<void> {
try {
const require = createRequire(import.meta.url);
const createStorybookPkg = require.resolve('create-storybook/package.json');
const skillsDir = join(dirname(createStorybookPkg), 'skills');
await executeCommand({
command: 'npx',
args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
stdio: 'inherit',
});
} catch (error) {
// Non-critical — don't fail the init if skill export fails
logger.warn(
`Could not install agent skills: ${error instanceof Error ? error.message : String(error)}`
);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/install-agent-skills.ts` around lines 24 - 26,
The package resolution for create-storybook is happening before the non-critical
error boundary, so move the require.resolve call into the existing try/catch (or
add a small try around it) to prevent throws from escaping; specifically, call
createRequire(import.meta.url) as now but delay resolving
'create-storybook/package.json' until inside the try that sets up skillsDir
(resolve into createStorybookPkg and then set skillsDir =
join(dirname(createStorybookPkg),'skills') within the try), and on failure log
the existing non-critical warning and continue without setting skillsDir.


try {
// Silently pipe output on success; surface it only when the command fails.
await executeCommand({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a logger.step to tell users what we're doing before doing something that can be a bit slow?

command: 'npx',
args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
stdio: 'pipe',
});
Comment on lines +28 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound the npx skills add call with a timeout.

This now runs on the init path, so a stalled npx resolution/install can block the whole setup indefinitely. The catch only helps once the child process exits.

🛡️ Suggested fix
 export async function installAgentSkills(): Promise<void> {
   const currentDir = dirname(fileURLToPath(import.meta.url));
   const skillsDir = join(currentDir, '..', '..', 'skills');
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 60_000);
 
   try {
     await executeCommand({
       command: 'npx',
       args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
       stdio: 'inherit',
+      signal: controller.signal,
     });
   } catch (error) {
     // Non-critical — don't fail the init if skill export fails
     logger.warn(
       `Could not install agent skills: ${error instanceof Error ? error.message : String(error)}`
     );
+  } finally {
+    clearTimeout(timeout);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await executeCommand({
command: 'npx',
args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
stdio: 'inherit',
});
export async function installAgentSkills(): Promise<void> {
const currentDir = dirname(fileURLToPath(import.meta.url));
const skillsDir = join(currentDir, '..', '..', 'skills');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60_000);
try {
await executeCommand({
command: 'npx',
args: ['skills', 'add', skillsDir, '--skill', '*', '--yes'],
stdio: 'inherit',
signal: controller.signal,
});
} catch (error) {
// Non-critical — don't fail the init if skill export fails
logger.warn(
`Could not install agent skills: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
clearTimeout(timeout);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/cli-storybook/src/ai/install-agent-skills.ts` around lines 22 - 27,
The npx skills add invocation in install-agent-skills.ts (the executeCommand
call that runs 'npx', args ['skills','add', skillsDir, '--skill','*','--yes'])
can hang indefinitely; wrap it with a bounded timeout (e.g., create an
AbortController or use a Promise.race with a setTimeout) so that if the timeout
elapses you kill the child/process and reject with a clear error. Specifically,
modify the code around executeCommand in install-agent-skills.ts to start a
timer (configurable timeout value), pass the abort signal to executeCommand or
call child.kill() when the timer fires, clear the timer on success, and surface
a descriptive error so the caller’s catch path runs instead of blocking the init
flow.

} catch (error) {
// Non-critical — don't fail the init if skill installation fails.
const message = error instanceof Error ? error.message : String(error);
// execa attaches the subprocess's stderr/stdout to the error; include them so
// users can see why the skills CLI failed.
const stderr =
error && typeof error === 'object' && 'stderr' in error ? String(error.stderr ?? '') : '';
const stdout =
error && typeof error === 'object' && 'stdout' in error ? String(error.stdout ?? '') : '';
const details = [stderr, stdout].filter(Boolean).join('\n');

logger.warn(`Could not install agent skills: ${message}${details ? `\n${details}` : ''}`);
}
}
Loading