feat: more work on storage

pull/718/head
Travis Fischer 2025-07-02 07:26:42 -05:00
rodzic 62b8246cc3
commit 762953e8c3
34 zmienionych plików z 571 dodań i 140 usunięć

Wyświetl plik

@ -47,6 +47,7 @@
"@sentry/node": "catalog:",
"bcryptjs": "catalog:",
"exit-hook": "catalog:",
"file-type": "^21.0.0",
"hono": "catalog:",
"ky": "catalog:",
"octokit": "catalog:",

Wyświetl plik

@ -19,6 +19,7 @@ import {
openapiErrorResponse409,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { uploadFileToStorage } from '@/lib/storage'
import { createDeploymentQuerySchema } from './schemas'
@ -152,7 +153,12 @@ export function registerV1CreateDeployment(
// - tool definitions
const agenticProjectConfig = await resolveAgenticProjectConfig(body, {
label: `deployment "${deploymentIdentifier}"`,
logger
logger,
uploadFileToStorage: async (source) => {
return uploadFileToStorage(source, {
projectIdentifier
})
}
})
// Create the deployment

Wyświetl plik

@ -1,3 +1,4 @@
import { sha256 } from '@agentic/platform-core'
import {
DeleteObjectCommand,
type DeleteObjectCommandInput,
@ -8,6 +9,8 @@ import {
S3Client
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { fileTypeFromBuffer } from 'file-type'
import ky from 'ky'
import { env } from './env'
@ -89,3 +92,55 @@ export async function getStorageSignedUploadUrl(
{ expiresIn }
)
}
export async function uploadFileToStorage(
input: string,
{
projectIdentifier
}: {
projectIdentifier: string
}
): Promise<string> {
let source: URL
try {
source = new URL(input)
} catch {
// Not a URL
throw new Error(`Invalid source file URL: ${input}`)
}
if (source.hostname === 'storage.agentic.so') {
// The source is already a public URL hosted on Agentic's blob storage.
return source.toString()
}
const sourceBuffer = await ky.get(source).arrayBuffer()
const [hash, fileType] = await Promise.all([
sha256(sourceBuffer),
fileTypeFromBuffer(sourceBuffer)
])
const filename = fileType ? `${hash}.${fileType.ext}` : hash
const key = `${projectIdentifier}/${filename}`
const publicObjectUrl = getStorageObjectPublicUrl(key)
try {
// Check if the object already exists.
await ky.head(publicObjectUrl)
} catch {
const signedUploadUrl = await getStorageSignedUploadUrl(key)
// Object doesn't exist yet, so upload it.
await ky.post(signedUploadUrl, {
body: sourceBuffer,
headers: {
'Content-Type': fileType?.mime ?? 'application/octet-stream'
}
})
}
return publicObjectUrl
}

Wyświetl plik

@ -15,7 +15,10 @@ export async function deployProjects(
const deployments = await pMap(
projects,
async (project) => {
const config = await loadAgenticConfig({ cwd: project })
const config = await loadAgenticConfig({
cwd: project,
agenticApiClient: client
})
const deployment = await client.createDeployment(config)
console.log(`Deployed ${project} => ${deployment.identifier}`)

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Wyświetl plik

@ -23,12 +23,11 @@
},
"readme": {
"type": "string",
"description": "A readme documenting the project (supports GitHub-flavored markdown)."
"description": "Optional markdown readme documenting the project (supports GitHub-flavored markdown)."
},
"iconUrl": {
"icon": {
"type": "string",
"format": "uri",
"description": "Optional logo image URL to use for the project. Logos should have a square aspect ratio."
"description": "Optional logo image to use for the project. Logos should have a square aspect ratio."
},
"sourceUrl": {
"type": "string",

Wyświetl plik

@ -0,0 +1,10 @@
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test Invalid Metadata 0',
origin: {
type: 'raw',
url: 'https://httpbin.org'
},
icon: false as any
})

Wyświetl plik

@ -0,0 +1,10 @@
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test Metadata 1',
origin: {
type: 'raw',
url: 'https://httpbin.org'
},
readme: './not-found.md'
})

Wyświetl plik

@ -8,38 +8,8 @@ export default defineConfig({
url: 'https://agentic-platform-fixtures-everything.onrender.com',
spec: 'https://agentic-platform-fixtures-everything.onrender.com/docs'
},
readme: `
# Test Everything OpenAPI
This is testing **readme rendering**.
## Misc
- [ ] Item 1
- [ ] Item 2
- [x] Item 3
---
- _italic_
- **bold**
- [link](https://www.google.com)
- \`code\`
## Code
\`\`\`ts
const a = 1
export function foo() {
console.log('hello world')
}
\`\`\`
## Images
![Image](https://placehold.co/600x400)
`,
icon: 'https://storage.agentic.so/agentic-dev-icon-circle-dark.svg',
readme: './readme.md',
toolConfigs: [
{
name: 'get_user',

Wyświetl plik

@ -0,0 +1,30 @@
# Test Everything OpenAPI
This is testing **readme rendering**.
## Misc
- [ ] Item 1
- [ ] Item 2
- [x] Item 3
---
- _italic_
- **bold**
- [link](https://www.google.com)
- `code`
## Code
```ts
const a = 1
export function foo() {
console.log('hello world')
}
```
## Images
![Image](https://placehold.co/600x400)

Wyświetl plik

@ -0,0 +1,9 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="48" cy="48" r="48" fill="#888888"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2922 73.2067C22.6248 73.255 18.9825 73.2065 15.3653 73.0611C26.752 55.048 38.1402 37.0309 49.5299 19.0097C51.1635 17.3293 52.9847 17.1107 54.9933 18.3541C57.491 20.4558 58.2438 23.0539 57.2515 26.1485C55.4913 29.1352 53.6945 32.0975 51.8609 35.0357C44.1878 47.1766 36.5149 59.3176 28.8417 71.4585C28.2861 72.4763 27.4363 73.059 26.2922 73.2067Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.4302 37.3668C55.7566 37.5224 56.0237 37.7651 56.2315 38.0952C64.3606 49.8615 72.5437 61.5897 80.7804 73.2796C77.1867 73.3768 73.5931 73.3768 69.9993 73.2796C68.9793 73.049 68.1295 72.5391 67.4497 71.7499C62.3379 64.2943 57.1902 56.864 52.0065 49.4591C51.4627 48.6715 50.9772 47.8459 50.5496 46.9824C50.3708 45.8664 50.6137 44.8466 51.278 43.9229C52.6986 41.7519 54.0827 39.5666 55.4302 37.3668Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.8803 73.061C59.7288 73.2064 55.5523 73.255 51.3509 73.2067C50.2417 71.5381 49.1005 69.887 47.9271 68.2532C46.8451 69.884 45.8009 71.5351 44.7948 73.2067C40.6904 73.255 36.611 73.2064 32.5567 73.061C36.3849 67.0221 40.2457 61.0003 44.1392 54.9953C45.7173 53.3419 47.5871 52.9292 49.7483 53.757C50.3223 54.0317 50.8322 54.396 51.278 54.8497C55.5109 60.9027 59.7117 66.9732 63.8803 73.061Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3652 73.0611C18.9825 73.2065 22.6247 73.255 26.2921 73.2067C22.625 73.4004 18.9342 73.4004 15.2195 73.2067C15.2376 73.1183 15.2861 73.0698 15.3652 73.0611Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5568 73.0611C36.6111 73.2065 40.6904 73.255 44.7948 73.2067C40.6907 73.4004 36.5629 73.4004 32.4111 73.2067C32.4292 73.1183 32.4777 73.0698 32.5568 73.0611Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.8803 73.0611C63.9594 73.0698 64.0079 73.1183 64.026 73.2067C59.7771 73.4005 55.552 73.4005 51.3509 73.2067C55.5523 73.255 59.7289 73.2065 63.8803 73.0611Z" fill="white"/>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Wyświetl plik

@ -0,0 +1,11 @@
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test Metadata 0',
origin: {
type: 'raw',
url: 'https://httpbin.org'
},
icon: './agentic-dev-icon-circle-dark.svg',
readme: './readme.md'
})

Wyświetl plik

@ -0,0 +1,30 @@
# Test Metadata
This is testing **readme rendering**.
## Misc
- [ ] Item 1
- [ ] Item 2
- [x] Item 3
---
- _italic_
- **bold**
- [link](https://www.google.com)
- `code`
## Code
```ts
const a = 1
export function foo() {
console.log('hello world')
}
```
## Images
![Image](https://placehold.co/600x400)

Wyświetl plik

@ -0,0 +1,12 @@
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test Metadata 1',
origin: {
type: 'raw',
url: 'https://httpbin.org'
},
icon: 'https://agentic.so/agentic-icon-circle-light.svg',
readme:
'https://raw.githubusercontent.com/transitive-bullshit/agentic/refs/heads/main/readme.md'
})

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -398,6 +398,11 @@ export class AgenticApiClient {
}
if (source instanceof URL) {
if (source.hostname === 'storage.agentic.so') {
// The source is already a public URL hosted on Agentic's blob storage.
return source.toString()
}
sourceBuffer = await defaultKy.get(source).arrayBuffer()
} else if (source instanceof ArrayBuffer) {
sourceBuffer = source

Wyświetl plik

@ -8,7 +8,8 @@ import type { Context } from '../types'
export function registerDebugCommand({
program,
logger,
handleError
handleError,
client
}: Context) {
const command = new Command('debug')
.description('Prints config for a local project.')
@ -26,7 +27,8 @@ export function registerDebugCommand({
// client.
const config = await oraPromise(
loadAgenticConfig({
cwd: opts.cwd
cwd: opts.cwd,
agenticApiClient: client
}),
{
text: `Loading Agentic config from ${opts.cwd ?? process.cwd()}`,

Wyświetl plik

@ -33,7 +33,8 @@ export function registerDeployCommand({
// client.
const config = await oraPromise(
loadAgenticConfig({
cwd: opts.cwd
cwd: opts.cwd,
agenticApiClient: client
}),
{
text: `Loading Agentic config from ${opts.cwd ?? process.cwd()}`,

Wyświetl plik

@ -18,7 +18,10 @@ export async function resolveDeployment({
populate?: ('user' | 'team' | 'project')[]
}): Promise<Deployment> {
if (!deploymentIdentifier) {
const config = await loadAgenticConfig({ cwd })
const config = await loadAgenticConfig({
cwd,
agenticApiClient: client
})
// TODO: re-add team support
const auth = AuthStore.getAuth()

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,5 +1,6 @@
export * from './define-config'
export * from './load-agentic-config'
export * from './resolve-agentic-project-config'
export type * from './types'
export * from './validate-agentic-project-config'
export { defaultFreePricingPlan } from '@agentic/platform-types'

Wyświetl plik

@ -15,7 +15,10 @@ const fixtures = [
'pricing-custom-0',
'basic-openapi',
'basic-mcp',
'everything-openapi'
'everything-openapi',
'metadata-0',
'metadata-1',
'metadata-2'
]
const invalidFixtures = [
@ -37,7 +40,9 @@ const invalidFixtures = [
'invalid-slug-1',
'invalid-slug-2',
'invalid-slug-3',
'invalid-slug-4'
'invalid-slug-4',
'invalid-metadata-0',
'invalid-metadata-1'
]
const fixturesDir = path.join(

Wyświetl plik

@ -1,4 +1,4 @@
import { parseZodSchema } from '@agentic/platform-core'
import { parseZodSchema, pruneUndefined } from '@agentic/platform-core'
import {
type AgenticProjectConfig,
agenticProjectConfigSchema,
@ -18,16 +18,18 @@ export function parseAgenticProjectConfig(
inputConfig: unknown,
{ strip = false, strict = false }: { strip?: boolean; strict?: boolean } = {}
): AgenticProjectConfig {
return parseZodSchema(
strip
? agenticProjectConfigSchema.strip()
: strict
? agenticProjectConfigSchema.strict()
: agenticProjectConfigSchema,
inputConfig,
{
statusCode: 400
}
return pruneUndefined(
parseZodSchema(
strip
? agenticProjectConfigSchema.strip()
: strict
? agenticProjectConfigSchema.strict()
: agenticProjectConfigSchema,
inputConfig,
{
statusCode: 400
}
)
) as AgenticProjectConfig
}
@ -38,15 +40,17 @@ export function parseResolvedAgenticProjectConfig(
inputConfig: unknown,
{ strip = false, strict = false }: { strip?: boolean; strict?: boolean } = {}
): ResolvedAgenticProjectConfig {
return parseZodSchema(
strip
? resolvedAgenticProjectConfigSchema.strip()
: strict
? resolvedAgenticProjectConfigSchema.strict()
: resolvedAgenticProjectConfigSchema,
inputConfig,
{
statusCode: 400
}
return pruneUndefined(
parseZodSchema(
strip
? resolvedAgenticProjectConfigSchema.strip()
: strict
? resolvedAgenticProjectConfigSchema.strict()
: resolvedAgenticProjectConfigSchema,
inputConfig,
{
statusCode: 400
}
)
) as ResolvedAgenticProjectConfig
}

Wyświetl plik

@ -5,24 +5,32 @@ import type {
ResolvedAgenticProjectConfig
} from '@agentic/platform-types'
import type { UploadFileToStorageFn } from './types'
import {
parseAgenticProjectConfig,
parseResolvedAgenticProjectConfig
} from './parse-agentic-project-config'
import { resolveMetadata } from './resolve-metadata'
import { resolveMetadataFiles } from './resolve-metadata-files'
import { resolveOriginAdapter } from './resolve-origin-adapter'
import { validatePricing } from './validate-pricing'
import { validateTools } from './validate-tools'
export async function resolveAgenticProjectConfig(
inputConfig: AgenticProjectConfig | AgenticProjectConfigRaw,
opts: { logger?: Logger; cwd?: string; label?: string } = {}
opts: {
logger?: Logger
cwd?: string
label?: string
uploadFileToStorage: UploadFileToStorageFn
}
): Promise<ResolvedAgenticProjectConfig> {
const config = parseAgenticProjectConfig(inputConfig)
const { slug, version } = resolveMetadata(config)
const { slug, version } = await resolveMetadata(config)
validatePricing(config)
const { readme, iconUrl } = await resolveMetadataFiles(config, opts)
const { origin, tools } = await resolveOriginAdapter({
slug,
version,
@ -35,6 +43,8 @@ export async function resolveAgenticProjectConfig(
...config,
slug,
version,
readme,
iconUrl,
origin,
tools
})

Wyświetl plik

@ -0,0 +1,22 @@
import type { UploadFileToStorageFn } from './types'
export async function resolveMetadataFile(
input: string | undefined,
{
label,
uploadFileToStorage
}: {
label: string
uploadFileToStorage: UploadFileToStorageFn
}
): Promise<string | undefined> {
if (!input) return
try {
const source = new URL(input)
return uploadFileToStorage(source.toString())
} catch {
throw new Error(`Invalid "${label}" must be a public URL: "${input}"`)
}
}

Wyświetl plik

@ -0,0 +1,35 @@
import type {
AgenticProjectConfig,
ResolvedAgenticProjectConfig
} from '@agentic/platform-types'
import type { UploadFileToStorageFn } from './types'
import { resolveMetadataFile } from './resolve-metadata-file'
export async function resolveMetadataFiles(
{ readme, icon }: Pick<AgenticProjectConfig, 'readme' | 'icon'>,
{
uploadFileToStorage
}: {
uploadFileToStorage: UploadFileToStorageFn
}
): Promise<Pick<ResolvedAgenticProjectConfig, 'readme' | 'iconUrl'>> {
if (readme) {
readme = await resolveMetadataFile(readme, {
label: 'readme',
uploadFileToStorage
})
}
if (icon) {
icon = await resolveMetadataFile(icon, {
label: 'icon',
uploadFileToStorage
})
}
return {
readme,
iconUrl: icon
}
}

Wyświetl plik

@ -6,13 +6,12 @@ import { assert, slugify } from '@agentic/platform-core'
import { isValidProjectSlug } from '@agentic/platform-validators'
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
export function resolveMetadata({
export async function resolveMetadata({
name,
slug,
version
}: Pick<AgenticProjectConfig, 'name' | 'slug' | 'version'>): Pick<
ResolvedAgenticProjectConfig,
'slug' | 'version'
}: Pick<AgenticProjectConfig, 'name' | 'slug' | 'version'>): Promise<
Pick<ResolvedAgenticProjectConfig, 'slug' | 'version'>
> {
slug ??= slugify(name)

Wyświetl plik

@ -0,0 +1 @@
export type UploadFileToStorageFn = (source: string) => Promise<string>

Wyświetl plik

@ -3,6 +3,7 @@ import { type Logger } from '@agentic/platform-core'
import { parseAgenticProjectConfig } from './parse-agentic-project-config'
import { resolveMetadata } from './resolve-metadata'
import { validateMetadataFiles } from './validate-metadata-files'
import { validateOriginAdapter } from './validate-origin-adapter'
import { validatePricing } from './validate-pricing'
@ -11,16 +12,22 @@ export async function validateAgenticProjectConfig(
{
strip = false,
...opts
}: { logger?: Logger; cwd?: string; strip?: boolean; label?: string } = {}
}: {
logger?: Logger
cwd?: string
strip?: boolean
label?: string
} = {}
): Promise<AgenticProjectConfig> {
const config = parseAgenticProjectConfig(inputConfig, {
strip,
strict: !strip
})
const { slug, version } = resolveMetadata(config)
const { slug, version } = await resolveMetadata(config)
validatePricing(config)
const { readme, icon } = await validateMetadataFiles(config, opts)
const origin = await validateOriginAdapter({
slug,
version,
@ -34,6 +41,8 @@ export async function validateAgenticProjectConfig(
...config,
slug,
version,
readme,
icon,
origin
},
{ strip, strict: !strip }

Wyświetl plik

@ -0,0 +1,35 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
export async function validateMetadataFile(
input: string | undefined,
{
label,
cwd
}: {
label: string
cwd: string
}
): Promise<string | undefined> {
if (!input) return
let source: string | ArrayBuffer | URL
try {
// Check if it's a URL.
source = new URL(input)
return source.toString()
} catch {
try {
// Not a URL; check if it's a local file path.
const buffer = await readFile(path.resolve(cwd, input))
return `data:application/octet-stream;base64,${buffer.toString('base64')}`
} catch {
throw new Error(
`Invalid "${label}" (must be a URL or a path to a local file): "${input}"`
)
}
}
}

Wyświetl plik

@ -0,0 +1,25 @@
import type { AgenticProjectConfig } from '@agentic/platform-types'
import { validateMetadataFile } from './validate-metadata-file'
export async function validateMetadataFiles(
{ readme, icon }: Pick<AgenticProjectConfig, 'readme' | 'icon'>,
{
cwd = process.cwd()
}: {
cwd?: string
}
): Promise<Pick<AgenticProjectConfig, 'readme' | 'icon'>> {
if (readme) {
readme = await validateMetadataFile(readme, { label: 'readme', cwd })
}
if (icon) {
icon = await validateMetadataFile(icon, { label: 'icon', cwd })
}
return {
readme,
icon
}
}

Wyświetl plik

@ -99,27 +99,37 @@ export const agenticProjectConfigSchema = z
.optional(),
/**
* Optional embedded markdown readme documenting the project (supports GitHub-flavored markdown).
* Optional markdown readme documenting the project (supports GitHub-flavored markdown).
*
* A string which may be either:
* - A URL to a remote markdown file (eg, `https://example.com/readme.md`)
* - A local file path (eg, `./readme.md`)
* - A data-uri string (eg, `data:text/markdown;base64,SGVsbG8gV29ybGQ=`)
*/
readme: z
.string()
.describe(
'A readme documenting the project (supports GitHub-flavored markdown).'
'Optional markdown readme documenting the project (supports GitHub-flavored markdown).'
)
.optional(),
/**
* Optional logo image URL to use for the project. Logos should have a
* square aspect ratio.
* Optional logo image to use for the project.
*
* A string which may be either:
* - A URL to a remote image (eg, `https://example.com/logo.png`)
* - A local file path (eg, `./logo.png`)
* - A data-uri string (eg, `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...`)
*
* Logos should have a square aspect ratio.
*
* @example "https://example.com/logo.png"
*/
iconUrl: z
icon: z
.string()
.url()
.optional()
.describe(
'Optional logo image URL to use for the project. Logos should have a square aspect ratio.'
'Optional logo image to use for the project. Logos should have a square aspect ratio.'
),
/**
@ -254,7 +264,10 @@ export type AgenticProjectConfigRaw = z.output<
typeof agenticProjectConfigSchema
>
export type AgenticProjectConfig = Simplify<
Omit<AgenticProjectConfigRaw, 'pricingPlans' | 'toolConfigs'> & {
Omit<
AgenticProjectConfigRaw,
'pricingPlans' | 'toolConfigs' | 'defaultRateLimit'
> & {
slug: string
pricingPlans: PricingPlanList
toolConfigs: ToolConfig[]
@ -266,14 +279,30 @@ export const resolvedAgenticProjectConfigSchema = agenticProjectConfigSchema
.required({
slug: true
})
.omit({
icon: true
})
.extend({
/**
* Optional logo image URL to use for the project. Logos should have a
* square aspect ratio.
*
* @example "https://example.com/logo.png"
*/
iconUrl: z
.string()
.optional()
.describe(
'Optional logo image URL to use for the project. Logos should have a square aspect ratio.'
),
origin: originAdapterSchema,
tools: z.array(toolSchema).default([])
})
export type ResolvedAgenticProjectConfig = Simplify<
Omit<
z.output<typeof resolvedAgenticProjectConfigSchema>,
'pricingPlans' | 'toolConfigs'
'pricingPlans' | 'toolConfigs' | 'defaultRateLimit'
> & {
slug: string
pricingPlans: PricingPlanList

Wyświetl plik

@ -792,12 +792,9 @@ export interface components {
name: string;
/** @description A short description of the project. */
description?: string;
/** @description A readme documenting the project (supports GitHub-flavored markdown). */
/** @description Optional markdown readme documenting the project (supports GitHub-flavored markdown). */
readme?: string;
/**
* Format: uri
* @description Optional logo image URL to use for the project. Logos should have a square aspect ratio.
*/
/** @description Optional logo image URL to use for the project. Logos should have a square aspect ratio. */
iconUrl?: string;
/**
* Format: uri
@ -1071,12 +1068,9 @@ export interface components {
name: string;
/** @description A short description of the project. */
description?: string;
/** @description A readme documenting the project (supports GitHub-flavored markdown). */
/** @description Optional markdown readme documenting the project (supports GitHub-flavored markdown). */
readme?: string;
/**
* Format: uri
* @description Optional logo image URL to use for the project. Logos should have a square aspect ratio.
*/
/** @description Optional logo image URL to use for the project. Logos should have a square aspect ratio. */
iconUrl?: string;
/**
* Format: uri
@ -2485,13 +2479,10 @@ export interface operations {
version?: string;
/** @description A short description of the project. */
description?: string;
/** @description A readme documenting the project (supports GitHub-flavored markdown). */
/** @description Optional markdown readme documenting the project (supports GitHub-flavored markdown). */
readme?: string;
/**
* Format: uri
* @description Optional logo image URL to use for the project. Logos should have a square aspect ratio.
*/
iconUrl?: string;
/** @description Optional logo image to use for the project. Logos should have a square aspect ratio. */
icon?: string;
/**
* Format: uri
* @description Optional URL to the source code of the project (eg, GitHub repo).

Wyświetl plik

@ -617,6 +617,9 @@ importers:
exit-hook:
specifier: 'catalog:'
version: 4.0.0
file-type:
specifier: ^21.0.0
version: 21.0.0
hono:
specifier: 'catalog:'
version: 4.8.3