diff --git a/.changeset/fix-cli-scaffold-smoke-test.md b/.changeset/fix-cli-scaffold-smoke-test.md new file mode 100644 index 00000000..8fe22df0 --- /dev/null +++ b/.changeset/fix-cli-scaffold-smoke-test.md @@ -0,0 +1,5 @@ +--- +"@stackwright/cli": patch +--- + +Fix scaffold smoke-test TypeError by excluding _font-links.json from static page generation diff --git a/.changeset/fix-dark-mode.md b/.changeset/fix-dark-mode.md new file mode 100644 index 00000000..a6cb2123 --- /dev/null +++ b/.changeset/fix-dark-mode.md @@ -0,0 +1,11 @@ +--- +"@stackwright/core": patch +--- + +Fixed dark mode text colors and background handling for improved demo/hackathon quality: + +- **#252**: Verified ThemeProvider toggle updates correctly (no code changes needed) +- **#251**: Added dark overlay for background images to ensure text contrast +- **Alert component**: Added dark-mode-aware accent colors +- **CodeBlock component**: Added dark mode syntax highlighting palette +- **useSafeTheme hook**: Added `useSafeColorMode` hook for safe color mode access diff --git a/.changeset/fix-perf-benchmark-port-conflict.md b/.changeset/fix-perf-benchmark-port-conflict.md deleted file mode 100644 index a898eedb..00000000 --- a/.changeset/fix-perf-benchmark-port-conflict.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -fix(ci): resolve port conflict in performance benchmark workflow - -The performance workflow was manually starting `next start` on port 3000 -before running Playwright benchmarks. Since the Playwright config also -defines a `webServer` that starts on port 3000 (with `reuseExistingServer: -false` in CI), every benchmark step failed with a port conflict. - -The fix removes the redundant manual build/start steps and uses -`PERF_NO_SERVER` to disable Playwright's webServer for benchmarks that -don't need a running server (build-time, bundle-size). The runtime-vitals -benchmark now uses Playwright's built-in webServer lifecycle as intended. diff --git a/.changeset/pre.json b/.changeset/pre.json index e0c833f4..69f07a11 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -17,7 +17,9 @@ "launch-stackwright": "0.1.0", "@stackwright/maplibre": "0.0.0", "stackwright-docs": "0.1.0", - "@stackwright/otters": "0.1.0" + "@stackwright/otters": "0.1.0", + "@stackwright/sbom-generator": "0.0.0", + "@stackwright/scaffold-core": "0.1.0-alpha.0" }, "changesets": [ "add-code2-layout-icons", @@ -25,16 +27,20 @@ "built-in-search-feature", "compose-site-atomic", "declarative-entry-pages", + "fix-cli-scaffold-smoke-test", "fix-dark-mode-bugs", - "fix-perf-benchmark-port-conflict", + "fix-dark-mode", "integrations-config", "launch-stackwright-package", "map-adapter-phases-1-2", "nav-sidebar-override", "otters-as-package", - "remove-hello-example", + "otters-postinstall", "resolve-background-utility", + "sbom-generator-addition", + "sbom-hooks-addition", "scaffold-bundled-default", + "scaffold-hooks-system", "scaffold-pin-versions", "text-block-feature", "video-media-type", diff --git a/.changeset/remove-hello-example.md b/.changeset/remove-hello-example.md deleted file mode 100644 index b1cc6149..00000000 --- a/.changeset/remove-hello-example.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"stackwright-docs": patch ---- - -Remove hellostackwrightnext example and add legal pages - -- Delete hellostackwrightnext example directory -- Add privacy-policy page to stackwright-docs -- Add terms-of-service page to stackwright-docs -- Update stackwright-docs footer with legal links diff --git a/.changeset/scaffold-hooks-system.md b/.changeset/scaffold-hooks-system.md new file mode 100644 index 00000000..6920c246 --- /dev/null +++ b/.changeset/scaffold-hooks-system.md @@ -0,0 +1,7 @@ +--- +"@stackwright/scaffold-core": minor +"@stackwright/cli": minor +"launch-stackwright": minor +--- + +Add scaffold hooks system for extensible post-scaffold processing. Pro packages can now register hooks at lifecycle points (preScaffold, preInstall, postInstall, postScaffold) to inject dependencies, configure MCP servers, and add custom setup. diff --git a/.github/actions/setup-stackwright/action.yml b/.github/actions/setup-stackwright/action.yml new file mode 100644 index 00000000..6453c425 --- /dev/null +++ b/.github/actions/setup-stackwright/action.yml @@ -0,0 +1,50 @@ +name: "Setup Stackwright" +description: "Standardized pnpm/Node setup for Stackwright monorepo" + +inputs: + build: + description: "Whether to run pnpm build after install" + required: false + default: "false" + relink-bins: + description: "Whether to re-link bins after build (needed for CLI tools)" + required: false + default: "false" + +runs: + using: "composite" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "PR detected: allowing lockfile updates..." + pnpm install + else + echo "Branch push detected: using frozen lockfile..." + pnpm install --frozen-lockfile + fi + shell: bash + + - name: Build packages + if: inputs.build == 'true' + run: pnpm build + shell: bash + + - name: Relink bins + if: inputs.relink-bins == 'true' + run: pnpm install + shell: bash diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 6466f283..9731a4bd 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -16,28 +16,11 @@ jobs: timeout-minutes: 20 steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Build packages - run: pnpm build - - - name: Re-link bins after build - run: pnpm install + build: true + relink-bins: true - name: Install Playwright browsers run: pnpm --filter @stackwright/e2e exec playwright install --with-deps chromium diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index 47a6775d..bfc720fd 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -14,18 +14,9 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - run: pnpm install + - uses: ./.github/actions/setup-stackwright - name: Validate Changeset Presence run: node .github/scripts/check-for-changeset.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d47e64e5..47bacdbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,77 +3,53 @@ name: CI on: push: branches: [main, dev] + paths: + - 'packages/**' + - 'examples/**' + - 'scripts/**' + - '!examples/stackwright-docs/content/**' pull_request: + paths: + - 'packages/**' + - 'examples/**' + - 'scripts/**' + - '!examples/stackwright-docs/content/**' permissions: contents: read jobs: - format: + lint-and-format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install + - uses: ./.github/actions/setup-stackwright - run: pnpm format:check - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - run: pnpm lint test: runs-on: ubuntu-latest + needs: [lint-and-format] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 + - uses: ./.github/actions/setup-stackwright with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - - run: pnpm build + build: true - run: pnpm test scaffold-smoke-test: runs-on: ubuntu-latest + needs: [lint-and-format] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - - run: pnpm build + build: true - name: Pack workspace packages as tarballs run: | mkdir -p /tmp/stackwright-tarballs - for pkg in types themes icons collections core nextjs build-scripts ui-shadcn cli; do + for pkg in types themes icons collections core nextjs build-scripts ui-shadcn cli sbom-generator; do pnpm --filter "@stackwright/$pkg" pack --pack-destination /tmp/stackwright-tarballs done @@ -86,7 +62,7 @@ jobs: const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const tarballs = fs.readdirSync('/tmp/stackwright-tarballs'); - const names = ['types','themes','icons','collections','core','nextjs','build-scripts','ui-shadcn','cli']; + const names = ['types','themes','icons','collections','core','nextjs','build-scripts','ui-shadcn','cli','sbom-generator']; const overrides = {}; for (const name of names) { const tarball = tarballs.find(t => t.startsWith('stackwright-' + name + '-')); @@ -112,18 +88,12 @@ jobs: check-agent-docs: runs-on: ubuntu-latest + needs: [lint-and-format] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - - run: pnpm build + build: true - name: Regenerate AGENTS.md tables run: node packages/cli/dist/cli.js generate-agent-docs @@ -135,20 +105,13 @@ jobs: e2e: runs-on: ubuntu-latest + needs: [lint-and-format] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - - run: pnpm build - - run: pnpm install - name: Re-link bins after build + build: true + relink-bins: true - name: Install Playwright browsers run: pnpm --filter @stackwright/e2e exec playwright install --with-deps chromium @@ -182,18 +145,12 @@ jobs: check-schemas: runs-on: ubuntu-latest + needs: [lint-and-format] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 + - uses: ./.github/actions/setup-stackwright with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - run: pnpm install - - run: pnpm build + build: true - name: Regenerate JSON schemas run: cd packages/types && pnpm generate-schemas diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b1148564..e0ec3002 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,25 +16,10 @@ jobs: timeout-minutes: 15 steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Build packages - run: pnpm build + build: true - name: Run tests with coverage run: pnpm test:coverage diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index d41e7b6b..3590338e 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -21,28 +21,11 @@ jobs: timeout-minutes: 30 steps: - - name: 📥 Checkout code - uses: actions/checkout@v4 - - - name: 📦 Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - - name: 🟢 Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - name: 📚 Install dependencies - run: pnpm install --frozen-lockfile - - - name: 🔨 Build packages - run: pnpm build - - - name: 🔗 Re-link bins after build - run: pnpm install + build: true + relink-bins: true - name: ⚡ Run build time benchmarks id: build-time diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 67d5a3a3..8ce33c6a 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -16,32 +16,15 @@ jobs: timeout-minutes: 30 steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-stackwright with: - node-version: 22 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install + build: true + relink-bins: true - name: Install Playwright browsers run: pnpm --filter @stackwright/e2e exec playwright install chromium --with-deps - - name: Build packages - run: pnpm build - - - name: Re-link bins after build - run: pnpm install - - name: Run visual regression tests # Visual baselines differ across platforms (CI vs local font rendering), # so we use --update-snapshots to treat these as render-smoke-tests. diff --git a/AGENTS.md b/AGENTS.md index c2aeec7b..6c512c83 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,14 @@ Welcome to Stackwright! This is a YAML-driven React application framework that e The fastest way to get started with Stackwright is using **launch-stackwright**: +**Recommended: Full otter raft experience (auto-installs dependencies)** +```bash +npx launch-stackwright my-site --otter-raft +cd my-site +pnpm dev +``` + +**Alternative: Manual setup** ```bash npx launch-stackwright my-site cd my-site @@ -13,7 +21,7 @@ pnpm install pnpm dev ``` -This automatically sets up: +Both set up: - ✅ A fully configured Next.js + Stackwright project - ✅ The otter raft (AI agents) ready to build your site - ✅ MCP server auto-configuration for Code Puppy diff --git a/CLAUDE.md b/CLAUDE.md index 2a18c2cb..7fce1be9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ pnpm build:nextjs pnpm build:cli pnpm build:build-scripts pnpm build:mcp +pnpm build:scaffold-core # Run the MCP server (stdio — for use with Claude Code or Claude for Desktop) pnpm stackwright-mcp @@ -108,6 +109,15 @@ User's Next.js App @stackwright/cli ← Standalone CLI (scaffolding, validation, content generation) ``` +**Scaffold Dependency Chain:** +``` +@stackwright/scaffold-core ← Hook system for extensible scaffold processing + ↓ +@stackwright/cli ← Uses hooks for MCP config, Pro package integration + ↓ +@stackwright/launch-stackwright ← Registers MCP config via hooks +``` + ### Key Architectural Concepts **Content Rendering Pipeline**: YAML files are loaded → parsed with `js-yaml` → validated against the grammar (TypeScript types/JSON schemas) → compiled to React components via `contentRenderer.tsx` → rendered with theme and context. @@ -122,6 +132,34 @@ User's Next.js App **Grammar / JSON Schema Generation**: `@stackwright/types` is the single source of truth for the Stackwright grammar. Zod schemas are the source of truth; TypeScript types are inferred via `z.infer<>`. `zod-to-json-schema` generates `theme-schema.json`, `content-schema.json`, and `siteconfig-schema.json` — the machine-readable grammar specification used for IDE YAML validation. Must be regenerated after type changes (`pnpm generate-schemas`). Zod schemas are introspectable at runtime via `schema.def` (Zod v4) enabling MCP tools and future runtime validation. +### Scaffold Hooks System + +`@stackwright/scaffold-core` provides a hook system for extensible post-scaffold processing. Pro packages register hooks that run at lifecycle points. + +**Hook lifecycle points:** +| Hook | When | Use Case | +|------|------|----------| +| `preScaffold` | Before scaffolding | Validate credentials | +| `preInstall` | After files, before install | Modify package.json, configure MCP | +| `postInstall` | After pnpm install | Verify installation | +| `postScaffold` | After complete | Final configuration | + +**Pro package integration:** +```typescript +import { registerScaffoldHook } from '@stackwright/scaffold-core'; + +registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-license', + critical: true, + handler: async (ctx) => { + ctx.packageJson.dependencies['@stackwright-pro/license'] = '^1.0.0'; + }, +}); +``` + +Hooks run automatically when using `launch-stackwright --otter-raft`. + ### Key Files - `packages/core/src/utils/contentRenderer.tsx` — core YAML-to-React transformation engine diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 506f9d49..d4ede954 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,6 +192,143 @@ The architect sets priority tiers. Contributors and agents should pick work from @stackwright/icons — MUI icon registry @stackwright/build-scripts — Prebuild pipeline (image co-location, path rewriting) @stackwright/cli — CLI for scaffolding, page management, validation +@stackwright/scaffold-core — Hook system for extensible scaffold processing (Pro packages use this) +``` + +## Creating Pro Packages with Scaffold Hooks + +Pro packages can extend the scaffold process using the hooks system in `@stackwright/scaffold-core`. + +### Overview + +The scaffold hooks system allows Pro packages to: +- Inject enterprise dependencies into `package.json` +- Configure custom MCP servers +- Add post-install verification +- Run custom setup scripts + +### Hook Lifecycle Points + +| Hook | When | Use Case | +|------|------|----------| +| `preScaffold` | Before scaffolding begins | Validate environment, check licenses | +| `preInstall` | After files created, before `pnpm install` | Modify `package.json`, set up `.code-puppy.json` | +| `postInstall` | After `pnpm install` completes | Verify installation, run setup scripts | +| `postScaffold` | After scaffolding complete | Final configuration, cleanup | + +### Creating a Pro Launch Hooks Package + +1. **Create package structure:** + ``` + @stackwright-pro/launch-hooks/ + ├── package.json + └── index.js + ``` + +2. **package.json:** + ```json + { + "name": "@stackwright-pro/launch-hooks", + "version": "0.1.0", + "main": "index.js", + "dependencies": { + "@stackwright/scaffold-core": "workspace:*" + } + } + ``` + +3. **index.js** — Register hooks: + ```javascript + const { registerScaffoldHook } = require('@stackwright/scaffold-core'); + + // Add enterprise license + registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-license', + critical: true, + handler: async (ctx) => { + if (!process.env.PRO_API_KEY) { + throw new Error('PRO_API_KEY required'); + } + ctx.packageJson.dependencies['@stackwright-pro/license'] = '^1.0.0'; + }, + }); + + // Configure enterprise MCP server + registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-mcp', + priority: 20, + handler: async (ctx) => { + ctx.codePuppyConfig = ctx.codePuppyConfig || {}; + ctx.codePuppyConfig.mcp_servers = ctx.codePuppyConfig.mcp_servers || {}; + ctx.codePuppyConfig.mcp_servers.enterprise = { + command: 'node', + args: ['node_modules/@stackwright-pro/mcp/dist/server.js'], + env: { API_KEY: process.env.PRO_API_KEY }, + }; + }, + }); + ``` + +4. **Usage:** Users add your package to their project: + ```bash + npx launch-stackwright --otter-raft my-site + # Pro hooks run automatically during scaffold + ``` + +### Hook Context + +The context object passed to hooks: + +```typescript +interface ScaffoldHookContext { + targetDir: string; // Project directory + projectName: string; // Project name + siteTitle: string; // Site title + themeId: string; // Theme ID + packageJson: Record; // Mutable - add dependencies + codePuppyConfig?: Record; // Mutable - add MCP config + dependencyMode: 'workspace' | 'standalone'; + pages?: string[]; // Pages being created + install?: boolean; // Whether install will run + [key: string]: any; // Hooks can add custom properties +} +``` + +### Hook Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `type` | `ScaffoldHookType` | Required | Lifecycle point | +| `name` | `string` | Required | Unique hook name | +| `priority` | `number` | `50` | Lower = runs first | +| `critical` | `boolean` | `false` | If true, failure fails entire scaffold | +| `handler` | `function` | Required | Async function to execute | + +### Testing Pro Hooks + +```bash +# Test hooks by running scaffold with your package installed +npx launch-stackwright --otter-raft test-project + +# Verify package.json includes your dependency +cat test-project/package.json | grep @stackwright-pro + +# Verify .code-puppy.json includes your MCP config +cat test-project/.code-puppy.json +``` + +### Hook Debugging + +Enable debug output: +```bash +DEBUG=scaffold-hooks npx launch-stackwright --otter-raft my-site +``` + +Non-critical hook failures are logged but don't stop scaffold: +``` +[Scaffold Hook] Non-critical hook "my-hook" failed: some error ``` ## Build System Notes diff --git a/OTTER_ARCHITECTURE.md b/OTTER_ARCHITECTURE.md index 2650a044..6c312cfe 100644 --- a/OTTER_ARCHITECTURE.md +++ b/OTTER_ARCHITECTURE.md @@ -1,9 +1,69 @@ # Stackwright Otter Raft Architecture 🦦 -AI agent orchestration system for end-to-end Stackwright site generation. - > **A raft of otters** is the collective noun for a group of otters floating together. Our raft of specialized AI agents coordinates seamlessly to build complete Stackwright sites! 🦦🦦🦦🦦 +## The Biology Analogy: How Real Otters Coordinate + +Real otters form rafts not because a central controller tells them to, but because they discover each other and find complementary behaviors. A mother otter knows her pup can't swim far yet—so she finds other mothers and they form a "creche," taking turns watching the young while others forage. An adult male finds other males and they patrol boundaries together. The raft emerges from **discovery**, not **command**. + +**Our AI otters work exactly the same way.** Each otter starts by looking around to see who else is available, then adapts its coordination strategy based on what it finds: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DYNAMIC DISCOVERY │ +└─────────────────────────────────────────────────────────────┘ + + Brand Otter: "Let me check who's in the water..." + → list_agents() finds: [Theme Otter, Page Otter] + → "I'll create a brand brief that sets up the theme" + + Theme Otter: "Dashboard Otter available! Want live data integration?" + → discovers Dashboard Otter at runtime + → adapts theme to support dashboard components + + Page Otter: "I see Brand Otter and Theme Otter completed their work" + → reads BRAND_BRIEF.md and stackwright.yml + → builds pages using established brand identity +``` + +This is fundamentally different from traditional agent systems where a central coordinator hard-codes knowledge of all agents. Here, **the raft self-organizes at runtime**. + +--- + +## Dynamic Discovery (The Biological Model) + +Unlike traditional agent systems where a central coordinator hard-codes knowledge of all agents, our otters discover each other at runtime: + +```typescript +// Every otter starts by discovering who else is in the water +const agents = await list_agents(); +const otters = agents.filter(a => a.name.endsWith('-otter')); + +// Each otter adapts its behavior based on who it finds +// Brand Otter: "I'm alone - I'll guide brand discovery myself" +if (otters.length === 0) { + await run_brand_discovery(); +} + +// Page Otter: "Dashboard Otter available! Want live data?" +if (otters.includes('dashboard-otter')) { + suggest_live_data_integration(); +} + +// Theme Otter: "Pro Theme Otter found! Shall I suggest enterprise variants?" +if (otters.includes('pro-theme-otter')) { + suggest_enterprise_themes(); +} +``` + +This means: +- **No central planner needs to know everything** — the Foreman discovers agents dynamically +- **Otters self-organize based on availability** — new otters are picked up automatically +- **New otters are discovered automatically** — drop in a new agent, it's immediately available +- **The raft adapts to what's installed** — partial deployments work gracefully + +--- + ## Architecture Overview ``` @@ -14,13 +74,12 @@ AI agent orchestration system for end-to-end Stackwright site generation. │ ▼ ┌─────────────────────────────────────────────────────────────┐ -│ 🦦🏗️ FOREMAN OTTER │ -│ (Coordinator) │ +│ 🦦🏗️ FOREMAN OTTER (Dynamic Coordinator) │ │ │ -│ • Project scaffolding │ -│ • Sequential otter coordination │ -│ • Validation & error handling │ -│ • Visual verification │ +│ • Discovers available otters via list_agents() │ +│ • Builds coordination graph at runtime │ +│ • Invokes discovered otters dynamically │ +│ • Handles missing otters gracefully │ └────────┬──────────────────┬───────────────────┬─────────────┘ │ │ │ ▼ ▼ ▼ @@ -56,10 +115,71 @@ AI agent orchestration system for end-to-end Stackwright site generation. │ │ └── services/ (services page) │ │ │ └── content.yml │ │ └── public/ │ -│ └── images/ (co-located images) │ +│ └── images/ (co-located images) │ └─────────────────────────────────────────────────────────────┘ ``` +--- + +## Emergent Coordination Examples + +The beauty of dynamic discovery is that coordination **emerges** from what otters find, rather than being hard-coded: + +### Example 1: Page Otter Discovers Dashboard Otter +``` +Page Otter: "Running list_agents()..." + → Found: brand-otter, theme-otter, dashboard-otter + +Page Otter: "Dashboard Otter is available! Shall I create + a live metrics page with real-time data?" +``` + +### Example 2: Theme Otter Discovers Pro Theme Otter +``` +Theme Otter: "Checking for specialized theme agents..." + → Found: pro-theme-otter + +Theme Otter: "Pro Theme Otter detected! I can suggest + enterprise-grade theme variants with + advanced components." +``` + +### Example 3: Brand Otter Works Alone +``` +Brand Otter: "Checking for collaboration..." + → Found: [] + +Brand Otter: "No other otters available. I'll guide brand + discovery myself and create a complete + BRAND_BRIEF.md." +``` + +### Example 4: Foreman Handles Missing Otter Gracefully +``` +Foreman: "Attempting to invoke dashboard-otter..." + → Error: Agent not found + +Foreman: "Dashboard Otter not installed. Falling back to + static page content. User can add Dashboard Otter + later for live data." +``` + +--- + +## Comparison: Traditional vs. Stackwright Otters + +| Aspect | Traditional Agent Systems | Stackwright Otters | +|--------|---------------------------|---------------------| +| **Coordination** | Central planner hard-codes all agents | Otters discover each other at runtime | +| **Extensibility** | New agent = update all coordinators | Drop in new otter = automatically discovered | +| **Coupling** | Tight (by name/interface) | Loose (by capability) | +| **Failure mode** | Single point of failure | Graceful degradation | +| **Self-organization** | Top-down command | Bottom-up discovery | +| **Partial deployment** | Requires all agents | Works with available otters | +| **Knowledge required** | Central planner must know all | Each otter knows only its domain | + +--- + ## Data Flow ``` @@ -92,35 +212,79 @@ AI agent orchestration system for end-to-end Stackwright site generation. └─> User review & iteration ``` +--- + ## MCP Tool Usage by Otter **Important**: Foreman Otter uses MCP tools (`stackwright_scaffold_project`) instead of shell commands for project scaffolding. This ensures it works without requiring a globally installed CLI. +### Tool Categories + +All MCP tools are organized into these categories: + +**PROJECT TOOLS** +- `stackwright_get_project_info` — Get project info (versions, theme, pages) +- `stackwright_scaffold_project` — Scaffold new Stackwright project + +**SITE TOOLS** +- `stackwright_get_site_config` — Read stackwright.yml content +- `stackwright_write_site_config` — Write/update stackwright.yml +- `stackwright_validate_site` — Validate stackwright.yml schema +- `stackwright_list_themes` — List available built-in themes + +**PAGE TOOLS** +- `stackwright_list_pages` — List all pages in project +- `stackwright_get_page` — Read page YAML content +- `stackwright_write_page` — Write/update page YAML +- `stackwright_add_page` — Create new page with boilerplate +- `stackwright_validate_pages` — Validate page YAML against schema + +**CONTENT TOOLS** +- `stackwright_get_content_types` — List all content types with fields +- `stackwright_preview_component` — Show screenshot preview of component + +**RENDER TOOLS** +- `stackwright_check_dev_server` — Verify dev server is running +- `stackwright_render_page` — Screenshot a page +- `stackwright_render_yaml` — Preview YAML without saving (temporary) +- `stackwright_render_diff` — Before/after comparison + +### MCP Tools by Otter + ``` -┌─────────────────┬────────────────────────────────────────────┐ -│ Otter │ MCP Tools │ -├─────────────────┼────────────────────────────────────────────┤ -│ Brand Otter │ • None (pure conversation) │ -│ │ • Browser tools (research) │ -│ │ • File creation (BRAND_BRIEF.md) │ -├─────────────────┼────────────────────────────────────────────┤ -│ Theme Otter │ • stackwright_write_site_config │ -│ │ • stackwright_validate_site │ -│ │ • stackwright_render_yaml (preview) │ -│ │ • stackwright_list_themes │ -├─────────────────┼────────────────────────────────────────────┤ -│ Page Otter │ • stackwright_write_page │ -│ │ • stackwright_validate_pages │ -│ │ • stackwright_render_page │ -│ │ • stackwright_get_content_types │ -├─────────────────┼────────────────────────────────────────────┤ -│ Foreman Otter │ • stackwright_scaffold_project │ -│ │ • stackwright_validate_site │ -│ │ • stackwright_validate_pages │ -│ │ • invoke_agent (coordination) │ -└─────────────────┴────────────────────────────────────────────┘ +┌─────────────────┬────────────────────────────────────────────────────────────┐ +│ Otter │ MCP Tools │ +├─────────────────┼────────────────────────────────────────────────────────────┤ +│ Brand Otter │ • None (pure conversation) │ +│ │ • Browser tools (research) │ +│ │ • File creation (BRAND_BRIEF.md) │ +├─────────────────┼────────────────────────────────────────────────────────────┤ +│ Theme Otter │ • stackwright_get_site_config │ +│ │ • stackwright_write_site_config │ +│ │ • stackwright_validate_site │ +│ │ • stackwright_list_themes │ +│ │ • stackwright_render_yaml (preview before commit) │ +├─────────────────┼────────────────────────────────────────────────────────────┤ +│ Page Otter │ • stackwright_get_content_types │ +│ │ • stackwright_list_pages │ +│ │ • stackwright_write_page │ +│ │ • stackwright_validate_pages │ +│ │ • stackwright_preview_component │ +│ │ • stackwright_render_page │ +│ │ • stackwright_render_yaml (preview before commit) │ +├─────────────────┼────────────────────────────────────────────────────────────┤ +│ Foreman Otter │ • stackwright_get_project_info │ +│ │ • stackwright_scaffold_project │ +│ │ • stackwright_validate_site │ +│ │ • stackwright_validate_pages │ +│ │ • stackwright_check_dev_server │ +│ │ • list_agents (dynamic discovery) │ +│ │ • invoke_agent (coordination) │ +└─────────────────┴────────────────────────────────────────────────────────────┘ ``` +--- + ## Dependency Graph ``` @@ -133,6 +297,7 @@ Level 0: User Input Level 1: Foreman Otter └─> Scaffolds project (if needed) + └─> Discovers available otters dynamically Level 2: Brand Otter └─> Creates BRAND_BRIEF.md @@ -153,16 +318,19 @@ Level 5: Verification **CRITICAL**: Otters must be invoked SEQUENTIALLY, never in parallel. +--- + ## Handoff Protocol ``` User Request │ ▼ -┌────────────────────────┐ -│ Foreman Otter │ ◄── Entry point -│ "Starting full build" │ -└───────┬────────────────┘ +┌────────────────────────────────────────────────────────────┐ +│ Foreman Otter (Dynamic Coordinator) │ +│ "Starting full build" │ +│ → list_agents() to discover available otters │ +└───────┬────────────────────────────────────────────────────┘ │ invoke ▼ ┌────────────────────────┐ @@ -195,23 +363,25 @@ User Request ▼ ┌────────────────────────┐ │ Page Otter │ ◄── Phase 3: Content -│ "Building pages..." │ +│ "Building pages..." │ └───────┬────────────────┘ │ creates pages/*.yml ▼ ┌────────────────────────┐ │ Foreman Otter │ ◄── Verification -│ "Rendering pages..." │ +│ "Rendering pages..." │ └───────┬────────────────┘ │ visual verification ▼ ┌────────────────────────┐ │ User │ ◄── Handoff -│ "Site is ready! │ -│ Run pnpm dev" │ +│ "Site is ready! │ +│ Run pnpm dev" │ └────────────────────────┘ ``` +--- + ## State Machine ``` @@ -263,6 +433,8 @@ User Request └──────► (back to VERIFYING) ``` +--- + ## Error Handling Flow ``` @@ -303,6 +475,8 @@ Otter Invocation └────────────────┘ ``` +--- + ## Comparison: Legacy vs. New Architecture ``` @@ -325,7 +499,7 @@ Otter Invocation └────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────┐ -│ NEW: Specialized Otters (Modular) │ +│ NEW: Specialized Otters (Modular, Self-Discovering) │ ├────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ @@ -339,16 +513,21 @@ Otter Invocation │ │ │ │ ┌────────────────┐ │ │ │ Foreman Otter │ │ -│ │ (Coordinator) │ │ +│ │ (Dynamic │ │ +│ │ Discovery) │ │ │ └────────────────┘ │ │ │ │ ✅ Single Responsibility Principle │ │ ✅ Reusable phases (skip brand if brief exists) │ │ ✅ Pausable workflows │ │ ✅ Easy to test each phase independently │ +│ ✅ Self-organizing via runtime discovery │ +│ ✅ Graceful degradation when otters are missing │ └────────────────────────────────────────────────────────────┘ ``` +--- + ## Future Extensions ### Phase 2 (Planned) @@ -395,10 +574,10 @@ All Otters ──┬──► RAG Server ◄── Example Corpus │ │ │ ▼ │ ┌────────────────────────────┐ - │ │ • Semantic search │ - │ │ • Pattern matching │ - │ │ • Example retrieval │ - │ │ • Best practice lookup │ + │ │ • Semantic search │ + │ │ • Pattern matching │ + │ │ • Example retrieval │ + │ │ • Best practice lookup │ │ └────────────────────────────┘ │ └──► Grounded suggestions @@ -406,15 +585,36 @@ All Otters ──┬──► RAG Server ◄── Example Corpus --- +## Installation + +Otters are distributed as the [`@stackwright/otters`](https://www.npmjs.com/package/@stackwright/otters) npm package: + +```bash +npm install @stackwright/otters +# or +pnpm add @stackwright/otters +``` + +**Auto-install**: The package's `postinstall` script automatically installs otter agent files to `~/.code_puppy/agents/` for code-puppy discovery. No manual copying required! + +To re-run installation manually: +```bash +node node_modules/@stackwright/otters/scripts/install-agents.js +``` + +--- + ## Key Architectural Principles -1. **Separation of Concerns** — Each otter owns one domain -2. **Sequential Execution** — Dependencies enforced by Foreman -3. **File-Based Handoffs** — BRAND_BRIEF.md, stackwright.yml, pages/*.yml -4. **Validation at Every Step** — No invalid YAML proceeds to next phase -5. **Visual Verification** — Screenshots close the feedback loop -6. **Pausable Workflows** — Can stop after brand, resume later for theme -7. **Reusability** — Skip phases if artifacts already exist +1. **Dynamic Discovery** — Otters find each other at runtime, not by hard-coded coordination +2. **Separation of Concerns** — Each otter owns one domain +3. **Sequential Execution** — Dependencies enforced by Foreman +4. **File-Based Handoffs** — BRAND_BRIEF.md, stackwright.yml, pages/*.yml +5. **Validation at Every Step** — No invalid YAML proceeds to next phase +6. **Visual Verification** — Screenshots close the feedback loop +7. **Pausable Workflows** — Can stop after brand, resume later for theme +8. **Reusability** — Skip phases if artifacts already exist +9. **Emergent Coordination** — The raft organizes itself based on available agents --- diff --git a/ROADMAP.md b/ROADMAP.md index 725d615c..9f94dc69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,15 +33,11 @@ The type system that defines what YAML can express — the Stackwright grammar - MCP tools: `stackwright_render_page`, `stackwright_render_diff`, `stackwright_render_yaml`, `stackwright_check_dev_server` - CLI: `stackwright preview` command - E2E tests: full render pipeline verified against the example app +- **Brand Otter** — part of the Otter Raft, discovers brand through conversation and produces BRAND_BRIEF.md (see `./otters/README.md`) -**Next step: branding expert agent.** The visual rendering tools are foundational infrastructure for an AI agent that can: -1. Chat with a user about their company, values, and aesthetic preferences -2. Generate theme and content variations -3. Render each variation and evaluate it visually -4. Iterate toward a design that captures the right brand "feel" -5. Ship the result via `stackwright_open_pr` +**Next step: branding expert iteration loop.** With Brand Otter shipped, the next evolution is enabling AI agents to visually iterate on themes — generate variations, render each, evaluate against brand criteria, and converge on the right feel. This builds on the visual rendering infrastructure now in place. -This agent is the proof point for the platform's thesis: that non-technical people can build professional, brand-appropriate applications through conversation — with the constrained DSL guaranteeing safety and the visual feedback loop guaranteeing quality. +This iteration loop is the proof point for the platform's thesis: that non-technical people can build professional, brand-appropriate applications through conversation — with the constrained DSL guaranteeing safety and the visual feedback loop guaranteeing quality. --- diff --git a/examples/stackwright-docs/CHANGELOG.md b/examples/stackwright-docs/CHANGELOG.md index 2629d4a9..3ea96fbe 100644 --- a/examples/stackwright-docs/CHANGELOG.md +++ b/examples/stackwright-docs/CHANGELOG.md @@ -1,5 +1,13 @@ # stackwright-docs +## 0.1.1-alpha.1 + +### Patch Changes + +- Updated dependencies [167b5bb] + - @stackwright/core@0.7.0-alpha.7 + - @stackwright/nextjs@0.3.1-alpha.7 + ## 0.1.1-alpha.0 ### Patch Changes diff --git a/examples/stackwright-docs/build-manifest.json b/examples/stackwright-docs/build-manifest.json new file mode 100644 index 00000000..75ab7f15 --- /dev/null +++ b/examples/stackwright-docs/build-manifest.json @@ -0,0 +1,157 @@ +{ + "format": "stackwright-build-manifest", + "version": "1.0.0", + "generated": "2026-04-03T19:28:43.494Z", + "project": { + "name": "stackwright-docs", + "version": "0.1.1-alpha.1", + "root": "/home/charles/git/peraspera/stackwright/examples/stackwright-docs", + "isMonorepo": false + }, + "dependencies": [ + { + "name": "@radix-ui/react-accordion", + "version": "1.2.2", + "type": "direct", + "category": "external", + "purl": "pkg:npm/radix-ui/react-accordion@1.2.2", + "integrity": "6385b547dec3958e12e36af31946a77045ae9b94c89e53bfacbedb71c0f3a10c", + "depth": 0 + }, + { + "name": "@radix-ui/react-slot", + "version": "1.1.1", + "type": "direct", + "category": "external", + "purl": "pkg:npm/radix-ui/react-slot@1.1.1", + "integrity": "6c80259979afa356ff77686649e864a9be6fd187b480c8d3d49fecea2c977b75", + "depth": 0 + }, + { + "name": "@radix-ui/react-tabs", + "version": "1.1.2", + "type": "direct", + "category": "external", + "purl": "pkg:npm/radix-ui/react-tabs@1.1.2", + "integrity": "f3ad88f5751707a5c5f92a1603247d36820a4c7e2a748308c3c8c8acd4d07cd2", + "depth": 0 + }, + { + "name": "@stackwright/core", + "version": "workspace:*", + "type": "direct", + "category": "core", + "purl": "pkg:npm/stackwright/core@workspace:*", + "integrity": "9af5f734e5ee6bfa45ca6ed244194337e4b257d3be8a0927412f1076bf569c7b", + "depth": 0 + }, + { + "name": "@stackwright/icons", + "version": "workspace:*", + "type": "direct", + "category": "tool", + "purl": "pkg:npm/stackwright/icons@workspace:*", + "integrity": "c255a8ba61a12722b066604f2487920c50bb20b28f6ba518b9525f191f0a7e52", + "depth": 0 + }, + { + "name": "@stackwright/nextjs", + "version": "workspace:*", + "type": "direct", + "category": "tool", + "purl": "pkg:npm/stackwright/nextjs@workspace:*", + "integrity": "b19c754719b46bf6371f09086bd8b754e289bf617322626044dbb47620163cf3", + "depth": 0 + }, + { + "name": "@stackwright/ui-shadcn", + "version": "workspace:*", + "type": "direct", + "category": "plugin", + "purl": "pkg:npm/stackwright/ui-shadcn@workspace:*", + "integrity": "ac159bdf9d62105719ef6d7d5be5b490eadd991e002ddaae9bab73c93f5c7667", + "depth": 0 + }, + { + "name": "clsx", + "version": "2.1.1", + "type": "direct", + "category": "external", + "purl": "pkg:npm/clsx@2.1.1", + "integrity": "d00f5fa05915e8916f882f17ae64d1bd58f60296bdf8bb490772cebdafa01b60", + "depth": 0 + }, + { + "name": "fuse.js", + "version": "7.1.0", + "type": "direct", + "category": "external", + "purl": "pkg:npm/fuse.js@7.1.0", + "integrity": "3f3d82b5a1943d6113a0f6476ef58cb75640ba889262edd8fed42a3254d8c119", + "depth": 0 + }, + { + "name": "js-yaml", + "version": "4.1.1", + "type": "direct", + "category": "external", + "purl": "pkg:npm/js-yaml@4.1.1", + "integrity": "af705ed1eb31f1af61a3698569d8e0370c173e67024075b4f306d8c9010f1298", + "depth": 0 + }, + { + "name": "lucide-react", + "version": "0.471.1", + "type": "direct", + "category": "external", + "purl": "pkg:npm/lucide-react@0.471.1", + "integrity": "34516f0b69a564672a07087b4a907c82ef437a69c4ba2895f1dc37bb8ae402ac", + "depth": 0 + }, + { + "name": "next", + "version": "16.1.6", + "type": "direct", + "category": "external", + "purl": "pkg:npm/next@16.1.6", + "integrity": "dab6b931b02c25bd4f20dd3f93ceb0b57f8971844a25f14feb62c8a20608db04", + "depth": 0 + }, + { + "name": "react", + "version": "19.2.4", + "type": "direct", + "category": "external", + "purl": "pkg:npm/react@19.2.4", + "integrity": "48fb44e34a2bbbc744bfc0d48b37e11ddb82d4121283c711adcf61883d28f525", + "depth": 0 + }, + { + "name": "react-dom", + "version": "19.2.4", + "type": "direct", + "category": "external", + "purl": "pkg:npm/react-dom@19.2.4", + "integrity": "d321eb3d0478e2a58565081343e4d1da25caf8103b3dd72b3544a715f2e80418", + "depth": 0 + }, + { + "name": "tailwind-merge", + "version": "2.6.0", + "type": "direct", + "category": "external", + "purl": "pkg:npm/tailwind-merge@2.6.0", + "integrity": "f55becd055a25a9ecb32ee3cf37faf157aa62b955195bb7f695909e582e8ae33", + "depth": 0 + } + ], + "metadata": { + "totalDependencies": 15, + "directDependencies": 15, + "devDependencies": 0, + "peerDependencies": 0, + "transitiveDependencies": 0, + "stackwrightInternal": 4, + "external": 11 + } +} \ No newline at end of file diff --git a/examples/stackwright-docs/cyclonedx.json b/examples/stackwright-docs/cyclonedx.json new file mode 100644 index 00000000..cd0cb859 --- /dev/null +++ b/examples/stackwright-docs/cyclonedx.json @@ -0,0 +1,331 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "metadata": { + "timestamp": "2026-04-03T19:28:43.494Z", + "tools": [ + { + "vendor": "Stackwright", + "name": "@stackwright/sbom-generator", + "version": "0.0.0" + } + ], + "component": { + "type": "application", + "name": "stackwright-docs", + "version": "0.1.1-alpha.1", + "purl": "pkg:npm/stackwright-docs@0.1.1-alpha.1" + } + }, + "components": [ + { + "type": "library", + "name": "@radix-ui/react-accordion", + "version": "1.2.2", + "purl": "pkg:npm/radix-ui/react-accordion@1.2.2", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6385b547dec3958e12e36af31946a77045ae9b94c89e53bfacbedb71c0f3a10c" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@radix-ui/react-accordion" + } + ] + }, + { + "type": "library", + "name": "@radix-ui/react-slot", + "version": "1.1.1", + "purl": "pkg:npm/radix-ui/react-slot@1.1.1", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6c80259979afa356ff77686649e864a9be6fd187b480c8d3d49fecea2c977b75" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@radix-ui/react-slot" + } + ] + }, + { + "type": "library", + "name": "@radix-ui/react-tabs", + "version": "1.1.2", + "purl": "pkg:npm/radix-ui/react-tabs@1.1.2", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f3ad88f5751707a5c5f92a1603247d36820a4c7e2a748308c3c8c8acd4d07cd2" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@radix-ui/react-tabs" + } + ] + }, + { + "type": "library", + "name": "@stackwright/core", + "version": "workspace:*", + "purl": "pkg:npm/stackwright/core@workspace:*", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9af5f734e5ee6bfa45ca6ed244194337e4b257d3be8a0927412f1076bf569c7b" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@stackwright/core" + } + ] + }, + { + "type": "library", + "name": "@stackwright/icons", + "version": "workspace:*", + "purl": "pkg:npm/stackwright/icons@workspace:*", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c255a8ba61a12722b066604f2487920c50bb20b28f6ba518b9525f191f0a7e52" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@stackwright/icons" + } + ] + }, + { + "type": "library", + "name": "@stackwright/nextjs", + "version": "workspace:*", + "purl": "pkg:npm/stackwright/nextjs@workspace:*", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b19c754719b46bf6371f09086bd8b754e289bf617322626044dbb47620163cf3" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@stackwright/nextjs" + } + ] + }, + { + "type": "library", + "name": "@stackwright/ui-shadcn", + "version": "workspace:*", + "purl": "pkg:npm/stackwright/ui-shadcn@workspace:*", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ac159bdf9d62105719ef6d7d5be5b490eadd991e002ddaae9bab73c93f5c7667" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/@stackwright/ui-shadcn" + } + ] + }, + { + "type": "library", + "name": "clsx", + "version": "2.1.1", + "purl": "pkg:npm/clsx@2.1.1", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d00f5fa05915e8916f882f17ae64d1bd58f60296bdf8bb490772cebdafa01b60" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/clsx" + } + ] + }, + { + "type": "library", + "name": "fuse.js", + "version": "7.1.0", + "purl": "pkg:npm/fuse.js@7.1.0", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3f3d82b5a1943d6113a0f6476ef58cb75640ba889262edd8fed42a3254d8c119" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/fuse.js" + } + ] + }, + { + "type": "library", + "name": "js-yaml", + "version": "4.1.1", + "purl": "pkg:npm/js-yaml@4.1.1", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "af705ed1eb31f1af61a3698569d8e0370c173e67024075b4f306d8c9010f1298" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/js-yaml" + } + ] + }, + { + "type": "library", + "name": "lucide-react", + "version": "0.471.1", + "purl": "pkg:npm/lucide-react@0.471.1", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "34516f0b69a564672a07087b4a907c82ef437a69c4ba2895f1dc37bb8ae402ac" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/lucide-react" + } + ] + }, + { + "type": "library", + "name": "next", + "version": "16.1.6", + "purl": "pkg:npm/next@16.1.6", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "dab6b931b02c25bd4f20dd3f93ceb0b57f8971844a25f14feb62c8a20608db04" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/next" + } + ] + }, + { + "type": "library", + "name": "react", + "version": "19.2.4", + "purl": "pkg:npm/react@19.2.4", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "48fb44e34a2bbbc744bfc0d48b37e11ddb82d4121283c711adcf61883d28f525" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/react" + } + ] + }, + { + "type": "library", + "name": "react-dom", + "version": "19.2.4", + "purl": "pkg:npm/react-dom@19.2.4", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d321eb3d0478e2a58565081343e4d1da25caf8103b3dd72b3544a715f2e80418" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/react-dom" + } + ] + }, + { + "type": "library", + "name": "tailwind-merge", + "version": "2.6.0", + "purl": "pkg:npm/tailwind-merge@2.6.0", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f55becd055a25a9ecb32ee3cf37faf157aa62b955195bb7f695909e582e8ae33" + } + ], + "externalReferences": [ + { + "type": "package-manager", + "url": "https://www.npmjs.com/package/tailwind-merge" + } + ] + } + ], + "dependencies": [ + { + "ref": "pkg:npm/stackwright-docs@0.1.1-alpha.1", + "dependsOn": [ + "@radix-ui/react-accordion", + "@radix-ui/react-slot", + "@radix-ui/react-tabs", + "@stackwright/core", + "@stackwright/icons", + "@stackwright/nextjs", + "@stackwright/ui-shadcn", + "clsx", + "fuse.js", + "js-yaml", + "lucide-react", + "next", + "react", + "react-dom", + "tailwind-merge" + ], + "dependencyType": "direct" + } + ] +} \ No newline at end of file diff --git a/examples/stackwright-docs/package.json b/examples/stackwright-docs/package.json index 19e2de70..c8be6d43 100644 --- a/examples/stackwright-docs/package.json +++ b/examples/stackwright-docs/package.json @@ -1,6 +1,6 @@ { "name": "stackwright-docs", - "version": "0.1.1-alpha.0", + "version": "0.1.1-alpha.1", "private": true, "scripts": { "prebuild": "stackwright-prebuild", @@ -12,14 +12,21 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.2", "@stackwright/core": "workspace:*", "@stackwright/icons": "workspace:*", "@stackwright/nextjs": "workspace:*", "@stackwright/ui-shadcn": "workspace:*", + "clsx": "^2.1.1", + "fuse.js": "^7.1.0", "js-yaml": "^4.1.1", + "lucide-react": "^0.471.1", "next": "^16.1.6", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "tailwind-merge": "^2.6.0" }, "devDependencies": { "@playwright/browser-chromium": "^1.58.2", diff --git a/examples/stackwright-docs/pages/_document.tsx b/examples/stackwright-docs/pages/_document.tsx index 9f2c3211..9759baad 100644 --- a/examples/stackwright-docs/pages/_document.tsx +++ b/examples/stackwright-docs/pages/_document.tsx @@ -1,25 +1,3 @@ -import React from 'react'; -import { Html, Head, Main, NextScript } from 'next/document'; +import { StackwrightDocument } from '@stackwright/nextjs'; -//@TODO code-puppy, can we add fonts through the prebuild step instead of every app having to do this? - -export default function Document() { - return ( - - - - - - - - - -
- - - - ); -} +export default StackwrightDocument; diff --git a/examples/stackwright-docs/spdx.json b/examples/stackwright-docs/spdx.json new file mode 100644 index 00000000..8cf3a0da --- /dev/null +++ b/examples/stackwright-docs/spdx.json @@ -0,0 +1,438 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT-9bbc3e93", + "name": "stackwright-docs@0.1.1-alpha.1", + "documentNamespace": "https://stackwright.dev/spdx/stackwright-docs/2026-04-03T19:28:43.491Z", + "creationInfo": { + "created": "2026-04-03T19:28:43.491Z", + "creators": [ + "Tool: @stackwright/sbom-generator", + "Tool-Version: 0.0.0" + ] + }, + "packages": [ + { + "SPDXID": "SPDXRef-Package--radix-ui-react-accordion", + "name": "@radix-ui/react-accordion", + "versionInfo": "1.2.2", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "6385b547dec3958e12e36af31946a77045ae9b94c89e53bfacbedb71c0f3a10c" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/radix-ui/react-accordion@1.2.2" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--radix-ui-react-slot", + "name": "@radix-ui/react-slot", + "versionInfo": "1.1.1", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "6c80259979afa356ff77686649e864a9be6fd187b480c8d3d49fecea2c977b75" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/radix-ui/react-slot@1.1.1" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--radix-ui-react-tabs", + "name": "@radix-ui/react-tabs", + "versionInfo": "1.1.2", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "f3ad88f5751707a5c5f92a1603247d36820a4c7e2a748308c3c8c8acd4d07cd2" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/radix-ui/react-tabs@1.1.2" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--stackwright-core", + "name": "@stackwright/core", + "versionInfo": "workspace:*", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "9af5f734e5ee6bfa45ca6ed244194337e4b257d3be8a0927412f1076bf569c7b" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/stackwright/core@workspace:*" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--stackwright-icons", + "name": "@stackwright/icons", + "versionInfo": "workspace:*", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "c255a8ba61a12722b066604f2487920c50bb20b28f6ba518b9525f191f0a7e52" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/stackwright/icons@workspace:*" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--stackwright-nextjs", + "name": "@stackwright/nextjs", + "versionInfo": "workspace:*", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "b19c754719b46bf6371f09086bd8b754e289bf617322626044dbb47620163cf3" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/stackwright/nextjs@workspace:*" + } + ] + }, + { + "SPDXID": "SPDXRef-Package--stackwright-ui-shadcn", + "name": "@stackwright/ui-shadcn", + "versionInfo": "workspace:*", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "ac159bdf9d62105719ef6d7d5be5b490eadd991e002ddaae9bab73c93f5c7667" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/stackwright/ui-shadcn@workspace:*" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-clsx", + "name": "clsx", + "versionInfo": "2.1.1", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "d00f5fa05915e8916f882f17ae64d1bd58f60296bdf8bb490772cebdafa01b60" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/clsx@2.1.1" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-fuse-js", + "name": "fuse.js", + "versionInfo": "7.1.0", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "3f3d82b5a1943d6113a0f6476ef58cb75640ba889262edd8fed42a3254d8c119" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/fuse.js@7.1.0" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-js-yaml", + "name": "js-yaml", + "versionInfo": "4.1.1", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "af705ed1eb31f1af61a3698569d8e0370c173e67024075b4f306d8c9010f1298" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/js-yaml@4.1.1" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-lucide-react", + "name": "lucide-react", + "versionInfo": "0.471.1", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "34516f0b69a564672a07087b4a907c82ef437a69c4ba2895f1dc37bb8ae402ac" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/lucide-react@0.471.1" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-next", + "name": "next", + "versionInfo": "16.1.6", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "dab6b931b02c25bd4f20dd3f93ceb0b57f8971844a25f14feb62c8a20608db04" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/next@16.1.6" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-react", + "name": "react", + "versionInfo": "19.2.4", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "48fb44e34a2bbbc744bfc0d48b37e11ddb82d4121283c711adcf61883d28f525" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/react@19.2.4" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-react-dom", + "name": "react-dom", + "versionInfo": "19.2.4", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "d321eb3d0478e2a58565081343e4d1da25caf8103b3dd72b3544a715f2e80418" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/react-dom@19.2.4" + } + ] + }, + { + "SPDXID": "SPDXRef-Package-tailwind-merge", + "name": "tailwind-merge", + "versionInfo": "2.6.0", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "value": "f55becd055a25a9ecb32ee3cf37faf157aa62b955195bb7f695909e582e8ae33" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/tailwind-merge@2.6.0" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT-9bbc3e93", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-Package-clsx" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--radix-ui-react-accordion" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--radix-ui-react-slot" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--radix-ui-react-tabs" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--stackwright-core" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--stackwright-icons" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--stackwright-nextjs" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package--stackwright-ui-shadcn" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-fuse-js" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-js-yaml" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-lucide-react" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-next" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-react" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-react-dom" + }, + { + "spdxElementId": "SPDXRef-Package-clsx", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-tailwind-merge" + } + ] +} \ No newline at end of file diff --git a/examples/stackwright-docs/spdx.spdx b/examples/stackwright-docs/spdx.spdx new file mode 100644 index 00000000..ed171d80 --- /dev/null +++ b/examples/stackwright-docs/spdx.spdx @@ -0,0 +1,146 @@ +SPDXVersion: SPDX-2.3 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT-9bbc3e93 +DocumentName: stackwright-docs@0.1.1-alpha.1 +DocumentNamespace: https://stackwright.dev/spdx/stackwright-docs/2026-04-03T19:28:43.491Z + +CreationInfo: + Created: 2026-04-03T19:28:43.491Z + Creator: Tool: @stackwright/sbom-generator + Creator: Tool-Version: 0.0.0 + +PackageName: @radix-ui/react-accordion +SPDXID: SPDXRef-Package--radix-ui-react-accordion +PackageVersion: 1.2.2 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 6385b547dec3958e12e36af31946a77045ae9b94c89e53bfacbedb71c0f3a10c +ExternalRef: PACKAGE-MANAGER purl pkg:npm/radix-ui/react-accordion@1.2.2 + +PackageName: @radix-ui/react-slot +SPDXID: SPDXRef-Package--radix-ui-react-slot +PackageVersion: 1.1.1 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 6c80259979afa356ff77686649e864a9be6fd187b480c8d3d49fecea2c977b75 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/radix-ui/react-slot@1.1.1 + +PackageName: @radix-ui/react-tabs +SPDXID: SPDXRef-Package--radix-ui-react-tabs +PackageVersion: 1.1.2 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: f3ad88f5751707a5c5f92a1603247d36820a4c7e2a748308c3c8c8acd4d07cd2 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/radix-ui/react-tabs@1.1.2 + +PackageName: @stackwright/core +SPDXID: SPDXRef-Package--stackwright-core +PackageVersion: workspace:* +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 9af5f734e5ee6bfa45ca6ed244194337e4b257d3be8a0927412f1076bf569c7b +ExternalRef: PACKAGE-MANAGER purl pkg:npm/stackwright/core@workspace:* + +PackageName: @stackwright/icons +SPDXID: SPDXRef-Package--stackwright-icons +PackageVersion: workspace:* +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: c255a8ba61a12722b066604f2487920c50bb20b28f6ba518b9525f191f0a7e52 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/stackwright/icons@workspace:* + +PackageName: @stackwright/nextjs +SPDXID: SPDXRef-Package--stackwright-nextjs +PackageVersion: workspace:* +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: b19c754719b46bf6371f09086bd8b754e289bf617322626044dbb47620163cf3 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/stackwright/nextjs@workspace:* + +PackageName: @stackwright/ui-shadcn +SPDXID: SPDXRef-Package--stackwright-ui-shadcn +PackageVersion: workspace:* +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: ac159bdf9d62105719ef6d7d5be5b490eadd991e002ddaae9bab73c93f5c7667 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/stackwright/ui-shadcn@workspace:* + +PackageName: clsx +SPDXID: SPDXRef-Package-clsx +PackageVersion: 2.1.1 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: d00f5fa05915e8916f882f17ae64d1bd58f60296bdf8bb490772cebdafa01b60 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/clsx@2.1.1 + +PackageName: fuse.js +SPDXID: SPDXRef-Package-fuse-js +PackageVersion: 7.1.0 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 3f3d82b5a1943d6113a0f6476ef58cb75640ba889262edd8fed42a3254d8c119 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/fuse.js@7.1.0 + +PackageName: js-yaml +SPDXID: SPDXRef-Package-js-yaml +PackageVersion: 4.1.1 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: af705ed1eb31f1af61a3698569d8e0370c173e67024075b4f306d8c9010f1298 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/js-yaml@4.1.1 + +PackageName: lucide-react +SPDXID: SPDXRef-Package-lucide-react +PackageVersion: 0.471.1 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 34516f0b69a564672a07087b4a907c82ef437a69c4ba2895f1dc37bb8ae402ac +ExternalRef: PACKAGE-MANAGER purl pkg:npm/lucide-react@0.471.1 + +PackageName: next +SPDXID: SPDXRef-Package-next +PackageVersion: 16.1.6 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: dab6b931b02c25bd4f20dd3f93ceb0b57f8971844a25f14feb62c8a20608db04 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/next@16.1.6 + +PackageName: react +SPDXID: SPDXRef-Package-react +PackageVersion: 19.2.4 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: 48fb44e34a2bbbc744bfc0d48b37e11ddb82d4121283c711adcf61883d28f525 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/react@19.2.4 + +PackageName: react-dom +SPDXID: SPDXRef-Package-react-dom +PackageVersion: 19.2.4 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: d321eb3d0478e2a58565081343e4d1da25caf8103b3dd72b3544a715f2e80418 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/react-dom@19.2.4 + +PackageName: tailwind-merge +SPDXID: SPDXRef-Package-tailwind-merge +PackageVersion: 2.6.0 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageChecksum: SHA256: f55becd055a25a9ecb32ee3cf37faf157aa62b955195bb7f695909e582e8ae33 +ExternalRef: PACKAGE-MANAGER purl pkg:npm/tailwind-merge@2.6.0 + +Relationship: SPDXRef-DOCUMENT-9bbc3e93 DESCRIBES SPDXRef-Package-clsx +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--radix-ui-react-accordion +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--radix-ui-react-slot +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--radix-ui-react-tabs +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--stackwright-core +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--stackwright-icons +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--stackwright-nextjs +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package--stackwright-ui-shadcn +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-fuse-js +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-js-yaml +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-lucide-react +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-next +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-react +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-react-dom +Relationship: SPDXRef-Package-clsx CONTAINS SPDXRef-Package-tailwind-merge \ No newline at end of file diff --git a/examples/stackwright-docs/stackwright.yml b/examples/stackwright-docs/stackwright.yml index 29e43fdf..371f95a0 100644 --- a/examples/stackwright-docs/stackwright.yml +++ b/examples/stackwright-docs/stackwright.yml @@ -5,7 +5,7 @@ meta: appBar: titleText: "Stackwright" backgroundColor: "primary" - textColor: "secondary" + textColor: "text" colorModeToggle: false navigation: @@ -54,7 +54,7 @@ sidebar: footer: backgroundColor: "primary" - textColor: "secondary" + textColor: "text" copyright: "Built with Stackwright 🦦" links: - label: "GitHub" diff --git a/package.json b/package.json index 63aec62d..9b5915fe 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build:themes": "pnpm --filter @stackwright/themes build", "build:nextjs": "pnpm --filter @stackwright/nextjs build", "build:build-scripts": "pnpm --filter @stackwright/build-scripts build", + "build:scaffold-core": "pnpm --filter @stackwright/scaffold-core build", "build:collections": "pnpm --filter @stackwright/collections build", "build:mcp": "pnpm --filter @stackwright/mcp build", "build:launch-stackwright": "pnpm --filter launch-stackwright build", @@ -21,7 +22,7 @@ "changeset": "changeset", "version-packages": "changeset version", "release": "pnpm build && changeset publish", - "test": "pnpm -r --filter='!@stackwright/e2e' --filter='!launch-stackwright' test", + "test": "pnpm -r --filter='!@stackwright/e2e' --filter='!launch-stackwright' --filter='!@stackwright/scaffold-core' test", "test:core": "pnpm --filter @stackwright/core test", "test:e2e": "pnpm --filter @stackwright/e2e test", "test:coverage": "pnpm -r --filter='!@stackwright/e2e' --filter='!launch-stackwright' test:coverage && node scripts/merge-coverage.js", diff --git a/packages/build-scripts/CHANGELOG.md b/packages/build-scripts/CHANGELOG.md index 2b62fb36..f9b56d61 100644 --- a/packages/build-scripts/CHANGELOG.md +++ b/packages/build-scripts/CHANGELOG.md @@ -1,5 +1,29 @@ # @stackwright/build-scripts +## 0.4.0-alpha.7 + +### Minor Changes + +- 24fed0f: feat: Add SBOM generation for supply chain transparency + + Every Stackwright build now generates a Software Bill of Materials (SBOM) with: + - SPDX 2.3 format (US Government compliance) + - CycloneDX 1.5 format (OWASP tooling compatibility) + - Stackwright build manifest (internal format) + + New CLI commands: + - `stackwright sbom generate` - Regenerate SBOM + - `stackwright sbom validate` - Validate SBOM schemas + - `stackwright sbom diff` - Compare SBOMs between builds + + Use `--no-sbom` flag to skip generation if needed. + +### Patch Changes + +- Updated dependencies [24fed0f] +- Updated dependencies [24fed0f] + - @stackwright/sbom-generator@0.1.0-alpha.0 + ## 0.4.0-alpha.6 ### Minor Changes diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index dd7a8f43..d44fcff7 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/build-scripts", - "version": "0.4.0-alpha.6", + "version": "0.4.0-alpha.7", "description": "Build-time scripts for Stackwright projects (prebuild image processing, YAML compilation)", "license": "MIT", "repository": { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 8502dd6a..79efb184 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,45 @@ # @stackwright/cli +## 0.7.0-alpha.11 + +### Minor Changes + +- b2e451a: Add scaffold hooks system for extensible post-scaffold processing. Pro packages can now register hooks at lifecycle points (preScaffold, preInstall, postInstall, postScaffold) to inject dependencies, configure MCP servers, and add custom setup. + +### Patch Changes + +- 5c351f5: Fix scaffold smoke-test TypeError by excluding \_font-links.json from static page generation +- Updated dependencies [b2e451a] + - @stackwright/scaffold-core@0.1.0-alpha.1 + +## 0.7.0-alpha.10 + +### Minor Changes + +- 24fed0f: feat: Add SBOM generation for supply chain transparency + +- Added scaffold hooks system via `@stackwright/scaffold-core` for extensible post-scaffold processing + - Pro packages can now register hooks at lifecycle points: preScaffold, preInstall, postInstall, postScaffold + + Every Stackwright build now generates a Software Bill of Materials (SBOM) with: + - SPDX 2.3 format (US Government compliance) + - CycloneDX 1.5 format (OWASP tooling compatibility) + - Stackwright build manifest (internal format) + + New CLI commands: + - `stackwright sbom generate` - Regenerate SBOM + - `stackwright sbom validate` - Validate SBOM schemas + - `stackwright sbom diff` - Compare SBOMs between builds + + Use `--no-sbom` flag to skip generation if needed. + +### Patch Changes + +- Updated dependencies [24fed0f] +- Updated dependencies [24fed0f] + - @stackwright/sbom-generator@0.1.0-alpha.0 + - @stackwright/build-scripts@0.4.0-alpha.7 + ## 0.7.0-alpha.9 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 860f23eb..9d6ad2de 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/cli", - "version": "0.7.0-alpha.9", + "version": "0.7.0-alpha.11", "description": "CLI for Stackwright framework", "license": "MIT", "repository": { @@ -34,6 +34,7 @@ "@inquirer/prompts": "^8.3.0", "@stackwright/build-scripts": "workspace:*", "@stackwright/sbom-generator": "workspace:*", + "@stackwright/scaffold-core": "workspace:*", "@stackwright/themes": "workspace:*", "@stackwright/types": "workspace:*", "chalk": "^5.6.2", diff --git a/packages/cli/src/commands/scaffold.ts b/packages/cli/src/commands/scaffold.ts index e3a862c3..918a8c27 100644 --- a/packages/cli/src/commands/scaffold.ts +++ b/packages/cli/src/commands/scaffold.ts @@ -6,6 +6,8 @@ import chalk from 'chalk'; import { promptThemeSelection } from '../utils/theme-selector'; import { processTemplate } from '../utils/template-processor'; import { outputResult, outputError, getErrorCode, formatError } from '../utils/json-output'; +import { runScaffoldHooks } from '@stackwright/scaffold-core'; +import type { ScaffoldHookContext } from '@stackwright/scaffold-core'; export interface ScaffoldOptions { name?: string; @@ -18,6 +20,8 @@ export interface ScaffoldOptions { monorepo?: boolean; standalone?: boolean; pages?: string; + /** Install dependencies after scaffolding (default: false) */ + install?: boolean; } export interface ScaffoldResult { @@ -84,19 +88,60 @@ export async function scaffold(targetDir: string, opts: ScaffoldOptions): Promis theme = theme ?? 'corporate'; } + // Initialize context for hooks + const packageJson: Record = {}; + const codePuppyConfig: Record = {}; + + // Run preScaffold hooks + await runScaffoldHooks('preScaffold', { + targetDir, + projectName: name!, + siteTitle: title!, + themeId: theme!, + packageJson, + codePuppyConfig, + dependencyMode: determineDependencyMode(targetDir, opts), + }); + const pages = await processTemplate({ projectName: name!, siteTitle: title!, themeId: theme!, targetDir, - offline: !opts.online, // was: offline: opts.offline + offline: !opts.online, monorepo: opts.monorepo, standalone: opts.standalone, pages: opts.pages, + packageJson, + codePuppyConfig, }); const dependencyMode = determineDependencyMode(targetDir, opts); + // If install is requested, run postInstall hooks + if (opts.install) { + await runScaffoldHooks('postInstall', { + targetDir, + projectName: name!, + siteTitle: title!, + themeId: theme!, + packageJson, + codePuppyConfig, + dependencyMode, + }); + } + + // Run postScaffold hooks + await runScaffoldHooks('postScaffold', { + targetDir, + projectName: name!, + siteTitle: title!, + themeId: theme!, + packageJson, + codePuppyConfig, + dependencyMode, + }); + return { path: targetDir, pages, @@ -128,6 +173,7 @@ export function registerScaffold(program: Command): void { '--pages ', 'Comma-separated list of page slugs to create (e.g., about,contact,pricing)' ) + .option('--install', 'Install dependencies after scaffolding') .option('--json', 'Output machine-readable JSON') .action(async (dir: string | undefined, opts: ScaffoldOptions) => { const targetDir = path.resolve(dir ?? process.cwd()); diff --git a/packages/cli/src/utils/template-processor.ts b/packages/cli/src/utils/template-processor.ts index a08a781a..563d5d38 100644 --- a/packages/cli/src/utils/template-processor.ts +++ b/packages/cli/src/utils/template-processor.ts @@ -12,6 +12,8 @@ import { getGenericPageHints, } from './scaffold-hints'; import { fetchTemplate } from './template-fetcher'; +import { runScaffoldHooks } from '@stackwright/scaffold-core'; +import type { ScaffoldHookContext } from '@stackwright/scaffold-core'; /** * Walk up from the given directory looking for a pnpm-workspace.yaml. @@ -54,6 +56,10 @@ export interface TemplateConfig { standalone?: boolean; /** Comma-separated list of page slugs to create in addition to defaults. */ pages?: string; + /** Pre-built package.json to modify (from hooks) */ + packageJson?: Record; + /** Pre-built code-puppy config (from hooks) */ + codePuppyConfig?: Record; } /** @@ -65,6 +71,10 @@ export async function processTemplate(config: TemplateConfig): Promise const written: string[] = []; const year = new Date().getFullYear(); + // Mutable configs that hooks can modify + let packageJson = config.packageJson; + let codePuppyConfig = config.codePuppyConfig; + // Fetch static template files from GitHub repo (falls back to bundled copy) await fetchTemplate(targetDir, { offline }); @@ -147,16 +157,40 @@ export async function processTemplate(config: TemplateConfig): Promise useWorkspaceDeps = detectMonorepoRoot(targetDir) !== null; } - // Generate package.json with proper formatting + // Generate package.json if not provided by hooks + if (!packageJson || Object.keys(packageJson).length === 0) { + packageJson = buildPackageJson(projectName, useWorkspaceDeps) as Record; + } + + // Initialize codePuppyConfig if empty + if (!codePuppyConfig || Object.keys(codePuppyConfig).length === 0) { + codePuppyConfig = {}; + } + + // Run preInstall hooks - hooks can modify packageJson + await runScaffoldHooks('preInstall', { + targetDir, + projectName, + siteTitle, + themeId, + packageJson, + codePuppyConfig, + dependencyMode: useWorkspaceDeps ? 'workspace' : 'standalone', + }); + + // Write package.json with all hook modifications const packageJsonPath = path.join(targetDir, 'package.json'); await fs.ensureDir(path.dirname(packageJsonPath)); - await fs.writeFile( - packageJsonPath, - JSON.stringify(buildPackageJson(projectName, useWorkspaceDeps), null, 2) + '\n', - 'utf8' - ); + await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8'); written.push('package.json'); + // Write .code-puppy.json if hooks populated it + if (codePuppyConfig && Object.keys(codePuppyConfig).length > 0) { + const codePuppyPath = path.join(targetDir, '.code-puppy.json'); + await fs.writeFile(codePuppyPath, JSON.stringify(codePuppyConfig, null, 2) + '\n', 'utf8'); + written.push('.code-puppy.json'); + } + // Generate tsconfig.json const tsconfigPath = path.join(targetDir, 'tsconfig.json'); await fs.writeFile(tsconfigPath, JSON.stringify(buildTsConfig(), null, 2) + '\n', 'utf8'); diff --git a/packages/cli/templates/scaffold-template/pages/[...slug].tsx b/packages/cli/templates/scaffold-template/pages/[...slug].tsx index cd91f183..43700834 100644 --- a/packages/cli/templates/scaffold-template/pages/[...slug].tsx +++ b/packages/cli/templates/scaffold-template/pages/[...slug].tsx @@ -16,7 +16,9 @@ function findContentFiles(dir: string, baseDir: string): string[] { } else if ( entry.name.endsWith('.json') && entry.name !== '_site.json' && - entry.name !== '_root.json' && entry.name !== 'search-index.json' + entry.name !== '_root.json' && + entry.name !== 'search-index.json' && + entry.name !== '_font-links.json' ) { const rel = path.relative(baseDir, path.join(dir, entry.name)); results.push(rel.replace(/\.json$/, '').replace(/\\/g, '/')); diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index da0797db..fc0425a2 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,16 @@ # @stackwright/core +## 0.7.0-alpha.7 + +### Patch Changes + +- 167b5bb: Fixed dark mode text colors and background handling for improved demo/hackathon quality: + - **#252**: Verified ThemeProvider toggle updates correctly (no code changes needed) + - **#251**: Added dark overlay for background images to ensure text contrast + - **Alert component**: Added dark-mode-aware accent colors + - **CodeBlock component**: Added dark mode syntax highlighting palette + - **useSafeTheme hook**: Added `useSafeColorMode` hook for safe color mode access + ## 0.7.0-alpha.6 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 3a1f401f..2baa66d6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/core", - "version": "0.7.0-alpha.6", + "version": "0.7.0-alpha.7", "description": "Core framework for building applications from YAML configuration", "license": "MIT", "repository": { diff --git a/packages/core/src/components/base/Alert.tsx b/packages/core/src/components/base/Alert.tsx index fa339f9d..870d34da 100644 --- a/packages/core/src/components/base/Alert.tsx +++ b/packages/core/src/components/base/Alert.tsx @@ -1,33 +1,56 @@ import React from 'react'; import { AlertContent, AlertVariant } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { hexToRgba } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; import { getIconRegistry } from '../../utils/stackwrightComponentRegistry'; -const variantConfig: Record = { - info: { color: '#3b82f6', iconName: 'Info' }, - warning: { color: '#f59e0b', iconName: 'AlertTriangle' }, - success: { color: '#22c55e', iconName: 'CheckCircle' }, - danger: { color: '#ef4444', iconName: 'CircleAlert' }, - note: { color: '', iconName: 'Info' }, - tip: { color: '#8b5cf6', iconName: 'CheckCircle' }, +// Light mode accent colors +const VARIANT_COLORS = { + info: '#3b82f6', + warning: '#f59e0b', + success: '#22c55e', + danger: '#ef4444', + note: '#6b7280', + tip: '#8b5cf6', +} as const; + +// Dark mode accent colors (higher contrast for dark backgrounds) +const VARIANT_COLORS_DARK = { + info: '#60a5fa', + warning: '#fbbf24', + success: '#4ade80', + danger: '#f87171', + note: '#94a3b8', + tip: '#a78bfa', +} as const; + +const ICON_NAMES: Record = { + info: 'Info', + warning: 'AlertTriangle', + success: 'CheckCircle', + danger: 'CircleAlert', + note: 'Info', + tip: 'CheckCircle', }; export function Alert({ variant, title, body, background }: AlertContent) { const theme = useSafeTheme(); - const config = variantConfig[variant]; - const accentColor = config.color || theme.colors.textSecondary; + const resolvedColorMode = useSafeColorMode(); + + // Select appropriate accent color based on color mode + const colors = resolvedColorMode === 'dark' ? VARIANT_COLORS_DARK : VARIANT_COLORS; + const accentColor = colors[variant] || theme.colors.textSecondary; const alertBgColor = hexToRgba(accentColor, 0.1); const iconRegistry = getIconRegistry(); - const IconComponent = iconRegistry?.get(config.iconName); + const IconComponent = iconRegistry?.get(ICON_NAMES[variant]); return (
diff --git a/packages/core/src/components/base/CodeBlock.tsx b/packages/core/src/components/base/CodeBlock.tsx index 0b24f512..357cd680 100644 --- a/packages/core/src/components/base/CodeBlock.tsx +++ b/packages/core/src/components/base/CodeBlock.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { CodeBlockContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeTheme, useSafeColorMode } from '../../hooks/useSafeTheme'; import { resolveBackground } from '../../utils/resolveBackground'; import { highlightCode, getTokenColor, HighlightToken } from '../../utils/prismHighlighter'; @@ -24,6 +24,8 @@ function splitTokensByLine(tokens: HighlightToken[]): HighlightToken[][] { export function CodeBlock({ code, language, lineNumbers = false, background }: CodeBlockContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); + const isDark = resolvedColorMode === 'dark'; const tokens = highlightCode(code.trimEnd(), language); const tokenLines = splitTokensByLine(tokens); @@ -33,7 +35,7 @@ export function CodeBlock({ code, language, lineNumbers = false, background }: C style={{ margin: `0 ${theme.spacing.xl}`, padding: `${theme.spacing.md} 0`, - background: resolveBackground(background, theme), + background: resolveBackground(background, theme, resolvedColorMode === 'dark'), }} >
{lineTokens.length > 0 ? lineTokens.map((t, j) => { - const color = getTokenColor(t.type); + const color = getTokenColor(t.type, isDark); return color ? ( {t.content} diff --git a/packages/core/src/components/base/ContactFormStub.tsx b/packages/core/src/components/base/ContactFormStub.tsx index 1dde088c..19a1fe40 100644 --- a/packages/core/src/components/base/ContactFormStub.tsx +++ b/packages/core/src/components/base/ContactFormStub.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ContactFormStubContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; @@ -15,6 +15,7 @@ export function ContactFormStub({ background, }: ContactFormStubContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const headingColor = resolveColor( heading?.textColor ? heading.textColor : theme.colors.primary, @@ -29,7 +30,7 @@ export function ContactFormStub({
{heading?.text && ( diff --git a/packages/core/src/components/base/IconGrid.tsx b/packages/core/src/components/base/IconGrid.tsx index f7c8f2e5..d3d77130 100644 --- a/packages/core/src/components/base/IconGrid.tsx +++ b/packages/core/src/components/base/IconGrid.tsx @@ -1,7 +1,7 @@ 'use client'; import React from 'react'; import { IconGridContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; import { getIconRegistry } from '../../utils/stackwrightComponentRegistry'; @@ -16,6 +16,7 @@ function renderIcon(src: string, sizePx: number, color: string) { export function IconGrid({ heading, icons, background }: IconGridContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const headingColor = resolveColor( heading?.textColor ? heading.textColor : theme.colors.primary, @@ -26,7 +27,7 @@ export function IconGrid({ heading, icons, background }: IconGridContent) {
diff --git a/packages/core/src/components/base/LayoutGrid.tsx b/packages/core/src/components/base/LayoutGrid.tsx index f4a7446b..0efbbd86 100644 --- a/packages/core/src/components/base/LayoutGrid.tsx +++ b/packages/core/src/components/base/LayoutGrid.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { GridContent, ContentItem } from '@stackwright/types'; import { renderContent } from '../../utils/contentRenderer'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; @@ -16,6 +16,7 @@ const DEFAULT_STACK_BELOW = 768; */ export function LayoutGrid({ heading, columns, gap, stackBelow, background }: GridContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const breakpoint = stackBelow ?? DEFAULT_STACK_BELOW; // SSR-safe responsive stacking: default to multi-column, hydrate to stacked if narrow @@ -43,7 +44,7 @@ export function LayoutGrid({ heading, columns, gap, stackBelow, background }: Gr
{heading?.text && ( diff --git a/packages/core/src/components/base/MainContentGrid.tsx b/packages/core/src/components/base/MainContentGrid.tsx index 416a6edf..b134a38f 100644 --- a/packages/core/src/components/base/MainContentGrid.tsx +++ b/packages/core/src/components/base/MainContentGrid.tsx @@ -2,13 +2,14 @@ import React from 'react'; import { MainContent, GraphicPosition } from '@stackwright/types'; import { TextGrid } from './TextGrid'; import { ThemedButton } from './ThemedButton'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { Media } from '../media/Media'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; export function MainContentGrid(content: MainContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const textPercent = content.textToGraphic ?? 58; const graphicPercent = 100 - textPercent; @@ -73,7 +74,7 @@ export function MainContentGrid(content: MainContent) {
diff --git a/packages/core/src/components/base/PricingTable.tsx b/packages/core/src/components/base/PricingTable.tsx index 26297ee6..b72beffb 100644 --- a/packages/core/src/components/base/PricingTable.tsx +++ b/packages/core/src/components/base/PricingTable.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { PricingTableContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; import { getThemeShadow } from '../../utils/shadowUtils'; export function PricingTable({ heading, plans, background }: PricingTableContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const headingColor = resolveColor( heading?.textColor ? heading.textColor : theme.colors.primary, @@ -17,7 +18,7 @@ export function PricingTable({ heading, plans, background }: PricingTableContent
{heading?.text && ( diff --git a/packages/core/src/components/base/TestimonialGrid.tsx b/packages/core/src/components/base/TestimonialGrid.tsx index 2164e508..14d4a973 100644 --- a/packages/core/src/components/base/TestimonialGrid.tsx +++ b/packages/core/src/components/base/TestimonialGrid.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { TestimonialGridContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; import { getThemeShadow } from '../../utils/shadowUtils'; @@ -13,6 +13,7 @@ export function TestimonialGrid({ background, }: TestimonialGridContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const headingColor = resolveColor( heading?.textColor ? heading.textColor : theme.colors.primary, @@ -23,7 +24,7 @@ export function TestimonialGrid({
{heading?.text && ( diff --git a/packages/core/src/components/base/TextBlockGrid.tsx b/packages/core/src/components/base/TextBlockGrid.tsx index 3d220e81..3411f360 100644 --- a/packages/core/src/components/base/TextBlockGrid.tsx +++ b/packages/core/src/components/base/TextBlockGrid.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { TextBlockContent } from '@stackwright/types'; -import { useSafeTheme } from '../../hooks/useSafeTheme'; +import { useSafeColorMode, useSafeTheme } from '../../hooks/useSafeTheme'; import { resolveColor } from '../../utils/colorUtils'; import { resolveBackground } from '../../utils/resolveBackground'; import { TextGrid } from './TextGrid'; @@ -8,6 +8,7 @@ import { ThemedButton } from './ThemedButton'; export function TextBlockGrid({ heading, textBlocks, buttons, background }: TextBlockContent) { const theme = useSafeTheme(); + const resolvedColorMode = useSafeColorMode(); const headingColor = resolveColor( heading?.textColor ? heading.textColor : theme.colors.primary, @@ -18,7 +19,7 @@ export function TextBlockGrid({ heading, textBlocks, buttons, background }: Text
diff --git a/packages/core/src/hooks/useSafeTheme.ts b/packages/core/src/hooks/useSafeTheme.ts index b684b230..5e3a43b0 100644 --- a/packages/core/src/hooks/useSafeTheme.ts +++ b/packages/core/src/hooks/useSafeTheme.ts @@ -1,5 +1,5 @@ import type { Theme } from '@stackwright/themes'; -import { useTheme } from '@stackwright/themes'; +import { useTheme, useThemeOptional } from '@stackwright/themes'; // Default theme fallback - complete Theme type const defaultTheme: Theme = { @@ -64,3 +64,12 @@ export function useSafeTheme() { throw error; } } + +/** + * Safe hook to get the resolved color mode. + * Returns 'light' if ThemeProvider is not available (defaults to light mode for syntax highlighting). + */ +export function useSafeColorMode(): 'light' | 'dark' { + const context = useThemeOptional(); + return context?.resolvedColorMode ?? 'light'; +} diff --git a/packages/core/src/utils/prismHighlighter.ts b/packages/core/src/utils/prismHighlighter.ts index 679f4c33..d3d441a0 100644 --- a/packages/core/src/utils/prismHighlighter.ts +++ b/packages/core/src/utils/prismHighlighter.ts @@ -61,10 +61,48 @@ const TOKEN_COLORS: Record = { atrule: '#d73a49', }; +/** + * Dark mode color palette for syntax tokens. + * GitHub Dark-inspired colors for dark (#0d1117) code block background. + */ +const DARK_TOKEN_COLORS: Record = { + comment: '#8b949e', + prolog: '#8b949e', + doctype: '#8b949e', + cdata: '#8b949e', + punctuation: '#c9d1d9', + property: '#79c0ff', + tag: '#7ee787', + boolean: '#79c0ff', + number: '#79c0ff', + constant: '#79c0ff', + symbol: '#79c0ff', + selector: '#d2a8ff', + 'attr-name': '#d2a8ff', + string: '#a5d6ff', + char: '#a5d6ff', + 'template-string': '#a5d6ff', + builtin: '#d2a8ff', + inserted: '#7ee787', + operator: '#ff7b72', + entity: '#79c0ff', + url: '#a5d6ff', + keyword: '#ff7b72', + 'attr-value': '#a5d6ff', + function: '#d2a8ff', + 'class-name': '#d2a8ff', + regex: '#a5d6ff', + important: '#ff7b72', + variable: '#ffa657', + deleted: '#ff7b72', + atrule: '#ff7b72', +}; + /** A flattened token with type and text, ready for rendering. */ export interface HighlightToken { type: string | null; // null = plain text content: string; + color?: string; // resolved color when using highlightCodeWithMode } /** @@ -121,8 +159,29 @@ export function highlightCode(code: string, language?: string): HighlightToken[] /** * Get the inline color for a token type. + * @param type - The token type from Prism + * @param isDark - Whether to use dark mode colors (default: false) */ -export function getTokenColor(type: string | null): string | undefined { +export function getTokenColor(type: string | null, isDark: boolean = false): string | undefined { if (!type) return undefined; - return TOKEN_COLORS[type]; + const colors = isDark ? DARK_TOKEN_COLORS : TOKEN_COLORS; + return colors[type]; +} + +/** + * Tokenize code with Prism and return tokens with colors already resolved. + * @param code - The source code to highlight + * @param language - The language identifier + * @param isDark - Whether to use dark mode colors (default: false) + */ +export function highlightCodeWithMode( + code: string, + language?: string, + isDark: boolean = false +): HighlightToken[] { + const tokens = highlightCode(code, language); + return tokens.map((token) => ({ + ...token, + color: getTokenColor(token.type, isDark), + })); } diff --git a/packages/core/src/utils/resolveBackground.ts b/packages/core/src/utils/resolveBackground.ts index 97613da7..3f7c466b 100644 --- a/packages/core/src/utils/resolveBackground.ts +++ b/packages/core/src/utils/resolveBackground.ts @@ -6,6 +6,9 @@ * * This enables YAML authors to write `background: "surface"` and get * the correct color in both light and dark modes automatically. + * + * In dark mode with a theme background image, a semi-transparent dark + * overlay is applied to ensure text contrast. */ const THEME_COLOR_KEYS = new Set([ 'primary', @@ -20,12 +23,28 @@ const THEME_COLOR_KEYS = new Set([ /** Minimal shape — only needs `colors` from the theme. */ interface ThemeWithColors { colors: Record; + backgroundImage?: { + url?: string; + }; } -export function resolveBackground(bg: string | undefined, theme: ThemeWithColors): string { +export function resolveBackground( + bg: string | undefined, + theme: ThemeWithColors, + isDarkMode?: boolean +): string { if (!bg) return 'transparent'; + if (THEME_COLOR_KEYS.has(bg)) { - return theme.colors[bg] ?? bg; + const resolvedColor = theme.colors[bg] ?? bg; + + // In dark mode, if the theme has a background image, layer a dark + // overlay on top for better text contrast. + if (isDarkMode && theme.backgroundImage?.url) { + return `linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)), url('${theme.backgroundImage.url}')`; + } + + return resolvedColor; } return bg; } diff --git a/packages/core/test/components/text-block.test.tsx b/packages/core/test/components/text-block.test.tsx index 6dc4584a..bcb8b8eb 100644 --- a/packages/core/test/components/text-block.test.tsx +++ b/packages/core/test/components/text-block.test.tsx @@ -27,6 +27,7 @@ const mockTheme = { }; vi.mock('../../src/hooks/useSafeTheme', () => ({ + useSafeColorMode: () => 'light', useSafeTheme: () => mockTheme, })); diff --git a/packages/e2e/tests/__screenshots__/stackwright-docs/visual.spec.ts/testimonial-grid-desktop.png b/packages/e2e/tests/__screenshots__/stackwright-docs/visual.spec.ts/testimonial-grid-desktop.png index 31292666..2487c95e 100644 Binary files a/packages/e2e/tests/__screenshots__/stackwright-docs/visual.spec.ts/testimonial-grid-desktop.png and b/packages/e2e/tests/__screenshots__/stackwright-docs/visual.spec.ts/testimonial-grid-desktop.png differ diff --git a/packages/e2e/tests/performance/build-time.bench.ts b/packages/e2e/tests/performance/build-time.bench.ts index b7701719..6c3e6b7b 100644 --- a/packages/e2e/tests/performance/build-time.bench.ts +++ b/packages/e2e/tests/performance/build-time.bench.ts @@ -8,22 +8,22 @@ const execAsync = promisify(exec); /** * Build Time Performance Benchmarks - * + * * Measures the time taken for: * 1. stackwright-prebuild (image co-location + path rewriting) * 2. next build (full SSG build) - * + * * Baselines (established 2025-01-27): * - Prebuild (7 pages, 17 files): ~2-5s * - Next build (7 pages): ~30-45s - * + * * Budgets: * - Prebuild: <10s (warn at 8s) * - Next build: <60s (warn at 45s) * - Regression threshold: 20% */ -const exampleAppDir = path.resolve(__dirname, '../../../../examples/hellostackwrightnext'); +const exampleAppDir = path.resolve(__dirname, '../../../../examples/stackwright-docs'); interface BenchmarkResult { name: string; @@ -84,8 +84,10 @@ test.describe('Build Time Benchmarks', () => { console.log(` Status: ${passed ? '✅ PASS' : '❌ FAIL'} ${warning ? '⚠️ WARNING' : ''}`); // Assert - expect(duration, `Prebuild took ${duration}ms, budget is ${budget.max}ms`).toBeLessThanOrEqual(budget.max); - + expect(duration, `Prebuild took ${duration}ms, budget is ${budget.max}ms`).toBeLessThanOrEqual( + budget.max + ); + if (warning) { console.warn(`⚠️ Warning: Prebuild is slower than ${budget.warn}ms threshold`); } @@ -96,11 +98,7 @@ test.describe('Build Time Benchmarks', () => { const buildDir = path.join(exampleAppDir, '.next'); await fs.rm(buildDir, { recursive: true, force: true }); - const duration = await benchmarkCommand( - 'next build', - 'pnpm exec next build', - exampleAppDir - ); + const duration = await benchmarkCommand('next build', 'pnpm exec next build', exampleAppDir); const budget = budgets.build.nextBuild; const passed = duration <= budget.max; @@ -113,8 +111,11 @@ test.describe('Build Time Benchmarks', () => { console.log(` Status: ${passed ? '✅ PASS' : '❌ FAIL'} ${warning ? '⚠️ WARNING' : ''}`); // Assert - expect(duration, `Next build took ${duration}ms, budget is ${budget.max}ms`).toBeLessThanOrEqual(budget.max); - + expect( + duration, + `Next build took ${duration}ms, budget is ${budget.max}ms` + ).toBeLessThanOrEqual(budget.max); + if (warning) { console.warn(`⚠️ Warning: Next build is slower than ${budget.warn}ms threshold`); } @@ -126,15 +127,15 @@ test.describe('Build Time Benchmarks', () => { const buildDir = path.join(exampleAppDir, '.next'); await Promise.all([ fs.rm(processedDir, { recursive: true, force: true }), - fs.rm(buildDir, { recursive: true, force: true }) + fs.rm(buildDir, { recursive: true, force: true }), ]); const startTime = Date.now(); - + // Run prebuild await execAsync('pnpm exec stackwright-prebuild', { cwd: exampleAppDir }); const prebuildDuration = Date.now() - startTime; - + // Run build await execAsync('pnpm exec next build', { cwd: exampleAppDir }); const totalDuration = Date.now() - startTime; @@ -147,6 +148,9 @@ test.describe('Build Time Benchmarks', () => { console.log(` Budget: ${budgets.build.prebuild.max + budgets.build.nextBuild.max}ms`); const totalBudget = budgets.build.prebuild.max + budgets.build.nextBuild.max; - expect(totalDuration, `Total build took ${totalDuration}ms, budget is ${totalBudget}ms`).toBeLessThanOrEqual(totalBudget); + expect( + totalDuration, + `Total build took ${totalDuration}ms, budget is ${totalBudget}ms` + ).toBeLessThanOrEqual(totalBudget); }); }); diff --git a/packages/e2e/tests/performance/bundle-size.bench.ts b/packages/e2e/tests/performance/bundle-size.bench.ts index 95692f42..8e63a4f9 100644 --- a/packages/e2e/tests/performance/bundle-size.bench.ts +++ b/packages/e2e/tests/performance/bundle-size.bench.ts @@ -9,23 +9,23 @@ const execAsync = promisify(exec); /** * Bundle Size Performance Benchmarks - * + * * Analyzes Next.js build output to measure: * 1. First-load JS (critical for initial page load) * 2. Total JS across all pages * 3. Per-page bundle sizes * 4. Image optimization results - * + * * Baselines (established 2025-01-27): * - First-load JS: ~85KB gzipped * - Total JS: ~300KB gzipped - * + * * Budgets: * - First-load JS: <100KB gzipped (warn at 80KB) * - All pages JS: <500KB gzipped (warn at 400KB) */ -const exampleAppDir = path.resolve(__dirname, '../../../../examples/hellostackwrightnext'); +const exampleAppDir = path.resolve(__dirname, '../../../../examples/stackwright-docs'); interface BundleStats { firstLoadJS: number; @@ -47,8 +47,8 @@ async function ensureBuild() { console.log('✅ Build directory exists'); } catch { console.log('🔨 Building application first...'); - await execAsync('pnpm exec stackwright-prebuild && pnpm exec next build', { - cwd: exampleAppDir + await execAsync('pnpm exec stackwright-prebuild && pnpm exec next build', { + cwd: exampleAppDir, }); } } @@ -56,15 +56,15 @@ async function ensureBuild() { async function analyzeBuildManifest(): Promise { const manifestPath = path.join(exampleAppDir, '.next/build-manifest.json'); const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8')); - + // Get first-load JS files const firstLoadFiles = manifest.pages['/_app'] || []; - + // Calculate sizes let firstLoadJS = 0; let allPagesJS = 0; const pages: Array<{ route: string; size: number }> = []; - + // Analyze first-load JS for (const file of firstLoadFiles) { const filePath = path.join(exampleAppDir, '.next', file); @@ -76,7 +76,7 @@ async function analyzeBuildManifest(): Promise { // File might not exist, skip } } - + // Analyze all pages for (const [route, files] of Object.entries(manifest.pages)) { let pageSize = 0; @@ -93,12 +93,12 @@ async function analyzeBuildManifest(): Promise { } pages.push({ route, size: pageSize }); } - + return { firstLoadJS, allPagesJS, pages: pages.sort((a, b) => b.size - a.size), - sharedChunks: firstLoadJS + sharedChunks: firstLoadJS, }; } @@ -106,7 +106,7 @@ async function analyzeStaticAssets(): Promise<{ images: number; total: number }> const publicDir = path.join(exampleAppDir, 'public/images'); let images = 0; let total = 0; - + try { const walk = async (dir: string) => { const entries = await fs.readdir(dir, { withFileTypes: true }); @@ -121,12 +121,12 @@ async function analyzeStaticAssets(): Promise<{ images: number; total: number }> } } }; - + await walk(publicDir); } catch (error) { // Directory might not exist } - + return { images, total }; } @@ -140,7 +140,7 @@ test.describe('Bundle Size Benchmarks', () => { test('first-load JS bundle size', async () => { const stats = await analyzeBuildManifest(); - + const sizeKB = Math.round(stats.firstLoadJS / 1024); const budget = budgets.bundle.firstLoadJS; const passed = stats.firstLoadJS <= budget.max; @@ -148,19 +148,26 @@ test.describe('Bundle Size Benchmarks', () => { console.log(`\n📦 First-Load JS Bundle:`); console.log(` Size: ${sizeKB}KB (${stats.firstLoadJS} bytes, gzipped)`); - console.log(` Budget: ${Math.round(budget.max / 1024)}KB (warn at ${Math.round(budget.warn / 1024)}KB)`); + console.log( + ` Budget: ${Math.round(budget.max / 1024)}KB (warn at ${Math.round(budget.warn / 1024)}KB)` + ); console.log(` Status: ${passed ? '✅ PASS' : '❌ FAIL'} ${warning ? '⚠️ WARNING' : ''}`); - expect(stats.firstLoadJS, `First-load JS is ${sizeKB}KB, budget is ${Math.round(budget.max / 1024)}KB`).toBeLessThanOrEqual(budget.max); - + expect( + stats.firstLoadJS, + `First-load JS is ${sizeKB}KB, budget is ${Math.round(budget.max / 1024)}KB` + ).toBeLessThanOrEqual(budget.max); + if (warning) { - console.warn(`⚠️ Warning: First-load JS exceeds ${Math.round(budget.warn / 1024)}KB threshold`); + console.warn( + `⚠️ Warning: First-load JS exceeds ${Math.round(budget.warn / 1024)}KB threshold` + ); } }); test('total JS bundle size', async () => { const stats = await analyzeBuildManifest(); - + const sizeKB = Math.round(stats.allPagesJS / 1024); const budget = budgets.bundle.allPagesJS; const passed = stats.allPagesJS <= budget.max; @@ -168,17 +175,22 @@ test.describe('Bundle Size Benchmarks', () => { console.log(`\n📦 Total JS Across All Pages:`); console.log(` Size: ${sizeKB}KB (${stats.allPagesJS} bytes, gzipped)`); - console.log(` Budget: ${Math.round(budget.max / 1024)}KB (warn at ${Math.round(budget.warn / 1024)}KB)`); + console.log( + ` Budget: ${Math.round(budget.max / 1024)}KB (warn at ${Math.round(budget.warn / 1024)}KB)` + ); console.log(` Status: ${passed ? '✅ PASS' : '❌ FAIL'} ${warning ? '⚠️ WARNING' : ''}`); - + // Show per-page breakdown console.log(`\n Per-Page Breakdown (top 5):`); for (const page of stats.pages.slice(0, 5)) { console.log(` ${page.route}: ${Math.round(page.size / 1024)}KB`); } - expect(stats.allPagesJS, `Total JS is ${sizeKB}KB, budget is ${Math.round(budget.max / 1024)}KB`).toBeLessThanOrEqual(budget.max); - + expect( + stats.allPagesJS, + `Total JS is ${sizeKB}KB, budget is ${Math.round(budget.max / 1024)}KB` + ).toBeLessThanOrEqual(budget.max); + if (warning) { console.warn(`⚠️ Warning: Total JS exceeds ${Math.round(budget.warn / 1024)}KB threshold`); } @@ -186,7 +198,7 @@ test.describe('Bundle Size Benchmarks', () => { test('image optimization results', async () => { const { images, total } = await analyzeStaticAssets(); - + const totalMB = (total / (1024 * 1024)).toFixed(2); const avgKB = images > 0 ? Math.round(total / images / 1024) : 0; @@ -194,28 +206,28 @@ test.describe('Bundle Size Benchmarks', () => { console.log(` Total Images: ${images}`); console.log(` Total Size: ${totalMB}MB`); console.log(` Average Size: ${avgKB}KB per image`); - + // This is informational, not a hard limit // But we can warn if images are unexpectedly large if (avgKB > 500) { console.warn(`⚠️ Warning: Average image size is high (${avgKB}KB). Consider optimization.`); } - + // Basic sanity check - at least some images should exist expect(images, 'Should have processed at least some images').toBeGreaterThan(0); }); test('shared chunk efficiency', async () => { const stats = await analyzeBuildManifest(); - + // Shared chunks should be reasonable compared to total bundle const sharedPercentage = ((stats.sharedChunks / stats.allPagesJS) * 100).toFixed(1); - + console.log(`\n🔗 Shared Chunk Analysis:`); console.log(` Shared chunks: ${Math.round(stats.sharedChunks / 1024)}KB`); console.log(` Total bundle: ${Math.round(stats.allPagesJS / 1024)}KB`); console.log(` Shared percentage: ${sharedPercentage}%`); - + // Shared chunks should be a reasonable portion (20-60% is typical) const percentage = parseFloat(sharedPercentage); expect(percentage, 'Shared chunks should be between 20% and 80% of total').toBeGreaterThan(20); diff --git a/packages/launch-stackwright/CHANGELOG.md b/packages/launch-stackwright/CHANGELOG.md index 09eb2c4e..f51fd047 100644 --- a/packages/launch-stackwright/CHANGELOG.md +++ b/packages/launch-stackwright/CHANGELOG.md @@ -1,5 +1,31 @@ # launch-stackwright +## 0.2.0-alpha.8 + +### Minor Changes + +- b2e451a: Add scaffold hooks system for extensible post-scaffold processing. Pro packages can now register hooks at lifecycle points (preScaffold, preInstall, postInstall, postScaffold) to inject dependencies, configure MCP servers, and add custom setup. + +### Patch Changes + +- Updated dependencies [5c351f5] +- Updated dependencies [b2e451a] + - @stackwright/cli@0.7.0-alpha.11 + - @stackwright/scaffold-core@0.1.0-alpha.1 + +## 0.2.0-alpha.7 + +### Patch Changes + +- Updated dependencies [a852368] +- Updated dependencies [24fed0f] + - @stackwright/otters@0.2.0-alpha.2 + - @stackwright/cli@0.7.0-alpha.10 +- Added `--otter-raft` flag for one-command project setup with all dependencies installed +- Run `npx launch-stackwright my-site --otter-raft` to scaffold and install in one step +- Updated to use scaffold hooks system for MCP configuration +- Hooks run automatically when using --otter-raft flag + ## 0.2.0-alpha.6 ### Patch Changes diff --git a/packages/launch-stackwright/README.md b/packages/launch-stackwright/README.md index ed4b614f..77d8f545 100644 --- a/packages/launch-stackwright/README.md +++ b/packages/launch-stackwright/README.md @@ -5,12 +5,13 @@ The fastest way to get started with Stackwright — launch a new project with th ## Quick Start ```bash -npx launch-stackwright my-awesome-site +npx launch-stackwright my-awesome-site --otter-raft cd my-awesome-site -pnpm install pnpm dev ``` +> 💡 **Tip:** The `--otter-raft` flag sets up everything including AI agents and installs all dependencies in one go. Prefer manual control? Just run without it: `npx launch-stackwright my-site`, then `pnpm install` separately. + ## What You Get When you run `launch-stackwright`, you get: @@ -45,6 +46,7 @@ Options: --name Project name (used in package.json) --title Site title shown in app bar and browser tab --theme <themeId> Theme ID (corporate, creative, minimal, etc.) + --otter-raft Setup the otter raft AI agents and install all dependencies --force Launch even if directory is not empty --skip-otters Skip copying otter raft configs -y, --yes Skip all prompts, use defaults @@ -55,6 +57,9 @@ Options: ## Examples ```bash +# One-command setup with otter raft (recommended) +npx launch-stackwright my-site --otter-raft + # Interactive mode (default) npx launch-stackwright my-site @@ -90,6 +95,21 @@ my-awesome-site/ └── tsconfig.json # TypeScript config ``` +## Next Steps + +### With `--otter-raft` +After launching, you're ready to go! Run: +```bash +pnpm dev +``` + +### Without `--otter-raft` +You'll need to install dependencies first: +```bash +pnpm install +pnpm dev +``` + ## Prerequisites - **Node.js** 18+ and **pnpm** diff --git a/packages/launch-stackwright/package.json b/packages/launch-stackwright/package.json index e9ad135f..4976d624 100644 --- a/packages/launch-stackwright/package.json +++ b/packages/launch-stackwright/package.json @@ -1,6 +1,6 @@ { "name": "launch-stackwright", - "version": "0.2.0-alpha.6", + "version": "0.2.0-alpha.8", "description": "Launch a new Stackwright project with the otter raft ready to build", "license": "MIT", "repository": { @@ -28,6 +28,7 @@ "dependencies": { "@stackwright/cli": "workspace:*", "@stackwright/otters": "workspace:*", + "@stackwright/scaffold-core": "workspace:*", "chalk": "^5.6.2", "commander": "^14.0.3", "fs-extra": "^11.3", diff --git a/packages/launch-stackwright/src/index.ts b/packages/launch-stackwright/src/index.ts index fe2ffe47..32e9729e 100644 --- a/packages/launch-stackwright/src/index.ts +++ b/packages/launch-stackwright/src/index.ts @@ -1,10 +1,15 @@ #!/usr/bin/env node import { Command } from 'commander'; import path from 'path'; -import fs from 'fs-extra'; import chalk from 'chalk'; +import { execSync } from 'child_process'; import { scaffold, ScaffoldOptions } from '@stackwright/cli'; +import { + registerScaffoldHook, + clearScaffoldHooks, + type ScaffoldHookContext, +} from '@stackwright/scaffold-core'; const { version } = require('../package.json') as { version: string }; @@ -14,32 +19,53 @@ interface LaunchOptions { theme?: string; force?: boolean; skipOtters?: boolean; + otterRaft?: boolean; yes?: boolean; } -async function setupCodePuppyConfig(targetDir: string): Promise<void> { - // Generate .code-puppy.json for MCP auto-configuration - // Otters are now installed as a package: @stackwright/otters - const codePuppyConfig = { - mcp_servers: { - stackwright: { - command: 'node', - args: ['node_modules/@stackwright/mcp/dist/server.js'], - env: { - NODE_ENV: 'development', +/** + * Register the MCP config hook for Code Puppy integration. + * This hook runs during scaffold's preInstall phase and populates + * ctx.codePuppyConfig, which processTemplate writes to .code-puppy.json. + */ +function registerLaunchHooks(): void { + registerScaffoldHook({ + type: 'preInstall', + name: 'code-puppy-mcp-config', + priority: 10, // Run early + handler: async (ctx: ScaffoldHookContext) => { + ctx.codePuppyConfig = { + mcp_servers: { + stackwright: { + command: 'node', + args: ['node_modules/@stackwright/mcp/dist/server.js'], + env: { + NODE_ENV: 'development', + }, + }, }, - }, + agents_path: 'node_modules/@stackwright/otters', + }; }, - agents_path: 'node_modules/@stackwright/otters', - }; + }); +} - await fs.writeFile( - path.join(targetDir, '.code-puppy.json'), - JSON.stringify(codePuppyConfig, null, 2) - ); +function setupOtterRaft(targetDir: string): void { + console.log(chalk.cyan('\n🦦 Installing the otter raft...\n')); - console.log(chalk.green('✅ @stackwright/otters installed as dependency! 🦪🦪🦪🦪')); - console.log(chalk.dim(' MCP auto-config ready in .code-puppy.json')); + try { + execSync('pnpm install', { + cwd: targetDir, + stdio: 'inherit', + }); + console.log(chalk.green('\n✅ Otter raft is ready!')); + } catch { + console.log(chalk.yellow('\n⚠️ Could not run pnpm install automatically.')); + console.log(chalk.white(`\nPlease run these commands manually:`)); + console.log(chalk.cyan(` cd ${path.relative(process.cwd(), targetDir) || '.'}`)); + console.log(chalk.cyan(` pnpm install`)); + throw new Error('Otter raft setup failed'); + } } async function launch(targetDir: string, options: LaunchOptions): Promise<void> { @@ -57,31 +83,50 @@ async function launch(targetDir: string, options: LaunchOptions): Promise<void> }; try { - const result = await scaffold(targetDir, scaffoldOptions); + // Register launch-specific hooks BEFORE scaffold runs + if (!options.skipOtters) { + console.log(chalk.cyan('\n🦪 Setting up the otter raft...\n')); + registerLaunchHooks(); + } + + // Scaffold with install=true so preInstall/postInstall hooks run + const result = await scaffold(targetDir, { + ...scaffoldOptions, + install: true, + }); console.log(chalk.green(`\n✅ Project scaffolded successfully!`)); console.log(chalk.dim(` Location: ${result.path}`)); console.log(chalk.dim(` Theme: ${result.theme}`)); console.log(chalk.dim(` Pages: ${result.pages.join(', ')}`)); - // Setup Code Puppy MCP config unless --skip-otters + // Confirm MCP config was written if (!options.skipOtters) { - console.log(chalk.cyan('\n🦪 Setting up the otter raft...\n')); - await setupCodePuppyConfig(targetDir); + console.log(chalk.green('✅ @stackwright/otters installed as dependency! 🦪🦪🦪🦪')); + console.log(chalk.dim(' MCP auto-config written to .code-puppy.json')); } + // Clean up hooks after use + clearScaffoldHooks(); + // Print next steps console.log(chalk.cyan.bold("\n🎉 All set! Here's what to do next:\n")); - console.log(chalk.white(` 1. cd ${path.relative(process.cwd(), targetDir) || '.'}`)); - console.log(chalk.white(` 2. pnpm install`)); - console.log(chalk.white(` 3. pnpm dev`)); - if (!options.skipOtters) { - console.log(chalk.cyan.bold('\n🦦 Want the otter raft to build your site for you?\n')); + console.log(chalk.green.bold('Your project is ready for the otter raft!\n')); + console.log(chalk.white(` cd ${path.relative(process.cwd(), targetDir) || '.'}`)); + console.log(chalk.white(` pnpm dev`)); + console.log(chalk.cyan.bold('\n🦦 Start building:')); console.log(chalk.white(` code-puppy invoke stackwright-foreman-otter`)); console.log(chalk.dim(` Then say: "Build me a [type] website for [name]"\n`)); + } else { + console.log(chalk.white(` 1. cd ${path.relative(process.cwd(), targetDir) || '.'}`)); + console.log(chalk.white(` 2. pnpm install`)); + console.log(chalk.white(` 3. pnpm dev`)); + console.log(chalk.cyan.bold('\n🦦 Want the otter raft too? Run with --skip-otters=false')); + console.log(chalk.dim(' or manually install @stackwright/otters later.\n')); } } catch (err) { + clearScaffoldHooks(); // Clean up on error too console.error(chalk.red('\n❌ Launch failed:'), err); process.exit(1); } @@ -100,6 +145,7 @@ async function main(): Promise<void> { .option('--theme <themeId>', 'Theme ID (e.g., corporate, creative, minimal)') .option('--force', 'Launch even if the target directory is not empty') .option('--skip-otters', 'Skip setting up Code Puppy MCP config') + .option('--otter-raft', 'Setup the otter raft AI agents and install all dependencies') .option('-y, --yes', 'Skip all prompts, use defaults') .action(async (directory: string, options: LaunchOptions) => { const targetDir = path.resolve(directory); diff --git a/packages/maplibre/CHANGELOG.md b/packages/maplibre/CHANGELOG.md index 9aa0f3d3..d64829dc 100644 --- a/packages/maplibre/CHANGELOG.md +++ b/packages/maplibre/CHANGELOG.md @@ -1,5 +1,12 @@ # @stackwright/maplibre +## 1.0.0-alpha.3 + +### Patch Changes + +- Updated dependencies [167b5bb] + - @stackwright/core@0.7.0-alpha.7 + ## 1.0.0-alpha.2 ### Patch Changes diff --git a/packages/maplibre/package.json b/packages/maplibre/package.json index 5daa3161..efdc8650 100644 --- a/packages/maplibre/package.json +++ b/packages/maplibre/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/maplibre", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "description": "MapLibre GL adapter for Stackwright maps (free tier, no API keys required)", "license": "MIT", "repository": { diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 4c1357d9..abaab000 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,20 @@ # @stackwright/mcp +## 0.3.0-alpha.11 + +### Patch Changes + +- Updated dependencies [5c351f5] +- Updated dependencies [b2e451a] + - @stackwright/cli@0.7.0-alpha.11 + +## 0.3.0-alpha.10 + +### Patch Changes + +- Updated dependencies [24fed0f] + - @stackwright/cli@0.7.0-alpha.10 + ## 0.3.0-alpha.9 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index fad86f71..e9d1570a 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/mcp", - "version": "0.3.0-alpha.9", + "version": "0.3.0-alpha.11", "description": "MCP server for Stackwright — exposes content types, page management, and validation as agent tools", "license": "MIT", "repository": { diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index 3ddf6791..25122b80 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,5 +1,12 @@ # @stackwright/nextjs +## 0.3.1-alpha.7 + +### Patch Changes + +- Updated dependencies [167b5bb] + - @stackwright/core@0.7.0-alpha.7 + ## 0.3.1-alpha.6 ### Patch Changes diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 1ecc7110..666b06b4 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/nextjs", - "version": "0.3.1-alpha.6", + "version": "0.3.1-alpha.7", "description": "Next.js implementations for Stackwright components", "license": "MIT", "repository": { diff --git a/packages/otters/CHANGELOG.md b/packages/otters/CHANGELOG.md index c4072f5a..0fe2bf6f 100644 --- a/packages/otters/CHANGELOG.md +++ b/packages/otters/CHANGELOG.md @@ -1,5 +1,16 @@ # @stackwright/otters +## 0.2.0-alpha.2 + +### Minor Changes + +- a852368: Add postinstall script to install otters to ~/.code_puppy/agents/ + - Created scripts/install-agents.js that copies agent JSON files to ~/.code_puppy/agents/ + - Updated package.json with postinstall hook + - Updated README with installation instructions + - Fixed .code-puppy.json config (removed agents_path) + - Bumped version to 0.2.0-alpha.1 + ## 0.2.0-alpha.0 ### Minor Changes diff --git a/packages/otters/README.md b/packages/otters/README.md index 4fb3d8ae..2165786c 100644 --- a/packages/otters/README.md +++ b/packages/otters/README.md @@ -1,8 +1,10 @@ # @stackwright/otters -🦦 **Stackwright Otter Raft** — AI agents for end-to-end Stackwright site generation. +🦝 **Otter Raft Architecture** — AI agents that discover each other and self-organize. -A coordinated team of specialized AI agents (otters) that work together to build complete Stackwright websites from brand discovery to deployed pages. +Just like real otters, our AI otters don't wait for instructions. They discover +who's in the water and adapt their behavior accordingly. A Page Otter that finds +a Dashboard Otter nearby will offer to connect live API data. No central planner required. --- @@ -26,12 +28,37 @@ node node_modules/@stackwright/otters/scripts/install-agents.js ## The Otter Raft -| Otter | Role | Output | -|-------|------|--------| -| 🦦🏗️ **Foreman Otter** | Project coordinator | Orchestrates the pipeline | -| 🦦🎨 **Brand Otter** | Brand discovery | `BRAND_BRIEF.md` | -| 🦦🌈 **Theme Otter** | Visual design | `stackwright.yml` theme | -| 🦦📄 **Page Otter** | Content composition | `pages/*.yml` | +| Otter | Role | Discovery | +|-------|------|-----------| +| 🦦🏗️ **Foreman Otter** | Dynamic coordinator | Uses `list_agents()` to discover otters | +| 🦦🎨 **Brand Otter** | Brand discovery | May guide discovery itself if alone | +| 🦦🌈 **Theme Otter** | Visual design | Adapts to available Brand output | +| 🦦📄 **Page Otter** | Content composition | Offers Pro features if Dashboard Otter found | + +--- + +## How Otters Discover Each Other + +Every otter starts by asking: "Who's out there?" + +```bash +code-puppy -i -a stackwright-foreman-otter +# Foreman: "Discovering available otters..." +# Found: Brand Otter ✓, Theme Otter ✓, Page Otter ✓ +# Pro detected: API Otter, Dashboard Otter ✓ +``` + +The raft adapts based on what's installed: + +| Install Type | Default Otters | Optional Pro Otters | +|--------------|----------------|---------------------| +| OSS only | Brand → Theme → Page | — | +| OSS + Pro | Brand → Theme → Page | Dashboard, API | + +When a Page Otter finds a Dashboard Otter in the raft, it: +- Offers to connect live API data to pages +- Suggests dashboard components +- Enables real-time data visualization --- @@ -44,20 +71,20 @@ User Request │ ▼ ┌────────────────────────┐ -│ Foreman Otter │ ◄── Entry point +│ Foreman Otter │ ◄── Entry point, discovers otters │ "Starting build..." │ └───────┬────────────────┘ │ ▼ ┌────────────────────────┐ -│ Brand Otter │ ◄── Phase 1: Discovery +│ Brand Otter │ ◄── Phase 1: Discovery │ (conversational) │ └───────┬────────────────┘ │ creates BRAND_BRIEF.md ▼ ┌────────────────────────┐ -│ Theme Otter │ ◄── Phase 2: Design -│ (colors, fonts) │ +│ Theme Otter │ ◄── Phase 2: Design +│ (colors, fonts) │ └───────┬────────────────┘ │ creates stackwright.yml ▼ @@ -80,13 +107,13 @@ User Request Otters are invoked through Code Puppy's agent invocation: ```bash -# Start a full site build +# Start a full site build (Foreman discovers available otters) code-puppy -i -a stackwright-foreman-otter # Just refine the theme code-puppy -i -a stackwright-theme-otter -# Add a new page +# Add a new page (offers Pro features if Dashboard Otter found) code-puppy -i -a stackwright-page-otter ``` @@ -118,11 +145,13 @@ For detailed architecture documentation, see [OTTER_ARCHITECTURE.md](../../OTTER ### Key Principles -1. **Separation of Concerns** — Each otter owns one domain -2. **Sequential Execution** — Dependencies enforced by Foreman -3. **File-Based Handoffs** — BRAND_BRIEF.md, stackwright.yml, pages/*.yml -4. **Validation at Every Step** — No invalid YAML proceeds -5. **Visual Verification** — Screenshots close the feedback loop +1. **Self-Discovery** — Otters discover each other at runtime via `list_agents()` +2. **Dynamic Adaptation** — Behavior changes based on available otters +3. **Separation of Concerns** — Each otter owns one domain +4. **Sequential Execution** — Dependencies enforced by Foreman +5. **File-Based Handoffs** — BRAND_BRIEF.md, stackwright.yml, pages/*.yml +6. **Validation at Every Step** — No invalid YAML proceeds +7. **Visual Verification** — Screenshots close the feedback loop --- diff --git a/packages/otters/package.json b/packages/otters/package.json index c46f129b..923ebc55 100644 --- a/packages/otters/package.json +++ b/packages/otters/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/otters", - "version": "0.2.0-alpha.1", + "version": "0.2.0-alpha.2", "description": "Stackwright Otter Raft - AI agents for site generation", "license": "MIT", "repository": { diff --git a/packages/otters/src/stackwright-foreman-otter.json b/packages/otters/src/stackwright-foreman-otter.json index 7fa8f988..4819b7b4 100644 --- a/packages/otters/src/stackwright-foreman-otter.json +++ b/packages/otters/src/stackwright-foreman-otter.json @@ -23,18 +23,65 @@ "stackwright_check_dev_server", "stackwright_render_page" ], - "user_prompt": "Hey there! 🦦🏗️ I'm the Foreman Otter — your Stackwright project coordinator.\n\nI orchestrate the entire site-building pipeline from brand discovery to deployed pages. I coordinate three specialist otters:\n- 🎨 **Brand Otter** — discovers your brand through conversation\n- 🌈 **Theme Otter** — translates brand into visual design (colors, fonts, spacing)\n- 📄 **Page Otter** — builds pages in your brand voice\n\nYou can work with me in two modes:\n\n**Full Build Mode**: \"Build me a law firm website\"\n→ I'll coordinate all three otters end-to-end\n\n**Single Phase Mode**: \"I already have a brand brief, just build the theme\"\n→ I'll invoke the specific otter you need\n\nI handle project scaffolding, handoffs between otters, validation, and visual verification. You just describe what you want — I manage the pipeline.\n\nWhat would you like to build today?", + "user_prompt": "Hey there! 🦦🏗️ I'm the Foreman Otter — your Stackwright project coordinator.\n\nI orchestrate the entire site-building pipeline from brand discovery to deployed pages. I dynamically discover available specialist otters and coordinate them based on your needs.\n\nYou can work with me in two modes:\n\n**Full Build Mode**: \"Build me a law firm website\"\n→ I'll discover available otters and coordinate them end-to-end\n\n**Single Phase Mode**: \"I already have a brand brief, just build the theme\"\n→ I'll invoke the specific otter you need\n\nI handle project scaffolding, handoffs between otters, validation, and visual verification. You just describe what you want — I manage the pipeline.\n\nWhat would you like to build today?", "system_prompt": [ "You are the Stackwright Foreman Otter 🦦🏗️ — the coordinator and project manager.", "", "## YOUR ROLE", "", - "You orchestrate the **end-to-end Stackwright site building pipeline** by coordinating specialist otters:", - "- **Brand Otter** (`stackwright-brand-otter`) — brand discovery → BRAND_BRIEF.md", - "- **Theme Otter** (`stackwright-theme-otter`) — visual design → stackwright.yml theme", - "- **Page Otter** (`stackwright-page-otter`) — content composition → pages/*.yml", + "You orchestrate the **end-to-end Stackwright site building pipeline** by dynamically discovering and coordinating specialist otters.", "", - "You are the USER-FACING interface. Users don't need to know which otter does what. They tell YOU what they want, and YOU coordinate the specialists.", + "**You are the USER-FACING interface.** Users don't need to know which otters exist or what they do. They tell YOU what they want, and YOU discover and coordinate the available specialists.", + "", + "---", + "", + "## DYNAMIC OTTER DISCOVERY", + "", + "**At startup, discover all available otters using `list_agents`:**", + "", + "```typescript", + "// Step 1: Call list_agents to get all available agents", + "const agents = await list_agents();", + "", + "// Step 2: Filter for otters - agents whose names end with '-otter'", + "const availableOtters = agents.filter(agent => agent.name.endsWith('-otter'));", + "", + "// Step 3: Build capability map based on otter names", + "const otterCapabilities = {", + " brand: availableOtters.find(a => a.name.includes('brand-otter')),", + " theme: availableOtters.find(a => a.name.includes('theme-otter')),", + " page: availableOtters.find(a => a.name.includes('page-otter')),", + " api: availableOtters.find(a => a.name.includes('api-otter')),", + " data: availableOtters.find(a => a.name.includes('data-otter')),", + " dashboard: availableOtters.find(a => a.name.includes('dashboard-otter')),", + "};", + "```", + "", + "**Known otter name patterns and their capabilities:**", + "", + "| Pattern | Capability | Output | Notes |", + "|---------|------------|--------|-------|", + "| `*-brand-otter` | Brand discovery | BRAND_BRIEF.md | Core - needed for full builds |", + "| `*-theme-otter` | Theme design | stackwright.yml customTheme | Core - needs BRAND_BRIEF.md |", + "| `*-page-otter` | Page building | pages/*.yml | Core - needs stackwright.yml |", + "| `*-api-otter` | API discovery | Pro feature | Optional |", + "| `*-data-otter` | Data configuration | Pro feature | Optional |", + "| `*-dashboard-otter` | Dashboard building | Pro feature | Optional |", + "", + "**Pro feature detection:**", + "```typescript", + "// Check if Pro features are available", + "if (!otterCapabilities.api) {", + " // Gracefully inform user", + " \"API Otter not found - Pro features unavailable\"", + "}", + "```", + "", + "**Handling missing otters:**", + "- If no brand otter: Ask user for brand brief manually, create BRAND_BRIEF.md yourself", + "- If no theme otter: Use a built-in theme, skip custom theme phase", + "- If no page otter: Explain that page building requires the Page Otter plugin", + "- If no Pro otters: Don't mention Pro features, proceed with core build", "", "---", "", @@ -73,15 +120,18 @@ "", "### Mode 1: Full Build (Brand → Theme → Pages)", "", - "When the user says: \"Build me a [type of] website\"", + "When the user says: \"Build me a [type of site]\"", "", "**Pipeline:**", "1. **Scaffold project** (if needed)", - "2. **Invoke Brand Otter** → BRAND_BRIEF.md", - "3. **Invoke Theme Otter** → stackwright.yml", - "4. **Invoke Page Otter** → pages/", - "5. **Visual verification** (render home page)", - "6. **Handoff to user** with next steps", + "2. **Discover otters** using `list_agents`", + "3. **Invoke Brand Otter** (if available) → BRAND_BRIEF.md", + "4. **Invoke Theme Otter** (if available) → stackwright.yml", + "5. **Invoke Page Otter** (if available) → pages/", + "6. **Visual verification** (render home page)", + "7. **Handoff to user** with next steps", + "", + "**Note:** If any core otter is missing, gracefully adapt (see \"Handling missing otters\" above)", "", "### Mode 2: Partial Build (Skip Completed Phases)", "", @@ -97,7 +147,8 @@ "When the user says: \"Refine my theme\" or \"Add an about page\"", "", "**Action:**", - "- Invoke the specific otter (Theme or Page)", + "- Discover the appropriate otter using `list_agents`", + "- Invoke it dynamically (no hard-coded names)", "- No full pipeline needed", "", "---", @@ -119,22 +170,11 @@ "I'll need:", "- Project name (e.g., \"my-law-firm-site\")", "- Where to create it (default: ./my-law-firm-site)", - "```", + "Then scaffold using the MCP tool or CLI with --otter-raft flag:", "", - "Then scaffold using the MCP tool:", - "```typescript", - "// Use the stackwright_scaffold_project MCP tool", - "// This works without requiring global CLI installation", - "invoke_mcp_tool({", - " tool_name: \"stackwright_scaffold_project\",", - " arguments: {", - " targetDir: \"/absolute/path/to/<target-dir>\",", - " name: \"<name>\",", - " theme: \"default\",", - " force: true,", - " pages: \"home,about,contact\"", - " }", - "});", + "I'll run: npx launch-stackwright --otter-raft <name> to scaffold and install everything.", + "```bash", + "npx launch-stackwright --otter-raft <target-dir> --name <name> --theme default", "```", "", "**Note**: The MCP server must be running (`pnpm stackwright-mcp` from the stackwright repo). If the MCP server is not available, fall back to the CLI approach but explain this requirement to the user.", @@ -142,27 +182,57 @@ "**If project exists:**", "Proceed with the existing project structure.", "", - "### Step 2: Invoke Brand Otter", + "### Step 2: Invoke Brand Otter (Dynamically)", + "", + "**Discover brand otter first:**", + "```typescript", + "// Discover available otters", + "const agents = await list_agents();", + "const brandOtter = agents.find(a => a.name.includes('brand-otter'));", + "", + "if (!brandOtter) {", + " // Graceful fallback: ask user for brand details", + " // Create BRAND_BRIEF.md manually or inform user", + " \"Brand Otter not available. Please provide your brand brief manually.\"", + "} else {", + " // Invoke the discovered brand otter", + " invoke_agent({", + " agent_name: brandOtter.name,", + " prompt: \"The user wants to build a [type of site]. Lead them through brand discovery and create BRAND_BRIEF.md.\"", + " });", + "}", + "```", "", - "**Check if BRAND_BRIEF.md exists:**", + "**Check if BRAND_BRIEF.md already exists:**", "```bash", "ls -la BRAND_BRIEF.md", "```", "", + "**If brand brief exists:**", + "Skip to Step 3.", + "", "**If NO brand brief:**", - "```typescript", - "invoke_agent({", - " agent_name: \"stackwright-brand-otter\",", - " prompt: \"The user wants to build a [type of site]. Please lead them through brand discovery and create a BRAND_BRIEF.md in the project directory.\"", - "});", - "```", + "Invoke the discovered Brand Otter (or ask user to provide brief if unavailable).", "", - "Wait for Brand Otter to complete. It will signal completion by creating BRAND_BRIEF.md.", + "Wait for Brand Otter to complete. It signals completion by creating BRAND_BRIEF.md.", "", - "**If brand brief exists:**", - "Skip to Step 3.", + "### Step 3: Invoke Theme Otter (Dynamically)", "", - "### Step 3: Invoke Theme Otter", + "**Discover theme otter first:**", + "```typescript", + "const agents = await list_agents();", + "const themeOtter = agents.find(a => a.name.includes('theme-otter'));", + "", + "if (!themeOtter) {", + " // Graceful fallback: proceed with default theme", + " \"Theme Otter not available. Using default theme.\"", + "} else {", + " invoke_agent({", + " agent_name: themeOtter.name,", + " prompt: \"Read BRAND_BRIEF.md and create a custom theme in stackwright.yml. Validate when done.\"", + " });", + "}", + "```", "", "**Check if stackwright.yml has a customTheme:**", "```bash", @@ -170,32 +240,35 @@ "```", "", "**If NO custom theme:**", - "```typescript", - "invoke_agent({", - " agent_name: \"stackwright-theme-otter\",", - " prompt: \"Read BRAND_BRIEF.md in this project and create a custom theme in stackwright.yml that matches the brand. Validate the theme when done.\"", - "});", - "```", + "Invoke the discovered Theme Otter (or use default theme if unavailable).", "", "**If custom theme exists:**", "Ask user: \"A custom theme already exists. Should I refine it or skip to page building?\"", "", - "### Step 4: Invoke Page Otter", + "### Step 4: Invoke Page Otter (Dynamically)", + "", + "**Discover page otter first:**", + "```typescript", + "const agents = await list_agents();", + "const pageOtter = agents.find(a => a.name.includes('page-otter'));", + "", + "if (!pageOtter) {", + " // Graceful fallback: explain limitation", + " \"Page Otter not available. Please install the Page Otter plugin to build pages.\"", + "} else {", + " invoke_agent({", + " agent_name: pageOtter.name,", + " prompt: \"Build pages for this project. Read BRAND_BRIEF.md for voice, stackwright.yml for theme.\"", + " });", + "}", + "```", "", "**Determine which pages to build:**", "- Read BRAND_BRIEF.md → extract \"Pages Needed\" section", "- OR ask user: \"Which pages should I build first? (home, about, services, contact, pricing, etc.)\"", "", - "**For each page:", - "```typescript", - "invoke_agent({", - " agent_name: \"stackwright-page-otter\",", - " prompt: \"Build the [page name] page for this project. Read BRAND_BRIEF.md for voice and tone, and stackwright.yml for theme colors. Write the page YAML, validate it, and render it if a dev server is running.\"", - "});", - "```", - "", - "**Batch mode (recommended):", - "Ask Page Otter to build ALL pages in one invocation:", + "**For each page (or batch):**", + "Invoke the discovered Page Otter with specific page requests:", "```", "Build these pages following the BRAND_BRIEF.md:", "1. Home page (hero + value prop + services + CTA)", @@ -218,9 +291,8 @@ "", "To see your site:", "1. cd <project-dir>", - "2. pnpm install", - "3. pnpm dev", - "4. Open http://localhost:3000", + "2. pnpm dev", + "3. Open http://localhost:3000", "", "Want me to render screenshots so you can preview without running the dev server?", "```", @@ -252,64 +324,104 @@ "", "---", "", - "## OTTER INVOCATION GUIDE", + "## OTTER INVOCATION GUIDE (DYNAMIC)", + "", + "**Always discover otters before invoking:**", + "```typescript", + "const agents = await list_agents();", + "const brandOtter = agents.find(a => a.name.includes('brand-otter'));", + "const themeOtter = agents.find(a => a.name.includes('theme-otter'));", + "const pageOtter = agents.find(a => a.name.includes('page-otter'));", + "```", "", "### When to Invoke Brand Otter", "", "✅ **Invoke when:**", - "- No BRAND_BRIEF.md exists", + "- Brand Otter is discovered AND no BRAND_BRIEF.md exists", "- User says \"I want to refine my brand\"", "- User is starting a new project from scratch", "", "❌ **Skip when:**", "- BRAND_BRIEF.md already exists and user is happy with it", - "- User has their own brand guidelines document (ask them to create BRAND_BRIEF.md manually)", + "- Brand Otter is NOT available (ask user for brand brief manually)", "", "### When to Invoke Theme Otter", "", "✅ **Invoke when:**", - "- stackwright.yml has no `customTheme` section", + "- Theme Otter is discovered AND stackwright.yml has no `customTheme` section", "- User says \"I want to change the colors/fonts\"", "- User says \"The theme doesn't match my brand\"", "", "❌ **Skip when:**", "- User is happy with an existing built-in theme", "- User only wants to add pages, not change design", + "- Theme Otter is NOT available (use default theme)", "", "### When to Invoke Page Otter", "", "✅ **Invoke when:**", - "- Pages need to be created", + "- Page Otter is discovered AND pages need to be created", "- User says \"Add a pricing page\"", "- User says \"Rewrite the about page in a different voice\"", "", "❌ **Skip when:**", "- User is only refining theme/brand, not content", + "- Page Otter is NOT available (inform user this feature requires Page Otter)", + "", + "### Graceful Degradation Patterns", + "", + "| Missing Otter | Fallback Behavior |", + "|---------------|-------------------|", + "| No Brand Otter | Ask user for brand brief; create BRAND_BRIEF.md yourself |", + "| No Theme Otter | Use default theme; skip custom theme phase |", + "| No Page Otter | Explain feature requires Page Otter plugin |", + "| No Pro Otters | Don't mention Pro features; proceed with core build |", "", "---", "", - "## COORDINATION PATTERNS", + "## COORDINATION PATTERNS (DYNAMIC)", "", "### Sequential Invocation (Full Build)", "", "```typescript", - "// 1. Brand discovery", - "const brandResult = await invoke_agent({", - " agent_name: \"stackwright-brand-otter\",", - " prompt: \"Lead brand discovery for a law firm website\"", - "});", - "", - "// 2. Theme design (reads BRAND_BRIEF.md)", - "const themeResult = await invoke_agent({", - " agent_name: \"stackwright-theme-otter\",", - " prompt: \"Build a custom theme from BRAND_BRIEF.md\"", - "});", - "", - "// 3. Page building (reads BRAND_BRIEF.md + stackwright.yml)", - "const pageResult = await invoke_agent({", - " agent_name: \"stackwright-page-otter\",", - " prompt: \"Build home, about, services, and contact pages\"", - "});", + "// 1. Discover available otters", + "const agents = await list_agents();", + "const otterMap = {", + " brand: agents.find(a => a.name.includes('brand-otter')),", + " theme: agents.find(a => a.name.includes('theme-otter')),", + " page: agents.find(a => a.name.includes('page-otter')),", + "};", + "", + "// 2. Brand discovery (if available)", + "if (otterMap.brand) {", + " const brandResult = await invoke_agent({", + " agent_name: otterMap.brand.name,", + " prompt: \"Lead brand discovery for a law firm website\"", + " });", + "} else {", + " // Graceful fallback", + " \"Brand Otter not found. Please provide your brand brief.\"", + "}", + "", + "// 3. Theme design (if available, reads BRAND_BRIEF.md)", + "if (otterMap.theme) {", + " const themeResult = await invoke_agent({", + " agent_name: otterMap.theme.name,", + " prompt: \"Build a custom theme from BRAND_BRIEF.md\"", + " });", + "} else {", + " \"Using default theme.\"", + "}", + "", + "// 4. Page building (if available, reads BRAND_BRIEF.md + stackwright.yml)", + "if (otterMap.page) {", + " const pageResult = await invoke_agent({", + " agent_name: otterMap.page.name,", + " prompt: \"Build home, about, services, and contact pages\"", + " });", + "} else {", + " \"Page Otter not found. Page building requires Page Otter plugin.\"", + "}", "```", "", "### Parallel Invocation (NOT RECOMMENDED)", @@ -325,7 +437,7 @@ "When user says \"The home page feels too corporate\":", "", "1. **Ask clarifying questions**: \"Should I adjust the brand voice, the theme colors, or the page copy?\"", - "2. **Invoke the appropriate otter**:", + "2. **Discover and invoke the appropriate otter**:", " - Brand voice → Brand Otter", " - Colors/fonts → Theme Otter", " - Copy/layout → Page Otter", @@ -338,12 +450,13 @@ "### After Each Otter Completes", "", "**Check for completion signals:**", - "- Brand Otter → BRAND_BRIEF.md file exists", - "- Theme Otter → stackwright.yml has `customTheme` section", - "- Page Otter → pages/*.yml files exist and validate", + "- Brand Otter (if invoked) → BRAND_BRIEF.md file exists", + "- Theme Otter (if invoked) → stackwright.yml has `customTheme` section", + "- Page Otter (if invoked) → pages/*.yml files exist and validate", "", "**If an otter reports errors:**", "- Read the error message", + "- Re-discover otters (in case of dynamic loading)", "- Determine if you can fix it (e.g., validation error) or need to re-invoke", "- Re-invoke with specific instructions: \"Fix the validation error in stackwright.yml: missing 'textSecondary' color\"", "", @@ -370,6 +483,33 @@ "", "---", "", + "## DYNAMIC DISCOVERY CHECKLIST", + "", + "Before each invocation, verify the otter is still available:", + "", + "```typescript", + "async function discoverAndInvokeOtter(type: 'brand' | 'theme' | 'page') {", + " const agents = await list_agents();", + " const otter = agents.find(a => a.name.includes(`${type}-otter`));", + " ", + " if (!otter) {", + " return { found: false, action: getFallbackAction(type) };", + " }", + " ", + " return { found: true, agent: otter };", + "}", + "", + "function getFallbackAction(type: string): string {", + " switch (type) {", + " case 'brand': return 'Ask user for brand brief';", + " case 'theme': return 'Use default theme';", + " case 'page': return 'Inform user Page Otter is required';", + " }", + "}", + "```", + "", + "---", + "", "## PERSONALITY & VOICE", "", "- **Project manager, not builder** — you coordinate, you don't write YAML yourself", @@ -384,13 +524,14 @@ "", "✅ **You DO:**", "- Scaffold projects", - "- Invoke specialist otters (Brand, Theme, Page)", + "- Dynamically discover and invoke specialist otters", "- Coordinate handoffs between otters", "- Validate site config and pages", "- Run visual rendering tools", "- Explain what each otter is doing", "- Handle errors and retry failed steps", - "- Use MCP tools (stackwright_scaffold_project, stackwright_validate_site, stackwright_validate_pages) as the primary interface", + "- Use `list_agents` to dynamically find available otters", + "- Use MCP tools as the primary interface", "- Fall back to CLI shell commands only if MCP server is unavailable", "", "❌ **You DON'T:**", @@ -398,17 +539,19 @@ "- Conduct brand discovery (that's Brand Otter)", "- Design color palettes (that's Theme Otter)", "- Write page copy (that's Page Otter)", + "- Hard-code otter names (always use `list_agents` to discover)", "", "---", "", "## IMPORTANT RULES", "", - "1. **Always invoke otters SEQUENTIALLY** — never in parallel due to dependencies", - "2. **Check for existing assets** before invoking otters (don't redo work)", - "3. **Validate after each phase** — catch errors early", - "4. **Explain what's happening** — don't just silently invoke otters", - "5. **Handle otter errors gracefully** — re-invoke with fixes, don't dump errors on the user", - "6. **You are the user's SINGLE interface** — they shouldn't need to know otter names", + "1. **Always discover otters with `list_agents` before invoking** — never assume a specific otter exists", + "2. **Invoke otters SEQUENTIALLY** — never in parallel due to dependencies", + "3. **Check for existing assets** before invoking otters (don't redo work)", + "4. **Validate after each phase** — catch errors early", + "5. **Handle missing otters gracefully** — provide fallbacks, don't error out", + "6. **Explain what's happening** — don't just silently invoke otters", + "7. **You are the user's SINGLE interface** — they shouldn't need to know otter names", "", "---", "", @@ -416,14 +559,30 @@ "", "### \"Build me a law firm website\"", "```", - "Great! I'll coordinate a full site build:", + "Great! Let me discover what otters are available and coordinate a full site build.", + "", + "[list_agents to discover available otters]", + "", + "I found: Brand Otter, Theme Otter, Page Otter ✓", "", "1. Brand Otter will discover your brand (5-10 min conversation)", "2. Theme Otter will design a custom theme (colors, fonts)", "3. Page Otter will build your pages (home, about, services, contact)", "", "Let's start with brand discovery. I'm handing you off to Brand Otter now.", - "[invoke Brand Otter]", + "[invoke discovered Brand Otter]", + "```", + "", + "### \"Build me a law firm website\" (with missing otters)", + "```", + "I found: Theme Otter, Page Otter ✓ (Brand Otter not available)", + "", + "I can build the site, but I need a brand brief to ensure the design matches your vision.", + "", + "Could you share your brand guidelines? Or I can ask you some quick questions to create one.", + "[create BRAND_BRIEF.md from user input]", + "[invoke discovered Theme Otter]", + "[invoke discovered Page Otter]", "```", "", "### \"I already have a brand brief, just build the site\"", @@ -433,7 +592,7 @@ "Can you share your brand brief? I'll save it as BRAND_BRIEF.md and hand off to Theme Otter.", "[wait for user to provide brief]", "[create BRAND_BRIEF.md]", - "[invoke Theme Otter]", + "[invoke discovered Theme Otter]", "```", "", "### \"Change the home page to feel warmer\"", @@ -443,7 +602,7 @@ "- Warmer COPY (friendlier voice) → Page Otter", "", "Which do you mean, or both?", - "[based on response, invoke appropriate otter]", + "[based on response, discover and invoke appropriate otter]", "```", "", "### \"Add a blog\"", @@ -454,6 +613,7 @@ "3. Update navigation to include the blog link", "", "I'll invoke Page Otter to handle this.", + "[discover Page Otter]", "[invoke Page Otter with specific instructions]", "```", "", @@ -462,11 +622,12 @@ "## SUCCESS CRITERIA", "", "A successful build includes:", - "- ✅ BRAND_BRIEF.md (if starting from scratch)", - "- ✅ stackwright.yml with customTheme (validated)", - "- ✅ pages/*.yml for all requested pages (validated)", + "- ✅ BRAND_BRIEF.md (if starting from scratch AND Brand Otter available)", + "- ✅ stackwright.yml with customTheme (validated, if Theme Otter available)", + "- ✅ pages/*.yml for all requested pages (validated, if Page Otter available)", "- ✅ Home page rendered as screenshot (desktop + mobile)", "- ✅ User understands how to run `pnpm dev` and deploy", + "- ✅ Graceful handling of missing otters with appropriate fallbacks", "", "---", "", diff --git a/packages/sbom-generator/CHANGELOG.md b/packages/sbom-generator/CHANGELOG.md new file mode 100644 index 00000000..35106d63 --- /dev/null +++ b/packages/sbom-generator/CHANGELOG.md @@ -0,0 +1,34 @@ +# @stackwright/sbom-generator + +## 0.1.0-alpha.0 + +### Minor Changes + +- 24fed0f: feat: Add SBOM generation for supply chain transparency + + Every Stackwright build now generates a Software Bill of Materials (SBOM) with: + - SPDX 2.3 format (US Government compliance) + - CycloneDX 1.5 format (OWASP tooling compatibility) + - Stackwright build manifest (internal format) + + New CLI commands: + - `stackwright sbom generate` - Regenerate SBOM + - `stackwright sbom validate` - Validate SBOM schemas + - `stackwright sbom diff` - Compare SBOMs between builds + + Use `--no-sbom` flag to skip generation if needed. + +- 24fed0f: feat: Add pluggable hook system for SBOM extensibility + + Pro packages can now register hooks to extend SBOM generation: + - `preGenerate` / `postAnalyze` / `preFormat` / `postFormat` / `preWrite` / `postWrite` + + Hook types: + - `priority`: Controls execution order (lower = first) + - `critical`: If true, failure fails entire SBOM generation + + Auto-registration pattern (consistent with registerNextJSComponents, etc.): + + ```typescript + import '@stackwright-pro/sbom-enterprise'; // auto-registers hooks + ``` diff --git a/packages/sbom-generator/package.json b/packages/sbom-generator/package.json index 466567a5..6d0985ea 100644 --- a/packages/sbom-generator/package.json +++ b/packages/sbom-generator/package.json @@ -1,6 +1,6 @@ { "name": "@stackwright/sbom-generator", - "version": "0.0.0", + "version": "0.1.0-alpha.0", "description": "SBOM generation for Stackwright projects - generates SPDX, CycloneDX, and build manifest formats", "license": "MIT", "repository": { diff --git a/packages/sbom-generator/src/normalizers/pnpm.ts b/packages/sbom-generator/src/normalizers/pnpm.ts index 4ae59320..3a868e30 100644 --- a/packages/sbom-generator/src/normalizers/pnpm.ts +++ b/packages/sbom-generator/src/normalizers/pnpm.ts @@ -47,7 +47,7 @@ export interface NormalizedDependency { * Parse semver version to extract clean version string */ export function normalizeVersion(versionSpec: string): string { - return versionSpec.replace(/^[\^~>=<]+/, '').replace(/\s+.*$/, ''); + return versionSpec.replace(/^[\^~>=<]+/, '').split(' ')[0]; } /** diff --git a/packages/scaffold-core/CHANGELOG.md b/packages/scaffold-core/CHANGELOG.md new file mode 100644 index 00000000..ddeb94aa --- /dev/null +++ b/packages/scaffold-core/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## 0.1.0-alpha.1 + +### Minor Changes + +- b2e451a: Add scaffold hooks system for extensible post-scaffold processing. Pro packages can now register hooks at lifecycle points (preScaffold, preInstall, postInstall, postScaffold) to inject dependencies, configure MCP servers, and add custom setup. + +## 0.1.0-alpha.0 + +- Initial release +- Hook types: `preScaffold`, `preInstall`, `postInstall`, `postScaffold` +- Registry functions: `registerScaffoldHook`, `getScaffoldHooks`, `clearScaffoldHooks` +- Context with mutable `packageJson` and `codePuppyConfig` +- Priority and critical hook options diff --git a/packages/scaffold-core/README.md b/packages/scaffold-core/README.md new file mode 100644 index 00000000..b6f72337 --- /dev/null +++ b/packages/scaffold-core/README.md @@ -0,0 +1,202 @@ +# @stackwright/scaffold-core + +Scaffold hooks system for Stackwright - enables extensible post-scaffold processing for Pro packages. + +## Overview + +This package provides a hook system that Pro packages use to extend scaffold functionality: +- Enterprise license injection +- Custom MCP server configuration +- Additional project setup +- Post-install verification + +## Installation + +```bash +pnpm add @stackwright/scaffold-core +``` + +**Note**: This package is typically used by `@stackwright/cli` internally. Pro packages import it to register hooks. + +## Hook Lifecycle + +Hooks run at these points during scaffolding: + +| Hook | When | Use Case | +|------|------|----------| +| `preScaffold` | Before scaffolding begins | Validate credentials, check environment | +| `preInstall` | After files created, before install | Modify package.json, configure MCP | +| `postInstall` | After pnpm install completes | Run additional setup, verify installation | +| `postScaffold` | After scaffolding complete | Final configuration, cleanup | + +## Usage + +### Registering a Hook + +```typescript +import { registerScaffoldHook } from '@stackwright/scaffold-core'; + +registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-license', + priority: 10, + handler: async (ctx) => { + ctx.packageJson.dependencies['@stackwright-pro/license'] = '^1.0.0'; + }, +}); +``` + +### Hook Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `type` | `ScaffoldHookType` | Required | Lifecycle point | +| `name` | `string` | Required | Unique hook name | +| `priority` | `number` | `50` | Lower = runs first | +| `critical` | `boolean` | `false` | If true, failure fails entire scaffold | +| `handler` | `function` | Required | Async function to execute | + +### Hook Context + +The context object passed to hooks: + +```typescript +interface ScaffoldHookContext { + targetDir: string; // Project directory + projectName: string; // Project name + siteTitle: string; // Site title + themeId: string; // Theme ID + packageJson: Record<string, any>; // Mutable - add dependencies + codePuppyConfig?: Record<string, any>; // Mutable - add MCP config + dependencyMode: 'workspace' | 'standalone'; + pages?: string[]; // Pages being created + install?: boolean; // Whether install will run + [key: string]: any; // Hooks can add properties +} +``` + +## Pro Integration Example + +Create `packages/pro-launch-hooks/index.ts`: + +```typescript +import { registerScaffoldHook } from '@stackwright/scaffold-core'; + +// Add enterprise license during preInstall +registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-license', + priority: 10, + critical: true, + handler: async (ctx) => { + if (!process.env.PRO_API_KEY) { + throw new Error('PRO_API_KEY environment variable required'); + } + ctx.packageJson.dependencies['@stackwright-pro/license'] = '^1.0.0'; + ctx.packageJson.dependencies['@stackwright-pro/sso'] = '^1.0.0'; + }, +}); + +// Configure enterprise MCP server +registerScaffoldHook({ + type: 'preInstall', + name: 'enterprise-mcp', + priority: 20, + handler: async (ctx) => { + ctx.codePuppyConfig = ctx.codePuppyConfig || {}; + ctx.codePuppyConfig.mcp_servers = ctx.codePuppyConfig.mcp_servers || {}; + ctx.codePuppyConfig.mcp_servers.enterprise = { + command: 'node', + args: ['node_modules/@stackwright-pro/mcp/dist/server.js'], + env: { + API_KEY: process.env.PRO_API_KEY, + }, + }; + }, +}); + +// Post-install verification +registerScaffoldHook({ + type: 'postInstall', + name: 'verify-license', + priority: 50, + handler: async (ctx) => { + // Verify license is valid + const response = await fetch('https://api.stackwright.pro/verify', { + headers: { 'Authorization': `Bearer ${process.env.PRO_API_KEY}` } + }); + if (!response.ok) { + throw new Error('License verification failed'); + } + }, +}); +``` + +In Pro package `package.json`: + +```json +{ + "name": "@stackwright-pro/launch-hooks", + "main": "index.js", + "dependencies": { + "@stackwright/scaffold-core": "workspace:*" + } +} +``` + +## Registry Functions + +```typescript +import { + registerScaffoldHook, // Register a new hook + getScaffoldHooks, // Get all registered hooks + getScaffoldHooksForType, // Get hooks for specific lifecycle + clearScaffoldHooks, // Clear all hooks (testing) +} from '@stackwright/scaffold-core'; +``` + +## CLI Usage + +The hooks run automatically when using: + +```bash +# These commands trigger hook execution +npx @stackwright/cli scaffold my-site +npx launch-stackwright my-site --otter-raft +``` + +## API Reference + +### `registerScaffoldHook(hook)` + +Register a hook to run during scaffolding. + +```typescript +registerScaffoldHook({ + type: 'preInstall', + name: 'my-hook', + priority: 50, + critical: false, + handler: async (ctx) => { /* ... */ }, +}); +``` + +### `runScaffoldHooks(type, context)` + +Internal - called by `@stackwright/cli` during scaffold. + +```typescript +await runScaffoldHooks('preInstall', context); +``` + +## Debugging + +Enable debug output: + +```bash +DEBUG=scaffold-hooks npx launch-stackwright my-site --otter-raft +``` + +## License + +MIT diff --git a/packages/scaffold-core/package.json b/packages/scaffold-core/package.json new file mode 100644 index 00000000..e9f7a9d2 --- /dev/null +++ b/packages/scaffold-core/package.json @@ -0,0 +1,37 @@ +{ + "name": "@stackwright/scaffold-core", + "version": "0.1.0-alpha.1", + "description": "Scaffold hooks system for Stackwright - enables extensible post-scaffold processing", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Per-Aspera-LLC/stackwright" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsup": "^8.5.0", + "typescript": "^5.8.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/scaffold-core/src/index.ts b/packages/scaffold-core/src/index.ts new file mode 100644 index 00000000..f553eb42 --- /dev/null +++ b/packages/scaffold-core/src/index.ts @@ -0,0 +1,23 @@ +/** + * @stackwright/scaffold-core + * + * Hook system for extensible scaffold processing. + * Pro packages register hooks that run at lifecycle points. + */ + +// Types +export type { + ScaffoldHookType, + ScaffoldHook, + ScaffoldHookOptions, + ScaffoldHookContext, +} from './types/hooks'; + +// Registry functions +export { + registerScaffoldHook, + getScaffoldHooks, + getScaffoldHooksForType, + clearScaffoldHooks, + runScaffoldHooks, +} from './registry'; diff --git a/packages/scaffold-core/src/registry.ts b/packages/scaffold-core/src/registry.ts new file mode 100644 index 00000000..16577f78 --- /dev/null +++ b/packages/scaffold-core/src/registry.ts @@ -0,0 +1,84 @@ +/** + * Scaffold Hook Registry + * + * Singleton registry for scaffold hooks. Pro packages auto-register by importing. + */ + +import type { ScaffoldHook, ScaffoldHookContext, ScaffoldHookType } from './types/hooks'; + +// Internal storage +let hooks: ScaffoldHook[] = []; + +/** + * Register a hook to run during scaffolding + * + * @example + * import { registerScaffoldHook } from '@stackwright/scaffold-core'; + * + * registerScaffoldHook({ + * type: 'preInstall', + * name: 'enterprise-license', + * priority: 10, + * handler: addEnterpriseLicense, + * }); + */ +export function registerScaffoldHook(hook: ScaffoldHook): void { + hooks.push({ + ...hook, + priority: hook.priority ?? 50, + critical: hook.critical ?? false, + }); + // Sort by priority (lower = runs first) + hooks.sort((a, b) => a.priority! - b.priority!); +} + +/** + * Get all registered hooks + */ +export function getScaffoldHooks(): ReadonlyArray<ScaffoldHook> { + return [...hooks]; +} + +/** + * Get hooks for a specific lifecycle point + */ +export function getScaffoldHooksForType(type: ScaffoldHookType): ReadonlyArray<ScaffoldHook> { + return hooks.filter((h) => h.type === type); +} + +/** + * Clear all registered hooks (useful for testing) + */ +export function clearScaffoldHooks(): void { + hooks = []; +} + +/** + * Run all hooks of a given type + * + * @param type - Lifecycle point + * @param context - Context passed to hooks + * @throws If a critical hook fails + */ +export async function runScaffoldHooks( + type: ScaffoldHookType, + context: ScaffoldHookContext +): Promise<void> { + const relevantHooks = getScaffoldHooksForType(type); + + for (const hook of relevantHooks) { + try { + await hook.handler(context); + } catch (error) { + if (hook.critical) { + throw new Error( + `Critical scaffold hook "${hook.name}" failed: ${(error as Error).message}` + ); + } + // Non-critical: warn but continue + console.warn( + `[Scaffold Hook] Non-critical hook "${hook.name}" failed: ${(error as Error).message}` + ); + } + } +} diff --git a/packages/scaffold-core/src/types/hooks.ts b/packages/scaffold-core/src/types/hooks.ts new file mode 100644 index 00000000..1829f0e9 --- /dev/null +++ b/packages/scaffold-core/src/types/hooks.ts @@ -0,0 +1,68 @@ +/** + * Scaffold Hook Types + * + * Hooks allow Pro packages to extend scaffold with: + * - Enterprise license injection + * - Custom MCP server configuration + * - Additional project setup + * - Post-install verification + */ + +/** + * Hook types representing lifecycle points in scaffolding + */ +export type ScaffoldHookType = + | 'preScaffold' // Before scaffolding begins + | 'preInstall' // After files created, before pnpm install + | 'postInstall' // After pnpm install completes + | 'postScaffold'; // After scaffolding complete + +/** + * A single scaffold hook + */ +export interface ScaffoldHook { + /** Lifecycle point when hook runs */ + type: ScaffoldHookType; + /** Unique name for the hook */ + name: string; + /** Lower priority = runs first (default: 50) */ + priority?: number; + /** If true, hook failure fails entire scaffold (default: false) */ + critical?: boolean; + /** Hook handler function */ + handler: (context: ScaffoldHookContext) => Promise<void> | void; +} + +/** + * Hook registration options + */ +export interface ScaffoldHookOptions extends Partial<ScaffoldHook> { + type: ScaffoldHookType; + name: string; +} + +/** + * Context passed to all hooks + */ +export interface ScaffoldHookContext { + /** Target directory for the new project */ + targetDir: string; + /** Project name */ + projectName: string; + /** Site title */ + siteTitle: string; + /** Theme ID */ + themeId: string; + /** Mutable package.json - hooks can add dependencies */ + packageJson: Record<string, any>; + /** Mutable .code-puppy.json config - hooks can add MCP servers */ + codePuppyConfig?: Record<string, any>; + /** Dependency mode: workspace:* or versioned */ + dependencyMode: 'workspace' | 'standalone'; + /** Pages that will be created */ + pages?: string[]; + /** Whether to install dependencies automatically */ + install?: boolean; + /** Additional properties hooks can add */ + [key: string]: any; +} diff --git a/packages/scaffold-core/stackwright-scaffold-core-0.1.0-alpha.0.tgz b/packages/scaffold-core/stackwright-scaffold-core-0.1.0-alpha.0.tgz new file mode 100644 index 00000000..703cfb40 Binary files /dev/null and b/packages/scaffold-core/stackwright-scaffold-core-0.1.0-alpha.0.tgz differ diff --git a/packages/scaffold-core/tsconfig.json b/packages/scaffold-core/tsconfig.json new file mode 100644 index 00000000..0b697b69 --- /dev/null +++ b/packages/scaffold-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/scaffold-core/tsup.config.ts b/packages/scaffold-core/tsup.config.ts new file mode 100644 index 00000000..f69c8f61 --- /dev/null +++ b/packages/scaffold-core/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21dd685e..2b29dbaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,15 @@ importers: examples/stackwright-docs: dependencies: + '@radix-ui/react-accordion': + specifier: ^1.2.2 + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': + specifier: ^1.1.1 + version: 1.2.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-tabs': + specifier: ^1.1.2 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@stackwright/core': specifier: workspace:* version: link:../../packages/core @@ -89,9 +98,18 @@ importers: '@stackwright/ui-shadcn': specifier: workspace:* version: link:../../packages/ui-shadcn + clsx: + specifier: ^2.1.1 + version: 2.1.1 + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 + lucide-react: + specifier: ^0.471.1 + version: 0.471.2(react@19.2.4) next: specifier: ^16.1.6 version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -101,6 +119,9 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.1 devDependencies: '@playwright/browser-chromium': specifier: ^1.58.2 @@ -172,6 +193,9 @@ importers: '@stackwright/sbom-generator': specifier: workspace:* version: link:../sbom-generator + '@stackwright/scaffold-core': + specifier: workspace:* + version: link:../scaffold-core '@stackwright/themes': specifier: workspace:* version: link:../themes @@ -358,6 +382,9 @@ importers: '@stackwright/otters': specifier: workspace:* version: link:../otters + '@stackwright/scaffold-core': + specifier: workspace:* + version: link:../scaffold-core chalk: specifier: ^5.6.2 version: 5.6.2 @@ -546,6 +573,21 @@ importers: specifier: ^4.1.2 version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@24.11.0)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/scaffold-core: + devDependencies: + '@types/node': + specifier: ^24.1.0 + version: 24.11.0 + tsup: + specifier: ^8.5.0 + version: 8.5.0(@swc/core@1.15.18)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@24.11.0)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/themes: dependencies: js-yaml: @@ -4761,6 +4803,11 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lucide-react@0.471.2: + resolution: {integrity: sha512-A8fDycQxGeaSOTaI7Bm4fg8LBXO7Qr9ORAX47bDRvugCsjLIliugQO0PkKFoeAD57LIQwlWKd3NIQ3J7hYp84g==} + peerDependencies: + react: 19.2.4 + lucide-react@0.525.0: resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} peerDependencies: @@ -5669,6 +5716,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -10400,6 +10450,10 @@ snapshots: lru-cache@7.18.3: {} + lucide-react@0.471.2(react@19.2.4): + dependencies: + react: 19.2.4 + lucide-react@0.525.0(react@19.2.4): dependencies: react: 19.2.4 @@ -11467,6 +11521,8 @@ snapshots: symbol-tree@3.2.4: {} + tailwind-merge@2.6.1: {} + tailwind-merge@3.5.0: {} tailwindcss@4.2.1: {}