From a58433d60c8ea7c65a22014dbc9a1644810547b4 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 27 May 2025 01:23:16 +0700 Subject: [PATCH] feat: tiny kitty bean toes --- apps/api/src/api-v1/index.ts | 32 +----- .../projects/get-project-by-identifier.ts | 57 ++++++++++ apps/api/src/api-v1/projects/get-project.ts | 2 +- apps/api/src/api-v1/projects/schemas.ts | 16 ++- apps/api/src/db/schema/project.ts | 4 + .../src/lib/deployments/publish-deployment.ts | 3 +- packages/api-client/src/agentic-api-client.ts | 10 ++ packages/api-client/src/openapi.d.ts | 48 +++++++- packages/cli/package.json | 4 +- packages/cli/src/commands/deploy.ts | 6 +- packages/cli/src/commands/publish.ts | 104 ++++++++++++++---- packages/cli/src/commands/rm.ts | 2 +- packages/cli/src/commands/signin.ts | 11 +- packages/cli/src/commands/signout.ts | 2 +- packages/cli/src/commands/whoami.ts | 2 +- packages/cli/src/index.ts | 7 +- .../cli/src/lib/{store.ts => auth-store.ts} | 0 packages/cli/src/lib/auth.ts | 2 +- packages/cli/src/lib/reset.d.ts | 1 + packages/cli/src/lib/resolve-deployment.ts | 2 +- packages/cli/src/types.ts | 1 + pnpm-lock.yaml | 15 +-- 22 files changed, 246 insertions(+), 85 deletions(-) create mode 100644 apps/api/src/api-v1/projects/get-project-by-identifier.ts rename packages/cli/src/lib/{store.ts => auth-store.ts} (100%) create mode 100644 packages/cli/src/lib/reset.d.ts diff --git a/apps/api/src/api-v1/index.ts b/apps/api/src/api-v1/index.ts index 5761e0ea..ca899655 100644 --- a/apps/api/src/api-v1/index.ts +++ b/apps/api/src/api-v1/index.ts @@ -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 -// // Users -// | ReturnType -// | ReturnType -// // Teams -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// // Team members -// | ReturnType -// | ReturnType -// | ReturnType -// // Projects -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// // Consumers -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// // Deployments -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType -// | ReturnType diff --git a/apps/api/src/api-v1/projects/get-project-by-identifier.ts b/apps/api/src/api-v1/projects/get-project-by-identifier.ts new file mode 100644 index 00000000..72df0132 --- /dev/null +++ b/apps/api/src/api-v1/projects/get-project-by-identifier.ts @@ -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 +) { + 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)) + }) +} diff --git a/apps/api/src/api-v1/projects/get-project.ts b/apps/api/src/api-v1/projects/get-project.ts index fb1e500d..692760fd 100644 --- a/apps/api/src/api-v1/projects/get-project.ts +++ b/apps/api/src/api-v1/projects/get-project.ts @@ -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', diff --git a/apps/api/src/api-v1/projects/schemas.ts b/apps/api/src/api-v1/projects/schemas.ts index ef873c48..568034b4 100644 --- a/apps/api/src/api-v1/projects/schemas.ts +++ b/apps/api/src/api-v1/projects/schemas.ts @@ -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 diff --git a/apps/api/src/db/schema/project.ts b/apps/api/src/db/schema/project.ts index 47fa27da..9a774e01 100644 --- a/apps/api/src/db/schema/project.ts +++ b/apps/api/src/db/schema/project.ts @@ -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 diff --git a/apps/api/src/lib/deployments/publish-deployment.ts b/apps/api/src/lib/deployments/publish-deployment.ts index a1d1dd41..3a63fcaf 100644 --- a/apps/api/src/lib/deployments/publish-deployment.ts +++ b/apps/api/src/lib/deployments/publish-deployment.ts @@ -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)) diff --git a/packages/api-client/src/agentic-api-client.ts b/packages/api-client/src/agentic-api-client.ts index 03bd2f97..6d6898f8 100644 --- a/packages/api-client/src/agentic-api-client.ts +++ b/packages/api-client/src/agentic-api-client.ts @@ -269,6 +269,16 @@ export class AgenticApiClient { .json() } + async getProjectByIdentifier( + searchParams: OperationParameters<'getProjectByIdentifier'> + ): Promise> { + return this.ky + .get(`v1/projects/by-identifier`, { + searchParams: sanitizeSearchParams(searchParams) + }) + .json() + } + async updateProject( project: OperationBody<'updateProject'>, { projectId, ...searchParams }: OperationParameters<'updateProject'> diff --git a/packages/api-client/src/openapi.d.ts b/packages/api-client/src/openapi.d.ts index c3eb9d57..733f701c 100644 --- a/packages/api-client/src/openapi.d.ts +++ b/packages/api-client/src/openapi.d.ts @@ -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?: { diff --git a/packages/cli/package.json b/packages/cli/package.json index 3f8b0a75..06c46f3c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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" diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 016efd1c..146dcc7b 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -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' } ) diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index b764c777..0fc28572 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -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 ', '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) diff --git a/packages/cli/src/commands/rm.ts b/packages/cli/src/commands/rm.ts index 2d308f84..60d0e8c3 100644 --- a/packages/cli/src/commands/rm.ts +++ b/packages/cli/src/commands/rm.ts @@ -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') diff --git a/packages/cli/src/commands/signin.ts b/packages/cli/src/commands/signin.ts index ba43cbdd..8edf6fec 100644 --- a/packages/cli/src/commands/signin.ts +++ b/packages/cli/src/commands/signin.ts @@ -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) diff --git a/packages/cli/src/commands/signout.ts b/packages/cli/src/commands/signout.ts index 2985f3ab..3976ad64 100644 --- a/packages/cli/src/commands/signout.ts +++ b/packages/cli/src/commands/signout.ts @@ -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') diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index 1b3766d8..64bb9b14 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -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') diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7beb3e6b..7a0c8969 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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() } diff --git a/packages/cli/src/lib/store.ts b/packages/cli/src/lib/auth-store.ts similarity index 100% rename from packages/cli/src/lib/store.ts rename to packages/cli/src/lib/auth-store.ts diff --git a/packages/cli/src/lib/auth.ts b/packages/cli/src/lib/auth.ts index f0e965e8..dc8d3156 100644 --- a/packages/cli/src/lib/auth.ts +++ b/packages/cli/src/lib/auth.ts @@ -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', diff --git a/packages/cli/src/lib/reset.d.ts b/packages/cli/src/lib/reset.d.ts new file mode 100644 index 00000000..e708f4a1 --- /dev/null +++ b/packages/cli/src/lib/reset.d.ts @@ -0,0 +1 @@ +import '@fisch0920/config/ts-reset' diff --git a/packages/cli/src/lib/resolve-deployment.ts b/packages/cli/src/lib/resolve-deployment.ts index 4f4f7908..0146f418 100644 --- a/packages/cli/src/lib/resolve-deployment.ts +++ b/packages/cli/src/lib/resolve-deployment.ts @@ -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, diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index f4ca3a10..36429af4 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -6,6 +6,7 @@ export type Context = { program: Command logger: { log: (...args: any[]) => void + info: (...args: any[]) => void error: (...args: any[]) => void } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4ff4f4a..b52c4d91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: