| 1 | import type { Provider } from "./integrations"; |
| 2 | |
| 3 | /** |
| 4 | * The connector catalog: every tool g1t connects to, or will, in one list. |
| 5 | * The workspace's Integrations page and your own Integrations settings are |
| 6 | * both drawn from it, so a new connector is added here, once. |
| 7 | * |
| 8 | * A connector is `available` when g1t can set it up today (its `href` is |
| 9 | * where), and `soon` while it is planned. `scopes` says who connects it: |
| 10 | * |
| 11 | * - `workspace`: an owner connects it once, for everyone in the workspace. |
| 12 | * - `personal`: each person connects their own account, and it follows them |
| 13 | * into every workspace they belong to (a calendar, a personal sign-in). |
| 14 | * |
| 15 | * A connector with both scopes can say something different for each, in |
| 16 | * `personal`: its own description, status, capabilities or setup page. |
| 17 | * `connectorView` resolves that into what one context shows. |
| 18 | * |
| 19 | * Paths in `href` are g1t's own pages. `:workspace` stands for the |
| 20 | * workspace's slug. |
| 21 | */ |
| 22 | |
| 23 | export type ConnectorScope = "workspace" | "personal"; |
| 24 | export type ConnectorStatus = "available" | "soon"; |
| 25 | |
| 26 | export type ConnectorCategory = |
| 27 | | "code" |
| 28 | | "issues" |
| 29 | | "chat" |
| 30 | | "calendar" |
| 31 | | "docs" |
| 32 | | "monitoring" |
| 33 | | "cloud" |
| 34 | | "ai" |
| 35 | | "data" |
| 36 | | "crm" |
| 37 | | "design" |
| 38 | | "security" |
| 39 | | "files"; |
| 40 | |
| 41 | /** What a connector lets g1t do, as the small tags on its card. */ |
| 42 | export type ConnectorCapability = |
| 43 | | "Agents can read" |
| 44 | | "Agents can act" |
| 45 | | "Opens issues" |
| 46 | | "Writes back" |
| 47 | | "Imports repositories" |
| 48 | | "Signs you in" |
| 49 | | "Sends events" |
| 50 | | "Alerts into chat" |
| 51 | | "Sets your status" |
| 52 | | "AI provider" |
| 53 | | "Deploys" |
| 54 | | "Syncs people"; |
| 55 | |
| 56 | export type Connector = { |
| 57 | /** Stable and unique: used in links and requests. */ |
| 58 | id: string; |
| 59 | name: string; |
| 60 | category: ConnectorCategory; |
| 61 | /** One plain line of what it does in g1t, for a workspace (or for you, when it is only personal). */ |
| 62 | description: string; |
| 63 | scopes: ConnectorScope[]; |
| 64 | status: ConnectorStatus; |
| 65 | /** Where an available connector is set up, for each scope it has. */ |
| 66 | href?: Partial<Record<ConnectorScope, string>>; |
| 67 | capabilities?: ConnectorCapability[]; |
| 68 | /** The integration provider behind it, whose connections show it as connected. */ |
| 69 | provider?: Provider; |
| 70 | /** What differs when you connect it for yourself. */ |
| 71 | personal?: { description?: string; status?: ConnectorStatus; capabilities?: ConnectorCapability[] }; |
| 72 | /** More words search should find it by. */ |
| 73 | keywords?: string[]; |
| 74 | }; |
| 75 | |
| 76 | /** A connector as one context (a workspace, or you) shows it. */ |
| 77 | export type ConnectorView = { |
| 78 | id: string; |
| 79 | name: string; |
| 80 | category: ConnectorCategory; |
| 81 | description: string; |
| 82 | status: ConnectorStatus; |
| 83 | href: string | null; |
| 84 | capabilities: ConnectorCapability[]; |
| 85 | provider: Provider | null; |
| 86 | keywords: string[]; |
| 87 | /** The other scope it can also be connected in. */ |
| 88 | alsoIn: ConnectorScope | null; |
| 89 | }; |
| 90 | |
| 91 | /** The categories, in the order the directory lists them. */ |
| 92 | export const CONNECTOR_CATEGORIES: { id: ConnectorCategory; title: string }[] = [ |
| 93 | { id: "code", title: "Code & CI" }, |
| 94 | { id: "issues", title: "Issues & projects" }, |
| 95 | { id: "chat", title: "Chat & meetings" }, |
| 96 | { id: "calendar", title: "Calendar & email" }, |
| 97 | { id: "docs", title: "Docs & knowledge" }, |
| 98 | { id: "monitoring", title: "Monitoring & incidents" }, |
| 99 | { id: "cloud", title: "Cloud & deploy" }, |
| 100 | { id: "ai", title: "AI models" }, |
| 101 | { id: "data", title: "Data & analytics" }, |
| 102 | { id: "crm", title: "CRM & support" }, |
| 103 | { id: "design", title: "Design" }, |
| 104 | { id: "security", title: "Security & identity" }, |
| 105 | { id: "files", title: "Files & storage" }, |
| 106 | ]; |
| 107 | |
| 108 | const W: ConnectorScope[] = ["workspace"]; |
| 109 | const P: ConnectorScope[] = ["personal"]; |
| 110 | const BOTH: ConnectorScope[] = ["workspace", "personal"]; |
| 111 | |
| 112 | /** An AI provider: its credentials are kept on the workspace's AI providers page. */ |
| 113 | function model(provider: Provider, name: string, description: string, keywords: string[] = []): Connector { |
| 114 | return { |
| 115 | id: provider.replace(/_/g, "-"), |
| 116 | name, |
| 117 | category: "ai", |
| 118 | description, |
| 119 | scopes: W, |
| 120 | status: "available", |
| 121 | href: { workspace: `/:workspace/-/integrations/models?add=${provider}#add` }, |
| 122 | capabilities: ["AI provider"], |
| 123 | provider, |
| 124 | keywords: ["model", "llm", ...keywords], |
| 125 | }; |
| 126 | } |
| 127 | |
| 128 | /** A planned connector. */ |
| 129 | function soon( |
| 130 | id: string, |
| 131 | name: string, |
| 132 | category: ConnectorCategory, |
| 133 | scopes: ConnectorScope[], |
| 134 | description: string, |
| 135 | capabilities: ConnectorCapability[] = [], |
| 136 | personal?: Connector["personal"], |
| 137 | ): Connector { |
| 138 | return { id, name, category, description, scopes, status: "soon", capabilities, ...(personal ? { personal } : {}) }; |
| 139 | } |
| 140 | |
| 141 | export const CONNECTORS: Connector[] = [ |
| 142 | // Code & CI ------------------------------------------------------------- |
| 143 | { |
| 144 | id: "github", |
| 145 | name: "GitHub", |
| 146 | category: "code", |
| 147 | description: "Import repositories, with their branches, tags and issues, through g1t's GitHub App.", |
| 148 | scopes: BOTH, |
| 149 | status: "available", |
| 150 | href: { workspace: "/new/github?workspace=:workspace", personal: "/settings/github" }, |
| 151 | capabilities: ["Imports repositories"], |
| 152 | personal: { |
| 153 | description: "Sign in with your GitHub account, and reach the repositories you can see there to import them.", |
| 154 | capabilities: ["Signs you in", "Imports repositories"], |
| 155 | }, |
| 156 | keywords: ["git", "import", "sign in"], |
| 157 | }, |
| 158 | { |
| 159 | id: "webhooks", |
| 160 | name: "Webhooks", |
| 161 | category: "code", |
| 162 | description: "Every repository's events, sent to your own addresses as they happen, signed.", |
| 163 | scopes: W, |
| 164 | status: "available", |
| 165 | href: { workspace: "/:workspace/-/webhooks" }, |
| 166 | capabilities: ["Sends events"], |
| 167 | keywords: ["http", "events", "outgoing"], |
| 168 | }, |
| 169 | soon("gitlab", "GitLab", "code", BOTH, "Import projects, with their branches, tags and issues.", ["Imports repositories"], { |
| 170 | description: "Sign in with GitLab, and reach your own projects to import them.", |
| 171 | capabilities: ["Signs you in", "Imports repositories"], |
| 172 | }), |
| 173 | soon("bitbucket", "Bitbucket", "code", BOTH, "Import repositories, with their branches and tags.", ["Imports repositories"], { |
| 174 | description: "Reach your own Bitbucket repositories to import them.", |
| 175 | capabilities: ["Imports repositories"], |
| 176 | }), |
| 177 | soon("circleci", "CircleCI", "code", W, "Pipeline results as checks on pull requests, which agents read when a build fails.", ["Agents can read"]), |
| 178 | soon("buildkite", "Buildkite", "code", W, "Build results as checks on pull requests, with the failing step for agents to fix.", ["Agents can read"]), |
| 179 | soon("docker-hub", "Docker Hub", "code", W, "Push images your workflows build, and pull private base images.", ["Deploys"]), |
| 180 | |
| 181 | // Issues & projects ----------------------------------------------------- |
| 182 | { |
| 183 | id: "linear", |
| 184 | name: "Linear", |
| 185 | category: "issues", |
| 186 | description: "Agents read ENG-42 when work mentions it; import issues, and they hear back when the work lands.", |
| 187 | scopes: BOTH, |
| 188 | status: "available", |
| 189 | href: { workspace: "/:workspace/-/integrations/trackers?add=linear#add" }, |
| 190 | capabilities: ["Agents can read", "Writes back"], |
| 191 | provider: "linear", |
| 192 | personal: { |
| 193 | description: "Issues assigned to you in Linear, in your g1t inbox beside everything else.", |
| 194 | status: "soon", |
| 195 | capabilities: ["Agents can read"], |
| 196 | }, |
| 197 | keywords: ["tracker", "tickets"], |
| 198 | }, |
| 199 | { |
| 200 | id: "jira", |
| 201 | name: "Jira", |
| 202 | category: "issues", |
| 203 | description: "Agents read TECH-1234 when work mentions it; import tickets, and they hear back when the work lands.", |
| 204 | scopes: W, |
| 205 | status: "available", |
| 206 | href: { workspace: "/:workspace/-/integrations/trackers?add=jira#add" }, |
| 207 | capabilities: ["Agents can read", "Writes back"], |
| 208 | provider: "jira", |
| 209 | keywords: ["atlassian", "tracker", "tickets"], |
| 210 | }, |
| 211 | soon("shortcut", "Shortcut", "issues", W, "Stories agents read when work mentions them, updated when the work lands.", ["Agents can read", "Writes back"]), |
| 212 | soon("clickup", "ClickUp", "issues", W, "Tasks agents can read and import, kept in step with their pull requests.", ["Agents can read", "Writes back"]), |
| 213 | soon("trello", "Trello", "issues", W, "Cards become issues, and move along their board as the work does.", ["Opens issues", "Writes back"]), |
| 214 | |
| 215 | // Chat & meetings ------------------------------------------------------- |
| 216 | soon("slack", "Slack", "chat", BOTH, "Alerts and updates in the channels your team already reads.", ["Alerts into chat"], { |
| 217 | description: "Your status in g1t follows the one you set in Slack, and the other way round.", |
| 218 | capabilities: ["Sets your status"], |
| 219 | }), |
| 220 | soon("microsoft-teams", "Microsoft Teams", "chat", BOTH, "Alerts and updates posted to Teams channels.", ["Alerts into chat"], { |
| 221 | description: "Your status in g1t follows your Teams presence.", |
| 222 | capabilities: ["Sets your status"], |
| 223 | }), |
| 224 | soon("discord", "Discord", "chat", W, "Releases, alerts and updates posted to a Discord server.", ["Alerts into chat"]), |
| 225 | soon("zoom", "Zoom", "chat", BOTH, "Meeting notes and recordings agents can read when work refers to them.", ["Agents can read"], { |
| 226 | description: "Sets your status to On a call while you are in a Zoom meeting.", |
| 227 | capabilities: ["Sets your status"], |
| 228 | }), |
| 229 | soon("google-meet", "Google Meet", "chat", P, "Sets your status to On a call while you are in a meeting, and starts a call from any channel.", ["Sets your status"]), |
| 230 | |
| 231 | // Calendar & email ------------------------------------------------------ |
| 232 | soon("google-calendar", "Google Calendar", "calendar", P, "Sets your status to In a meeting while you are in one.", ["Sets your status"]), |
| 233 | soon("outlook-calendar", "Outlook Calendar", "calendar", P, "Sets your status to In a meeting while you are in one.", ["Sets your status"]), |
| 234 | soon("gmail", "Gmail", "calendar", P, "Turn an email into an issue, from your own inbox.", ["Opens issues"]), |
| 235 | soon("outlook-mail", "Outlook Mail", "calendar", P, "Turn an email into an issue, from your own inbox.", ["Opens issues"]), |
| 236 | |
| 237 | // Docs & knowledge ------------------------------------------------------ |
| 238 | soon("google-docs", "Google Docs", "docs", BOTH, "Agents read the specs and plans linked from issues and chat.", ["Agents can read"], { |
| 239 | description: "Docs you share in chat open for agents as well as people.", |
| 240 | capabilities: ["Agents can read"], |
| 241 | }), |
| 242 | soon("coda", "Coda", "docs", W, "Agents read the docs and tables linked from issues and chat.", ["Agents can read"]), |
| 243 | |
| 244 | // Monitoring & incidents ------------------------------------------------ |
| 245 | { |
| 246 | id: "sentry", |
| 247 | name: "Sentry", |
| 248 | category: "monitoring", |
| 249 | description: "New errors open issues with the stack trace, and resolve in Sentry when the fix merges.", |
| 250 | scopes: W, |
| 251 | status: "available", |
| 252 | href: { workspace: "/:workspace/-/integrations/alerts?add=sentry#add" }, |
| 253 | capabilities: ["Opens issues", "Agents can read", "Writes back"], |
| 254 | provider: "sentry", |
| 255 | keywords: ["errors", "alerts", "exceptions"], |
| 256 | }, |
| 257 | { |
| 258 | id: "datadog", |
| 259 | name: "Datadog", |
| 260 | category: "monitoring", |
| 261 | description: "Monitors that trigger open issues, and recoveries are noted on them.", |
| 262 | scopes: W, |
| 263 | status: "available", |
| 264 | href: { workspace: "/:workspace/-/integrations/alerts?add=datadog#add" }, |
| 265 | capabilities: ["Opens issues"], |
| 266 | provider: "datadog", |
| 267 | keywords: ["monitors", "alerts", "apm"], |
| 268 | }, |
| 269 | { |
| 270 | id: "alerts-webhook", |
| 271 | name: "Alerts webhook", |
| 272 | category: "monitoring", |
| 273 | description: "Anything that can send signed JSON opens issues: your own scripts, or any monitoring tool.", |
| 274 | scopes: W, |
| 275 | status: "available", |
| 276 | href: { workspace: "/:workspace/-/integrations/alerts?add=webhook#add" }, |
| 277 | capabilities: ["Opens issues"], |
| 278 | provider: "webhook", |
| 279 | keywords: ["incoming", "http", "alerts", "pagerduty", "grafana"], |
| 280 | }, |
| 281 | soon("pagerduty", "PagerDuty", "monitoring", BOTH, "Incidents open issues, and resolve when the fix lands.", ["Opens issues", "Writes back"], { |
| 282 | description: "Sets your status to On call while you are on call.", |
| 283 | capabilities: ["Sets your status"], |
| 284 | }), |
| 285 | soon("opsgenie", "Opsgenie", "monitoring", W, "Alerts open issues, and close when the fix lands.", ["Opens issues", "Writes back"]), |
| 286 | soon("incident-io", "incident.io", "monitoring", W, "Incidents get a channel in g1t, with what changed alongside.", ["Opens issues", "Alerts into chat"]), |
| 287 | soon("grafana", "Grafana", "monitoring", W, "Alert rules open issues, and agents read the dashboards they link to.", ["Opens issues", "Agents can read"]), |
| 288 | soon("new-relic", "New Relic", "monitoring", W, "Alerts open issues, with the traces agents need to find the cause.", ["Opens issues", "Agents can read"]), |
| 289 | soon("honeycomb", "Honeycomb", "monitoring", W, "Triggers open issues, and agents query traces while they work.", ["Opens issues", "Agents can read"]), |
| 290 | soon("better-stack", "Better Stack", "monitoring", W, "Uptime incidents and log alerts open issues.", ["Opens issues"]), |
| 291 | |
| 292 | // Cloud & deploy -------------------------------------------------------- |
| 293 | soon("aws", "Amazon Web Services", "cloud", W, "Workflows and deployments reach AWS with short-lived credentials, no stored keys.", ["Deploys"]), |
| 294 | soon("google-cloud", "Google Cloud", "cloud", W, "Workflows and deployments reach Google Cloud with short-lived credentials.", ["Deploys"]), |
| 295 | soon("azure", "Microsoft Azure", "cloud", W, "Workflows and deployments reach Azure with short-lived credentials.", ["Deploys"]), |
| 296 | soon("netlify", "Netlify", "cloud", W, "Deploy previews as checks on pull requests.", ["Deploys", "Agents can read"]), |
| 297 | soon("fly-io", "Fly.io", "cloud", W, "Deploy apps from workflows, and roll back from the deployment's page.", ["Deploys"]), |
| 298 | soon("kubernetes", "Kubernetes", "cloud", W, "Roll out to your own clusters from workflows, with each rollout's status on g1t.", ["Deploys"]), |
| 299 | |
| 300 | // AI models ------------------------------------------------------------- |
| 301 | model("anthropic", "Anthropic", "Keep your team's Anthropic API key, sealed, for the workspace.", ["claude"]), |
| 302 | model("openai", "OpenAI", "Keep your team's OpenAI API key, sealed, for the workspace.", ["gpt"]), |
| 303 | model("gemini", "Google Gemini", "Keep your team's Google AI Studio key, sealed, for the workspace.", ["google"]), |
| 304 | model("xai", "xAI", "Keep your team's xAI API key, sealed, for the workspace.", ["grok"]), |
| 305 | model("mistral", "Mistral", "Keep your team's Mistral API key, sealed, for the workspace."), |
| 306 | model("deepseek", "DeepSeek", "Keep your team's DeepSeek API key, sealed, for the workspace."), |
| 307 | model("azure_openai", "Azure OpenAI", "Keep your Azure OpenAI resource, deployment and key for the workspace.", ["microsoft"]), |
| 308 | model("openrouter", "OpenRouter", "Keep your team's OpenRouter key, sealed, for the workspace."), |
| 309 | model("groq", "Groq", "Keep your team's Groq API key, sealed, for the workspace."), |
| 310 | model("together", "Together AI", "Keep your team's Together AI key, sealed, for the workspace."), |
| 311 | model("fireworks", "Fireworks AI", "Keep your team's Fireworks AI key, sealed, for the workspace."), |
| 312 | model("cerebras", "Cerebras", "Keep your team's Cerebras API key, sealed, for the workspace."), |
| 313 | model("anthropic_endpoint", "Anthropic-compatible endpoint", "Any server that speaks the Anthropic API, such as your own gateway or proxy.", ["self-hosted", "proxy"]), |
| 314 | model("openai_endpoint", "OpenAI-compatible endpoint", "Any server that speaks the OpenAI API, such as a model you host yourself.", ["self-hosted", "proxy", "ollama", "vllm"]), |
| 315 | { |
| 316 | id: "mcp", |
| 317 | name: "MCP clients", |
| 318 | category: "ai", |
| 319 | description: "Your own agent, connected to g1t's MCP server, works in g1t as you.", |
| 320 | scopes: P, |
| 321 | status: "available", |
| 322 | href: { personal: "/settings/applications" }, |
| 323 | capabilities: ["Agents can act"], |
| 324 | keywords: ["model context protocol", "agent", "applications", "oauth"], |
| 325 | }, |
| 326 | soon("amazon-bedrock", "Amazon Bedrock", "ai", W, "Models in your own AWS account, billed there.", ["AI provider"]), |
| 327 | soon("vertex-ai", "Google Vertex AI", "ai", W, "Models in your own Google Cloud project, billed there.", ["AI provider"]), |
| 328 | |
| 329 | // Data & analytics ------------------------------------------------------ |
| 330 | soon("postgresql", "PostgreSQL", "data", W, "Agents read your schema, and run read-only queries while they investigate.", ["Agents can read"]), |
| 331 | soon("snowflake", "Snowflake", "data", W, "Agents answer questions from your warehouse with read-only queries.", ["Agents can read"]), |
| 332 | soon("bigquery", "BigQuery", "data", W, "Agents answer questions from your datasets with read-only queries.", ["Agents can read"]), |
| 333 | soon("amplitude", "Amplitude", "data", W, "Agents read the charts that work refers to.", ["Agents can read"]), |
| 334 | soon("segment", "Segment", "data", W, "Agents read your tracking plan, and check new events against it.", ["Agents can read"]), |
| 335 | |
| 336 | // CRM & support --------------------------------------------------------- |
| 337 | soon("zendesk", "Zendesk", "crm", W, "Turn a ticket into an issue, and tell the customer when the fix ships.", ["Opens issues", "Writes back"]), |
| 338 | soon("salesforce", "Salesforce", "crm", W, "Agents read the account and case behind a request.", ["Agents can read"]), |
| 339 | |
| 340 | // Design ---------------------------------------------------------------- |
| 341 | soon("miro", "Miro", "design", W, "Agents read the boards linked from issues and chat.", ["Agents can read"]), |
| 342 | soon("storybook", "Storybook", "design", W, "Each pull request's stories as a check, with what changed.", ["Agents can read"]), |
| 343 | |
| 344 | // Security & identity --------------------------------------------------- |
| 345 | soon("okta", "Okta", "security", W, "Single sign-on, and people added and removed with your directory.", ["Signs you in", "Syncs people"]), |
| 346 | soon("entra-id", "Microsoft Entra ID", "security", W, "Single sign-on, and people added and removed with your directory.", ["Signs you in", "Syncs people"]), |
| 347 | soon("google-workspace", "Google Workspace", "security", BOTH, "Single sign-on, and people added and removed with your directory.", ["Signs you in", "Syncs people"], { |
| 348 | description: "Sign in to g1t with your Google account.", |
| 349 | capabilities: ["Signs you in"], |
| 350 | }), |
| 351 | soon("1password", "1Password", "security", W, "Secrets from a vault, read by workflows and deployments when they run.", ["Deploys"]), |
| 352 | soon("snyk", "Snyk", "security", W, "Findings open issues, and an agent opens the fix.", ["Opens issues"]), |
| 353 | |
| 354 | // Files & storage ------------------------------------------------------- |
| 355 | soon("google-drive", "Google Drive", "files", BOTH, "Agents read the files linked from issues and chat.", ["Agents can read"], { |
| 356 | description: "Attach files from your Drive in chat, without downloading them first.", |
| 357 | capabilities: ["Agents can read"], |
| 358 | }), |
| 359 | soon("dropbox", "Dropbox", "files", BOTH, "Agents read the files linked from issues and chat.", ["Agents can read"], { |
| 360 | description: "Attach files from your Dropbox in chat.", |
| 361 | capabilities: ["Agents can read"], |
| 362 | }), |
| 363 | soon("onedrive", "OneDrive and SharePoint", "files", BOTH, "Agents read the files linked from issues and chat.", ["Agents can read"], { |
| 364 | description: "Attach files from your OneDrive in chat.", |
| 365 | capabilities: ["Agents can read"], |
| 366 | }), |
| 367 | soon("box", "Box", "files", W, "Agents read the files linked from issues and chat.", ["Agents can read"]), |
| 368 | ]; |
| 369 | |
| 370 | /** The connector by its id. */ |
| 371 | export function connectorById(id: string): Connector | undefined { |
| 372 | return CONNECTORS.find((connector) => connector.id === id); |
| 373 | } |
| 374 | |
| 375 | /** A connector as `scope` shows it, or null when it cannot be connected there. */ |
| 376 | export function connectorView(connector: Connector, scope: ConnectorScope): ConnectorView | null { |
| 377 | if (!connector.scopes.includes(scope)) return null; |
| 378 | const own = scope === "personal" ? connector.personal : undefined; |
| 379 | const status = own?.status ?? connector.status; |
| 380 | const other: ConnectorScope = scope === "workspace" ? "personal" : "workspace"; |
| 381 | return { |
| 382 | id: connector.id, |
| 383 | name: connector.name, |
| 384 | category: connector.category, |
| 385 | description: own?.description ?? connector.description, |
| 386 | status, |
| 387 | href: status === "available" ? (connector.href?.[scope] ?? null) : null, |
| 388 | capabilities: own?.capabilities ?? connector.capabilities ?? [], |
| 389 | provider: connector.provider ?? null, |
| 390 | keywords: connector.keywords ?? [], |
| 391 | alsoIn: connector.scopes.includes(other) ? other : null, |
| 392 | }; |
| 393 | } |
| 394 | |
| 395 | /** Every connector `scope` can show, in catalog order. */ |
| 396 | export function connectorsFor(scope: ConnectorScope): ConnectorView[] { |
| 397 | return CONNECTORS.map((connector) => connectorView(connector, scope)).filter((view): view is ConnectorView => view != null); |
| 398 | } |
| 399 | |
| 400 | /** A setup path with its `:workspace` filled in. */ |
| 401 | export function connectorPath(path: string, workspace: string): string { |
| 402 | return path.replace(/:workspace\b/g, encodeURIComponent(workspace)); |
| 403 | } |