# Marketplace navigation and searchable catalog — implementation plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Spec:** `docs/superpowers/specs/2026-05-13-marketplace-search-design.md` **Goal:** Replace the 96-item flat marketplace sidebar with category groups, and add a searchable catalog page driven by per-file frontmatter and a generated TypeScript data module. **Architecture:** Single source of truth = `category:` frontmatter in each marketplace markdown. A Node generator scans frontmatter and emits `connectors.generated.ts`, which a new Markdoc-registered React component (``) imports. The sidebar is restructured into 22 category subgroups via `agen-for-work/sidebars.yaml`. A CI validator enforces frontmatter correctness and data-file freshness. **Tech Stack:** - Node 22 with the built-in `node:test` runner (no extra test deps) - `gray-matter` and `js-yaml` (already present via Redocly toolchain) - React 19 + `styled-components` (already used by `ProductCard`) - Redocly Realm Markdoc components ## File map **Created:** - `@theme/markdoc/components/ConnectorCatalog/categories.ts` — `CONNECTOR_CATEGORIES` array and `ConnectorCategory` type - `@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts` — auto-generated `CONNECTORS` array (committed) - `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.tsx` — React UI - `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.markdoc.ts` — Markdoc schema - `@theme/markdoc/components/ConnectorCatalog/index.ts` — barrel export - `scripts/generate-connectors.mjs` — frontmatter → data file generator - `scripts/generate-connectors.test.mjs` — unit tests (node:test) - `scripts/check-marketplace-frontmatter.mjs` — validator (CI) - `scripts/check-marketplace-frontmatter.test.mjs` — unit tests (node:test) - `scripts/bootstrap-connector-categories.mjs` — one-time, deleted after PR - `scripts/bootstrap-connector-categories.test.mjs` — one-time, deleted after PR - `agen-for-work/connectors/marketplace.md` — landing page with `{% ConnectorCatalog /%}` **Modified:** - `@theme/markdoc/schema.ts` — register `ConnectorCatalog` - `@theme/markdoc/components.tsx` — register `ConnectorCatalog` - `agen-for-work/sidebars.yaml` — replace flat Marketplace list with 22 category subgroups - `agen-for-work/connectors/marketplace/*.md` (96 files) — prepend frontmatter with `category:` - `package.json` — add `generate:connectors` and `check:marketplace-frontmatter` npm scripts **Not modified:** any non-marketplace markdown; `redocly.yaml`; CSS theme files (component uses inline styled-components only). ## Task ordering 1. Pre-flight checks (deps, sample frontmatter format) 2. `categories.ts` 3. Generator (TDD) 4. Validator (TDD) 5. `package.json` npm scripts wiring 6. Bootstrap script (TDD, one-shot run) 7. Run bootstrap → commit 96 frontmatter additions 8. Run generator → commit `connectors.generated.ts` 9. Markdoc schema + React component 10. Register component 11. Landing page 12. Sidebar restructure 13. Final validation + smoke test Each task ends with one atomic commit. ## Task 1: Pre-flight checks **Files:** none modified — verification only. - [ ] **Step 1: Confirm Node version is ≥18 (built-in `node:test` requires it)** Run: `node --version` Expected: `v22.x` or higher. - [ ] **Step 2: Confirm `gray-matter` is already installed** Run: `ls node_modules/gray-matter/index.js` Expected: file exists. If missing, run `npm install --save-dev gray-matter`. - [ ] **Step 3: Confirm `js-yaml` is already installed** Run: `ls node_modules/js-yaml/index.js` Expected: file exists. If missing, run `npm install --save-dev js-yaml`. - [ ] **Step 4: Confirm no marketplace MD currently has frontmatter** Run: ```bash count=0 for f in agen-for-work/connectors/marketplace/*.md; do head -1 "$f" | grep -q '^---' && count=$((count+1)) done echo "files with frontmatter: $count" ``` Expected: `files with frontmatter: 0` If the count is non-zero, stop and reconcile manually before proceeding — the bootstrap (Task 6) assumes a clean slate. No commit. ## Task 2: Create `categories.ts` **Files:** - Create: `@theme/markdoc/components/ConnectorCatalog/categories.ts` - [ ] **Step 1: Create the directory and file** Path: `@theme/markdoc/components/ConnectorCatalog/categories.ts` ```typescript // Source of truth: dashboard repo, // app/v2/src/tools/stores/envStore/workforceApp/workforceApp.types.ts // → `ConnectorCategory` enum (as of 2026-05-13). // Refresh manually when the product taxonomy changes. export const CONNECTOR_CATEGORIES = [ 'Productivity', 'Communication', 'Project Management', 'CRM', 'Development', 'Storage', 'Finance', 'Customer Support', 'Marketing', 'E-commerce', 'Analytics', 'Monitoring', 'DevOps', 'HR', 'Video Conferencing', 'Scheduling', 'Identity', 'Design', 'Forms', 'IT Service Management', 'Documents', 'Automation', 'Data', 'Social Media', 'Feature Flags', 'Content Management', 'Sales Intelligence', 'Product Management', 'Infrastructure', 'AI', ] as const export type ConnectorCategory = (typeof CONNECTOR_CATEGORIES)[number] ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit @theme/markdoc/components/ConnectorCatalog/categories.ts` Expected: no output, exit code 0. If `tsc` complains about lack of a tsconfig context, run `npx tsc --noEmit` from the repo root and confirm the file appears in the program's emit. - [ ] **Step 3: Commit** ```bash git add @theme/markdoc/components/ConnectorCatalog/categories.ts git commit -m "feat(theme): add ConnectorCategory taxonomy for marketplace catalog" ``` ## Task 3: TDD `generate-connectors.mjs` **Files:** - Create: `scripts/generate-connectors.mjs` - Test: `scripts/generate-connectors.test.mjs` - [ ] **Step 1: Write the failing test** Path: `scripts/generate-connectors.test.mjs` ```javascript import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { generateConnectors } from './generate-connectors.mjs' function makeFixture() { const root = mkdtempSync(join(tmpdir(), 'connectors-test-')) const dir = join(root, 'marketplace') mkdirSync(dir, { recursive: true }) writeFileSync( join(dir, 'slack.md'), `---\ncategory: Communication\n---\n\n## Slack integration\n`, ) writeFileSync( join(dir, 'monday.md'), `---\ncategory: Project Management\ndisplayName: monday.com\n---\n\n## monday.com integration\n`, ) writeFileSync( join(dir, 'asana.md'), `---\ncategory: Project Management\n---\n\n## Asana integration\n`, ) return { root, dir } } test('generateConnectors returns entries sorted by category-then-name', () => { const { root, dir } = makeFixture() try { const entries = generateConnectors({ marketplaceDir: dir, hrefPrefix: 'connectors/marketplace', }) assert.deepEqual( entries.map((e) => `${e.category}:${e.name}`), [ 'Communication:Slack', 'Project Management:Asana', 'Project Management:monday.com', ], ) } finally { rmSync(root, { recursive: true, force: true }) } }) test('generateConnectors uses displayName when present, otherwise humanizes filename', () => { const { root, dir } = makeFixture() try { const entries = generateConnectors({ marketplaceDir: dir, hrefPrefix: 'connectors/marketplace', }) const monday = entries.find((e) => e.id === 'monday') assert.equal(monday.name, 'monday.com') const slack = entries.find((e) => e.id === 'slack') assert.equal(slack.name, 'Slack') } finally { rmSync(root, { recursive: true, force: true }) } }) test('generateConnectors throws when a file is missing category', () => { const { root, dir } = makeFixture() writeFileSync(join(dir, 'broken.md'), `## Broken integration\n`) try { assert.throws( () => generateConnectors({ marketplaceDir: dir, hrefPrefix: 'connectors/marketplace', }), /broken\.md.*category/, ) } finally { rmSync(root, { recursive: true, force: true }) } }) test('generateConnectors throws when category is not in CONNECTOR_CATEGORIES', () => { const { root, dir } = makeFixture() writeFileSync( join(dir, 'rogue.md'), `---\ncategory: NotARealCategory\n---\n\n## Rogue\n`, ) try { assert.throws( () => generateConnectors({ marketplaceDir: dir, hrefPrefix: 'connectors/marketplace', }), /rogue\.md.*NotARealCategory/, ) } finally { rmSync(root, { recursive: true, force: true }) } }) ``` - [ ] **Step 2: Run the test and watch it fail** Run: `node --test scripts/generate-connectors.test.mjs` Expected: FAIL with `Cannot find module './generate-connectors.mjs'`. - [ ] **Step 3: Implement `scripts/generate-connectors.mjs`** Path: `scripts/generate-connectors.mjs` ```javascript #!/usr/bin/env node import { readFileSync, readdirSync, writeFileSync } from 'node:fs' import { join, basename, resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import matter from 'gray-matter' const HERE = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(HERE, '..') const CONNECTOR_CATEGORIES = [ 'Productivity', 'Communication', 'Project Management', 'CRM', 'Development', 'Storage', 'Finance', 'Customer Support', 'Marketing', 'E-commerce', 'Analytics', 'Monitoring', 'DevOps', 'HR', 'Video Conferencing', 'Scheduling', 'Identity', 'Design', 'Forms', 'IT Service Management', 'Documents', 'Automation', 'Data', 'Social Media', 'Feature Flags', 'Content Management', 'Sales Intelligence', 'Product Management', 'Infrastructure', 'AI', ] function humanize(id) { return id .split('-') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' ') } export function generateConnectors({ marketplaceDir, hrefPrefix }) { const files = readdirSync(marketplaceDir) .filter((f) => f.endsWith('.md')) .sort() const entries = files.map((file) => { const fullPath = join(marketplaceDir, file) const raw = readFileSync(fullPath, 'utf8') const { data } = matter(raw) const id = basename(file, '.md') if (!data.category) { throw new Error( `${file}: missing required frontmatter field "category"`, ) } if (!CONNECTOR_CATEGORIES.includes(data.category)) { throw new Error( `${file}: category "${data.category}" is not in CONNECTOR_CATEGORIES`, ) } return { id, name: data.displayName || humanize(id), category: data.category, href: `${hrefPrefix}/${file}`, } }) entries.sort((a, b) => { const ai = CONNECTOR_CATEGORIES.indexOf(a.category) const bi = CONNECTOR_CATEGORIES.indexOf(b.category) if (ai !== bi) return ai - bi return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }) }) return entries } function render(entries) { const lines = [ `// AUTO-GENERATED by scripts/generate-connectors.mjs — do not edit by hand.`, `// Re-generate with: npm run generate:connectors`, `import type { ConnectorCategory } from './categories'`, ``, `export interface ConnectorEntry {`, ` id: string`, ` name: string`, ` category: ConnectorCategory`, ` href: string`, `}`, ``, `export const CONNECTORS: ConnectorEntry[] = ${JSON.stringify(entries, null, 2)}`, ``, ] return lines.join('\n') } // Allow `import { generateConnectors }` from the test file and CLI execution // from npm script. const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) if (isMain) { const marketplaceDir = resolve(ROOT, 'agen-for-work/connectors/marketplace') const outFile = resolve( ROOT, '@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts', ) const entries = generateConnectors({ marketplaceDir, hrefPrefix: 'connectors/marketplace', }) writeFileSync(outFile, render(entries)) console.log(`Wrote ${entries.length} connectors to ${outFile}`) } ``` - [ ] **Step 4: Re-run the test** Run: `node --test scripts/generate-connectors.test.mjs` Expected: all 4 tests pass. - [ ] **Step 5: Commit** ```bash git add scripts/generate-connectors.mjs scripts/generate-connectors.test.mjs git commit -m "feat(scripts): add connector frontmatter→data generator" ``` ## Task 4: TDD `check-marketplace-frontmatter.mjs` **Files:** - Create: `scripts/check-marketplace-frontmatter.mjs` - Test: `scripts/check-marketplace-frontmatter.test.mjs` - [ ] **Step 1: Write the failing test** Path: `scripts/check-marketplace-frontmatter.test.mjs` ```javascript import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { checkMarketplace } from './check-marketplace-frontmatter.mjs' function setup() { const root = mkdtempSync(join(tmpdir(), 'check-test-')) const marketplaceDir = join(root, 'agen-for-work/connectors/marketplace') const sidebarsPath = join(root, 'agen-for-work/sidebars.yaml') const generatedPath = join( root, '@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts', ) mkdirSync(marketplaceDir, { recursive: true }) mkdirSync(join(root, '@theme/markdoc/components/ConnectorCatalog'), { recursive: true, }) return { root, marketplaceDir, sidebarsPath, generatedPath } } function writeValidFixture({ marketplaceDir, sidebarsPath, generatedPath }) { writeFileSync( join(marketplaceDir, 'slack.md'), `---\ncategory: Communication\n---\n\n## Slack\n`, ) writeFileSync( sidebarsPath, `- group: Marketplace\n items:\n - label: Slack\n page: connectors/marketplace/slack.md\n`, ) // generated will be filled by the function under test using the // generator, so we write what generateConnectors would produce. writeFileSync( generatedPath, [ '// AUTO-GENERATED by scripts/generate-connectors.mjs — do not edit by hand.', '// Re-generate with: npm run generate:connectors', "import type { ConnectorCategory } from './categories'", '', 'export interface ConnectorEntry {', ' id: string', ' name: string', ' category: ConnectorCategory', ' href: string', '}', '', 'export const CONNECTORS: ConnectorEntry[] = [', ' {', ' "id": "slack",', ' "name": "Slack",', ' "category": "Communication",', ' "href": "connectors/marketplace/slack.md"', ' }', ']', '', ].join('\n'), ) } test('checkMarketplace passes on a fully valid fixture', () => { const ctx = setup() try { writeValidFixture(ctx) const result = checkMarketplace({ marketplaceDir: ctx.marketplaceDir, sidebarsPath: ctx.sidebarsPath, generatedPath: ctx.generatedPath, hrefPrefix: 'connectors/marketplace', }) assert.deepEqual(result.errors, []) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) test('checkMarketplace reports stale generated file', () => { const ctx = setup() try { writeValidFixture(ctx) writeFileSync(ctx.generatedPath, '// stale\n') const result = checkMarketplace({ marketplaceDir: ctx.marketplaceDir, sidebarsPath: ctx.sidebarsPath, generatedPath: ctx.generatedPath, hrefPrefix: 'connectors/marketplace', }) assert.equal(result.errors.length, 1) assert.match(result.errors[0], /stale/i) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) test('checkMarketplace reports file present on disk but missing from sidebar', () => { const ctx = setup() try { writeValidFixture(ctx) writeFileSync( join(ctx.marketplaceDir, 'orphan.md'), `---\ncategory: Communication\n---\n\n## Orphan\n`, ) const result = checkMarketplace({ marketplaceDir: ctx.marketplaceDir, sidebarsPath: ctx.sidebarsPath, generatedPath: ctx.generatedPath, hrefPrefix: 'connectors/marketplace', }) assert.ok( result.errors.some((e) => /orphan/.test(e)), `expected an error mentioning orphan, got: ${JSON.stringify(result.errors)}`, ) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) test('checkMarketplace reports sidebar entry pointing at a missing file', () => { const ctx = setup() try { writeValidFixture(ctx) writeFileSync( ctx.sidebarsPath, `- group: Marketplace\n items:\n - label: Slack\n page: connectors/marketplace/slack.md\n - label: Ghost\n page: connectors/marketplace/ghost.md\n`, ) const result = checkMarketplace({ marketplaceDir: ctx.marketplaceDir, sidebarsPath: ctx.sidebarsPath, generatedPath: ctx.generatedPath, hrefPrefix: 'connectors/marketplace', }) assert.ok( result.errors.some((e) => /ghost/.test(e)), `expected an error mentioning ghost, got: ${JSON.stringify(result.errors)}`, ) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) ``` - [ ] **Step 2: Run the test and watch it fail** Run: `node --test scripts/check-marketplace-frontmatter.test.mjs` Expected: FAIL with `Cannot find module './check-marketplace-frontmatter.mjs'`. - [ ] **Step 3: Implement `scripts/check-marketplace-frontmatter.mjs`** Path: `scripts/check-marketplace-frontmatter.mjs` ```javascript #!/usr/bin/env node import { readFileSync, readdirSync, existsSync } from 'node:fs' import { join, resolve, dirname, basename } from 'node:path' import { fileURLToPath } from 'node:url' import yaml from 'js-yaml' import { generateConnectors } from './generate-connectors.mjs' const HERE = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(HERE, '..') function collectMarketplacePagesFromSidebar(node, found = []) { if (!node) return found if (Array.isArray(node)) { for (const item of node) collectMarketplacePagesFromSidebar(item, found) return found } if (typeof node === 'object') { if (typeof node.page === 'string' && node.page.startsWith('connectors/marketplace/')) { found.push(node.page) } if (Array.isArray(node.items)) { collectMarketplacePagesFromSidebar(node.items, found) } } return found } function renderGeneratedTs(entries) { const lines = [ `// AUTO-GENERATED by scripts/generate-connectors.mjs — do not edit by hand.`, `// Re-generate with: npm run generate:connectors`, `import type { ConnectorCategory } from './categories'`, ``, `export interface ConnectorEntry {`, ` id: string`, ` name: string`, ` category: ConnectorCategory`, ` href: string`, `}`, ``, `export const CONNECTORS: ConnectorEntry[] = ${JSON.stringify(entries, null, 2)}`, ``, ] return lines.join('\n') } export function checkMarketplace({ marketplaceDir, sidebarsPath, generatedPath, hrefPrefix, }) { const errors = [] let entries try { entries = generateConnectors({ marketplaceDir, hrefPrefix }) } catch (err) { errors.push(err.message) return { errors } } if (!existsSync(generatedPath)) { errors.push(`Generated file is missing: ${generatedPath}`) } else { const actual = readFileSync(generatedPath, 'utf8') const expected = renderGeneratedTs(entries) if (actual.trim() !== expected.trim()) { errors.push( `connectors.generated.ts is stale — run \`npm run generate:connectors\` and commit the result.`, ) } } const sidebarYaml = readFileSync(sidebarsPath, 'utf8') const sidebar = yaml.load(sidebarYaml) const pagesInSidebar = new Set( collectMarketplacePagesFromSidebar(sidebar).map((p) => basename(p, '.md')), ) const filesOnDisk = new Set( readdirSync(marketplaceDir) .filter((f) => f.endsWith('.md')) .map((f) => basename(f, '.md')), ) for (const id of filesOnDisk) { if (!pagesInSidebar.has(id)) { errors.push( `${id}.md exists on disk but is not referenced in sidebars.yaml under Marketplace.`, ) } } for (const id of pagesInSidebar) { if (!filesOnDisk.has(id)) { errors.push( `sidebars.yaml references connectors/marketplace/${id}.md but the file does not exist.`, ) } } return { errors } } const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) if (isMain) { const { errors } = checkMarketplace({ marketplaceDir: resolve(ROOT, 'agen-for-work/connectors/marketplace'), sidebarsPath: resolve(ROOT, 'agen-for-work/sidebars.yaml'), generatedPath: resolve( ROOT, '@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts', ), hrefPrefix: 'connectors/marketplace', }) if (errors.length > 0) { for (const e of errors) console.error(`✗ ${e}`) process.exit(1) } console.log(`✓ Marketplace frontmatter and sidebar are in sync.`) } ``` - [ ] **Step 4: Re-run the test** Run: `node --test scripts/check-marketplace-frontmatter.test.mjs` Expected: all 4 tests pass. - [ ] **Step 5: Commit** ```bash git add scripts/check-marketplace-frontmatter.mjs scripts/check-marketplace-frontmatter.test.mjs git commit -m "feat(scripts): add marketplace frontmatter and sidebar validator" ``` ## Task 5: Wire npm scripts **Files:** - Modify: `package.json` - [ ] **Step 1: Add two scripts to `package.json`** Edit `package.json` `"scripts"` block. Current: ```json "scripts": { "dev": "redocly preview", "start": "redocly preview-docs", "build": "redocly bundle sample@v1 -o dist/bundle.yaml", "test": "redocly lint", "check:content-style": "node scripts/check-content-style.mjs" } ``` Change to: ```json "scripts": { "dev": "redocly preview", "start": "redocly preview-docs", "build": "redocly bundle sample@v1 -o dist/bundle.yaml", "test": "redocly lint", "check:content-style": "node scripts/check-content-style.mjs", "check:marketplace-frontmatter": "node scripts/check-marketplace-frontmatter.mjs", "generate:connectors": "node scripts/generate-connectors.mjs" } ``` - [ ] **Step 2: Verify the new scripts exist (even though they will fail right now)** Run: `npm run generate:connectors --silent || true` Expected: script runs, then likely fails because frontmatter isn't there yet — that's fine. Confirms the wiring works. - [ ] **Step 3: Commit** ```bash git add package.json git commit -m "chore(scripts): wire generate:connectors and check:marketplace-frontmatter" ``` ## Task 6: TDD bootstrap script (one-shot) **Files:** - Create: `scripts/bootstrap-connector-categories.mjs` - Test: `scripts/bootstrap-connector-categories.test.mjs` The bootstrap script reads `IntegrationCategoryMap` and `ConnectorCategory` from the dashboard repo (path supplied via `DASHBOARD_REPO` env var) and prepends `---\ncategory: \n---\n\n` to each marketplace markdown file that does not yet have frontmatter. - [ ] **Step 1: Write the failing test** Path: `scripts/bootstrap-connector-categories.test.mjs` ```javascript import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { applyBootstrap } from './bootstrap-connector-categories.mjs' function setup() { const root = mkdtempSync(join(tmpdir(), 'bootstrap-test-')) const marketplaceDir = join(root, 'marketplace') mkdirSync(marketplaceDir, { recursive: true }) return { root, marketplaceDir } } test('applyBootstrap prepends frontmatter to a file without it', () => { const ctx = setup() try { writeFileSync( join(ctx.marketplaceDir, 'slack.md'), `## Slack integration\n\nDescription.\n`, ) const updated = applyBootstrap({ marketplaceDir: ctx.marketplaceDir, idToCategory: { slack: 'Communication' }, }) assert.equal(updated, 1) const after = readFileSync(join(ctx.marketplaceDir, 'slack.md'), 'utf8') assert.equal( after, `---\ncategory: Communication\n---\n\n## Slack integration\n\nDescription.\n`, ) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) test('applyBootstrap leaves files with existing frontmatter untouched', () => { const ctx = setup() try { writeFileSync( join(ctx.marketplaceDir, 'jira.md'), `---\ncategory: Old\n---\n\n## Jira integration\n`, ) const updated = applyBootstrap({ marketplaceDir: ctx.marketplaceDir, idToCategory: { jira: 'Project Management' }, }) assert.equal(updated, 0) const after = readFileSync(join(ctx.marketplaceDir, 'jira.md'), 'utf8') assert.equal(after, `---\ncategory: Old\n---\n\n## Jira integration\n`) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) test('applyBootstrap throws if a file has no entry in idToCategory', () => { const ctx = setup() try { writeFileSync(join(ctx.marketplaceDir, 'unknown.md'), `## Unknown\n`) assert.throws( () => applyBootstrap({ marketplaceDir: ctx.marketplaceDir, idToCategory: {}, }), /unknown/, ) } finally { rmSync(ctx.root, { recursive: true, force: true }) } }) ``` - [ ] **Step 2: Run the test and watch it fail** Run: `node --test scripts/bootstrap-connector-categories.test.mjs` Expected: FAIL with `Cannot find module './bootstrap-connector-categories.mjs'`. - [ ] **Step 3: Implement the bootstrap script** Path: `scripts/bootstrap-connector-categories.mjs` ```javascript #!/usr/bin/env node import { readFileSync, readdirSync, writeFileSync } from 'node:fs' import { join, basename, resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const HERE = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(HERE, '..') export function applyBootstrap({ marketplaceDir, idToCategory }) { const files = readdirSync(marketplaceDir).filter((f) => f.endsWith('.md')) let updated = 0 for (const file of files) { const id = basename(file, '.md') const full = join(marketplaceDir, file) const text = readFileSync(full, 'utf8') if (text.startsWith('---\n')) { continue } const category = idToCategory[id] if (!category) { throw new Error( `No category mapping for "${id}" — extend IntegrationCategoryMap in the dashboard, or hand-pick a value before continuing.`, ) } const frontmatter = `---\ncategory: ${category}\n---\n\n` writeFileSync(full, frontmatter + text) updated++ } return updated } // Parse the dashboard's workforceApp.types.ts to extract the // integration-id → category map. Regex-based: brittle but ok for one-shot. function parseDashboardMap(typesFilePath) { const src = readFileSync(typesFilePath, 'utf8') const enumBlock = src.match(/export enum ConnectorCategory \{([\s\S]+?)\}/) if (!enumBlock) throw new Error('Could not find ConnectorCategory enum') const categoryEnum = {} for (const m of enumBlock[1].matchAll(/(\w+)\s*=\s*'([^']+)'/g)) { categoryEnum[m[1]] = m[2] } const intBlock = src.match( /export enum WorkforceIntegrationEnum \{([\s\S]+?)\}/, ) if (!intBlock) throw new Error('Could not find WorkforceIntegrationEnum') const integrationEnum = {} for (const m of intBlock[1].matchAll(/(\w+)\s*=\s*'([^']+)'/g)) { integrationEnum[m[1]] = m[2] } const mapBlock = src.match( /export const IntegrationCategoryMap[\s\S]+?\{([\s\S]+?)\};/, ) if (!mapBlock) throw new Error('Could not find IntegrationCategoryMap') const idToCategory = {} for (const m of mapBlock[1].matchAll( /WorkforceIntegrationEnum\.(\w+)\]:\s*ConnectorCategory\.(\w+)/g, )) { const id = integrationEnum[m[1]] const category = categoryEnum[m[2]] if (id && category) idToCategory[id] = category } return idToCategory } const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) if (isMain) { const dashboardRepo = process.env.DASHBOARD_REPO if (!dashboardRepo) { console.error( 'Set DASHBOARD_REPO=/path/to/dashboard to point at a local checkout.', ) process.exit(1) } const typesFile = resolve( dashboardRepo, 'app/v2/src/tools/stores/envStore/workforceApp/workforceApp.types.ts', ) const idToCategory = parseDashboardMap(typesFile) const updated = applyBootstrap({ marketplaceDir: resolve(ROOT, 'agen-for-work/connectors/marketplace'), idToCategory, }) console.log(`Updated ${updated} files with frontmatter.`) } ``` - [ ] **Step 4: Re-run the test** Run: `node --test scripts/bootstrap-connector-categories.test.mjs` Expected: all 3 tests pass. - [ ] **Step 5: Commit** ```bash git add scripts/bootstrap-connector-categories.mjs scripts/bootstrap-connector-categories.test.mjs git commit -m "chore(scripts): add one-shot bootstrap for connector frontmatter" ``` ## Task 7: Run bootstrap, commit 96 file changes **Files:** modifies all `agen-for-work/connectors/marketplace/*.md`. - [ ] **Step 1: Verify dashboard repo path** Run: `ls /Users/serslon/Projects/FrontEgg/dashboard/app/v2/src/tools/stores/envStore/workforceApp/workforceApp.types.ts` Expected: file exists. If the path is different on your machine, adjust the env var below. - [ ] **Step 2: Run the bootstrap** Run: ```bash DASHBOARD_REPO=/Users/serslon/Projects/FrontEgg/dashboard node scripts/bootstrap-connector-categories.mjs ``` Expected: `Updated 96 files with frontmatter.` - [ ] **Step 3: Sanity-check the diff** Run: `git status --short agen-for-work/connectors/marketplace/ | wc -l` Expected: `96`. Run: `head -5 agen-for-work/connectors/marketplace/slack.md` (or any file) Expected: starts with ``` --- category: Communication --- ``` followed by the original first H2. - [ ] **Step 4: Confirm content-style checker still passes** Run: `npm run check:content-style` Expected: no errors. (Frontmatter blocks should be skipped by the content-style checker — see `scripts/check-content-style.mjs`. If new failures appear, stop and fix the script before committing.) - [ ] **Step 5: Commit** ```bash git add agen-for-work/connectors/marketplace/ git commit -m "docs(marketplace): add category frontmatter to all 96 connector guides" ``` ## Task 8: Generate and commit `connectors.generated.ts` **Files:** - Create: `@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts` - [ ] **Step 1: Run the generator** Run: `npm run generate:connectors` Expected: `Wrote 96 connectors to /Users/serslon/Projects/FrontEgg/docs/@theme/markdoc/components/ConnectorCatalog/connectors.generated.ts` - [ ] **Step 2: Inspect the output** Run: `head -20 @theme/markdoc/components/ConnectorCatalog/connectors.generated.ts` Expected: - starts with the auto-generated header comment; - declares `CONNECTORS` as an `ConnectorEntry[]`; - first entry is from Productivity (e.g. Airtable); - pretty-printed JSON, 2-space indent. - [ ] **Step 3: Confirm validator passes** Run: `npm run check:marketplace-frontmatter` Expected: `✓ Marketplace frontmatter and sidebar are in sync.` (sidebar check passes against the current 96-item flat structure; we restructure later in Task 12, and the validator will still find every file referenced.) - [ ] **Step 4: Commit** ```bash git add @theme/markdoc/components/ConnectorCatalog/connectors.generated.ts git commit -m "feat(theme): generate initial connectors.generated.ts (96 entries)" ``` ## Task 9: Markdoc schema and React component **Files:** - Create: `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.markdoc.ts` - Create: `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.tsx` - Create: `@theme/markdoc/components/ConnectorCatalog/index.ts` - [ ] **Step 1: Create the Markdoc schema** Path: `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.markdoc.ts` ```typescript import { Schema } from '@markdoc/markdoc' export const ConnectorCatalog: Schema & { tagName: string } = { attributes: {}, render: 'ConnectorCatalog', tagName: 'ConnectorCatalog', } ``` - [ ] **Step 2: Create the React component** Path: `@theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.tsx` ```tsx import React, { useMemo, useState } from 'react' import styled from 'styled-components' import { CONNECTORS } from './connectors.generated' import { CONNECTOR_CATEGORIES, type ConnectorCategory } from './categories' type CategoryFilter = 'all' | ConnectorCategory export const ConnectorCatalog = () => { const [query, setQuery] = useState('') const [category, setCategory] = useState('all') const activeCategories = useMemo( () => CONNECTOR_CATEGORIES.filter((c) => CONNECTORS.some((entry) => entry.category === c), ), [], ) const filtered = useMemo(() => { const q = query.trim().toLowerCase() return CONNECTORS.filter((entry) => { if (category !== 'all' && entry.category !== category) return false if (q && !entry.name.toLowerCase().includes(q)) return false return true }) }, [query, category]) const sectioned = query.trim() === '' && category === 'all' const groups = useMemo(() => { if (!sectioned) { return [ { category: category === 'all' ? null : category, entries: [...filtered].sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }), ), }, ] } return activeCategories.map((c) => ({ category: c, entries: filtered.filter((e) => e.category === c), })) }, [filtered, activeCategories, sectioned, category]) const reset = () => { setQuery('') setCategory('all') } return ( setQuery(e.target.value)} placeholder={`Search ${CONNECTORS.length} connectors…`} aria-label="Search connectors" /> setCategory(e.target.value as CategoryFilter)} aria-label="Filter by category" > All categories {activeCategories.map((c) => ( {c} ))} {filtered.length === 0 ? ( No connectors match your search. Clear filters ) : ( groups.map((group, idx) => ( {group.category && {group.category}} {group.entries.map((entry) => ( {entry.name} ))} )) )} ) } const Wrapper = styled.div` font-family: var(--font-family-base); ` const Controls = styled.div` display: flex; gap: 12px; margin: 16px 0 24px; flex-wrap: wrap; ` const SearchInput = styled.input` flex: 1 1 240px; padding: 10px 14px; font-size: 14px; border: 1px solid var(--card-border-color, #2d2d37); border-radius: 8px; background: transparent; color: inherit; &:focus { outline: none; border-color: var(--card-border-color-hover, #3d3d47); } ` const Select = styled.select` flex: 0 0 220px; padding: 10px 14px; font-size: 14px; border: 1px solid var(--card-border-color, #2d2d37); border-radius: 8px; background: transparent; color: inherit; ` const Section = styled.section` margin-bottom: 28px; ` const SectionTitle = styled.h3` font-size: 16px; font-weight: 600; margin: 0 0 12px; color: var(--icon-card-title-color); ` const Grid = styled.div` display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; ` const Tile = styled.a` display: block; padding: 12px 14px; border: 1px solid var(--card-border-color, #2d2d37); border-radius: 6px; font-size: 14px; font-weight: 500; text-decoration: none !important; color: inherit; transition: border-color 0.2s ease; &:hover { border-color: var(--card-border-color-hover, #3d3d47); } ` const Empty = styled.div` text-align: center; padding: 32px 16px; color: var(--icon-card-description-color); ` const ResetButton = styled.button` margin-top: 12px; padding: 8px 16px; background: transparent; border: 1px solid var(--card-border-color, #2d2d37); border-radius: 6px; cursor: pointer; color: inherit; font-family: inherit; font-size: 14px; ` ``` - [ ] **Step 3: Create the barrel export** Path: `@theme/markdoc/components/ConnectorCatalog/index.ts` ```typescript export { ConnectorCatalog } from './ConnectorCatalog' ``` - [ ] **Step 4: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors. - [ ] **Step 5: Commit** ```bash git add @theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.markdoc.ts git add @theme/markdoc/components/ConnectorCatalog/ConnectorCatalog.tsx git add @theme/markdoc/components/ConnectorCatalog/index.ts git commit -m "feat(theme): add ConnectorCatalog Markdoc component" ``` ## Task 10: Register the component **Files:** - Modify: `@theme/markdoc/schema.ts` - Modify: `@theme/markdoc/components.tsx` - [ ] **Step 1: Add the schema export** Edit `@theme/markdoc/schema.ts`. After line `export { ProductCard } from './components/ProductCard/ProductCard.markdoc'`, append: ```typescript export { ConnectorCatalog } from './components/ConnectorCatalog/ConnectorCatalog.markdoc' ``` - [ ] **Step 2: Add the component export** Edit `@theme/markdoc/components.tsx`. After line `export { ProductCard } from './components/ProductCard/ProductCard'`, append: ```typescript export { ConnectorCatalog } from './components/ConnectorCatalog/ConnectorCatalog' ``` - [ ] **Step 3: Verify TypeScript still compiles** Run: `npx tsc --noEmit` Expected: no errors. - [ ] **Step 4: Commit** ```bash git add @theme/markdoc/schema.ts @theme/markdoc/components.tsx git commit -m "feat(theme): register ConnectorCatalog in Markdoc schema and components" ``` ## Task 11: Create the landing page **Files:** - Create: `agen-for-work/connectors/marketplace.md` - [ ] **Step 1: Write the landing page** Path: `agen-for-work/connectors/marketplace.md` ```markdown --- title: Marketplace --- ## Marketplace Browse all available connectors. Use the search field to find a connector by name, or filter by category. ``` - [ ] **Step 2: Verify content-style checker passes for this file** Run: `npm run check:content-style` Expected: no new errors. The first heading is H2 (`## Marketplace`); no H1; no emojis; no `->` / `=>` arrows. - [ ] **Step 3: Commit** ```bash git add agen-for-work/connectors/marketplace.md git commit -m "docs(marketplace): add searchable catalog landing page" ``` ## Task 12: Restructure `sidebars.yaml` **Files:** - Modify: `agen-for-work/sidebars.yaml` This is a mechanical replacement of one block. The current `- group: Marketplace` block (between approximately lines 53 and 268, containing 96 flat items) is replaced with a Marketplace group whose `page:` points to the landing and whose `items:` is 22 category subgroups. - [ ] **Step 1: Read the current Marketplace block** Run: `sed -n '53,268p' agen-for-work/sidebars.yaml | head -20` Expected: shows `- group: Marketplace`, `expanded: true`, then 96 `- label: ... page: ...` entries. - [ ] **Step 2: Build the replacement using the generator output** The generator already sorts entries by `(category-index, name)`. Use it to build the 22 subgroups. Add this temporary helper (you'll delete it after pasting): Run: ```bash node --input-type=module -e " import { generateConnectors } from './scripts/generate-connectors.mjs' const entries = generateConnectors({ marketplaceDir: 'agen-for-work/connectors/marketplace', hrefPrefix: 'connectors/marketplace', }) const seen = new Set(entries.map((e) => e.category)) const orderedCategories = [ 'Productivity','Communication','Project Management','CRM','Development','Storage','Finance','Customer Support','Marketing','E-commerce','Analytics','Monitoring','DevOps','HR','Video Conferencing','Scheduling','Identity','Design','Forms','IT Service Management','Documents','Automation','Data','Social Media','Feature Flags','Content Management','Sales Intelligence','Product Management','Infrastructure','AI', ].filter((c) => seen.has(c)) const lines = [] lines.push(' - group: Marketplace') lines.push(' page: connectors/marketplace.md') lines.push(' expanded: false') lines.push(' items:') for (const cat of orderedCategories) { lines.push(\` - group: \${cat}\`) lines.push(' expanded: false') lines.push(' items:') for (const e of entries.filter((x) => x.category === cat)) { lines.push(\` - label: \${e.name}\`) lines.push(\` page: \${e.href}\`) } } console.log(lines.join('\n')) " > /tmp/marketplace-block.yaml ``` - [ ] **Step 3: Replace the old Marketplace block in `sidebars.yaml`** Open `agen-for-work/sidebars.yaml` and replace the existing block that begins at: ```yaml - group: Marketplace expanded: true items: - label: Acuity Scheduling ``` …and ends with the last marketplace entry (e.g. `page: connectors/marketplace/zoom.md`), with the content of `/tmp/marketplace-block.yaml`. Preserve the four-space indentation. - [ ] **Step 4: Verify YAML parses** Run: `node --input-type=module -e "import yaml from 'js-yaml'; import { readFileSync } from 'fs'; yaml.load(readFileSync('agen-for-work/sidebars.yaml', 'utf8')); console.log('ok')"` Expected: `ok` - [ ] **Step 5: Run Redocly lint** Run: `npm run test` Expected: no errors. - [ ] **Step 6: Run the marketplace validator** Run: `npm run check:marketplace-frontmatter` Expected: `✓ Marketplace frontmatter and sidebar are in sync.` - [ ] **Step 7: Clean up the temp file** Run: `rm /tmp/marketplace-block.yaml` - [ ] **Step 8: Commit** ```bash git add agen-for-work/sidebars.yaml git commit -m "docs(marketplace): group 96 connectors into 22 category subgroups in sidebar" ``` ## Task 13: Final smoke test + cleanup **Files:** none modified. - [ ] **Step 1: Run the full check suite** Run: `npm run test && npm run check:content-style && npm run check:marketplace-frontmatter` Expected: all three pass with no errors. - [ ] **Step 2: Run unit tests for the new scripts** Run: `node --test scripts/generate-connectors.test.mjs scripts/check-marketplace-frontmatter.test.mjs scripts/bootstrap-connector-categories.test.mjs` Expected: all tests pass. - [ ] **Step 3: Smoke test in the browser** Run: `npm run dev` Open the local preview (URL shown in the dev server output, typically `http://localhost:8080`). Navigate to `/agen-for-work/connectors/marketplace`. Verify: - Page renders without console errors. - 22 H3 section headers visible (Productivity first), 96 tiles total distributed across them. - Typing "sla" in the search input narrows the result to Slack. - Selecting "CRM" in the dropdown shows only the CRM section. - Combining "sla" + "Communication" filters correctly. - Empty result ("zzzz" in search) shows the "Clear filters" button; clicking it resets state. - Sidebar shows `Marketplace` collapsed; expanding shows 22 collapsed subgroups. - Clicking any connector navigates to its guide and auto-expands its category group in the sidebar. - [ ] **Step 4: Delete the one-shot bootstrap script** The bootstrap has been run and committed. It is not used in CI and should not stay in the repo. Run: ```bash rm scripts/bootstrap-connector-categories.mjs scripts/bootstrap-connector-categories.test.mjs git add -A git commit -m "chore(scripts): remove one-shot bootstrap after successful run" ``` - [ ] **Step 5: Push and open PR (optional, gated on user approval)** Ask the user before pushing — repo policy says PRs require at least one reviewer and the user prefers explicit confirmation for shared-state operations.
No connectors match your search.