Files
sim/scripts/check-integration-catalog.ts
T
Waleed d1bc99a9c4 improvement(integrations): add Managed Agents templates and guard docs links (#6608)
* improvement(integrations): add Managed Agents templates and guard docs links

The Claude Managed Agents block shipped without templates or suggested
skills, so its integration detail page had nothing to offer and the
"Add to Sim" chat handoff was the only affordance. Add nine templates
and seven skills, each grounded in an operation the block actually
exposes.

Its docsLink also pointed at integrations/managed-agent while the page
is managed_agent, so the link 404'd. Five more blocks had the same
class of bug via a stale tools/ prefix. Nothing validated these,
because the catalog check only compares deployment fields.

Add that validation, and collapse the three copies of the docs-URL
contract onto one exported helper so the checker and the generator
cannot drift apart.

* fix(integrations): resolve docsLink once when checking the vendor allowlist

The stale-allowlist predicate read block.docsLink directly while the main
loop read the resolved link, so an allowlisted block that dropped its
explicit docsLink produced undefined from the optional chain, negated to
true, and was treated as still vendor-linked — the stale entry went
undetected.

Record vendor-linked types during the single pass that already resolves
each link, so both checks agree by construction.

* fix(managed-agents): name every integration a template's alsoIntegrations claims

alsoIntegrations is documented as the blocks a template's prompt
references, and it drives which catalog pages the template cross-lists
on plus the icon cluster on the detail page. Three prompts named a
service only implicitly, or not at all: the runaway stopper claimed
Slack without mentioning it, the PR reviewer said "pull request" rather
than GitHub, and the weekly report said "emails" rather than Gmail.

Name the service in each prompt so the field is accurate and the
templates surface on the right pages.

* fix(integrations): point every visible integration at Sim's own docs

Six blocks had a vendor documentation URL in docsLink — cursor, enrich,
enrow, google_groups, qdrant, and similarweb — which was accidental
rather than deliberate. Each already has a generated Sim page, and each
already carries the vendor's homepage on BlockMeta.url, so the vendor
link in docsLink only sent readers away from our own documentation.

Point all six at their Sim page and drop the allowlist that had been
tolerating them. Every visible integration gets a generated page, so a
docsLink outside docs.sim.ai is now always an error.
2026-08-12 01:31:47 -07:00

182 lines
6.3 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { stripVersionSuffix } from '@sim/utils/string'
/**
* Verifies the registry-free integration catalog matches the executable block
* registry fields that deployment availability depends on.
*/
import { BLOCK_REGISTRY } from '../apps/sim/blocks/registry-maps'
import { AuthMode, type BlockConfig } from '../apps/sim/blocks/types'
import integrationsJson from '../apps/sim/lib/integrations/integrations.json'
import { DOCS_ORIGIN, DOCS_OUTPUT_PATH, defaultIntegrationDocsUrl } from './generate-docs'
type CatalogAuthType = 'oauth' | 'api-key' | 'none'
interface CatalogEntry {
type: string
slug: string
name: string
category: string
integrationType: string
authType: CatalogAuthType
oauthServiceId?: string
}
function resolveAuthType(block: BlockConfig): CatalogAuthType {
if (block.authMode === AuthMode.OAuth) return 'oauth'
if (block.authMode === AuthMode.ApiKey || block.authMode === AuthMode.BotToken) return 'api-key'
if (block.subBlocks.some((subBlock) => subBlock.type === 'oauth-input')) return 'oauth'
if (
block.subBlocks.some((subBlock) => ['apiKey', 'api_key', 'accessToken'].includes(subBlock.id))
) {
return 'api-key'
}
return 'none'
}
function resolveOAuthServiceId(block: BlockConfig): string | undefined {
const serviceIds = new Set(
block.subBlocks
.filter((subBlock) => subBlock.type === 'oauth-input')
.map((subBlock) => subBlock.serviceId)
.filter((serviceId): serviceId is string => Boolean(serviceId))
)
if (serviceIds.size > 1) {
throw new Error(
`Integration block "${block.type}" declares more than one OAuth service ID: ${[...serviceIds].join(', ')}`
)
}
return serviceIds.values().next().value
}
function expectedEntry(block: BlockConfig): CatalogEntry {
if (!block.integrationType) {
throw new Error(`Integration block "${block.type}" is missing integrationType`)
}
const authType = resolveAuthType(block)
const oauthServiceId = authType === 'oauth' ? resolveOAuthServiceId(block) : undefined
if (authType === 'oauth' && !oauthServiceId) {
throw new Error(`OAuth integration block "${block.type}" is missing an OAuth service ID`)
}
return {
type: block.type,
slug: block.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, ''),
name: block.name,
category: block.category,
integrationType: block.integrationType,
authType,
...(oauthServiceId ? { oauthServiceId } : {}),
}
}
/**
* A `docsLink` path carries its own section segment (`integrations/…`), so it
* resolves against the locale root rather than the integrations directory.
*/
const DOCS_LOCALE_ROOT = dirname(DOCS_OUTPUT_PATH)
/**
* Verifies every visible integration's `docsLink` resolves to a real Sim docs
* page.
*
* A hand-written `docsLink` overrides {@link defaultIntegrationDocsUrl}, so a
* typo — a hyphen where the page uses an underscore, a stale `tools/` prefix,
* or a vendor URL pasted in place of ours — silently ships a link that misses
* Sim's own page, and no other check looks at it. Every visible integration
* gets a generated page, so pointing anywhere else is always a mistake; link to
* the vendor from `BlockMeta.url` instead.
*/
function verifyDocsLinks(blocks: readonly BlockConfig[]): void {
const issues: string[] = []
for (const block of blocks) {
const docsLink = block.docsLink ?? defaultIntegrationDocsUrl(block.type)
if (!docsLink.startsWith(DOCS_ORIGIN)) {
issues.push(
`"${block.type}" docsLink points outside ${DOCS_ORIGIN} (${docsLink}) — link to Sim's own page and put the vendor's docs on BlockMeta.url`
)
continue
}
const page = docsLink.slice(DOCS_ORIGIN.length)
if (!existsSync(join(DOCS_LOCALE_ROOT, `${page}.mdx`))) {
issues.push(`"${block.type}" docsLink 404s — no docs page at en/${page}.mdx (${docsLink})`)
}
}
if (issues.length > 0) {
throw new Error(
`Integration docs links are broken:\n- ${issues.join('\n- ')}\nPoint docsLink at an existing page, or drop it to use the generated default.`
)
}
}
function verifyIntegrationCatalog(): void {
const expected = Object.values(BLOCK_REGISTRY).filter(
(block) => block.category === 'tools' && !block.hideFromToolbar && !block.preview
)
verifyDocsLinks(expected)
const expectedBaseTypes = new Map<string, string>()
for (const block of expected) {
const baseType = stripVersionSuffix(block.type)
const existing = expectedBaseTypes.get(baseType)
if (existing) {
throw new Error(
`Visible integration blocks "${existing}" and "${block.type}" share base type "${baseType}"`
)
}
expectedBaseTypes.set(baseType, block.type)
}
const actual = integrationsJson.integrations as readonly CatalogEntry[]
const expectedByType = new Map(expected.map((block) => [block.type, expectedEntry(block)]))
const actualByType = new Map<string, CatalogEntry>()
const actualSlugs = new Set<string>()
for (const entry of actual) {
if (actualByType.has(entry.type)) {
throw new Error(`Generated integration catalog contains duplicate type "${entry.type}"`)
}
if (actualSlugs.has(entry.slug)) {
throw new Error(`Generated integration catalog contains duplicate slug "${entry.slug}"`)
}
actualByType.set(entry.type, entry)
actualSlugs.add(entry.slug)
}
const issues: string[] = []
for (const [type, entry] of expectedByType) {
const generated = actualByType.get(type)
if (!generated) {
issues.push(`missing generated entry for "${type}"`)
continue
}
for (const field of [
'name',
'slug',
'category',
'integrationType',
'authType',
'oauthServiceId',
] as const) {
if (generated[field] !== entry[field]) {
issues.push(`"${type}" has stale ${field}`)
}
}
}
for (const type of actualByType.keys()) {
if (!expectedByType.has(type)) issues.push(`unexpected generated entry for "${type}"`)
}
if (issues.length > 0) {
throw new Error(
`Generated integration catalog is stale:\n- ${issues.join('\n- ')}\nRun \`bun run scripts/generate-docs.ts\` and commit the generated catalog.`
)
}
process.stdout.write(
`Integration deployment metadata is in sync (${actual.length} integrations).\n`
)
}
verifyIntegrationCatalog()