version: '3' # Tasks operate from the workspace root (frontend/). Editor commands pass # `editor` as the vite project root (positional after `build` / before the # mode flag) or use `--project editor/...` for tsc — so the editor lives # under frontend/editor/ without each task needing a cd. vars: # Dev-only browser-tab label so concurrent worktrees are distinguishable. Only # the worktree folder basename (e.g. "wt1") is exposed — never the full path, # hostname, or user. Dropped from production builds. DEV_LABEL: sh: >- {{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}} tasks: install: desc: "Install dependencies" run: once cmds: - '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}' sources: - package-lock.json - package.json status: - npm ls --depth=0 env: CI: '{{ .CI | default "false" }}' prepare:env: internal: true run: when_changed deps: [install] vars: MODE: '{{.MODE | default ""}}' cmds: - npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}} sources: - editor/scripts/setup-env.mts generates: - editor/.env.local - editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local prepare:icons: internal: true run: once deps: [install] cmds: - node editor/scripts/generate-icons.js prepare:og: internal: true run: when_changed desc: "Regenerate OG/social-preview metadata from the tool registry" cmds: - node editor/scripts/generate-og-metadata.mjs sources: - editor/src/core/types/toolId.ts - editor/src/core/utils/urlMapping.ts - editor/src/core/data/useTranslatedToolRegistry.tsx - editor/public/og_images/*.png generates: - editor/src/core/data/ogImageMap.json - editor/public/og-metadata.json prepare: desc: "Set up dev environment" run: when_changed vars: MODE: '{{.MODE | default ""}}' deps: - task: prepare:env vars: { MODE: '{{.MODE}}' } - prepare:icons - prepare:og # ============================================================ # Development # ============================================================ dev:_run: internal: true ignore_error: true vars: MODE: '{{.MODE}}' PORT: '{{.PORT | default "5173"}}' BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' OPEN: '{{.OPEN | default ""}}' env: BACKEND_URL: '{{.BACKEND_URL}}' STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' cmds: - npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}} # Separate from dev:_run rather than a flag on it: Task sets an `env:` key even # when its value resolves to empty, and Vite treats an empty process.env VITE_* as # authoritative over the committed editor/.env, so folding these in blanks Supabase # config for the core, proprietary and desktop dev servers. dev:_run:saas: internal: true ignore_error: true # The backend's own env files, so both halves target one project. Paths are # relative to this taskfile's dir, `frontend`. dotenv: ['../app/.env.saas.local', '../app/.env.saas'] vars: PORT: '{{.PORT | default "5173"}}' BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' OPEN: '{{.OPEN | default ""}}' SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' env: BACKEND_URL: '{{.BACKEND_URL}}' STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' SAAS_ENV: '{{.SAAS_ENV}}' # A real process.env VITE_* beats a committed .env in Vite (loadEnv applies # process.env last), which is what lets this override editor/.env. # # These must stay `sh:`, not Go templates: dotenv values are visible to Task's # embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is # always empty. VITE_SUPABASE_URL: sh: | case "${SAAS_ENV:-dev}" in staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;; *) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;; esac echo "https://${ref}.supabase.co" VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: sh: | case "${SAAS_ENV:-dev}" in staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;; *) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;; esac cmds: - 'echo ">> frontend {{.SAAS_ENV}}: Supabase $VITE_SUPABASE_URL, backend $BACKEND_URL"' - npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}} dev: desc: "Start frontend dev server" cmds: - task: dev:proprietary vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:core: desc: "Start frontend dev server in core mode" deps: [prepare] cmds: - task: dev:_run vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:proprietary: desc: "Start frontend dev server in proprietary mode" deps: [prepare] cmds: - task: dev:_run vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:saas: desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)" deps: - task: prepare vars: { MODE: saas } vars: SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' # prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves # the committed editor/.env alone. RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}' cmds: - task: '{{.RUNNER}}' vars: MODE: saas PORT: '{{.PORT}}' BACKEND_URL: '{{.BACKEND_URL}}' OPEN: '{{.OPEN}}' SAAS_ENV: '{{.SAAS_ENV}}' staging:saas: desc: "Start frontend dev server against the shared v3 staging project" cmds: - task: dev:saas vars: SAAS_ENV: staging PORT: '{{.PORT}}' BACKEND_URL: '{{.BACKEND_URL}}' OPEN: '{{.OPEN}}' dev:desktop: desc: "Start frontend dev server in desktop mode" deps: - task: prepare vars: { MODE: desktop } cmds: - task: dev:_run vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:prototypes: desc: "Start frontend dev server in prototypes mode" deps: [prepare] cmds: - task: dev:_run vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } # ============================================================ # Build # ============================================================ build: desc: "Production build (default mode)" deps: [prepare] cmds: - npx vite build editor build:core: desc: "Build for core mode" deps: [prepare] cmds: - npx vite build editor --mode core build:proprietary: desc: "Build for proprietary mode" deps: [prepare] vars: PREVIEW: '{{.PREVIEW | default ""}}' cmds: - '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary' build:saas: desc: "Build for SaaS mode" deps: - task: prepare vars: { MODE: saas } cmds: - npx vite build editor --mode saas build:desktop: desc: "Build for desktop mode" deps: - task: prepare vars: { MODE: desktop } cmds: - npx vite build editor --mode desktop build:prototypes: desc: "Build for prototypes mode" deps: [prepare] cmds: - npx vite build editor --mode prototypes storybook: desc: "Start Storybook dev server" deps: [prepare] cmds: - npx storybook dev -p 6006 {{.CLI_ARGS}} storybook:build: desc: "Build static Storybook" deps: [prepare] cmds: - npx storybook build {{.CLI_ARGS}} storybook:browser: internal: true desc: "Install the Chromium build the story scan runs in" run: once deps: [install] cmds: - npx playwright install chromium storybook:test: desc: "Scan every story in real Chromium: it must render and pass axe" deps: [prepare, storybook:browser] cmds: # Runs each story as a browser test. Pass a filter through, e.g. # task frontend:storybook:test -- Button - npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}} storybook:a11y:light: desc: "a11y gate over every story in light mode" deps: [prepare, storybook:browser] cmds: - node .storybook/a11y-scan.mjs {{.CLI_ARGS}} - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt storybook:a11y:dark: desc: "a11y gate over every story in dark mode" deps: [prepare, storybook:browser] cmds: - SCAN_THEME=dark node .storybook/a11y-scan.mjs {{.CLI_ARGS}} - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json storybook:a11y: desc: "a11y gate over every story, light and dark" cmds: - task: storybook:a11y:light - task: storybook:a11y:dark storybook:a11y:changed: desc: "a11y gate over the stories this branch affects (default base origin/main)" summary: | Scans the stories a branch affects, which is what pull requests run — a full scan takes ~30 minutes, far too long to sit in front of every merge. A story is affected if its file changed, or if a same-named sibling source file changed (editing Button.tsx or Button.css re-scans Button.stories.tsx — the story renders the live component, so a component edit changes what the story shows without touching the story file). Changes that ripple further than a component's own stories are covered by the nightly full sweep. Pass a base ref through CLI_ARGS, e.g. task frontend:storybook:a11y:changed -- origin/release vars: BASE: '{{.CLI_ARGS | default "origin/main"}}' CHANGED: sh: node .storybook/a11y-changed.mjs {{.CLI_ARGS | default "origin/main"}} cmds: - cmd: | if [ -z '{{.CHANGED}}' ]; then echo "a11y: no story files affected vs {{.BASE}} — nothing to check" exit 0 fi rc=0 task frontend:storybook:a11y:light -- {{.CHANGED}} || rc=1 task frontend:storybook:a11y:dark -- {{.CHANGED}} || rc=1 exit $rc storybook:a11y:record: desc: "Re-record both a11y baselines (run after intentionally fixing/adding violations)" deps: [prepare, storybook:browser] cmds: - node .storybook/a11y-scan.mjs - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record - SCAN_THEME=dark node .storybook/a11y-scan.mjs - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record --baseline .storybook/a11y-baseline.dark.json # ============================================================ # Code quality # ============================================================ lint: desc: "Run linting" deps: [install] cmds: - task: lint:oxlint - task: lint:colors - task: lint:css lint:css: desc: "Lint stylesheets for duplicate selectors" deps: [install] cmds: # Covers the whole editor tree, including the portal/processor layer and # public/css. Vendored CSS and build output are excluded via ignoreFiles # in stylelint.config.mjs. - npx stylelint "editor/**/*.css" lint:colors: desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components" aliases: [lint:colours] deps: [install] cmds: - node editor/scripts/lint/theme-lint.mjs - node editor/scripts/lint/theme-lint.mjs css-colors - node editor/scripts/lint/theme-lint.mjs code-colors - node editor/scripts/lint/theme-lint.mjs no-primitives contrast: desc: "Report low-contrast theme token pairs (warning only, never blocks)" deps: [install] cmds: - node editor/scripts/lint/theme-lint.mjs contrast lint:oxlint: desc: "Run oxlint linting" deps: [install] cmds: - npx oxlint --config oxlint.config.ts --max-warnings=0 lint:fix: desc: "Auto-fix lint issues" deps: [install] cmds: - npx oxlint --config oxlint.config.ts --fix format: desc: "Auto-fix code formatting" deps: [install] cmds: - npx oxfmt --write . format:check: desc: "Check code formatting" deps: [install] cmds: - npx oxfmt --check . fix: desc: "Auto-fix lint and format" cmds: - task: format - task: lint:fix typecheck: desc: "Typecheck default build of the app" cmds: - task: typecheck:proprietary typecheck:_run: internal: true cmds: - 'npx tsc --noEmit --project {{.PROJECT}}' typecheck:core: desc: "Typecheck core build variant" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: editor/src/core/tsconfig.json } typecheck:proprietary: desc: "Typecheck proprietary build variant" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: editor/src/proprietary/tsconfig.json } typecheck:saas: desc: "Typecheck SaaS build variant" deps: - task: prepare vars: { MODE: saas } cmds: - task: typecheck:_run vars: { PROJECT: editor/src/saas/tsconfig.json } typecheck:desktop: desc: "Typecheck desktop build variant" deps: - task: prepare vars: { MODE: desktop } cmds: - task: typecheck:_run vars: { PROJECT: editor/src/desktop/tsconfig.json } typecheck:cloud: desc: "Typecheck cloud shared layer (standalone)" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: editor/src/cloud/tsconfig.json } typecheck:scripts: desc: "Typecheck scripts" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: editor/scripts/tsconfig.json } typecheck:prototypes: desc: "Typecheck prototypes build variant" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: editor/src/prototypes/tsconfig.json } typecheck:portal: desc: "Typecheck developer portal build variant" deps: [install] cmds: - task: typecheck:_run vars: { PROJECT: editor/src/portal/tsconfig.json } typecheck:storybook: desc: "Typecheck Storybook config and stories" deps: [prepare] cmds: - task: typecheck:_run vars: { PROJECT: .storybook/tsconfig.json } typecheck:all: desc: "Typecheck all build variants" cmds: - task: typecheck:core - task: typecheck:proprietary - task: typecheck:saas - task: typecheck:desktop - task: typecheck:cloud - task: typecheck:scripts - task: typecheck:prototypes - task: typecheck:portal - task: typecheck:storybook # ============================================================ # Quality Gate # ============================================================ check: desc: "Quick quality gate for local development" cmds: - task: typecheck - task: lint - task: format:check - task: test og:check: desc: "Fail if committed OG/social-preview metadata is out of date" cmds: - node editor/scripts/generate-og-metadata.mjs --check check:all: desc: "Full CI quality gate" cmds: # Runs first, before prepare regenerates: guards the committed og-metadata.json / # ogImageMap.json that the Cloudflare Pages (plain `vite build`) deploy relies on. - task: og:check - task: typecheck:all - task: lint - task: format:check - task: build - task: test - task: storybook:build # ============================================================ # Test # ============================================================ test: desc: "Run tests" cmds: - task: test:editor test:editor: desc: "Run editor tests" deps: [prepare] vars: COVERAGE: '{{.COVERAGE | default .CI | default "false"}}' cmds: - > npx vitest run --root editor {{if eq .COVERAGE "true"}}--coverage --coverage.provider=v8 --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reporter=html --coverage.reportsDirectory=./coverage{{end}} test:watch: desc: "Run tests in watch mode" deps: [prepare] cmds: - npx vitest --watch --root editor test:coverage: desc: "Run tests with coverage (one-shot; CI-friendly)." cmds: - task: test:editor vars: { COVERAGE: "true" } # ============================================================ # Code Generation # ============================================================ tool-models: desc: "Generate tool API types from the Java OpenAPI spec" deps: [install, ":backend:swagger"] cmds: - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts - task: format sources: - editor/scripts/generate-tool-api-types.mts - ../SwaggerDoc.json generates: - editor/src/core/types/toolApiTypes.ts - editor/src/core/types/toolIO.ts tool-models:check: desc: "Fail if committed tool API types are out of date" cmds: - task: tool-models - git diff --exit-code -- editor/src/core/types/toolApiTypes.ts editor/src/core/types/toolIO.ts licenses:generate: desc: "Generate frontend license report" deps: [install] cmds: - node editor/scripts/generate-licenses.js # ============================================================ # Clean # ============================================================ clean: desc: "Clean build artifacts and caches" cmds: - cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist platforms: [windows] - cmd: rm -rf node_modules/.vite editor/dist dist platforms: [linux, darwin]