Skip to content
Merged
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-prompt-nudge';
export interface Dependency {
version: string | undefined;
versionSpecifier?: string;
Expand Down
9 changes: 2 additions & 7 deletions code/lib/cli-storybook/src/bin/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
HandledError,
JsPackageManagerFactory,
PackageManagerName,
isCI,
optionalEnvToBoolean,
removeAddon as remove,
versions,
Expand Down Expand Up @@ -106,14 +105,10 @@ command('init')
.option('-y --yes', 'Answer yes to all prompts')
.option('-b --builder <webpack5 | vite>', 'Builder library')
.option('-l --linkable', 'Prepare installation for link (contributor helper)')
.option(
'--dev',
'Launch the development server after completing initialization. Enabled by default (default: true)',
!isCI() && !optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX)
)
.option('--dev', 'Launch the development server after completing initialization')
.option(
'--no-dev',
'Complete the initialization of Storybook without launching the Storybook development server'
'Do not launch the Storybook development server after completing initialization (default)'
);

command('add <addon>')
Expand Down
6 changes: 2 additions & 4 deletions code/lib/create-storybook/src/bin/run.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProjectType } from 'storybook/internal/cli';
import { PackageManagerName, isCI, optionalEnvToBoolean } from 'storybook/internal/common';
import { PackageManagerName, optionalEnvToBoolean } from 'storybook/internal/common';
import { logTracker, logger } from 'storybook/internal/node-logger';
import { addToGlobalContext } from 'storybook/internal/telemetry';
import { Feature, SupportedBuilder } from 'storybook/internal/types';
Expand Down Expand Up @@ -116,10 +116,8 @@ const createStorybookProgram = program

createStorybookProgram
.action(async (options) => {
const isNeitherCiNorSandbox =
!isCI() && !optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX);
options.debug = options.debug ?? false;
options.dev = options.dev ?? isNeitherCiNorSandbox;
options.dev = options.dev ?? false;

if (options.features === false) {
// Ensure features are treated as empty when --no-features is set
Expand Down
186 changes: 184 additions & 2 deletions code/lib/create-storybook/src/commands/FinalizationCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from 'storybook/internal/node-logger';

import * as find from 'empathic/find';

import { FinalizationCommand } from './FinalizationCommand.ts';
import { FinalizationCommand, executeFinalization } from './FinalizationCommand.ts';

vi.mock('node:fs/promises', { spy: true });
vi.mock('storybook/internal/common', { spy: true });
Expand All @@ -18,7 +18,11 @@ describe('FinalizationCommand', () => {
let command: FinalizationCommand;

beforeEach(() => {
command = new FinalizationCommand(undefined);
command = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: false,
});

vi.mocked(getProjectRoot).mockReturnValue('/test/project');
vi.mocked(logger.step).mockImplementation(() => {});
Expand Down Expand Up @@ -107,4 +111,182 @@ describe('FinalizationCommand', () => {
expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('ng run my-app:storybook'));
});
});

describe('agent mode', () => {
it('should show agent-specific message when showAgentFollowUp=true', async () => {
const agentCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: true,
showAiInstructions: false,
});
vi.mocked(find.up).mockReturnValue(undefined);

await agentCommand.execute({});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('is not entirely set up yet')
);
expect(logger.step).toHaveBeenCalledWith(expect.stringContaining('npx storybook ai prepare'));
});

it('should show standard success message when showAgentFollowUp=false with AI instructions', async () => {
const agentCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: true,
});
vi.mocked(find.up).mockReturnValue(undefined);

await agentCommand.execute({});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('Storybook was successfully installed')
);
// Ensure the agent message is NOT shown
const stepCalls = vi.mocked(logger.step).mock.calls.map((c) => String(c[0]));
expect(stepCalls.some((msg) => msg.includes('is not entirely set up yet'))).toBe(false);
});

it('should show standard success message when showAgentFollowUp=false', async () => {
const nonAgentCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: false,
});
vi.mocked(find.up).mockReturnValue(undefined);

await nonAgentCommand.execute({});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('Storybook was successfully installed')
);
// Ensure the agent message is NOT shown
const stepCalls = vi.mocked(logger.step).mock.calls.map((c) => String(c[0]));
expect(stepCalls.some((msg) => msg.includes('is not entirely set up yet'))).toBe(false);
});
});

describe('AI instructions', () => {
it('should show AI instructions when showAiInstructions=true', async () => {
const aiCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: true,
});
vi.mocked(find.up).mockReturnValue(undefined);

await aiCommand.execute({});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('To finalize setting up with AI')
);
expect(logger.step).toHaveBeenCalledWith(expect.stringContaining('npx storybook ai prepare'));
});

it('should NOT show AI instructions when showAiInstructions=false', async () => {
const noAiCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: false,
});
vi.mocked(find.up).mockReturnValue(undefined);

await noAiCommand.execute({});

const stepCalls = vi.mocked(logger.step).mock.calls.map((c) => String(c[0]));
expect(stepCalls.some((msg) => msg.includes('To finalize setting up with AI'))).toBe(false);
});

it('should show both agent message and AI instructions when both are true', async () => {
const bothCommand = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: true,
showAiInstructions: true,
});
vi.mocked(find.up).mockReturnValue(undefined);

await bothCommand.execute({});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('is not entirely set up yet')
);
expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('To finalize setting up with AI')
);
});
});

describe('storybookCommand message', () => {
it('should print "To run Storybook, run" with the command', async () => {
const cmd = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: false,
});
vi.mocked(find.up).mockReturnValue(undefined);

await cmd.execute({ storybookCommand: 'npm run storybook' });

expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('To run Storybook, run'));
expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('npm run storybook'));
});

it('should not print storybook command message when storybookCommand is null', async () => {
const cmd = new FinalizationCommand({
logfile: undefined,
showAgentFollowUp: false,
showAiInstructions: false,
});
vi.mocked(find.up).mockReturnValue(undefined);

await cmd.execute({ storybookCommand: null });

const logCalls = vi.mocked(logger.log).mock.calls.map((c) => String(c[0]));
expect(logCalls.some((msg) => msg.includes('To run Storybook, run'))).toBe(false);
});
});

describe('executeFinalization helper', () => {
it('should show agent follow-up when showAgentFollowUp=true', async () => {
vi.mocked(find.up).mockReturnValue(undefined);

await executeFinalization({
showAgentFollowUp: true,
showAiInstructions: false,
logfile: undefined,
});

// Agent mode should show agent-specific message
expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('is not entirely set up yet')
);
});

it('should pass showAiInstructions=true through to the command', async () => {
vi.mocked(find.up).mockReturnValue(undefined);

await executeFinalization({
showAgentFollowUp: false,
showAiInstructions: true,
logfile: undefined,
});

expect(logger.step).toHaveBeenCalledWith(
expect.stringContaining('To finalize setting up with AI')
);
});

it('should forward storybookCommand to execute', async () => {
vi.mocked(find.up).mockReturnValue(undefined);

Comment thread
Sidnioulz marked this conversation as resolved.
await executeFinalization({
showAgentFollowUp: false,
showAiInstructions: false,
logfile: undefined,
storybookCommand: 'yarn storybook',
});

expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('yarn storybook'));
});
});
});
44 changes: 36 additions & 8 deletions code/lib/create-storybook/src/commands/FinalizationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { ErrorCollector } from 'storybook/internal/telemetry';
import * as find from 'empathic/find';
import { dedent } from 'ts-dedent';

export type FinalizationCommandOptions = {
logfile: string | boolean | undefined;
/** When true, show the agent follow-up message ("run npx storybook ai prepare") instead of the normal success message */
showAgentFollowUp: boolean;
/** When true, show the "paste this prompt to your AI agent" instructions */
showAiInstructions: boolean;
};

type ExecuteFinalizationParams = {
storybookCommand?: string | null;
};
Expand All @@ -22,7 +30,8 @@ type ExecuteFinalizationParams = {
* - Show next steps
*/
export class FinalizationCommand {
constructor(private logfile: string | boolean | undefined) {}
constructor(private options: FinalizationCommandOptions) {}

/** Execute finalization steps */
async execute({ storybookCommand }: ExecuteFinalizationParams): Promise<void> {
// Update .gitignore
Expand Down Expand Up @@ -69,33 +78,52 @@ export class FinalizationCommand {
this.printNextSteps(storybookCommand);

try {
const logFile = await logTracker.writeToFile(this.logfile);
const logFile = await logTracker.writeToFile(this.options.logfile);
logger.warn(`Debug logs are written to: ${logFile}`);
} catch {}
}

/** Print success message with feature summary */
private printSuccessMessage(storybookCommand?: string | null): void {
logger.step(CLI_COLORS.success('Storybook was successfully installed in your project!'));
if (this.options.showAgentFollowUp) {
logger.step(
CLI_COLORS.storybook(
dedent`Storybook is installed but is not entirely set up yet.
To finish setting up, now run \`npx storybook ai prepare\` and follow its instructions precisely.`
)
);
} else {
logger.step(CLI_COLORS.success('Storybook was successfully installed in your project!'));
}
Comment thread
Sidnioulz marked this conversation as resolved.
this.printNextSteps(storybookCommand);
}

private printNextSteps(storybookCommand?: string | null): void {
if (storybookCommand) {
logger.log(
`To run Storybook manually, run ${CLI_COLORS.cta(storybookCommand)}. CTRL+C to stop.`
);
logger.log(`To run Storybook, run ${CLI_COLORS.cta(storybookCommand)}. CTRL+C to stop.`);
}

logger.log(dedent`
Want to learn more about Storybook? ${CLI_COLORS.cta('https://storybook.js.org/')}
Having trouble or want to chat? ${CLI_COLORS.cta('https://discord.gg/storybook/')}
`);

if (this.options.showAiInstructions) {
logger.step(dedent`To finalize setting up with AI, paste this prompt to your AI agent:

${CLI_COLORS.storybook(`Run \`npx storybook ai prepare\` and follow its instructions precisely.`)}
`);
}
}
}

export const executeFinalization = ({
logfile,
showAgentFollowUp,
showAiInstructions,
...params
}: ExecuteFinalizationParams & { logfile: string | boolean | undefined }) => {
return new FinalizationCommand(logfile).execute(params);
}: ExecuteFinalizationParams & FinalizationCommandOptions) => {
return new FinalizationCommand({ logfile, showAgentFollowUp, showAiInstructions }).execute(
params
);
};
Loading
Loading