diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 9f505c54..ae3edbf1 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,16 +1,16 @@ {"_type":"issue","id":"stackwright-a1g","title":"feat: Otter Agents AI-assisted site generation pipeline — complete remaining work","description":"Four-otter pipeline (Foreman, Brand, Theme, Page) core architecture is complete. Remaining work: end-to-end testing of full pipeline, create 3-5 example sites generated by the otter raft, refine handoff protocol between otters, design Collection Otter for Phase 2. Affects packages/otters. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/236","status":"open","priority":1,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:48Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:48Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-ufs","title":"feat(types,core): migrate to explicit type field discrimination in content renderer","description":"Content renderer currently uses Object.entries(item)[0] to discriminate content types — relies on JS object insertion order (not guaranteed), prevents TypeScript discriminated unions, produces poor error messages. Migrate to explicit type field on every content item (z.object({ type: z.literal('...'), ... })). Breaking change — coordinate with next major version bump. Acceptance: update Zod schemas, content renderer, TypeScript unions, all YAML files, tests, JSON schemas, AGENTS.md tables. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/344","status":"closed","priority":1,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:24:59Z","closed_at":"2026-05-19T00:24:59Z","close_reason":"Already implemented: contentRenderer.tsx uses item.type for discrimination, content.ts uses z.literal('type') on all schemas. Shipped before this triage run.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-8je","title":"a11y: dark mode #FCC03E amber text on near-white backgrounds fails WCAG AA","description":"Dark theme uses #FCC03E (amber/yellow) for headings and sidebar links but darkColors resolves to near-white backgrounds (#fdfdfd, #f5f5f5, #f6f6f6), producing contrast ratios of 1.51–1.61. WCAG AA requires 4.5:1. Fix: darken darkColors backgrounds to ~#1a1a1a/#2a2a2a. Affects @stackwright/themes dark color configuration. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/439","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:07Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:03Z","closed_at":"2026-05-19T00:25:03Z","close_reason":"Fixed in PR #448 (fix/a11y-cluster): darkColors.primary changed to #92400E and accent to #B45309 in stackwright-docs.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-6hl","title":"a11y: code_block tabindex=0 on \u003cpre\u003e creates keyboard trap (WCAG 2.1.2)","description":"code_block component adds tabindex=0 to \u003cpre\u003e elements making them focusable, but Tab cannot escape once focus enters. WCAG 2.1 SC 2.1.2 No Keyboard Trap violation. Fix: remove tabindex or add keyboard escape mechanism. Affects Getting Started, CLI Reference, and Otter Raft pages. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/440","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:02Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:01Z","closed_at":"2026-05-19T00:25:01Z","close_reason":"Fixed in PR #448 (fix/a11y-cluster): removed tabIndex={0} from CodeBlock \u003cpre\u003e.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-mca","title":"a11y: skip link present but does not move focus to main content","description":"Skip link is in DOM but clicking/activating it does not move focus to main content area. Fix: add id='main-content' to \u003cmain\u003e wrapper in page layout and ensure skip link href points to #main-content. Affects @stackwright/core page layout. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/441","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:57Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:00Z","closed_at":"2026-05-19T00:25:00Z","close_reason":"Already fixed: PageLayout.tsx line 103 has href='#main-content' on skip link, line 173 has id='main-content' on \u003cmain\u003e. Correct implementation already shipped.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-ufs","title":"feat(types,core): migrate to explicit type field discrimination in content renderer","description":"Content renderer currently uses Object.entries(item)[0] to discriminate content types — relies on JS object insertion order (not guaranteed), prevents TypeScript discriminated unions, produces poor error messages. Migrate to explicit type field on every content item (z.object({ type: z.literal('...'), ... })). Breaking change — coordinate with next major version bump. Acceptance: update Zod schemas, content renderer, TypeScript unions, all YAML files, tests, JSON schemas, AGENTS.md tables. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/344","status":"closed","priority":1,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:10:14Z","closed_at":"2026-05-19T00:10:14Z","close_reason":"Already implemented: contentRenderer.tsx uses item.type for discrimination, content.ts uses z.literal('type') on all schemas. Shipped before this triage run.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-8je","title":"a11y: dark mode #FCC03E amber text on near-white backgrounds fails WCAG AA","description":"Dark theme uses #FCC03E (amber/yellow) for headings and sidebar links but darkColors resolves to near-white backgrounds (#fdfdfd, #f5f5f5, #f6f6f6), producing contrast ratios of 1.51–1.61. WCAG AA requires 4.5:1. Fix: darken darkColors backgrounds to ~#1a1a1a/#2a2a2a. Affects @stackwright/themes dark color configuration. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/439","status":"open","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:07Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:34:07Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-6hl","title":"a11y: code_block tabindex=0 on \u003cpre\u003e creates keyboard trap (WCAG 2.1.2)","description":"code_block component adds tabindex=0 to \u003cpre\u003e elements making them focusable, but Tab cannot escape once focus enters. WCAG 2.1 SC 2.1.2 No Keyboard Trap violation. Fix: remove tabindex or add keyboard escape mechanism. Affects Getting Started, CLI Reference, and Otter Raft pages. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/440","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:02Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:15:57Z","started_at":"2026-05-19T00:15:57Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-mca","title":"a11y: skip link present but does not move focus to main content","description":"Skip link is in DOM but clicking/activating it does not move focus to main content area. Fix: add id='main-content' to \u003cmain\u003e wrapper in page layout and ensure skip link href points to #main-content. Affects @stackwright/core page layout. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/441","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:57Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:10:25Z","closed_at":"2026-05-19T00:10:25Z","close_reason":"Already fixed: PageLayout.tsx line 103 has href='#main-content' on skip link, line 173 has id='main-content' on \u003cmain\u003e element. Correct implementation already shipped.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-a45","title":"feat(cli): stackwright test:a11y — portable WCAG accessibility audit for any Stackwright site","description":"Add stackwright test:a11y CLI command. Auto-discovers pages from prebuild output, tests WCAG 2.1 AA via axe-core + Playwright, tests both light/dark modes. Phased implementation: (1) page discovery utility, (2) portable axe runner, (3) CLI command, (4) launch-stackwright integration, (5) MCP/Otter integration. High-value differentiator for enterprise/regulated-environment angle. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/270","status":"open","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:54Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-u4y","title":"a11y: tabbed_content component missing ARIA arrow-key keyboard pattern","description":"The tabbed_content component does not respond to ArrowLeft/ArrowRight keys to switch tabs, violating ARIA APG Tabs pattern. Only mouse/click works. Fix: add keydown handler on each tab button to move focus and activate prev/next tab on arrow keys. Affects @stackwright/ui-shadcn or @stackwright/core tabs component. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/442","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:53Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:02Z","closed_at":"2026-05-19T00:25:02Z","close_reason":"Fixed in PR #448 (fix/a11y-cluster): added ArrowLeft/ArrowRight/Home/End keyboard handler to TabbedContentGrid tablist.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-als","title":"feat(cli): add CLI commands for integration management (list/get/add)","description":"Add stackwright integrations list/get/add CLI commands with interactive prompts (inquirer) and colored output (chalk). Depends on integration schema. Estimated 2-3 hours. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/238","status":"open","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:51Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:51Z","dependencies":[{"issue_id":"stackwright-als","depends_on_id":"stackwright-5ak","type":"blocks","created_at":"2026-05-18T18:34:27Z","created_by":"Stackwright Bot","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-y6m","title":"a11y: Content Types page missing \u003ctitle\u003e element and lang attribute on \u003chtml\u003e","description":"The /content-types page is missing a non-empty \u003ctitle\u003e element and the lang attribute on \u003chtml\u003e. Both are WCAG 2.1 Level A requirements (SC 2.4.2 Page Titled, SC 3.1.1 Language of Page). Fix: ensure page \u003chead\u003e includes \u003ctitle\u003e and \u003chtml\u003e has lang='en'. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/443","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:49Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:01Z","closed_at":"2026-05-19T00:25:01Z","close_reason":"Already fixed: content-types/content.yml has meta.title set, StackwrightDocument defaults lang='en'. No code change needed.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-2o8","title":"feat(mcp): add MCP tools for integration management (list/get/add)","description":"Add stackwright_list_integrations, stackwright_get_integration, stackwright_add_integration MCP tools. Reads/writes stackwright.yml. Depends on integrations config schema issue. Estimated 3-4 hours. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/239","status":"open","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:47Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:47Z","dependencies":[{"issue_id":"stackwright-2o8","depends_on_id":"stackwright-5ak","type":"blocks","created_at":"2026-05-18T18:34:26Z","created_by":"Stackwright Bot","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-5y5","title":"a11y: carousel white text on amber background fails WCAG AA (2.14:1 contrast)","description":"Carousel/feature cards use color:#FFFFFF on background:#f59e0b (amber), producing 2.14:1 contrast ratio. WCAG AA requires 4.5:1 normal / 3:1 large text. Fix: darken background, change text color, or adjust card background YAML field. Affects @stackwright/core carousel component and the hellostackwright showcase page. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/444","status":"closed","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:03Z","closed_at":"2026-05-19T00:25:03Z","close_reason":"Fixed in PR #448 (fix/a11y-cluster): OverflowImageCard now uses getBetterTextColor('#1a1a1a','#FFFFFF',backgroundColor).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-5ak","title":"feat(types): add integrations config field to siteConfigSchema","description":"Add integrations field to siteConfigSchema in @stackwright/types. Schema accepts array of integration objects with type (openapi|graphql|rest), name, and passthrough additional properties. Estimated 1-2 hours. This is a prerequisite for the MCP and CLI integration management tools. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/240","status":"closed","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:43Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:00Z","closed_at":"2026-05-19T00:25:00Z","close_reason":"Already implemented: integrationConfigSchema fully built in siteConfig.ts with openapi|graphql|rest enum, name kebab-case validation, path traversal protection. Unblocks stackwright-2o8 and stackwright-als.","dependency_count":0,"dependent_count":2,"comment_count":0} -{"_type":"issue","id":"stackwright-34q","title":"a11y: TopAppBar renders empty \u003ch1\u003e when title empty and logo present","description":"When title is empty and wordmark logo is used, TopAppBar renders \u003ch1\u003e\u003c/h1\u003e. Screen readers skip it and rely solely on logo alt text. Fix: add visually-hidden \u003ch1\u003e (not display:none, but zero-size off-screen) to give screen reader users the site title. Affects @stackwright/core TopAppBar component. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/438","status":"closed","priority":3,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:12Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:25:02Z","closed_at":"2026-05-19T00:25:02Z","close_reason":"Fixed in PR #448 (fix/a11y-cluster): added visually-hidden h1 to TopAppBar when title empty + logo present.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-u4y","title":"a11y: tabbed_content component missing ARIA arrow-key keyboard pattern","description":"The tabbed_content component does not respond to ArrowLeft/ArrowRight keys to switch tabs, violating ARIA APG Tabs pattern. Only mouse/click works. Fix: add keydown handler on each tab button to move focus and activate prev/next tab on arrow keys. Affects @stackwright/ui-shadcn or @stackwright/core tabs component. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/442","status":"open","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:53Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:53Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-als","title":"feat(cli): add CLI commands for integration management (list/get/add)","description":"Add stackwright integrations list/get/add CLI commands with interactive prompts (inquirer) and colored output (chalk). Depends on integration schema. Estimated 2-3 hours. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/238","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:51Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:16:44Z","started_at":"2026-05-19T00:16:44Z","dependencies":[{"issue_id":"stackwright-als","depends_on_id":"stackwright-5ak","type":"blocks","created_at":"2026-05-18T18:34:27Z","created_by":"Stackwright Bot","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-y6m","title":"a11y: Content Types page missing \u003ctitle\u003e element and lang attribute on \u003chtml\u003e","description":"The /content-types page is missing a non-empty \u003ctitle\u003e element and the lang attribute on \u003chtml\u003e. Both are WCAG 2.1 Level A requirements (SC 2.4.2 Page Titled, SC 3.1.1 Language of Page). Fix: ensure page \u003chead\u003e includes \u003ctitle\u003e and \u003chtml\u003e has lang='en'. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/443","status":"open","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:49Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:49Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-2o8","title":"feat(mcp): add MCP tools for integration management (list/get/add)","description":"Add stackwright_list_integrations, stackwright_get_integration, stackwright_add_integration MCP tools. Reads/writes stackwright.yml. Depends on integrations config schema issue. Estimated 3-4 hours. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/239","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:47Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:16:44Z","started_at":"2026-05-19T00:16:44Z","dependencies":[{"issue_id":"stackwright-2o8","depends_on_id":"stackwright-5ak","type":"blocks","created_at":"2026-05-18T18:34:26Z","created_by":"Stackwright Bot","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-5y5","title":"a11y: carousel white text on amber background fails WCAG AA (2.14:1 contrast)","description":"Carousel/feature cards use color:#FFFFFF on background:#f59e0b (amber), producing 2.14:1 contrast ratio. WCAG AA requires 4.5:1 normal / 3:1 large text. Fix: darken background, change text color, or adjust card background YAML field. Affects @stackwright/core carousel component and the hellostackwright showcase page. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/444","status":"open","priority":2,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:44Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-5ak","title":"feat(types): add integrations config field to siteConfigSchema","description":"Add integrations field to siteConfigSchema in @stackwright/types. Schema accepts array of integration objects with type (openapi|graphql|rest), name, and passthrough additional properties. Estimated 1-2 hours. This is a prerequisite for the MCP and CLI integration management tools. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/240","status":"closed","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:43Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:10:14Z","closed_at":"2026-05-19T00:10:14Z","close_reason":"Already implemented: integrationConfigSchema fully built in siteConfig.ts with openapi|graphql|rest enum, name kebab-case validation, path traversal protection, and integrations field in siteConfigSchema. Unblocks stackwright-2o8 and stackwright-als.","dependency_count":0,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"stackwright-34q","title":"a11y: TopAppBar renders empty \u003ch1\u003e when title empty and logo present","description":"When title is empty and wordmark logo is used, TopAppBar renders \u003ch1\u003e\u003c/h1\u003e. Screen readers skip it and rely solely on logo alt text. Fix: add visually-hidden \u003ch1\u003e (not display:none, but zero-size off-screen) to give screen reader users the site title. Affects @stackwright/core TopAppBar component. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/438","status":"open","priority":3,"issue_type":"bug","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:12Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:34:12Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-bls","title":"feat(collections): implement CollectionProvider interface, FileCollectionProvider, and S3CollectionProvider","description":"Full Collections system. Architecture confirmed (Option A): content/\u003cname\u003e/ dirs compiled to public/stackwright-content/collections/. Implement CollectionProvider interface + FileCollectionProvider + S3CollectionProvider (peer dep @aws-sdk/client-s3). New @stackwright/collections package. Full acceptance criteria: prebuild pipeline, type generation, scaffold template update, example posts/ collection. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/85","status":"open","priority":3,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:06Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:34:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-614","title":"feat: internationalization — multi-language content support","description":"No i18n support currently. Design needed: per-locale content dirs vs inline locale maps in YAML. Locale-aware routing. Longer-term — design must be scoped before implementation begins. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/112","status":"open","priority":3,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:34:00Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:34:00Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-cch","title":"feat(mcp): live page preview tool — render and screenshot agent-authored content","description":"stackwright_preview_page MCP tool accepts slug, ensures dev server running, triggers prebuild, Playwright screenshot, returns as MCP image block. Depends on server lifecycle management work. Open questions: inline YAML support, mobile viewport. Response time target \u003c10s. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/123","status":"open","priority":3,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:55Z","created_by":"Stackwright Bot","updated_at":"2026-05-18T22:33:55Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.changeset/feat-integration-management.md b/.changeset/feat-integration-management.md new file mode 100644 index 00000000..04e8e595 --- /dev/null +++ b/.changeset/feat-integration-management.md @@ -0,0 +1,6 @@ +--- +"@stackwright/cli": minor +"@stackwright/mcp": minor +--- + +Add integration management commands and MCP tools: `stackwright integrations list/get/add` CLI commands and `stackwright_list_integrations`, `stackwright_get_integration`, `stackwright_add_integration` MCP tools for managing OpenAPI, GraphQL, and REST integrations in stackwright.yml. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c241cc74..8c4cab86 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -12,6 +12,7 @@ import { registerGenerateAgentDocs } from './commands/generate-agent-docs'; import { registerGitOps } from './commands/git-ops'; import { registerBoard } from './commands/board'; import { registerCollection } from './commands/collection'; +import { registerIntegration } from './commands/integration'; import { registerCompose } from './commands/compose'; import { registerPreview } from './commands/preview'; import { registerSBOM } from './commands/sbom'; @@ -39,6 +40,7 @@ async function main(): Promise { registerGitOps(program); registerBoard(program); registerCollection(program); + registerIntegration(program); registerCompose(program); registerPreview(program); registerSBOM(program); diff --git a/packages/cli/src/commands/integration.ts b/packages/cli/src/commands/integration.ts new file mode 100644 index 00000000..f4225d0c --- /dev/null +++ b/packages/cli/src/commands/integration.ts @@ -0,0 +1,194 @@ +import { Command } from 'commander'; +import path from 'path'; +import fs from 'fs-extra'; +import chalk from 'chalk'; +import yaml from 'js-yaml'; +import { outputResult, outputError, formatError } from '../utils/json-output'; +import { readSiteConfig, writeSiteConfig } from './site'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface IntegrationEntry { + type: 'openapi' | 'graphql' | 'rest'; + name: string; + [key: string]: unknown; +} + +export interface ListIntegrationsResult { + integrations: IntegrationEntry[]; + path: string; +} + +export interface GetIntegrationResult { + integration: IntegrationEntry | null; + path: string; +} + +export interface AddIntegrationResult { + path: string; + created: boolean; + updated: boolean; +} + +// --------------------------------------------------------------------------- +// Pure functions (exported for programmatic use and MCP) +// --------------------------------------------------------------------------- + +export function listIntegrations(siteConfigPath: string): ListIntegrationsResult { + const { content, path: resolvedPath } = readSiteConfig(siteConfigPath); + const raw = yaml.load(content) as Record; + const integrations = (raw?.integrations as IntegrationEntry[] | undefined) ?? []; + return { integrations, path: resolvedPath }; +} + +export function getIntegration(siteConfigPath: string, name: string): GetIntegrationResult { + const { integrations, path: resolvedPath } = listIntegrations(siteConfigPath); + const integration = integrations.find((i) => i.name === name) ?? null; + return { integration, path: resolvedPath }; +} + +export function addIntegration( + siteConfigPath: string, + entry: IntegrationEntry +): AddIntegrationResult { + const { content, path: resolvedPath } = readSiteConfig(siteConfigPath); + const raw = yaml.load(content) as Record; + const integrations = (raw?.integrations as IntegrationEntry[] | undefined) ?? []; + + const existingIdx = integrations.findIndex((i) => i.name === entry.name); + const updated = existingIdx >= 0; + if (updated) { + integrations[existingIdx] = entry; + } else { + integrations.push(entry); + } + + raw.integrations = integrations; + const newContent = yaml.dump(raw, { lineWidth: 120 }); + writeSiteConfig(resolvedPath, newContent); + return { path: resolvedPath, created: !updated, updated }; +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +function resolveSiteConfig(projectRoot: string): string { + const candidates = ['stackwright.yml', 'stackwright.yaml']; + for (const name of candidates) { + const p = path.join(projectRoot, name); + if (fs.existsSync(p)) return p; + } + return path.join(projectRoot, 'stackwright.yml'); +} + +// --------------------------------------------------------------------------- +// Commander registration +// --------------------------------------------------------------------------- + +export function registerIntegration(program: Command): void { + const integration = program + .command('integrations') + .description('Manage Stackwright integrations (OpenAPI, GraphQL, REST)'); + + integration + .command('list') + .description('List all configured integrations') + .option('--project-root ', 'Path to project root', process.cwd()) + .option('--json', 'Output as JSON') + .action((opts: { projectRoot: string; json?: boolean }) => { + const json = Boolean(opts.json); + try { + const siteConfigPath = resolveSiteConfig(opts.projectRoot); + const result = listIntegrations(siteConfigPath); + outputResult(result, { json }, () => { + if (result.integrations.length === 0) { + console.log(chalk.dim('No integrations configured.')); + return; + } + console.log(chalk.bold(`Integrations (${result.integrations.length}):`)); + for (const i of result.integrations) { + console.log(` ${chalk.cyan(i.name)} ${chalk.dim(`[${i.type}]`)}`); + } + }); + } catch (err) { + outputError(formatError(err), 'LIST_INTEGRATIONS_FAILED', { json }); + } + }); + + integration + .command('get ') + .description('Show details for a specific integration') + .option('--project-root ', 'Path to project root', process.cwd()) + .option('--json', 'Output as JSON') + .action((name: string, opts: { projectRoot: string; json?: boolean }) => { + const json = Boolean(opts.json); + try { + const siteConfigPath = resolveSiteConfig(opts.projectRoot); + const result = getIntegration(siteConfigPath, name); + if (!result.integration) { + outputError(`Integration "${name}" not found.`, 'NOT_FOUND', { json }); + } + outputResult(result.integration, { json }, () => { + console.log(chalk.bold(`Integration: ${result.integration!.name}`)); + console.log(yaml.dump(result.integration, { indent: 2 })); + }); + } catch (err) { + outputError(formatError(err), 'GET_INTEGRATION_FAILED', { json }); + } + }); + + integration + .command('add') + .description('Add or update an integration in stackwright.yml') + .requiredOption('--name ', 'Integration name (kebab-case)') + .requiredOption('--type ', 'Integration type: openapi, graphql, or rest') + .option('--spec ', 'Path to OpenAPI spec file (for openapi type)') + .option('--endpoint ', 'API endpoint URL (for graphql/rest type)') + .option('--project-root ', 'Path to project root', process.cwd()) + .option('--json', 'Output as JSON') + .action( + (opts: { + name: string; + type: string; + spec?: string; + endpoint?: string; + projectRoot: string; + json?: boolean; + }) => { + const json = Boolean(opts.json); + const { type } = opts; + if (!['openapi', 'graphql', 'rest'].includes(type)) { + outputError(`Invalid type "${type}". Must be: openapi, graphql, rest`, 'INVALID_TYPE', { + json, + }); + } + try { + const entry: IntegrationEntry = { + type: type as 'openapi' | 'graphql' | 'rest', + name: opts.name, + ...(opts.spec ? { spec: opts.spec } : {}), + ...(opts.endpoint ? { endpoint: opts.endpoint } : {}), + }; + const siteConfigPath = resolveSiteConfig(opts.projectRoot); + const result = addIntegration(siteConfigPath, entry); + const verb = result.updated ? 'Updated' : 'Added'; + outputResult({ verb, path: result.path, integration: entry }, { json }, () => { + console.log( + chalk.green(`✓ ${verb} integration "${entry.name}" [${entry.type}] in ${result.path}`) + ); + }); + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + outputError( + formatError(err), + code === 'VALIDATION_FAILED' ? 'VALIDATION_FAILED' : 'ADD_INTEGRATION_FAILED', + { json }, + 2 + ); + } + } + ); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a83594f1..e07dfcf4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,6 +17,7 @@ export { generateAgentDocs } from './commands/generate-agent-docs'; export { stageChanges, openPr } from './commands/git-ops'; export { getBoard, parseBoard } from './commands/board'; export { listCollections, addCollection, resolveContentDir } from './commands/collection'; +export { listIntegrations, getIntegration, addIntegration } from './commands/integration'; export { composeSite } from './commands/compose'; export { preview } from './commands/preview'; export { validateSiteComposition } from './utils/site-validator'; @@ -55,6 +56,12 @@ export type { CollectionListResult, AddCollectionResult, } from './commands/collection'; +export type { + IntegrationEntry, + ListIntegrationsResult, + GetIntegrationResult, + AddIntegrationResult, +} from './commands/integration'; export type { ComposeSiteResult, ComposeSiteOptions } from './commands/compose'; export type { PreviewResult, PreviewOptions } from './commands/preview'; export type { diff --git a/packages/core/src/components/base/CodeBlock.tsx b/packages/core/src/components/base/CodeBlock.tsx index c288021b..6c6b7dbd 100644 --- a/packages/core/src/components/base/CodeBlock.tsx +++ b/packages/core/src/components/base/CodeBlock.tsx @@ -69,7 +69,6 @@ export function CodeBlock({ code, language, lineNumbers = false, background }: C )}
 {
+      try {
+        const siteConfigPath = resolveSiteConfig(projectRoot);
+        const result = listIntegrations(siteConfigPath);
+        if (result.integrations.length === 0) {
+          return { content: [{ type: 'text', text: 'No integrations configured.' }] };
+        }
+        const lines = result.integrations.map((i) => {
+          const specPart = i.spec ? ` — spec: ${String(i.spec)}` : '';
+          const endpointPart = i.endpoint ? ` — endpoint: ${String(i.endpoint)}` : '';
+          return `  ${i.name} [${i.type}]${specPart}${endpointPart}`;
+        });
+        return {
+          content: [
+            {
+              type: 'text',
+              text: `Integrations (${result.integrations.length}):\n${lines.join('\n')}`,
+            },
+          ],
+        };
+      } catch (err) {
+        return {
+          content: [{ type: 'text', text: `Error: ${(err as Error).message}` }],
+          isError: true,
+        };
+      }
+    }
+  );
+
+  server.tool(
+    'stackwright_get_integration',
+    'Get details for a specific integration by name from stackwright.yml.',
+    {
+      projectRoot: z.string().describe('Absolute path to the root of the Stackwright project'),
+      name: z.string().describe('The integration name (e.g. "logistics", "inventory")'),
+    },
+    async ({ projectRoot, name }) => {
+      try {
+        const siteConfigPath = resolveSiteConfig(projectRoot);
+        const result = getIntegration(siteConfigPath, name);
+        if (!result.integration) {
+          return {
+            content: [{ type: 'text', text: `Integration "${name}" not found.` }],
+            isError: true,
+          };
+        }
+        return {
+          content: [{ type: 'text', text: JSON.stringify(result.integration, null, 2) }],
+        };
+      } catch (err) {
+        return {
+          content: [{ type: 'text', text: `Error: ${(err as Error).message}` }],
+          isError: true,
+        };
+      }
+    }
+  );
+
+  server.tool(
+    'stackwright_add_integration',
+    'Add or update an integration in stackwright.yml. Supports OpenAPI, GraphQL, and REST integrations.',
+    {
+      projectRoot: z.string().describe('Absolute path to the root of the Stackwright project'),
+      name: z
+        .string()
+        .min(1)
+        .max(50)
+        .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, 'Name must be kebab-case')
+        .describe('Unique integration name (kebab-case, e.g. "logistics-api")'),
+      type: z.enum(['openapi', 'graphql', 'rest']).describe('Integration type'),
+      spec: z.string().optional().describe('Path to OpenAPI spec file (for openapi type)'),
+      endpoint: z.string().optional().describe('API endpoint URL (for graphql/rest type)'),
+    },
+    async ({ projectRoot, name, type, spec, endpoint }) => {
+      try {
+        const siteConfigPath = resolveSiteConfig(projectRoot);
+        const entry = {
+          type,
+          name,
+          ...(spec ? { spec } : {}),
+          ...(endpoint ? { endpoint } : {}),
+        };
+        const result = addIntegration(siteConfigPath, entry);
+        const verb = result.updated ? 'Updated' : 'Added';
+        return {
+          content: [
+            { type: 'text', text: `✓ ${verb} integration "${name}" [${type}] in ${result.path}` },
+          ],
+        };
+      } catch (err) {
+        return {
+          content: [{ type: 'text', text: `Error: ${(err as Error).message}` }],
+          isError: true,
+        };
+      }
+    }
+  );
+}