History

add sources to homepage

Rhys Sullivan committed 7 months ago

Commit becd9101 parent bc3e83a6 files · +559 −31Browse files

Unified · Split

6 files
  1. apps/marketing/astro.config.mjs +7 −1
  2. apps/marketing/package.json +1 −0
  3. apps/marketing/src/pages/api/detect.ts +260 −0
  4. apps/marketing/src/pages/index.astro +270 −30
  5. apps/marketing/src/styles/global.css +16 −0
  6. bun.lock +5 −0
apps/marketing/astro.config.mjs+7−1
@@ -3,9 +3,15 @@ import { defineConfig } from 'astro/config'; · 2 unmodified lines
3 3
4import tailwindcss from '@tailwindcss/vite';4import tailwindcss from '@tailwindcss/vite';
5 5
6import node from '@astrojs/node';
7
6// https://astro.build/config8// https://astro.build/config
7export default defineConfig({9export default defineConfig({
8 vite: {10 vite: {
9 plugins: [tailwindcss()]11 plugins: [tailwindcss()]
10 }12 },
13
14 adapter: node({
15 mode: 'standalone'
16 })
11});17});
apps/marketing/package.json+1−0
@@ -12,6 +12,7 @@ · 11 unmodified lines
12 "astro": "astro"12 "astro": "astro"
13 },13 },
14 "dependencies": {14 "dependencies": {
15 "@astrojs/node": "^10.0.4",
15 "@astrojs/tailwind": "^6.0.2",16 "@astrojs/tailwind": "^6.0.2",
16 "@tailwindcss/vite": "^4.2.2",17 "@tailwindcss/vite": "^4.2.2",
17 "astro": "^6.1.3",18 "astro": "^6.1.3",
apps/marketing/src/pages/api/detect.ts+260−0
@@ -0,0 +1,260 @@
1import type { APIRoute } from 'astro'
2
3export const prerender = false
4
5interface DetectedTool {
6 name: string
7 desc: string
8 method: string
9 policy: 'read' | 'write' | 'destructive'
10}
11
12interface DetectionResult {
13 kind: 'openapi' | 'graphql' | 'mcp' | 'googleDiscovery'
14 name: string
15 count: number
16 tools: DetectedTool[]
17}
18
19function policyFromMethod(method: string): 'read' | 'write' | 'destructive' {
20 const m = method.toUpperCase()
21 if (m === 'GET' || m === 'HEAD' || m === 'OPTIONS' || m === 'QUERY') return 'read'
22 if (m === 'DELETE') return 'destructive'
23 return 'write'
24}
25
26async function tryOpenAPI(url: string, signal: AbortSignal): Promise<DetectionResult | null> {
27 try {
28 const res = await fetch(url, { signal, headers: { Accept: 'application/json, application/yaml, text/yaml, */*' } })
29 if (!res.ok) return null
30 const text = await res.text()
31 let doc: any
32 try {
33 doc = JSON.parse(text)
34 } catch {
35 // Could be YAML - check for openapi/swagger string markers
36 if (text.includes('openapi:') || text.includes('swagger:')) {
37 // Basic YAML detection - we can't fully parse YAML without a dep,
38 // but we can confirm it's an OpenAPI spec
39 return {
40 kind: 'openapi',
41 name: 'API',
42 count: 0,
43 tools: [{ name: 'spec.detected', desc: 'OpenAPI spec detected (YAML)', method: 'GET', policy: 'read' }],
44 }
45 }
46 return null
47 }
48
49 // Check for OpenAPI 3.x or Swagger 2.x
50 if (!doc.openapi && !doc.swagger) return null
51
52 const title = doc.info?.title || 'API'
53 const tools: DetectedTool[] = []
54
55 if (doc.paths) {
56 for (const [path, methods] of Object.entries(doc.paths as Record<string, any>)) {
57 for (const method of ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']) {
58 const op = methods[method]
59 if (!op) continue
60 const opId = op.operationId || `${method}.${path.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`
61 tools.push({
62 name: opId,
63 desc: op.summary || op.description?.slice(0, 80) || `${method.toUpperCase()} ${path}`,
64 method: method.toUpperCase(),
65 policy: policyFromMethod(method),
66 })
67 }
68 }
69 }
70
71 return {
72 kind: 'openapi',
73 name: title,
74 count: tools.length,
75 tools: tools.slice(0, 12),
76 }
77 } catch {
78 return null
79 }
80}
81
82async function tryGraphQL(url: string, signal: AbortSignal): Promise<DetectionResult | null> {
83 try {
84 const introspectionQuery = `{
85 __schema {
86 queryType { name }
87 mutationType { name }
88 types {
89 kind name
90 fields(includeDeprecated: false) { name description }
91 }
92 }
93 }`
94
95 const res = await fetch(url, {
96 method: 'POST',
97 signal,
98 headers: { 'Content-Type': 'application/json' },
99 body: JSON.stringify({ query: introspectionQuery }),
100 })
101 if (!res.ok) return null
102 const json = await res.json() as any
103
104 const schema = json.data?.__schema || json.__schema
105 if (!schema) return null
106
107 const tools: DetectedTool[] = []
108 const queryTypeName = schema.queryType?.name || 'Query'
109 const mutationTypeName = schema.mutationType?.name || 'Mutation'
110
111 for (const type of schema.types || []) {
112 if (type.name === queryTypeName && type.fields) {
113 for (const field of type.fields) {
114 if (field.name.startsWith('__')) continue
115 tools.push({
116 name: field.name,
117 desc: field.description?.slice(0, 80) || `Query ${field.name}`,
118 method: 'query',
119 policy: 'read',
120 })
121 }
122 }
123 if (type.name === mutationTypeName && type.fields) {
124 for (const field of type.fields) {
125 if (field.name.startsWith('__')) continue
126 const isDestructive = /delete|remove|destroy/i.test(field.name)
127 tools.push({
128 name: field.name,
129 desc: field.description?.slice(0, 80) || `Mutation ${field.name}`,
130 method: 'mutation',
131 policy: isDestructive ? 'destructive' : 'write',
132 })
133 }
134 }
135 }
136
137 // Derive name from URL hostname
138 const hostname = new URL(url).hostname.replace('api.', '').split('.')[0]
139 const name = hostname.charAt(0).toUpperCase() + hostname.slice(1)
140
141 return {
142 kind: 'graphql',
143 name: name || 'GraphQL API',
144 count: tools.length,
145 tools: tools.slice(0, 12),
146 }
147 } catch {
148 return null
149 }
150}
151
152async function tryGoogleDiscovery(url: string, signal: AbortSignal): Promise<DetectionResult | null> {
153 try {
154 // Only probe URLs that look like Google Discovery docs
155 if (!url.includes('googleapis.com') && !url.includes('/discovery/')) return null
156
157 const res = await fetch(url, { signal })
158 if (!res.ok) return null
159 const doc = await res.json() as any
160
161 if (!doc.discoveryVersion && !doc.kind?.includes('discovery')) return null
162
163 const title = doc.title || doc.name || 'Google API'
164 const tools: DetectedTool[] = []
165
166 function extractMethods(resources: any, prefix = '') {
167 if (!resources) return
168 for (const [resourceName, resource] of Object.entries(resources as Record<string, any>)) {
169 if (resource.methods) {
170 for (const [methodName, method] of Object.entries(resource.methods as Record<string, any>)) {
171 const httpMethod = (method.httpMethod || 'GET').toUpperCase()
172 tools.push({
173 name: `${prefix}${resourceName}.${methodName}`,
174 desc: method.description?.slice(0, 80) || `${httpMethod} ${method.path || ''}`,
175 method: httpMethod,
176 policy: policyFromMethod(httpMethod),
177 })
178 }
179 }
180 if (resource.resources) {
181 extractMethods(resource.resources, `${prefix}${resourceName}.`)
182 }
183 }
184 }
185
186 extractMethods(doc.resources)
187
188 // Also check top-level methods
189 if (doc.methods) {
190 for (const [methodName, method] of Object.entries(doc.methods as Record<string, any>)) {
191 const httpMethod = (method.httpMethod || 'GET').toUpperCase()
192 tools.push({
193 name: methodName,
194 desc: method.description?.slice(0, 80) || `${httpMethod} ${method.path || ''}`,
195 method: httpMethod,
196 policy: policyFromMethod(httpMethod),
197 })
198 }
199 }
200
201 return {
202 kind: 'googleDiscovery',
203 name: title,
204 count: tools.length,
205 tools: tools.slice(0, 12),
206 }
207 } catch {
208 return null
209 }
210}
211
212export const POST: APIRoute = async ({ request }) => {
213 try {
214 const body = await request.json() as { url?: string }
215 const url = body.url?.trim()
216 if (!url) {
217 return new Response(JSON.stringify({ error: 'URL is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } })
218 }
219
220 // Validate URL
221 try {
222 new URL(url)
223 } catch {
224 return new Response(JSON.stringify({ error: 'Invalid URL' }), { status: 400, headers: { 'Content-Type': 'application/json' } })
225 }
226
227 const controller = new AbortController()
228 const timeout = setTimeout(() => controller.abort(), 8000)
229
230 // Run all detectors in parallel
231 const [openapi, graphql, google] = await Promise.all([
232 tryOpenAPI(url, controller.signal),
233 tryGraphQL(url, controller.signal),
234 tryGoogleDiscovery(url, controller.signal),
235 ])
236
237 clearTimeout(timeout)
238
239 // Return first successful detection (prefer by confidence: most tools wins)
240 const results = [openapi, graphql, google].filter((r): r is DetectionResult => r !== null)
241 results.sort((a, b) => b.count - a.count)
242
243 if (results.length === 0) {
244 return new Response(
245 JSON.stringify({ error: 'Could not detect an API at this URL. Try an OpenAPI spec, GraphQL endpoint, or Google Discovery document.' }),
246 { status: 404, headers: { 'Content-Type': 'application/json' } }
247 )
248 }
249
250 return new Response(JSON.stringify(results[0]), {
251 status: 200,
252 headers: { 'Content-Type': 'application/json' },
253 })
254 } catch (err) {
255 return new Response(
256 JSON.stringify({ error: 'Detection failed' }),
257 { status: 500, headers: { 'Content-Type': 'application/json' } }
258 )
259 }
260}
apps/marketing/src/pages/index.astro+270−30
@@ -48,6 +48,34 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 47 unmodified lines
48 </div>48 </div>
49 </div>49 </div>
50 50
51 <!-- URL input bar -->
52 <div class="rise-d4 mb-4" id="detect-bar">
53 <div class="relative">
54 <div class="flex items-center gap-3 bg-surface-raised border border-rule rounded-xl px-5 py-3.5 focus-within:border-teal/30 transition-colors">
55 <svg class="w-4 h-4 text-ink-faint shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.414-5.874a4.5 4.5 0 00-6.364 0l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
56 <input
57 id="detect-input"
58 type="url"
59 placeholder="Paste any API URL to detect tools..."
60 class="flex-1 bg-transparent text-[14px] text-ink placeholder:text-ink-faint outline-none font-mono"
61 autocomplete="off"
62 spellcheck="false"
63 />
64 <button id="detect-btn" class="text-[12px] font-medium text-surface bg-teal hover:bg-teal-dim px-4 py-1.5 rounded-md transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed">
65 Detect
66 </button>
67 </div>
68 <div id="detect-status" class="hidden absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center"></div>
69 <div id="detect-examples" class="mt-3 flex flex-wrap items-center gap-2 justify-center">
70 <span class="text-[11px] text-ink-faint">Try:</span>
71 <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json">GitHub REST</button>
72 <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://api.linear.app/graphql">Linear GraphQL</button>
73 <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest">Google Calendar</button>
74 <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json">Stripe</button>
75 </div>
76 </div>
77 </div>
78
51 <!-- Full-width interactive catalog UI -->79 <!-- Full-width interactive catalog UI -->
52 <div class="rise-d4 relative" id="hero-catalog">80 <div class="rise-d4 relative" id="hero-catalog">
53 <div class="absolute -inset-px rounded-xl bg-gradient-to-b from-teal/10 via-transparent to-transparent pointer-events-none"></div>81 <div class="absolute -inset-px rounded-xl bg-gradient-to-b from-teal/10 via-transparent to-transparent pointer-events-none"></div>
@@ -123,19 +151,22 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 69 unmodified lines
123 </button>151 </button>
124 </div>152 </div>
125 153
126 <!-- Main: tool list -->154 <!-- Main: tool tree -->
127 <div class="flex-1 min-w-0">155 <div class="flex-1 min-w-0 flex flex-col">
128 <!-- Search -->156 <!-- Search -->
129 <div class="px-4 py-3 border-b border-rule">157 <div class="px-4 py-3 border-b border-rule shrink-0">
130 <div class="flex items-center gap-2 px-3 py-2 rounded-md border border-rule bg-surface text-ink-faint text-[12px]">158 <div class="flex items-center gap-2 px-3 py-2 rounded-md border border-rule bg-surface text-ink-faint text-[12px]">
131 <svg class="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg>159 <svg class="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg>
132 <span data-search-label>Search 347 tools...</span>160 <span data-search-label>Search 347 tools...</span>
133 </div>161 </div>
134 </div>162 </div>
135 163
136 <!-- Tool rows container -->164 <!-- Scrollable tool tree with bottom fade -->
137 <div class="divide-y divide-rule" data-tool-list>165 <div class="relative flex-1 min-h-0">
138 <!-- Populated by JS -->166 <div class="overflow-y-auto h-[340px] scrollbar-thin" data-tool-list>
167 <!-- Populated by JS -->
168 </div>
169 <div class="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-surface-raised to-transparent pointer-events-none transition-opacity duration-300" data-fade></div>
139 </div>170 </div>
140 </div>171 </div>
141 172
@@ -149,60 +180,116 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 7 unmodified lines
149 name: 'GitHub', count: 347, tools: [180 name: 'GitHub', count: 347, tools: [
150 { name: 'issues.list', desc: 'List repository issues', policy: 'read', method: 'GET' },181 { name: 'issues.list', desc: 'List repository issues', policy: 'read', method: 'GET' },
151 { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'POST' },182 { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'POST' },
183 { name: 'issues.get', desc: 'Get a single issue', policy: 'read', method: 'GET' },
184 { name: 'issues.update', desc: 'Update an issue', policy: 'write', method: 'PATCH' },
185 { name: 'issues.lock', desc: 'Lock an issue conversation', policy: 'write', method: 'PUT' },
186 { name: 'issues.comments.list', desc: 'List issue comments', policy: 'read', method: 'GET' },
187 { name: 'issues.comments.create', desc: 'Create an issue comment', policy: 'write', method: 'POST' },
188 { name: 'issues.labels.list', desc: 'List labels on an issue', policy: 'read', method: 'GET' },
189 { name: 'issues.labels.add', desc: 'Add labels to an issue', policy: 'write', method: 'POST' },
152 { name: 'pulls.list', desc: 'List pull requests', policy: 'read', method: 'GET' },190 { name: 'pulls.list', desc: 'List pull requests', policy: 'read', method: 'GET' },
191 { name: 'pulls.get', desc: 'Get a pull request', policy: 'read', method: 'GET' },
192 { name: 'pulls.create', desc: 'Create a pull request', policy: 'write', method: 'POST' },
153 { name: 'pulls.merge', desc: 'Merge a pull request', policy: 'destructive', method: 'PUT' },193 { name: 'pulls.merge', desc: 'Merge a pull request', policy: 'destructive', method: 'PUT' },
194 { name: 'pulls.reviews.list', desc: 'List reviews on a PR', policy: 'read', method: 'GET' },
195 { name: 'pulls.reviews.create', desc: 'Create a PR review', policy: 'write', method: 'POST' },
154 { name: 'repos.get', desc: 'Get a repository', policy: 'read', method: 'GET' },196 { name: 'repos.get', desc: 'Get a repository', policy: 'read', method: 'GET' },
197 { name: 'repos.list', desc: 'List user repositories', policy: 'read', method: 'GET' },
198 { name: 'repos.create', desc: 'Create a repository', policy: 'write', method: 'POST' },
199 { name: 'repos.delete', desc: 'Delete a repository', policy: 'destructive', method: 'DELETE' },
155 { name: 'actions.workflows.list', desc: 'List repository workflows', policy: 'read', method: 'GET' },200 { name: 'actions.workflows.list', desc: 'List repository workflows', policy: 'read', method: 'GET' },
201 { name: 'actions.workflows.dispatch', desc: 'Trigger a workflow run', policy: 'write', method: 'POST' },
202 { name: 'actions.runs.list', desc: 'List workflow runs', policy: 'read', method: 'GET' },
203 { name: 'actions.runs.cancel', desc: 'Cancel a workflow run', policy: 'destructive', method: 'POST' },
156 ]204 ]
157 },205 },
158 slack: {206 slack: {
159 name: 'Slack', count: 42, tools: [207 name: 'Slack', count: 42, tools: [
160 { name: 'chat.postMessage', desc: 'Send a message to a channel', policy: 'write', method: 'POST' },208 { name: 'chat.postMessage', desc: 'Send a message to a channel', policy: 'write', method: 'POST' },
209 { name: 'chat.update', desc: 'Update an existing message', policy: 'write', method: 'POST' },
210 { name: 'chat.delete', desc: 'Delete a message', policy: 'destructive', method: 'POST' },
161 { name: 'conversations.list', desc: 'List all channels', policy: 'read', method: 'GET' },211 { name: 'conversations.list', desc: 'List all channels', policy: 'read', method: 'GET' },
162 { name: 'conversations.history', desc: 'Fetch message history', policy: 'read', method: 'GET' },212 { name: 'conversations.history', desc: 'Fetch message history', policy: 'read', method: 'GET' },
213 { name: 'conversations.create', desc: 'Create a new channel', policy: 'write', method: 'POST' },
214 { name: 'conversations.archive', desc: 'Archive a channel', policy: 'destructive', method: 'POST' },
215 { name: 'conversations.members', desc: 'List channel members', policy: 'read', method: 'GET' },
163 { name: 'users.list', desc: 'List all users in workspace', policy: 'read', method: 'GET' },216 { name: 'users.list', desc: 'List all users in workspace', policy: 'read', method: 'GET' },
217 { name: 'users.info', desc: 'Get user profile info', policy: 'read', method: 'GET' },
164 { name: 'reactions.add', desc: 'Add a reaction to a message', policy: 'write', method: 'POST' },218 { name: 'reactions.add', desc: 'Add a reaction to a message', policy: 'write', method: 'POST' },
219 { name: 'reactions.remove', desc: 'Remove a reaction', policy: 'write', method: 'POST' },
165 { name: 'files.upload', desc: 'Upload a file to a channel', policy: 'write', method: 'POST' },220 { name: 'files.upload', desc: 'Upload a file to a channel', policy: 'write', method: 'POST' },
221 { name: 'files.list', desc: 'List files in workspace', policy: 'read', method: 'GET' },
166 ]222 ]
167 },223 },
168 calendar: {224 calendar: {
169 name: 'Google Calendar', count: 18, tools: [225 name: 'Google Calendar', count: 18, tools: [
170 { name: 'events.list', desc: 'List upcoming events', policy: 'read', method: 'GET' },226 { name: 'events.list', desc: 'List upcoming events', policy: 'read', method: 'GET' },
227 { name: 'events.get', desc: 'Get a single event', policy: 'read', method: 'GET' },
171 { name: 'events.insert', desc: 'Create a new event', policy: 'write', method: 'POST' },228 { name: 'events.insert', desc: 'Create a new event', policy: 'write', method: 'POST' },
229 { name: 'events.update', desc: 'Update an existing event', policy: 'write', method: 'PATCH' },
172 { name: 'events.delete', desc: 'Delete an event', policy: 'destructive', method: 'DELETE' },230 { name: 'events.delete', desc: 'Delete an event', policy: 'destructive', method: 'DELETE' },
231 { name: 'events.move', desc: 'Move event to another calendar', policy: 'write', method: 'POST' },
173 { name: 'calendars.get', desc: 'Get calendar metadata', policy: 'read', method: 'GET' },232 { name: 'calendars.get', desc: 'Get calendar metadata', policy: 'read', method: 'GET' },
174 { name: 'events.update', desc: 'Update an existing event', policy: 'write', method: 'PATCH' },233 { name: 'calendars.insert', desc: 'Create a secondary calendar', policy: 'write', method: 'POST' },
234 { name: 'calendars.delete', desc: 'Delete a calendar', policy: 'destructive', method: 'DELETE' },
235 { name: 'calendarList.list', desc: 'List calendars on user list', policy: 'read', method: 'GET' },
175 { name: 'freebusy.query', desc: 'Check availability', policy: 'read', method: 'POST' },236 { name: 'freebusy.query', desc: 'Check availability', policy: 'read', method: 'POST' },
237 { name: 'acl.list', desc: 'List access control rules', policy: 'read', method: 'GET' },
238 { name: 'acl.insert', desc: 'Add an access control rule', policy: 'write', method: 'POST' },
176 ]239 ]
177 },240 },
178 linear: {241 linear: {
179 name: 'Linear', count: 56, tools: [242 name: 'Linear', count: 56, tools: [
180 { name: 'issues.list', desc: 'List issues with filters', policy: 'read', method: 'query' },243 { name: 'issues.list', desc: 'List issues with filters', policy: 'read', method: 'query' },
244 { name: 'issues.get', desc: 'Get a single issue', policy: 'read', method: 'query' },
181 { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'mutation' },245 { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'mutation' },
182 { name: 'issues.update', desc: 'Update issue status or fields', policy: 'write', method: 'mutation' },246 { name: 'issues.update', desc: 'Update issue status or fields', policy: 'write', method: 'mutation' },
247 { name: 'issues.delete', desc: 'Delete an issue permanently', policy: 'destructive', method: 'mutation' },
248 { name: 'issues.archive', desc: 'Archive an issue', policy: 'write', method: 'mutation' },
183 { name: 'projects.list', desc: 'List all projects', policy: 'read', method: 'query' },249 { name: 'projects.list', desc: 'List all projects', policy: 'read', method: 'query' },
250 { name: 'projects.create', desc: 'Create a new project', policy: 'write', method: 'mutation' },
251 { name: 'projects.update', desc: 'Update project details', policy: 'write', method: 'mutation' },
252 { name: 'comments.list', desc: 'List comments on an issue', policy: 'read', method: 'query' },
184 { name: 'comments.create', desc: 'Add a comment to an issue', policy: 'write', method: 'mutation' },253 { name: 'comments.create', desc: 'Add a comment to an issue', policy: 'write', method: 'mutation' },
185 { name: 'issues.delete', desc: 'Delete an issue permanently', policy: 'destructive', method: 'mutation' },254 { name: 'cycles.list', desc: 'List all cycles', policy: 'read', method: 'query' },
255 { name: 'teams.list', desc: 'List all teams', policy: 'read', method: 'query' },
256 { name: 'labels.list', desc: 'List all labels', policy: 'read', method: 'query' },
186 ]257 ]
187 },258 },
188 resend: {259 resend: {
189 name: 'Resend', count: 19, tools: [260 name: 'Resend', count: 19, tools: [
190 { name: 'emails.send', desc: 'Send a transactional email', policy: 'write', method: 'POST' },261 { name: 'emails.send', desc: 'Send a transactional email', policy: 'write', method: 'POST' },
191 { name: 'emails.get', desc: 'Get email delivery status', policy: 'read', method: 'GET' },262 { name: 'emails.get', desc: 'Get email delivery status', policy: 'read', method: 'GET' },
263 { name: 'emails.batch', desc: 'Send a batch of emails', policy: 'write', method: 'POST' },
192 { name: 'domains.list', desc: 'List verified domains', policy: 'read', method: 'GET' },264 { name: 'domains.list', desc: 'List verified domains', policy: 'read', method: 'GET' },
265 { name: 'domains.get', desc: 'Get domain details', policy: 'read', method: 'GET' },
266 { name: 'domains.create', desc: 'Add a new domain', policy: 'write', method: 'POST' },
193 { name: 'domains.verify', desc: 'Verify a domain', policy: 'write', method: 'POST' },267 { name: 'domains.verify', desc: 'Verify a domain', policy: 'write', method: 'POST' },
268 { name: 'domains.delete', desc: 'Remove a domain', policy: 'destructive', method: 'DELETE' },
194 { name: 'apiKeys.list', desc: 'List API keys', policy: 'read', method: 'GET' },269 { name: 'apiKeys.list', desc: 'List API keys', policy: 'read', method: 'GET' },
270 { name: 'apiKeys.create', desc: 'Create an API key', policy: 'write', method: 'POST' },
271 { name: 'contacts.list', desc: 'List contacts in audience', policy: 'read', method: 'GET' },
195 { name: 'contacts.create', desc: 'Create a new contact', policy: 'write', method: 'POST' },272 { name: 'contacts.create', desc: 'Create a new contact', policy: 'write', method: 'POST' },
273 { name: 'contacts.remove', desc: 'Remove a contact', policy: 'destructive', method: 'DELETE' },
196 ]274 ]
197 },275 },
198 axiom: {276 axiom: {
199 name: 'Axiom', count: 17, tools: [277 name: 'Axiom', count: 17, tools: [
200 { name: 'queryDataset', desc: 'Query datasets using APL', policy: 'read', method: 'tool' },278 { name: 'datasets.query', desc: 'Query datasets using APL', policy: 'read', method: 'tool' },
201 { name: 'listDatasets', desc: 'List all available datasets', policy: 'read', method: 'tool' },279 { name: 'datasets.list', desc: 'List all available datasets', policy: 'read', method: 'tool' },
202 { name: 'getDatasetFields', desc: 'List all fields in a dataset', policy: 'read', method: 'tool' },280 { name: 'datasets.fields', desc: 'List all fields in a dataset', policy: 'read', method: 'tool' },
203 { name: 'checkMonitors', desc: 'Check all monitors and their statuses', policy: 'read', method: 'tool' },281 { name: 'datasets.info', desc: 'Get dataset metadata and stats', policy: 'read', method: 'tool' },
204 { name: 'listDashboards', desc: 'List all available dashboards', policy: 'read', method: 'tool' },282 { name: 'datasets.create', desc: 'Create a new dataset', policy: 'write', method: 'tool' },
205 { name: 'deleteDashboard', desc: 'Delete a dashboard by UID', policy: 'destructive', method: 'tool' },283 { name: 'datasets.trim', desc: 'Trim data from a dataset', policy: 'destructive', method: 'tool' },
284 { name: 'monitors.list', desc: 'List all monitors', policy: 'read', method: 'tool' },
285 { name: 'monitors.check', desc: 'Check monitor statuses', policy: 'read', method: 'tool' },
286 { name: 'monitors.create', desc: 'Create a new monitor', policy: 'write', method: 'tool' },
287 { name: 'dashboards.list', desc: 'List all dashboards', policy: 'read', method: 'tool' },
288 { name: 'dashboards.get', desc: 'Get dashboard by ID', policy: 'read', method: 'tool' },
289 { name: 'dashboards.create', desc: 'Create a new dashboard', policy: 'write', method: 'tool' },
290 { name: 'dashboards.delete', desc: 'Delete a dashboard by UID', policy: 'destructive', method: 'tool' },
291 { name: 'annotations.list', desc: 'List all annotations', policy: 'read', method: 'tool' },
292 { name: 'annotations.create', desc: 'Create an annotation', policy: 'write', method: 'tool' },
206 ]293 ]
207 },294 },
208 };295 };
@@ -229,11 +316,17 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 20 unmodified lines
229 316
230 const catalog = document.getElementById('hero-catalog');317 const catalog = document.getElementById('hero-catalog');
231 const toolList = catalog.querySelector('[data-tool-list]');318 const toolList = catalog.querySelector('[data-tool-list]');
319 const fadeEl = catalog.querySelector('[data-fade]');
232 const breadcrumb = catalog.querySelector('[data-breadcrumb]');320 const breadcrumb = catalog.querySelector('[data-breadcrumb]');
233 const totalTools = catalog.querySelector('[data-total-tools]');321 const totalTools = catalog.querySelector('[data-total-tools]');
234 const searchLabel = catalog.querySelector('[data-search-label]');322 const searchLabel = catalog.querySelector('[data-search-label]');
235 const buttons = catalog.querySelectorAll('.catalog-source');
236 323
324 function updateFade() {
325 const atBottom = toolList.scrollHeight - toolList.scrollTop - toolList.clientHeight < 8;
326 const noOverflow = toolList.scrollHeight <= toolList.clientHeight;
327 fadeEl.style.opacity = (atBottom || noOverflow) ? '0' : '1';
328 }
329 toolList.addEventListener('scroll', updateFade);
237 function renderSource(key) {330 function renderSource(key) {
238 const src = sources[key];331 const src = sources[key];
239 breadcrumb.textContent = src.name;332 breadcrumb.textContent = src.name;
@@ -242,7 +335,7 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 2 unmodified lines
242 const totalCount = Object.values(sources).reduce((s, v) => s + v.count, 0);335 const totalCount = Object.values(sources).reduce((s, v) => s + v.count, 0);
243 totalTools.textContent = `${totalCount} tools indexed`;336 totalTools.textContent = `${totalCount} tools indexed`;
244 337
245 buttons.forEach(btn => {338 catalog.querySelectorAll('.catalog-source').forEach(btn => {
246 const isActive = btn.dataset.source === key;339 const isActive = btn.dataset.source === key;
247 if (isActive) {340 if (isActive) {
248 btn.className = btn.className341 btn.className = btn.className
@@ -257,26 +350,173 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 8 unmodified lines
257 }350 }
258 });351 });
259 352
260 toolList.innerHTML = src.tools.map((tool, i) => {353 // Build tree from dot-separated tool names
261 const isLast = i === src.tools.length - 1;354 const tree = {};
262 const colors = policyColors[tool.policy];355 for (const tool of src.tools) {
263 const mColor = methodColors[tool.method] || 'text-ink-faint';356 const parts = tool.name.split('.');
264 return `<div class="flex items-center gap-4 px-4 py-3${isLast ? ' opacity-40' : ''}">357 let node = tree;
265 <div class="flex-1 min-w-0">358 for (let i = 0; i < parts.length - 1; i++) {
266 <div class="font-mono text-[12px] text-ink">${tool.name}</div>359 if (!node[parts[i]]) node[parts[i]] = { _children: {} };
267 <div class="text-[11px] text-ink-faint">${tool.desc}</div>360 node = node[parts[i]]._children;
268 </div>361 }
269 <span class="text-[10px] font-mono ${colors} rounded px-1.5 py-0.5 shrink-0">${tool.policy}</span>362 const leaf = parts[parts.length - 1];
270 <span class="text-[10px] font-mono ${mColor} shrink-0">${tool.method}</span>363 node[leaf] = { _tool: tool };
271 </div>`;364 }
272 }).join('');365
366 function renderTree(node, depth = 0) {
367 let html = '';
368 const keys = Object.keys(node);
369 keys.forEach((key, idx) => {
370 const entry = node[key];
371 const isLast = idx === keys.length - 1;
372 const indent = depth * 20;
373 const connector = isLast ? '└' : '├';
374
375 if (entry._tool) {
376 // Leaf: actual tool
377 const tool = entry._tool;
378 const colors = policyColors[tool.policy];
379 const mColor = methodColors[tool.method] || 'text-ink-faint';
380 html += `<div class="flex items-center gap-3 py-1.5 pr-4 hover:bg-surface-overlay/50 transition-colors" style="padding-left:${16 + indent}px">
381 <span class="text-ink-faint font-mono text-[11px] w-3 shrink-0 select-none">${depth > 0 ? connector : '·'}</span>
382 <span class="font-mono text-[12px] text-ink flex-1 min-w-0 truncate">${key}</span>
383 <span class="text-[10px] font-mono ${colors} rounded px-1.5 py-0.5 shrink-0">${tool.policy}</span>
384 <span class="text-[10px] font-mono ${mColor} shrink-0 w-14 text-right">${tool.method}</span>
385 </div>`;
386 } else {
387 // Branch: namespace group
388 const childCount = Object.keys(entry._children).length;
389 html += `<div class="flex items-center gap-3 py-1.5 pr-4" style="padding-left:${16 + indent}px">
390 <span class="text-ink-faint font-mono text-[11px] w-3 shrink-0 select-none">${depth > 0 ? connector : '▸'}</span>
391 <span class="font-mono text-[12px] text-ink-muted">${key}</span>
392 <span class="text-[10px] text-ink-faint">${childCount}</span>
393 </div>`;
394 html += renderTree(entry._children, depth + 1);
395 }
396 });
397 return html;
398 }
399
400 toolList.innerHTML = renderTree(tree);
401 toolList.scrollTop = 0;
402 updateFade();
273 }403 }
274 404
275 buttons.forEach(btn => {405 catalog.querySelectorAll('.catalog-source').forEach(btn => {
276 btn.addEventListener('click', () => renderSource(btn.dataset.source));406 btn.addEventListener('click', () => renderSource(btn.dataset.source));
277 });407 });
278 408
279 renderSource('github');409 renderSource('github');
410
411 // ── Live detection ──
412 const detectInput = document.getElementById('detect-input');
413 const detectBtn = document.getElementById('detect-btn');
414 const detectStatus = document.getElementById('detect-status');
415 const detectExamples = document.getElementById('detect-examples');
416
417 const kindColors = {
418 openapi: { bg: 'bg-emerald-400/15', text: 'text-emerald-400', letter: 'O' },
419 graphql: { bg: 'bg-pink-400/15', text: 'text-pink-400', letter: 'Q' },
420 mcp: { bg: 'bg-cyan-400/15', text: 'text-cyan-400', letter: 'M' },
421 googleDiscovery: { bg: 'bg-yellow-400/15', text: 'text-yellow-400', letter: 'G' },
422 };
423
424 // Tailwind safelist for dynamic classes:
425 // bg-emerald-400/15 text-emerald-400 bg-pink-400/15 text-pink-400 bg-yellow-400/15 text-yellow-400
426
427 const kindLabels = { openapi: 'openapi', graphql: 'graphql', mcp: 'mcp', googleDiscovery: 'google' };
428
429 let detectedCount = 0;
430
431 async function detectUrl(url) {
432 detectBtn.disabled = true;
433 detectBtn.textContent = 'Detecting...';
434 detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-ink-muted';
435 detectStatus.textContent = 'Probing URL...';
436
437 try {
438 const res = await fetch('/api/detect', {
439 method: 'POST',
440 headers: { 'Content-Type': 'application/json' },
441 body: JSON.stringify({ url }),
442 });
443 const data = await res.json();
444
445 if (!res.ok) {
446 detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-red-400/70';
447 detectStatus.textContent = data.error || 'Detection failed';
448 return;
449 }
450
451 // Add detected source to the catalog
452 detectedCount++;
453 const key = `detected_${detectedCount}`;
454 const colors = kindColors[data.kind] || kindColors.openapi;
455 const kindLabel = kindLabels[data.kind] || data.kind;
456
457 sources[key] = {
458 name: data.name,
459 count: data.count,
460 tools: data.tools.map(t => ({
461 name: t.name,
462 desc: t.desc,
463 policy: t.policy,
464 method: t.method,
465 })),
466 };
467
468 // Add sidebar button
469 const sidebar = catalog.querySelector('.space-y-1');
470 const btn = document.createElement('button');
471 btn.dataset.source = key;
472 btn.className = 'catalog-source w-full flex items-center gap-2.5 px-2 py-2 rounded-md text-left transition-colors border border-transparent hover:bg-surface-overlay';
473 btn.innerHTML = `
474 <div class="w-5 h-5 rounded ${colors.bg} flex items-center justify-center text-[9px] ${colors.text} font-mono font-medium shrink-0">${data.name.charAt(0).toUpperCase()}</div>
475 <div class="flex-1 min-w-0">
476 <div class="text-ink-muted text-[12px] truncate">${data.name}</div>
477 <div class="text-[10px] text-ink-faint">${data.count} tools</div>
478 </div>
479 <span class="text-[9px] font-mono text-ink-faint shrink-0">${kindLabel}</span>
480 `;
481 btn.addEventListener('click', () => renderSource(key));
482 sidebar.appendChild(btn);
483
484 renderSource(key);
485
486 detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-teal';
487 detectStatus.textContent = `${data.kind} detected — ${data.count} tools found`;
488 detectExamples.classList.add('hidden');
489
490 // Clear input
491 detectInput.value = '';
492 } catch (err) {
493 detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-red-400/70';
494 detectStatus.textContent = 'Network error — could not reach detection API';
495 } finally {
496 detectBtn.disabled = false;
497 detectBtn.textContent = 'Detect';
498 }
499 }
500
501 detectBtn.addEventListener('click', () => {
502 const url = detectInput.value.trim();
503 if (url) detectUrl(url);
504 });
505
506 detectInput.addEventListener('keydown', (e) => {
507 if (e.key === 'Enter') {
508 const url = detectInput.value.trim();
509 if (url) detectUrl(url);
510 }
511 });
512
513 // Example buttons
514 document.querySelectorAll('.detect-example').forEach(btn => {
515 btn.addEventListener('click', () => {
516 detectInput.value = btn.dataset.url;
517 detectUrl(btn.dataset.url);
518 });
519 });
280 </script>520 </script>
281 </div>521 </div>
282 522
apps/marketing/src/styles/global.css+16−0
@@ -66,3 +66,19 @@ body { · 65 unmodified lines
66 opacity: 1;66 opacity: 1;
67 transform: translateY(0);67 transform: translateY(0);
68}68}
69
70/* Thin scrollbar for catalog */
71.scrollbar-thin {
72 scrollbar-width: thin;
73 scrollbar-color: rgba(255,255,255,0.08) transparent;
74}
75.scrollbar-thin::-webkit-scrollbar {
76 width: 4px;
77}
78.scrollbar-thin::-webkit-scrollbar-track {
79 background: transparent;
80}
81.scrollbar-thin::-webkit-scrollbar-thumb {
82 background: rgba(255,255,255,0.1);
83 border-radius: 2px;
84}
bun.lock+5−0
@@ -42,6 +42,7 @@ · 41 unmodified lines
42 "name": "@executor/marketing",42 "name": "@executor/marketing",
43 "version": "0.0.1",43 "version": "0.0.1",
44 "dependencies": {44 "dependencies": {
45 "@astrojs/node": "^10.0.4",
45 "@astrojs/tailwind": "^6.0.2",46 "@astrojs/tailwind": "^6.0.2",
46 "@tailwindcss/vite": "^4.2.2",47 "@tailwindcss/vite": "^4.2.2",
47 "astro": "^6.1.3",48 "astro": "^6.1.3",
@@ -571,6 +572,8 @@ · 523 unmodified lines
571 572
572 "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/prism": "4.0.1", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ=="],573 "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/prism": "4.0.1", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ=="],
573 574
575 "@astrojs/node": ["@astrojs/node@10.0.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-7pVgiVSscQHRC2WqjlXcnbbcKMYp2GXrYpmuvdGg5zgA8J1lFm2vmwVhHZFuZK3Ik5PzoxiDROaEgoDGLbfhLw=="],
576
574 "@astrojs/prism": ["@astrojs/prism@4.0.1", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ=="],577 "@astrojs/prism": ["@astrojs/prism@4.0.1", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ=="],
575 578
576 "@astrojs/tailwind": ["@astrojs/tailwind@6.0.2", "", { "dependencies": { "autoprefixer": "^10.4.21", "postcss": "^8.5.3", "postcss-load-config": "^4.0.2" }, "peerDependencies": { "astro": "^3.0.0 || ^4.0.0 || ^5.0.0", "tailwindcss": "^3.0.24" } }, "sha512-j3mhLNeugZq6A8dMNXVarUa8K6X9AW+QHU9u3lKNrPLMHhOQ0S7VeWhHwEeJFpEK1BTKEUY1U78VQv2gN6hNGg=="],579 "@astrojs/tailwind": ["@astrojs/tailwind@6.0.2", "", { "dependencies": { "autoprefixer": "^10.4.21", "postcss": "^8.5.3", "postcss-load-config": "^4.0.2" }, "peerDependencies": { "astro": "^3.0.0 || ^4.0.0 || ^5.0.0", "tailwindcss": "^3.0.24" } }, "sha512-j3mhLNeugZq6A8dMNXVarUa8K6X9AW+QHU9u3lKNrPLMHhOQ0S7VeWhHwEeJFpEK1BTKEUY1U78VQv2gN6hNGg=="],
@@ -2367,6 +2370,8 @@ · 1790 unmodified lines
2367 2370
2368 "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],2371 "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
2369 2372
2373 "server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
2374
2370 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],2375 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
2371 2376
2372 "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],2377 "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t