pull/715/head
Travis Fischer 2025-05-27 02:09:46 +07:00
rodzic c85d2e449a
commit 09198abd29
12 zmienionych plików z 253 dodań i 10 usunięć

Wyświetl plik

@ -3,10 +3,12 @@ import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
import type { AuthenticatedEnv } from '@/lib/types'
import { and, db, eq, schema } from '@/db'
import { acl } from '@/lib/acl'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { tryGetProjectByIdentifier } from '@/lib/projects/try-get-project-by-identifier'
import { paginationAndPopulateAndFilterDeploymentSchema } from './schemas'
@ -43,18 +45,31 @@ export function registerV1DeploymentsListDeployments(
sort = 'desc',
sortBy = 'createdAt',
populate = [],
projectId
projectIdentifier,
deploymentIdentifier
} = c.req.valid('query')
const userId = c.get('userId')
const teamMember = c.get('teamMember')
let projectId: string | undefined
if (projectIdentifier) {
const project = await tryGetProjectByIdentifier(c, {
projectIdentifier
})
await acl(c, project, { label: 'Project' })
projectId = project.id
}
const deployments = await db.query.deployments.findMany({
where: and(
teamMember
? eq(schema.deployments.teamId, teamMember.teamId)
: eq(schema.deployments.userId, userId),
projectId ? eq(schema.deployments.projectId, projectId) : undefined
projectId ? eq(schema.deployments.projectId, projectId) : undefined,
deploymentIdentifier
? eq(schema.deployments.identifier, deploymentIdentifier)
: undefined
),
with: {
...Object.fromEntries(populate.map((field) => [field, true]))

Wyświetl plik

@ -5,7 +5,7 @@ import {
deploymentIdSchema,
deploymentRelationsSchema,
paginationSchema,
projectIdSchema
projectIdentifierSchema
} from '@/db'
export const deploymentIdParamsSchema = z.object({
@ -23,7 +23,8 @@ export const createDeploymentQuerySchema = z.object({
})
export const filterDeploymentSchema = z.object({
projectId: projectIdSchema.optional()
projectIdentifier: projectIdentifierSchema.optional(),
deploymentIdentifier: deploymentIdentifierSchema.optional()
})
export const populateDeploymentSchema = z.object({

Wyświetl plik

@ -2,7 +2,7 @@ import { assert } from '@agentic/platform-core'
import { parseFaasIdentifier } from '@agentic/platform-validators'
import type { AuthenticatedContext } from '@/lib/types'
import { db, eq, type RawDeployment, schema } from '@/db'
import { db, deploymentIdSchema, eq, type RawDeployment, schema } from '@/db'
import { ensureAuthUser } from '@/lib/ensure-auth-user'
/**
@ -28,6 +28,16 @@ export async function tryGetDeploymentByIdentifier(
): Promise<RawDeployment> {
const user = await ensureAuthUser(ctx)
// First check if the identifier is a deployment ID
if (deploymentIdSchema.safeParse(deploymentIdentifier).success) {
const deployment = await db.query.deployments.findFirst({
...dbQueryOpts,
where: eq(schema.deployments.id, deploymentIdentifier)
})
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
return deployment
}
const teamMember = ctx.get('teamMember')
const namespace = teamMember ? teamMember.teamSlug : user.username
const parsedFaas = parseFaasIdentifier(deploymentIdentifier, {

Wyświetl plik

@ -0,0 +1,60 @@
import { assert } from '@agentic/platform-core'
import { parseFaasIdentifier } from '@agentic/platform-validators'
import type { AuthenticatedContext } from '@/lib/types'
import { db, eq, projectIdSchema, type RawProject, schema } from '@/db'
import { ensureAuthUser } from '@/lib/ensure-auth-user'
/**
* Attempts to find the Project matching the given identifier.
*
* Throws a HTTP 404 error if not found.
*
* Does not take care of ACLs.
*/
export async function tryGetProjectByIdentifier(
ctx: AuthenticatedContext,
{
projectIdentifier,
...dbQueryOpts
}: {
projectIdentifier: string
with?: {
user?: true
team?: true
lastPublishedproject?: true
lastproject?: true
}
}
): Promise<RawProject> {
const user = await ensureAuthUser(ctx)
// First check if the identifier is a project ID
if (projectIdSchema.safeParse(projectIdentifier).success) {
const project = await db.query.projects.findFirst({
...dbQueryOpts,
where: eq(schema.projects.id, projectIdentifier)
})
assert(project, 404, `project not found "${projectIdentifier}"`)
return project
}
const teamMember = ctx.get('teamMember')
const namespace = teamMember ? teamMember.teamSlug : user.username
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
namespace
})
assert(
parsedFaas?.projectIdentifier,
400,
`Invalid project identifier "${projectIdentifier}"`
)
const project = await db.query.projects.findFirst({
...dbQueryOpts,
where: eq(schema.projects.identifier, parsedFaas.projectIdentifier)
})
assert(project, 404, `Project not found "${projectIdentifier}"`)
return project
}

Wyświetl plik

@ -1379,8 +1379,10 @@ export interface operations {
sort?: "asc" | "desc";
sortBy?: "createdAt" | "updatedAt";
populate?: ("user" | "team" | "project")[];
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
projectId?: string;
/** @description Public project identifier (e.g. "namespace/project-name") */
projectIdentifier?: components["schemas"]["ProjectIdentifier"];
/** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */
deploymentIdentifier?: components["schemas"]["DeploymentIdentifier"];
};
header?: never;
path?: never;

Wyświetl plik

@ -27,6 +27,7 @@
"@agentic/platform-api-client": "workspace:*",
"@agentic/platform-core": "workspace:*",
"@agentic/platform-schemas": "workspace:*",
"@agentic/platform-validators": "workspace:*",
"@clack/prompts": "^0.11.0",
"@hono/node-server": "^1.14.1",
"commander": "^14.0.0",

Wyświetl plik

@ -5,6 +5,8 @@ import { Command } from 'commander'
import restoreCursor from 'restore-cursor'
import { registerDeployCommand } from './commands/deploy'
import { registerGetDeploymentCommand } from './commands/get'
import { registerListDeploymentsCommand } from './commands/list'
import { registerPublishCommand } from './commands/publish'
import { registerSigninCommand } from './commands/signin'
import { registerSignoutCommand } from './commands/signout'
@ -75,6 +77,8 @@ async function main() {
registerSignoutCommand(ctx)
registerDeployCommand(ctx)
registerPublishCommand(ctx)
registerGetDeploymentCommand(ctx)
registerListDeploymentsCommand(ctx)
program.parse()
}

Wyświetl plik

@ -0,0 +1,33 @@
import { Command } from 'commander'
import { oraPromise } from 'ora'
import type { Context } from '../types'
import { AuthStore } from '../lib/auth-store'
export function registerGetDeploymentCommand({
client,
program,
logger
}: Context) {
const command = new Command('get')
.description('Gets details for a specific deployment')
.argument('<deploymentIdentifier>', 'Deployment ID or identifier')
.action(async (deploymentIdentifier) => {
AuthStore.requireAuth()
const deployment = await oraPromise(
client.getDeploymentByIdentifier({
deploymentIdentifier
}),
{
text: 'Resolving deployment...',
successText: 'Resolved deployment',
failText: 'Failed to resolve deployment'
}
)
logger.log(deployment)
})
program.addCommand(command)
}

Wyświetl plik

@ -0,0 +1,92 @@
import type { Deployment } from '@agentic/platform-api-client'
import { parseFaasIdentifier } from '@agentic/platform-validators'
import { Command } from 'commander'
import { oraPromise } from 'ora'
import type { Context } from '../types'
import { AuthStore } from '../lib/auth-store'
import { pruneDeployment } from '../lib/utils'
export function registerListDeploymentsCommand({
client,
program,
logger
}: Context) {
const command = new Command('list')
.alias('ls')
.description('Lists deployments')
.argument('[projectIdentifier]', 'Optional project identifier')
.option('-v, --verbose', 'Display full deployments', false)
.action(async (projectIdentifier, opts) => {
AuthStore.requireAuth()
const query: Parameters<typeof client.listDeployments>[0] = {}
let label = 'Fetching all projects and deployments'
if (projectIdentifier) {
const auth = AuthStore.getAuth()
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
// TODO: use team slug if available
namespace: auth.user.username
})
if (!parsedFaas) {
throw new Error(`Invalid project identifier "${projectIdentifier}"`)
}
if (parsedFaas.deploymentIdentifier) {
query.deploymentIdentifier = parsedFaas.deploymentIdentifier
label = `Fetching deployment "${query.deploymentIdentifier}"`
} else {
query.projectIdentifier = parsedFaas.projectIdentifier
label = `Fetching deployments for project "${query.projectIdentifier}"`
}
}
const deployments = await oraPromise(client.listDeployments(query), label)
const projectIdToDeploymentsMap: Record<string, Deployment[]> = {}
const sortedProjects: {
projectId: string
deployments: Deployment[]
}[] = []
// Aggregate deployments by project
for (const deployment of deployments) {
const prunedDeployment = pruneDeployment(deployment, opts)
const { projectId } = deployment
if (!projectIdToDeploymentsMap[projectId]) {
projectIdToDeploymentsMap[projectId] = []
}
projectIdToDeploymentsMap[projectId].push(prunedDeployment)
}
// Sort deployments within each project with recently created first
for (const projectId of Object.keys(projectIdToDeploymentsMap)) {
const deployments = projectIdToDeploymentsMap[projectId]!
deployments.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
sortedProjects.push({
projectId,
deployments
})
}
// Sort projects with most recently created first
sortedProjects.sort(
(a, b) =>
new Date(b.deployments[0]!.createdAt).getTime() -
new Date(a.deployments[0]!.createdAt).getTime()
)
// TODO: better output formatting
logger.log(sortedProjects)
})
program.addCommand(command)
}

Wyświetl plik

@ -0,0 +1,22 @@
import type { Deployment } from '@agentic/platform-api-client'
export function pruneDeployment(
deployment: Deployment,
{ verbose = false }: { verbose?: boolean }
): Deployment {
if (!verbose) {
const d = structuredClone(deployment)
if (d.readme) {
d.readme = '<omitted>'
}
if (d.originAdapter?.type === 'openapi') {
d.originAdapter.spec = '<omitted>'
}
return d
}
return deployment
}

Wyświetl plik

@ -6,17 +6,17 @@ import type { ParsedFaasIdentifier } from './types'
// namespace/project-name@deploymentHash/servicePath
// project@deploymentHash/servicePath
const projectDeploymentServiceRe =
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
// namespace/project-name@version/servicePath
// project@version/servicePath
const projectVersionServiceRe =
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
// namespace/project-name/servicePath
// project/servicePath (latest version)
const projectServiceRe =
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
const pdsMatch = uri.match(projectDeploymentServiceRe)

Wyświetl plik

@ -241,6 +241,9 @@ importers:
'@agentic/platform-schemas':
specifier: workspace:*
version: link:../schemas
'@agentic/platform-validators':
specifier: workspace:*
version: link:../validators
'@clack/prompts':
specifier: ^0.11.0
version: 0.11.0