feat: tiny kitty bean toes

pull/715/head
Travis Fischer 2025-05-27 01:23:16 +07:00
rodzic 2114b73c3a
commit a58433d60c
22 zmienionych plików z 246 dodań i 85 usunięć

Wyświetl plik

@ -20,6 +20,7 @@ import { registerV1DeploymentsUpdateDeployment } from './deployments/update-depl
import { registerHealthCheck } from './health-check'
import { registerV1ProjectsCreateProject } from './projects/create-project'
import { registerV1ProjectsGetProject } from './projects/get-project'
import { registerV1ProjectsGetProjectByIdentifier } from './projects/get-project-by-identifier'
import { registerV1ProjectsListProjects } from './projects/list-projects'
import { registerV1ProjectsUpdateProject } from './projects/update-project'
import { registerV1TeamsCreateTeam } from './teams/create-team'
@ -85,6 +86,7 @@ registerV1TeamsMembersDeleteTeamMember(privateRouter)
// Projects
registerV1ProjectsCreateProject(privateRouter)
registerV1ProjectsListProjects(privateRouter)
registerV1ProjectsGetProjectByIdentifier(privateRouter) // must be before `registerV1ProjectsGetProject`
registerV1ProjectsGetProject(privateRouter)
registerV1ProjectsUpdateProject(privateRouter)
@ -121,33 +123,3 @@ apiV1.route('/', privateRouter)
// NOTE: Removing for now because Hono's RPC client / types are clunky and slow.
// export type ApiRoutes =
// | ReturnType<typeof registerHealthCheck>
// // Users
// | ReturnType<typeof registerV1UsersGetUser>
// | ReturnType<typeof registerV1UsersUpdateUser>
// // Teams
// | ReturnType<typeof registerV1TeamsCreateTeam>
// | ReturnType<typeof registerV1TeamsListTeams>
// | ReturnType<typeof registerV1TeamsGetTeam>
// | ReturnType<typeof registerV1TeamsDeleteTeam>
// | ReturnType<typeof registerV1TeamsUpdateTeam>
// // Team members
// | ReturnType<typeof registerV1TeamsMembersCreateTeamMember>
// | ReturnType<typeof registerV1TeamsMembersUpdateTeamMember>
// | ReturnType<typeof registerV1TeamsMembersDeleteTeamMember>
// // Projects
// | ReturnType<typeof registerV1ProjectsCreateProject>
// | ReturnType<typeof registerV1ProjectsListProjects>
// | ReturnType<typeof registerV1ProjectsGetProject>
// | ReturnType<typeof registerV1ProjectsUpdateProject>
// // Consumers
// | ReturnType<typeof registerV1ConsumersGetConsumer>
// | ReturnType<typeof registerV1ConsumersCreateConsumer>
// | ReturnType<typeof registerV1ConsumersUpdateConsumer>
// | ReturnType<typeof registerV1ConsumersRefreshConsumerToken>
// | ReturnType<typeof registerV1ProjectsListConsumers>
// // Deployments
// | ReturnType<typeof registerV1DeploymentsGetDeployment>
// | ReturnType<typeof registerV1DeploymentsCreateDeployment>
// | ReturnType<typeof registerV1DeploymentsUpdateDeployment>
// | ReturnType<typeof registerV1DeploymentsListDeployments>
// | ReturnType<typeof registerV1DeploymentsPublishDeployment>

Wyświetl plik

@ -0,0 +1,57 @@
import { assert, parseZodSchema } from '@agentic/platform-core'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
import type { AuthenticatedEnv } from '@/lib/types'
import { db, eq, schema } from '@/db'
import { acl } from '@/lib/acl'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponse404,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { projectIdentifierAndPopulateSchema } from './schemas'
const route = createRoute({
description: 'Gets a project by public identifier',
tags: ['projects'],
operationId: 'getProjectByIdentifier',
method: 'get',
path: 'projects/by-identifier',
security: openapiAuthenticatedSecuritySchemas,
request: {
query: projectIdentifierAndPopulateSchema
},
responses: {
200: {
description: 'A project',
content: {
'application/json': {
schema: schema.projectSelectSchema
}
}
},
...openapiErrorResponses,
...openapiErrorResponse404
}
})
export function registerV1ProjectsGetProjectByIdentifier(
app: OpenAPIHono<AuthenticatedEnv>
) {
return app.openapi(route, async (c) => {
const { projectIdentifier, populate = [] } = c.req.valid('query')
const project = await db.query.projects.findFirst({
where: eq(schema.projects.identifier, projectIdentifier),
with: {
lastPublishedDeployment: true,
...Object.fromEntries(populate.map((field) => [field, true]))
}
})
assert(project, 404, `Project not found "${projectIdentifier}"`)
await acl(c, project, { label: 'Project' })
return c.json(parseZodSchema(schema.projectSelectSchema, project))
})
}

Wyświetl plik

@ -13,7 +13,7 @@ import {
import { populateProjectSchema, projectIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Gets a project',
description: 'Gets a project by ID',
tags: ['projects'],
operationId: 'getProject',
method: 'get',

Wyświetl plik

@ -1,6 +1,11 @@
import { z } from '@hono/zod-openapi'
import { paginationSchema, projectIdSchema, projectRelationsSchema } from '@/db'
import {
paginationSchema,
projectIdentifierSchema,
projectIdSchema,
projectRelationsSchema
} from '@/db'
export const projectIdParamsSchema = z.object({
projectId: projectIdSchema.openapi({
@ -12,10 +17,19 @@ export const projectIdParamsSchema = z.object({
})
})
export const projectIdentifierQuerySchema = z.object({
projectIdentifier: projectIdentifierSchema
})
export const populateProjectSchema = z.object({
populate: z.array(projectRelationsSchema).default([]).optional()
})
export const projectIdentifierAndPopulateSchema = z.object({
...populateProjectSchema.shape,
...projectIdentifierQuerySchema.shape
})
export const paginationAndPopulateProjectSchema = z.object({
...paginationSchema.shape,
...populateProjectSchema.shape

Wyświetl plik

@ -76,6 +76,10 @@ export const projects = pgTable(
// Most recent Deployment if one exists
lastDeploymentId: deploymentId(),
// Semver version of the most recently published Deployment (if one exists)
// (denormalized for convenience)
lastPublishedDeploymentVersion: text(),
applicationFeePercent: integer().default(20).notNull(),
// TODO: This is going to need to vary from dev to prod

Wyświetl plik

@ -53,7 +53,8 @@ export async function publishDeployment(
tx
.update(schema.projects)
.set({
lastPublishedDeploymentId: deployment.id
lastPublishedDeploymentId: deployment.id,
lastPublishedDeploymentVersion: version
})
.where(eq(schema.projects.id, project.id))

Wyświetl plik

@ -269,6 +269,16 @@ export class AgenticApiClient {
.json()
}
async getProjectByIdentifier(
searchParams: OperationParameters<'getProjectByIdentifier'>
): Promise<OperationResponse<'getProjectByIdentifier'>> {
return this.ky
.get(`v1/projects/by-identifier`, {
searchParams: sanitizeSearchParams(searchParams)
})
.json()
}
async updateProject(
project: OperationBody<'updateProject'>,
{ projectId, ...searchParams }: OperationParameters<'updateProject'>

Wyświetl plik

@ -149,6 +149,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/projects/by-identifier": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Gets a project by public identifier */
get: operations["getProjectByIdentifier"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/projects/{projectId}": {
parameters: {
query?: never;
@ -156,7 +173,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** @description Gets a project */
/** @description Gets a project by ID */
get: operations["getProject"];
put?: never;
/** @description Updates a project. */
@ -388,6 +405,7 @@ export interface components {
lastPublishedDeploymentId?: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastDeploymentId?: string;
lastPublishedDeploymentVersion?: string;
applicationFeePercent: number;
defaultPricingInterval: components["schemas"]["PricingInterval"];
/** @enum {string} */
@ -1010,6 +1028,34 @@ export interface operations {
403: components["responses"]["403"];
};
};
getProjectByIdentifier: {
parameters: {
query: {
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
/** @description Public project identifier (e.g. "namespace/project-name") */
projectIdentifier: components["schemas"]["ProjectIdentifier"];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description A project */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Project"];
};
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
404: components["responses"]["404"];
};
};
getProject: {
parameters: {
query?: {

Wyświetl plik

@ -42,12 +42,14 @@
"open": "^10.1.2",
"ora": "^8.2.0",
"restore-cursor": "catalog:",
"semver": "^7.7.2",
"unconfig": "^7.3.2"
},
"devDependencies": {
"@agentic/platform-fixtures": "workspace:*",
"@commander-js/extra-typings": "^14.0.0",
"@types/inquirer": "^9.0.7"
"@types/inquirer": "^9.0.7",
"@types/semver": "^7.7.0"
},
"publishConfig": {
"access": "public"

Wyświetl plik

@ -2,8 +2,8 @@ import { Command } from 'commander'
import { oraPromise } from 'ora'
import type { Context } from '../types'
import { AuthStore } from '../lib/auth-store'
import { loadAgenticConfig } from '../lib/load-agentic-config'
import { AuthStore } from '../lib/store'
export function registerDeployCommand({ client, program, logger }: Context) {
const command = new Command('deploy')
@ -47,8 +47,8 @@ export function registerDeployCommand({ client, program, logger }: Context) {
}),
{
text: `Creating deployment for project "${config.name}"`,
successText: `Deployment created successfully.`,
failText: 'Failed to create deployment.'
successText: `Deployment created successfully`,
failText: 'Failed to create deployment'
}
)

Wyświetl plik

@ -1,13 +1,18 @@
import { select } from '@clack/prompts'
import { Command } from 'commander'
import { oraPromise } from 'ora'
import semver from 'semver'
import type { Context } from '../types'
import { AuthStore } from '../lib/auth-store'
import { resolveDeployment } from '../lib/resolve-deployment'
import { AuthStore } from '../lib/store'
export function registerPublishCommand({ client, program, logger }: Context) {
const command = new Command('publish')
.description('Publishes a deployment')
.argument('[deploymentIdentifier]', 'Deployment identifier')
.description(
'Publishes a deployment. Defaults to the most recent deployment for the project in the target directory. If a deployment identifier is provided, it will be used instead.'
)
.argument('[deploymentIdentifier]', 'Optional deployment identifier')
.option(
'-c, --cwd <dir>',
'The directory to load the Agentic project config from (defaults to cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
@ -19,13 +24,21 @@ export function registerPublishCommand({ client, program, logger }: Context) {
// TODO: parseFaasIdentifier
}
const deployment = await resolveDeployment({
client,
deploymentIdentifier,
fuzzyDeploymentIdentifierVersion: 'dev',
cwd: opts.cwd,
populate: ['project']
})
const deployment = await oraPromise(
resolveDeployment({
client,
deploymentIdentifier,
fuzzyDeploymentIdentifierVersion: 'dev',
cwd: opts.cwd,
populate: ['project']
}),
{
text: 'Resolving deployment...',
successText: 'Resolved deployment',
failText: 'Failed to resolve deployment'
}
)
const { project } = deployment
if (deployment.published) {
logger.error(
@ -36,21 +49,68 @@ export function registerPublishCommand({ client, program, logger }: Context) {
return
}
// TODO
// const version = deployment.version
if (!project) {
logger.error(
deploymentIdentifier
? `Deployment "${deploymentIdentifier}" failed to fetch project "${deployment.projectId}"`
: `Latest deployment "${deployment.identifier}" failed to fetch project "${deployment.projectId}"`
)
return
}
// // TODO: prompt user for version or bump
const initialVersion = deployment.version
const baseVersion =
initialVersion || project.lastPublishedDeploymentVersion || '0.0.0'
// await client.publishDeployment(
// {
// version
// },
// {
// deploymentId: deployment.id
// }
// )
const options = [
initialVersion
? { value: initialVersion, label: initialVersion }
: null,
{
value: semver.inc(baseVersion, 'patch'),
label: `${semver.inc(baseVersion, 'patch')} (patch)`
},
{
value: semver.inc(baseVersion, 'minor'),
label: `${semver.inc(baseVersion, 'minor')} (minor)`
},
{
value: semver.inc(baseVersion, 'major'),
label: `${semver.inc(baseVersion, 'major')} (major)`
}
].filter(Boolean)
logger.log(deployment)
if (project.lastPublishedDeploymentVersion) {
logger.info(
`Project "${project.identifier}" latest published version is "${project.lastPublishedDeploymentVersion}".\n`
)
} else {
logger.info(`Project "${project.identifier}" is not published yet.\n`)
}
const version = await select({
message: `Select version of deployment "${deployment.identifier}" to publish:`,
options
})
if (!version || typeof version !== 'string') {
logger.error('No version selected')
return
}
const publishedDeployment = await client.publishDeployment(
{
version
},
{
deploymentId: deployment.id
}
)
logger.info(
`Deployment "${publishedDeployment.identifier}" published with version "${publishedDeployment.version}"`
)
logger.log(publishedDeployment)
})
program.addCommand(command)

Wyświetl plik

@ -2,7 +2,7 @@ import { Command } from 'commander'
import inquirer from 'inquirer'
import ora from 'ora'
import { AuthStore } from '../lib/store'
import { AuthStore } from '../lib/auth-store'
export const rm = new Command('rm')
.description('Removes deployments')

Wyświetl plik

@ -11,16 +11,7 @@ export function registerSigninCommand({ client, program, logger }: Context) {
)
.option('-e, --email', 'Log in using email and password')
.action(async (opts) => {
if (opts.email) {
await auth({
client,
provider: 'password'
// email: opts.email,
// password: opts.password
})
} else {
await auth({ client, provider: 'github' })
}
await auth({ client, provider: opts.email ? 'password' : 'github' })
const user = await client.getMe()
logger.log(user)

Wyświetl plik

@ -1,7 +1,7 @@
import { Command } from 'commander'
import type { Context } from '../types'
import { AuthStore } from '../lib/store'
import { AuthStore } from '../lib/auth-store'
export function registerSignoutCommand({ client, program, logger }: Context) {
const command = new Command('logout')

Wyświetl plik

@ -1,7 +1,7 @@
import { Command } from 'commander'
import type { Context } from '../types'
import { AuthStore } from '../lib/store'
import { AuthStore } from '../lib/auth-store'
export function registerWhoAmICommand({ client, program, logger }: Context) {
const command = new Command('whoami')

Wyświetl plik

@ -5,10 +5,11 @@ import { Command } from 'commander'
import restoreCursor from 'restore-cursor'
import { registerDeployCommand } from './commands/deploy'
import { registerPublishCommand } from './commands/publish'
import { registerSigninCommand } from './commands/signin'
import { registerSignoutCommand } from './commands/signout'
import { registerWhoAmICommand } from './commands/whoami'
import { AuthStore } from './lib/store'
import { AuthStore } from './lib/auth-store'
async function main() {
restoreCursor()
@ -52,6 +53,9 @@ async function main() {
console.log(...args)
}
},
info: (...args: any[]) => {
console.info(...args)
},
error: (...args: any[]) => {
console.error(...args)
}
@ -68,6 +72,7 @@ async function main() {
registerWhoAmICommand(ctx)
registerSignoutCommand(ctx)
registerDeployCommand(ctx)
registerPublishCommand(ctx)
program.parse()
}

Wyświetl plik

@ -7,7 +7,7 @@ import open from 'open'
import { oraPromise } from 'ora'
import type { AuthSession } from '../types'
import { AuthStore } from './store'
import { AuthStore } from './auth-store'
const providerToLabel = {
github: 'GitHub',

Wyświetl plik

@ -0,0 +1 @@
import '@fisch0920/config/ts-reset'

Wyświetl plik

@ -1,7 +1,7 @@
import type { AgenticApiClient, Deployment } from '@agentic/platform-api-client'
import { AuthStore } from './auth-store'
import { loadAgenticConfig } from './load-agentic-config'
import { AuthStore } from './store'
export async function resolveDeployment({
client,

Wyświetl plik

@ -6,6 +6,7 @@ export type Context = {
program: Command
logger: {
log: (...args: any[]) => void
info: (...args: any[]) => void
error: (...args: any[]) => void
}
}

Wyświetl plik

@ -134,9 +134,6 @@ importers:
'@agentic/platform-core':
specifier: workspace:*
version: link:../../packages/core
'@agentic/platform-openapi':
specifier: workspace:*
version: link:../../packages/openapi
'@agentic/platform-schemas':
specifier: workspace:*
version: link:../../packages/schemas
@ -228,9 +225,6 @@ importers:
type-fest:
specifier: 'catalog:'
version: 4.41.0
zod:
specifier: 'catalog:'
version: 3.24.4
devDependencies:
openapi-typescript:
specifier: ^7.8.0
@ -280,12 +274,12 @@ importers:
restore-cursor:
specifier: 'catalog:'
version: 5.1.0
semver:
specifier: ^7.7.2
version: 7.7.2
unconfig:
specifier: ^7.3.2
version: 7.3.2
zod:
specifier: 'catalog:'
version: 3.24.4
devDependencies:
'@agentic/platform-fixtures':
specifier: workspace:*
@ -296,6 +290,9 @@ importers:
'@types/inquirer':
specifier: ^9.0.7
version: 9.0.8
'@types/semver':
specifier: ^7.7.0
version: 7.7.0
packages/core:
dependencies: