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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,14 @@ This auto-generates the matrix entries in `.github/workflows/tests-entry.yml`. N
| `pnpm-workspace.yaml` | Workspace package locations and hoisting config |
| `packages/cli/turbo.json` | CLI-specific build dependencies |
| `packages/features/src/features.ts` | Feature flag definitions |
| `packages/cli/integrations.ts` | CLI-only integrations registry (e.g. `--storybook`) |
| `packages/cli/types.ts` | Integration types (`Integration`, `IntegrationContext`) |
| `packages/features/src/rules/rules.ts` | Feature compatibility rules |

**Important:** CLI options are now split in two groups:
- Feature flags come from `packages/features/src/features.ts`.
- CLI-only integrations (that aren't product features) are registered in `packages/cli/integrations.ts` and provide their own arg definition + runtime hook.

## Adding/Modifying Boilerplates

**MAINTAINABILITY is the top priority.** Strive for clean code and good separation of concerns.
Expand Down
101 changes: 77 additions & 24 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { type ArgDef, type CommandDef, defineCommand, type ParsedArgs, runMain,
import * as colorette from "colorette";
import { blue, blueBright, bold, cyan, gray, green, red, underline, yellow } from "colorette";
import { kebabCase } from "scule";
import { getEnabledIntegrations, getIntegrationArgDefs, runEnabledIntegrations } from "./integrations.js";
import packageJson from "./package.json" with { type: "json" };
import { type RuleMessage, rulesMessages } from "./rules.js";
import type { BoilerplateDef, BoilerplateDefWithConfig, Hook } from "./types.js";
import type { BatiArgDef, BoilerplateDef, BoilerplateDefWithConfig, Hook } from "./types.js";

printInit();

Expand All @@ -25,7 +26,6 @@ const isWin = process.platform === "win32";
const pm = packageManager();

type FeatureOrCategory = Flags | CategoryLabels;
type BatiArgDef = ArgDef & { invisible?: boolean };
type BatiArgsDef = Record<string, BatiArgDef>;

function boilerplatesDir() {
Expand Down Expand Up @@ -160,7 +160,13 @@ function printInit() {
console.log(`\n🔨 ${cyan("Vike Scaffolder")} 🔨 ${gray(`v${version}`)}\n`);
}

async function printOK(dist: string, flags: string[], nextSteps: BatiConfigStep[]) {
async function printOK(
dist: string,
flags: string[],
nextSteps: BatiConfigStep[],
extraLabels: string[] = [],
integrationNextSteps: BatiConfigStep[] = [],
) {
const indent = 1;
const list = withIcon("-", gray, indent);
const cmd = withIcon("$", gray, indent);
Expand All @@ -174,6 +180,10 @@ async function printOK(dist: string, flags: string[], nextSteps: BatiConfigStep[
console.log(list(feature.label));
}

for (const label of extraLabels) {
console.log(list(label));
}

console.log(`\n${bold("Next steps:")}`);
// Step 1
console.log(cmd(`cd ${distPretty}`));
Expand All @@ -194,6 +204,14 @@ async function printOK(dist: string, flags: string[], nextSteps: BatiConfigStep[
// Step 4
console.log(cmd(`${pm.run} dev`));

for (const step of integrationNextSteps) {
if (step.type === "command") {
console.log(cmd(step.step));
} else {
console.log(withIcon("•️", gray, indent)(step.step));
}
}

console.log("\nHappy coding! 🚀\n");
}

Expand Down Expand Up @@ -229,7 +247,8 @@ type Args = typeof defaultDef &
required: boolean;
description: string | undefined;
}
>;
> &
Record<string, ArgDef>;

export default function yn(value: unknown, default_?: boolean) {
if (value === undefined || value === null) {
Expand Down Expand Up @@ -378,13 +397,11 @@ async function checkFlagsIncludesUiFramework(flags: string[]) {
}
}

function checkFlagsExist(flags: string[]) {
function checkFlagsExist(flags: string[], knownOptions: string[]) {
const normalizedKnownOptions = new Set(knownOptions.map((option) => kebabCase(option)));

const inValidOptions = flags.reduce((acc: string[], flag: string) => {
if (
!Object.hasOwn(defaultDef, flag) &&
!Object.hasOwn(defaultDef, kebabCase(flag)) &&
!features.some((f) => f.flag === flag || kebabCase(f.flag) === kebabCase(flag))
) {
if (!normalizedKnownOptions.has(kebabCase(flag))) {
acc.push(flag);
}
return acc;
Expand All @@ -398,6 +415,10 @@ function checkFlagsExist(flags: string[]) {
}
}

function isFeatureFlag(flag: string): flag is Flags {
return features.some((f) => f.flag === flag);
}

function checkRules(flags: string[]) {
const potentialRulesMessages = execRules(flags as FeatureOrCategory[], rulesMessages);

Expand Down Expand Up @@ -499,7 +520,14 @@ async function run() {
const dir = boilerplatesDir();
const boilerplates = await loadBoilerplates(dir);

const optsArgs = Object.assign({}, defaultDef, ...cliFlags.map((k) => toArg(k, findFeature(k)))) as Args;
const integrationArgs = getIntegrationArgDefs();
const optsArgs = Object.assign(
{},
defaultDef,
integrationArgs,
...cliFlags.map((k) => toArg(k, findFeature(k))),
) as Args;
const knownOptionKeys = Object.keys(optsArgs);

const main = defineCommand({
meta: {
Expand All @@ -513,23 +541,34 @@ async function run() {

const sources: string[] = [];
const hooks: string[] = [];
const flags = [
const selectedFlags = [
...new Set(
Object.entries(args)
.filter(([, val]) => val === true)
.flatMap(([key]) => {
const flag: string[] = [key];
const dependsOn = (features as ReadonlyArray<Feature>).find((f) => f.flag === key)?.dependsOn;

if (dependsOn) {
flag.push(...dependsOn);
}
return flag;
}),
.filter(([, val]) => typeof val === "boolean" && val)
.map(([key]) => key),
),
];

checkFlagsExist(selectedFlags, knownOptionKeys);

const flags = [
...new Set(
selectedFlags.flatMap((key) => {
if (!isFeatureFlag(key)) {
return [];
}

const flag: string[] = [key];
const dependsOn = (features as ReadonlyArray<Feature>).find((f) => f.flag === key)?.dependsOn;

if (dependsOn) {
flag.push(...dependsOn);
}
return flag;
}),
),
];

checkFlagsExist(flags);
await checkFlagsIncludesUiFramework(flags);
checkRules(flags);

Expand Down Expand Up @@ -583,12 +622,26 @@ async function run() {
gitInit(args.project);
}

const enabledIntegrations = getEnabledIntegrations(args as unknown as Record<string, unknown>);
const appliedIntegrations = await runEnabledIntegrations(enabledIntegrations, {
project: args.project,
flags,
allFeatures: features,
packageManagerExec: pm.exec,
});

const nextSteps = filteredBoilerplates
.flatMap((b) => b.config.nextSteps?.(meta, pm.run, colorette))
.filter(Boolean) as BatiConfigStep[];
nextSteps.sort((s) => s.order ?? 0);

await printOK(args.project, flags, nextSteps);
const integrationNextSteps = appliedIntegrations
.flatMap((integration) => integration.nextSteps?.(pm.run) ?? [])
.filter(Boolean) as BatiConfigStep[];

const extraLabels = appliedIntegrations.map((integration) => integration.label);

await printOK(args.project, flags, nextSteps, extraLabels, integrationNextSteps);
},
});

Expand Down
34 changes: 34 additions & 0 deletions packages/cli/integrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { storybookIntegration } from "./storybook.js";
import type { BatiArgDef, Integration, IntegrationContext } from "./types.js";

export const integrations: ReadonlyArray<Integration> = [storybookIntegration];

function isEnabled(args: Record<string, unknown>, flag: string): boolean {
return args[flag] === true;
}

export function getIntegrationArgDefs(): Record<string, BatiArgDef> {
return Object.fromEntries(integrations.map((integration) => [integration.flag, integration.arg]));
}

export function getEnabledIntegrations(args: Record<string, unknown>): Integration[] {
return integrations.filter((integration) => isEnabled(args, integration.flag));
}

export async function runEnabledIntegrations(
enabledIntegrations: ReadonlyArray<Integration>,
context: IntegrationContext,
): Promise<Integration[]> {
const appliedIntegrations: Integration[] = [];

for (const integration of enabledIntegrations) {
const wasApplied = await integration.run(context);
if (wasApplied === false) {
continue;
}

appliedIntegrations.push(integration);
}

return appliedIntegrations;
}
62 changes: 62 additions & 0 deletions packages/cli/storybook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { execSync } from "node:child_process";
import { confirm } from "@inquirer/prompts";
import type { Feature } from "@batijs/features";
import { red } from "colorette";
import type { Integration } from "./types.js";

const supportedStorybookFrameworks = ["react", "vue", "solid"];

export function getUiFrameworkFlag(flags: string[], allFeatures: ReadonlyArray<Feature>) {
const uiFrameworkFlags: string[] = allFeatures
.filter((feature) => feature.category === "UI Framework")
.map((feature) => feature.flag);
return flags.find((flag) => uiFrameworkFlags.includes(flag));
}

export function isStorybookFrameworkSupported(uiFramework: string | undefined) {
return Boolean(uiFramework && supportedStorybookFrameworks.includes(uiFramework));
}

export async function initStorybook(
cwd: string,
packageManagerExec: string,
interactive: boolean = true,
): Promise<boolean> {
let shouldUseDefaultConfig = !interactive;

// Prompt user if they want to initialize Storybook
if (interactive) {
shouldUseDefaultConfig = await confirm({
message: "Use default Storybook configuration?",
default: true,
});
}

// Run Storybook init with interactive questionnaire
const command = `${packageManagerExec} storybook@latest init --no-dev${shouldUseDefaultConfig ? " --yes" : ""}`;
execSync(command, { cwd, stdio: "inherit" });
Comment on lines +20 to +37
Copy link

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== initStorybook call sites ==\n'
rg -n 'initStorybook\(' packages/cli

printf '\n== prompt / CI / TTY plumbing in packages/cli ==\n'
rg -n 'confirm\(|interactive|BATI_TEST|process\.env\.CI|isTTY' packages/cli

Repository: vikejs/bati

Length of output: 782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== IntegrationContext definition ==\n'
rg -B3 -A10 'interface IntegrationContext|type IntegrationContext' packages/cli

printf '\n== storybookIntegration.run() definition ==\n'
rg -B5 -A15 'storybookIntegration.*=|const storybookIntegration' packages/cli

printf '\n== How integrations are called in index.ts ==\n'
rg -B3 -A3 'storybookIntegration\.run\(|integration\.run\(' packages/cli/index.ts

printf '\n== Other integration implementations for interactive handling ==\n'
fd '\.(ts|tsx)$' packages/cli/integrations --exec rg -l 'interactive|BATI_TEST|confirm' {} \;

Repository: vikejs/bati

Length of output: 2090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Integration.run() call site in index.ts (context around line 577) ==\n'
sed -n '540,620p' packages/cli/index.ts

printf '\n== Check how IntegrationContext is constructed ==\n'
rg -B10 -A5 'as IntegrationContext|IntegrationContext.*=' packages/cli/index.ts | head -60

printf '\n== Another integration implementation (e.g., tailwind) ==\n'
ls -la packages/cli/integrations/ 2>/dev/null || ls -la packages/cli/*.ts | grep -E '(tailwind|prettier|vitest)' || true

Repository: vikejs/bati

Length of output: 2526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Direct search for integration.run invocation ==\n'
rg -n -B5 -A5 'integration\.run\(' packages/cli/index.ts

printf '\n== Search for all Integration usage in index.ts ==\n'
rg -n 'integrations|Integration' packages/cli/index.ts

printf '\n== Full storybook.ts file to see the full picture ==\n'
wc -l packages/cli/storybook.ts

Repository: vikejs/bati

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Full storybook.ts content ==\n'
cat -n packages/cli/storybook.ts

printf '\n== Search for storybookIntegration usage across codebase ==\n'
rg -n 'storybookIntegration' --type ts packages/cli

Repository: vikejs/bati

Length of output: 3008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== integrations.ts full content ==\n'
cat -n packages/cli/integrations.ts

printf '\n== Search for integrations array usage ==\n'
rg -n 'integrations\[|for.*integrations|integrations\..*run\|integrations\.filter' packages/cli

printf '\n== Search for where integration features are added to boilerplates ==\n'
rg -n 'storybookIntegration\|integrations' packages/cli/index.ts | head -20

Repository: vikejs/bati

Length of output: 1488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for runEnabledIntegrations invocation ==\n'
rg -n -B10 -A10 'runEnabledIntegrations' packages/cli/index.ts

printf '\n== Check types.ts Integration interface fully ==\n'
rg -B5 -A15 'export interface Integration' packages/cli/types.ts

Repository: vikejs/bati

Length of output: 2789


Make --storybook explicitly non-interactive when the CLI is unattended.

Line 23 defaults interactive to true, and the only callsite on line 57 relies on that default without passing an explicit value. The IntegrationContext passed to integration.run() at index.ts:626 does not include CI/test mode information, even though BATI_IS_CI and BATI_TEST are already available in meta at that location. This prevents integrations from disabling prompts in CI, test, or scripted runs.

Add an interactive field to IntegrationContext (derived from BATI_IS_CI and BATI_TEST in index.ts) and pass it to initStorybook() at line 57.

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

In `@packages/cli/storybook.ts` around lines 20 - 37, Add an explicit interactive
flag to the integration context and thread it into initStorybook so Storybook is
non-interactive in CI/test runs: extend IntegrationContext with an interactive
boolean derived from BATI_IS_CI || BATI_TEST in index.ts, ensure
integration.run(...) receives that context, and update the callsite that invokes
initStorybook (the call to initStorybook(...) around the integration.run path)
to pass the interactive value instead of relying on the default; this ensures
initStorybook(interactive) will run with interactive=false for unattended
CI/test environments.

Copy link
Member

Choose a reason for hiding this comment

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

We will indeed need a non-interactive mode for the CI, with sensible default values

Copy link

Choose a reason for hiding this comment

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

@magne4000 Agreed! Here are the three changes needed to wire up a proper non-interactive mode:

1. packages/cli/types.ts — extend IntegrationContext

 export interface IntegrationContext {
   project: string;
   flags: string[];
   allFeatures: ReadonlyArray<Feature>;
   packageManagerExec: string;
+  interactive: boolean;
 }

2. packages/cli/index.ts — derive interactive from BATI_IS_CI / BATI_TEST when building the context

-const context: IntegrationContext = {
+const context: IntegrationContext = {
   project,
   flags,
   allFeatures: features,
   packageManagerExec,
+  interactive: !meta.BATI_IS_CI && !meta.BATI_TEST,
 };

3. packages/cli/storybook.ts — pass context.interactive to initStorybook

-  const result = await initStorybook(context.project, context.packageManagerExec);
+  const result = await initStorybook(context.project, context.packageManagerExec, context.interactive);

This way:

  • Interactive terminal (normal developer flow): prompts are shown, defaulting to --yes if the user accepts.
  • CI / test runs (BATI_IS_CI or BATI_TEST): interactive = false, so shouldUseDefaultConfig = true and --yes is always passed, skipping all prompts with sensible defaults.

🧠 Learnings used
Learnt from: CR
Repo: vikejs/bati PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T09:49:22.825Z
Learning: Applies to boilerplates/*/bati.config.ts : Create UI-framework-specific boilerplate variants (e.g., react-sentry, vue-sentry) when features require framework-specific code, using combined conditions in bati.config.ts

return true;
}

export const storybookIntegration: Integration = {
flag: "storybook",
label: "Storybook",
arg: {
type: "boolean",
description: "If true, initializes Storybook in the generated app (React, Vue, Solid only)",
required: false,
},
async run({ project, flags, allFeatures, packageManagerExec }) {
const uiFramework = getUiFrameworkFlag(flags, allFeatures);

if (!isStorybookFrameworkSupported(uiFramework)) {
console.error(`${red("⚠")} The \`--storybook\` flag is currently supported only with React, Vue, or Solid.`);
process.exit(6);
}

return await initStorybook(project, packageManagerExec);
},
nextSteps(packageManagerRun) {
return [{ type: "command", step: `${packageManagerRun} storybook` }];
},
};
21 changes: 20 additions & 1 deletion packages/cli/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { VikeMeta } from "@batijs/core";
import type { BatiConfig } from "@batijs/core/config";
import type { BatiConfig, BatiConfigStep } from "@batijs/core/config";
import type { Feature } from "@batijs/features";
import type { ArgDef } from "citty";

export interface BoilerplateDef {
folder: string;
Expand All @@ -17,3 +19,20 @@ export interface ToBeCopied extends BoilerplateDef {
}

export type Hook = (cwd: string, meta: VikeMeta) => Promise<void> | void;

export type BatiArgDef = ArgDef & { invisible?: boolean };

export interface IntegrationContext {
project: string;
flags: string[];
allFeatures: ReadonlyArray<Feature>;
packageManagerExec: string;
}

export interface Integration {
flag: string;
label: string;
arg: BatiArgDef;
run: (context: IntegrationContext) => Promise<boolean | void> | boolean | void;
nextSteps?: (packageManagerRun: string) => BatiConfigStep[];
}
2 changes: 2 additions & 0 deletions packages/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

All features that should be visible in the WebUI and the CLI are defined in [src/features.ts](src/features.ts).

In the CLI, feature flags also come from [src/features.ts](src/features.ts), but some non-feature CLI options can be provided by integrations (for example `--storybook`).

All rules (conflicts/dependencies between features, features in beta, etc.) are defined in [src/rules](src/rules).
2 changes: 1 addition & 1 deletion packages/tests/src/load-test-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function loadTestFileMatrix(filepath: string) {
assert(Array.isArray(exclude), `\`exclude\` export in "${filepath}" must be of type \`string[][]\``);
}

const validKeys = new Set<unknown>(flags);
const validKeys = new Set<unknown>([...flags, "storybook"]);

for (const m of matrix as unknown[]) {
if (Array.isArray(m)) {
Expand Down
41 changes: 41 additions & 0 deletions packages/tests/tests/FRAMEWORK+storybook.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describeBati } from "@batijs/tests-utils";

export const matrix = [["solid", "react", "vue"], "storybook", "eslint"];

await describeBati(({ test, expect }) => {
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");

const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();

const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;

for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}

expect(configFileExists).toBe(true);
});

test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");

const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();

const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));

expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
Comment on lines +6 to +39
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use process.cwd() instead of shelling out to pwd.

Line 10 and Line 33 invoke pwd, which isn’t portable to Windows shells. That will break this new E2E spec as soon as the matrix runs on Windows; process.cwd() is cheaper and cross-platform.

💡 Suggested change
   test("storybook config files", async () => {
     const fs = await import("fs/promises");
     const path = await import("path");
 
-    const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
+    const storybookDir = process.cwd();
@@
   test("storybook scripts", async () => {
     const fs = await import("fs/promises");
     const path = await import("path");
 
-    const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
+    const cwd = process.cwd();
📝 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
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;
for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}
expect(configFileExists).toBe(true);
});
test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const storybookDir = process.cwd();
const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;
for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}
expect(configFileExists).toBe(true);
});
test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tests/tests/FRAMEWORK`+storybook.spec.ts around lines 6 - 39,
Replace the use of child_process.execSync("pwd", ...) with the cross-platform
Node API process.cwd() in both tests: use process.cwd() for storybookDir in the
"storybook config files" test (where storybookDir is set) and for cwd in the
"storybook scripts" test (where packageJsonPath is built); update references to
storybookDir and cwd accordingly so the tests no longer shell out and work on
Windows.

});
});