kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: fix cli and publishing flow
rodzic
cf4a404fd0
commit
6a00a49de0
|
@ -19,7 +19,10 @@ export const deploymentIdParamsSchema = z.object({
|
|||
})
|
||||
|
||||
export const createDeploymentQuerySchema = z.object({
|
||||
publish: z.boolean().default(false).optional()
|
||||
publish: z
|
||||
.union([z.literal('true'), z.literal('false')])
|
||||
.default('false')
|
||||
.transform((p) => p === 'true')
|
||||
})
|
||||
|
||||
export const filterDeploymentSchema = z.object({
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import { fromError } from 'zod-validation-error'
|
||||
|
||||
import type { AuthenticatedHonoEnv } from '@/lib/types'
|
||||
import * as middleware from '@/lib/middleware'
|
||||
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
||||
import { defaultHook, registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
||||
|
||||
import { registerV1GitHubOAuthCallback } from './auth/github-callback'
|
||||
import { registerV1GitHubOAuthExchange } from './auth/github-exchange'
|
||||
|
@ -55,20 +54,7 @@ import { registerV1StripeWebhook } from './webhooks/stripe-webhook'
|
|||
// Note that the order of some of these routes is important because of
|
||||
// wildcards, so be careful when updating them or adding new routes.
|
||||
|
||||
export const apiV1 = new OpenAPIHono<DefaultHonoEnv>({
|
||||
defaultHook: (result, ctx) => {
|
||||
if (!result.success) {
|
||||
const requestId = ctx.get('requestId')
|
||||
return ctx.json(
|
||||
{
|
||||
error: fromError(result.error).toString(),
|
||||
requestId
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
export const apiV1 = new OpenAPIHono<DefaultHonoEnv>({ defaultHook })
|
||||
|
||||
apiV1.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', {
|
||||
type: 'http',
|
||||
|
@ -79,10 +65,10 @@ apiV1.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', {
|
|||
registerOpenAPIErrorResponses(apiV1)
|
||||
|
||||
// Public routes
|
||||
const publicRouter = new OpenAPIHono<DefaultHonoEnv>()
|
||||
const publicRouter = new OpenAPIHono<DefaultHonoEnv>({ defaultHook })
|
||||
|
||||
// Private, authenticated routes
|
||||
const privateRouter = new OpenAPIHono<AuthenticatedHonoEnv>()
|
||||
const privateRouter = new OpenAPIHono<AuthenticatedHonoEnv>({ defaultHook })
|
||||
|
||||
registerHealthCheck(publicRouter)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { fromError } from 'zod-validation-error'
|
||||
|
||||
import type { HonoApp } from './types'
|
||||
|
||||
export const openapiErrorResponses = {
|
||||
|
@ -85,3 +87,17 @@ export function registerOpenAPIErrorResponses(app: HonoApp) {
|
|||
content: openapiErrorContent
|
||||
})
|
||||
}
|
||||
|
||||
export function defaultHook(result: any, ctx: any) {
|
||||
if (!result.success) {
|
||||
const requestId = ctx.get('requestId')
|
||||
|
||||
return ctx.json(
|
||||
{
|
||||
error: fromError(result.error).toString(),
|
||||
requestId
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,10 @@
|
|||
"@hono/node-server": "catalog:",
|
||||
"commander": "catalog:",
|
||||
"conf": "catalog:",
|
||||
"exit-hook": "catalog:",
|
||||
"get-port": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"ky": "catalog:",
|
||||
"open": "catalog:",
|
||||
"ora": "catalog:",
|
||||
"restore-cursor": "catalog:",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AgenticApiClient } from '@agentic/platform-api-client'
|
||||
import { Command } from 'commander'
|
||||
import restoreCursor from 'restore-cursor'
|
||||
|
||||
import type { Context } from './types'
|
||||
import { registerDebugCommand } from './commands/debug'
|
||||
import { registerDeployCommand } from './commands/deploy'
|
||||
import { registerGetDeploymentCommand } from './commands/get'
|
||||
|
@ -12,9 +12,11 @@ import { registerSignoutCommand } from './commands/signout'
|
|||
import { registerSignupCommand } from './commands/signup'
|
||||
import { registerWhoAmICommand } from './commands/whoami'
|
||||
import { AuthStore } from './lib/auth-store'
|
||||
import { initExitHooks } from './lib/exit-hooks'
|
||||
import { createErrorHandler } from './lib/handle-error'
|
||||
|
||||
async function main() {
|
||||
restoreCursor()
|
||||
initExitHooks()
|
||||
|
||||
// Initialize the API client
|
||||
const client = new AgenticApiClient({
|
||||
|
@ -64,12 +66,18 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
const partialCtx = {
|
||||
client,
|
||||
program,
|
||||
logger
|
||||
}
|
||||
|
||||
const errorHandler = createErrorHandler(partialCtx)
|
||||
const ctx: Context = {
|
||||
...partialCtx,
|
||||
handleError: errorHandler
|
||||
}
|
||||
|
||||
// Register all commands
|
||||
registerSigninCommand(ctx)
|
||||
registerSignupCommand(ctx)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { loadAgenticConfig } from '@agentic/platform'
|
||||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
|
||||
export function registerDebugCommand({ program, logger }: Context) {
|
||||
export function registerDebugCommand({
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('debug')
|
||||
.description('Prints config for a local project.')
|
||||
.option(
|
||||
|
@ -12,27 +17,32 @@ export function registerDebugCommand({ program, logger }: Context) {
|
|||
'The directory to load the Agentic project config from (defaults to cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
|
||||
)
|
||||
.action(async (opts) => {
|
||||
// Load the Agentic project config, parse, and validate it. This will also
|
||||
// validate any origin adapter config such as OpenAPI or MCP specs and
|
||||
// embed them if they point to local files or URLs. Note that the server
|
||||
// also performs validation; this is just a client-side convenience for
|
||||
// failing fast and sharing 99% of the validation code between server and
|
||||
// client.
|
||||
const config = await oraPromise(
|
||||
loadAgenticConfig({
|
||||
cwd: opts.cwd
|
||||
}),
|
||||
{
|
||||
text: `Loading Agentic config from ${opts.cwd}`,
|
||||
successText: `Agentic config loaded successfully.`,
|
||||
failText: 'Failed to load Agentic config.'
|
||||
}
|
||||
)
|
||||
try {
|
||||
// Load the Agentic project config, parse, and validate it. This will also
|
||||
// validate any origin adapter config such as OpenAPI or MCP specs and
|
||||
// embed them if they point to local files or URLs. Note that the server
|
||||
// also performs validation; this is just a client-side convenience for
|
||||
// failing fast and sharing 99% of the validation code between server and
|
||||
// client.
|
||||
const config = await oraPromise(
|
||||
loadAgenticConfig({
|
||||
cwd: opts.cwd
|
||||
}),
|
||||
{
|
||||
text: `Loading Agentic config from ${opts.cwd}`,
|
||||
successText: `Agentic config loaded successfully.`,
|
||||
failText: 'Failed to load Agentic config.'
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: we may want to resolve the resulting agentic config so we see
|
||||
// the inferred `tools` (and `toolToOperationMap` for mcp servers)
|
||||
// TODO: we may want to resolve the resulting agentic config so we see
|
||||
// the inferred `tools` (and `toolToOperationMap` for mcp servers)
|
||||
|
||||
logger.log(config)
|
||||
logger.log(config)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { loadAgenticConfig } from '@agentic/platform'
|
||||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { AuthStore } from '../lib/auth-store'
|
||||
|
||||
export function registerDeployCommand({ client, program, logger }: Context) {
|
||||
export function registerDeployCommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('deploy')
|
||||
.description('Creates a new deployment.')
|
||||
.option(
|
||||
|
@ -17,42 +23,48 @@ export function registerDeployCommand({ client, program, logger }: Context) {
|
|||
.action(async (opts) => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
// Load the Agentic project config, parse, and validate it. This will also
|
||||
// validate any origin adapter config such as OpenAPI or MCP specs and
|
||||
// embed them if they point to local files or URLs. Note that the server
|
||||
// also performs validation; this is just a client-side convenience for
|
||||
// failing fast and sharing 99% of the validation code between server and
|
||||
// client.
|
||||
const config = await oraPromise(
|
||||
loadAgenticConfig({
|
||||
cwd: opts.cwd
|
||||
}),
|
||||
{
|
||||
text: `Loading Agentic config from ${opts.cwd}`,
|
||||
successText: `Agentic config loaded successfully.`,
|
||||
failText: 'Failed to load Agentic config.'
|
||||
}
|
||||
)
|
||||
try {
|
||||
// Load the Agentic project config, parse, and validate it. This will also
|
||||
// validate any origin adapter config such as OpenAPI or MCP specs and
|
||||
// embed them if they point to local files or URLs. Note that the server
|
||||
// also performs validation; this is just a client-side convenience for
|
||||
// failing fast and sharing 99% of the validation code between server and
|
||||
// client.
|
||||
const config = await oraPromise(
|
||||
loadAgenticConfig({
|
||||
cwd: opts.cwd
|
||||
}),
|
||||
{
|
||||
text: `Loading Agentic config from ${opts.cwd}`,
|
||||
successText: `Agentic config loaded successfully.`,
|
||||
failText: 'Failed to load Agentic config.'
|
||||
}
|
||||
)
|
||||
|
||||
if (opts.debug) {
|
||||
logger.log(config)
|
||||
return
|
||||
if (opts.debug) {
|
||||
logger.log(config)
|
||||
gracefulExit(0)
|
||||
return
|
||||
}
|
||||
|
||||
// Create the deployment on the backend, validate it, and optionally
|
||||
// publish it.
|
||||
const deployment = await oraPromise(
|
||||
client.createDeployment(config, {
|
||||
publish: !!opts.publish
|
||||
}),
|
||||
{
|
||||
text: `Creating deployment for project "${config.name}"`,
|
||||
successText: `Deployment created successfully`,
|
||||
failText: 'Failed to create deployment'
|
||||
}
|
||||
)
|
||||
|
||||
logger.log(deployment)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
|
||||
// Create the deployment on the backend, validate it, and optionally
|
||||
// publish it.
|
||||
const deployment = await oraPromise(
|
||||
client.createDeployment(config, {
|
||||
publish: !!opts.publish
|
||||
}),
|
||||
{
|
||||
text: `Creating deployment for project "${config.name}"`,
|
||||
successText: `Deployment created successfully`,
|
||||
failText: 'Failed to create deployment'
|
||||
}
|
||||
)
|
||||
|
||||
logger.log(deployment)
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
|
@ -7,7 +8,8 @@ import { AuthStore } from '../lib/auth-store'
|
|||
export function registerGetDeploymentCommand({
|
||||
client,
|
||||
program,
|
||||
logger
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('get')
|
||||
.description('Gets details for a specific deployment.')
|
||||
|
@ -15,18 +17,23 @@ export function registerGetDeploymentCommand({
|
|||
.action(async (deploymentIdentifier) => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
const deployment = await oraPromise(
|
||||
client.getDeploymentByIdentifier({
|
||||
deploymentIdentifier
|
||||
}),
|
||||
{
|
||||
text: 'Resolving deployment...',
|
||||
successText: 'Resolved deployment',
|
||||
failText: 'Failed to resolve deployment'
|
||||
}
|
||||
)
|
||||
try {
|
||||
const deployment = await oraPromise(
|
||||
client.getDeploymentByIdentifier({
|
||||
deploymentIdentifier
|
||||
}),
|
||||
{
|
||||
text: 'Resolving deployment...',
|
||||
successText: 'Resolved deployment',
|
||||
failText: 'Failed to resolve deployment'
|
||||
}
|
||||
)
|
||||
|
||||
logger.log(deployment)
|
||||
logger.log(deployment)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { Deployment } from '@agentic/platform-types'
|
||||
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
||||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
|
@ -10,7 +11,8 @@ import { pruneDeployment } from '../lib/utils'
|
|||
export function registerListDeploymentsCommand({
|
||||
client,
|
||||
program,
|
||||
logger
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('list')
|
||||
.alias('ls')
|
||||
|
@ -23,74 +25,82 @@ export function registerListDeploymentsCommand({
|
|||
.action(async (identifier, opts) => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
const query: Parameters<typeof client.listDeployments>[0] = {}
|
||||
let label = 'Fetching all projects and deployments'
|
||||
try {
|
||||
const query: Parameters<typeof client.listDeployments>[0] = {}
|
||||
let label = 'Fetching all projects and deployments'
|
||||
|
||||
if (identifier) {
|
||||
const parsedDeploymentIdentifier = parseDeploymentIdentifier(
|
||||
identifier,
|
||||
{
|
||||
strict: false
|
||||
if (identifier) {
|
||||
const parsedDeploymentIdentifier = parseDeploymentIdentifier(
|
||||
identifier,
|
||||
{
|
||||
strict: false
|
||||
}
|
||||
)
|
||||
|
||||
query.projectIdentifier = parsedDeploymentIdentifier.projectIdentifier
|
||||
label = `Fetching deployments for project "${query.projectIdentifier}"`
|
||||
|
||||
// TODO: this logic needs tweaking.
|
||||
if (
|
||||
parsedDeploymentIdentifier.deploymentVersion !== 'latest' ||
|
||||
identifier.includes('@latest')
|
||||
) {
|
||||
query.deploymentIdentifier =
|
||||
parsedDeploymentIdentifier.deploymentIdentifier
|
||||
label = `Fetching deployment "${query.deploymentIdentifier}"`
|
||||
}
|
||||
}
|
||||
|
||||
const deployments = await oraPromise(
|
||||
client.listDeployments(query),
|
||||
label
|
||||
)
|
||||
|
||||
query.projectIdentifier = parsedDeploymentIdentifier.projectIdentifier
|
||||
label = `Fetching deployments for project "${query.projectIdentifier}"`
|
||||
const projectIdToDeploymentsMap: Record<string, Deployment[]> = {}
|
||||
const sortedProjects: {
|
||||
projectId: string
|
||||
deployments: Deployment[]
|
||||
}[] = []
|
||||
|
||||
// TODO: this logic needs tweaking.
|
||||
if (
|
||||
parsedDeploymentIdentifier.deploymentVersion !== 'latest' ||
|
||||
identifier.includes('@latest')
|
||||
) {
|
||||
query.deploymentIdentifier =
|
||||
parsedDeploymentIdentifier.deploymentIdentifier
|
||||
label = `Fetching deployment "${query.deploymentIdentifier}"`
|
||||
}
|
||||
}
|
||||
// Aggregate deployments by project
|
||||
for (const deployment of deployments) {
|
||||
const prunedDeployment = pruneDeployment(deployment, opts)
|
||||
|
||||
const deployments = await oraPromise(client.listDeployments(query), label)
|
||||
const { projectId } = deployment
|
||||
if (!projectIdToDeploymentsMap[projectId]) {
|
||||
projectIdToDeploymentsMap[projectId] = []
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
// Sort deployments within each project with recently created first
|
||||
for (const projectId of Object.keys(projectIdToDeploymentsMap)) {
|
||||
const deployments = projectIdToDeploymentsMap[projectId]!
|
||||
deployments.sort(
|
||||
sortedProjects.push({
|
||||
projectId,
|
||||
deployments
|
||||
})
|
||||
}
|
||||
|
||||
// Sort projects with most recently created first
|
||||
sortedProjects.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
new Date(b.deployments[0]!.createdAt).getTime() -
|
||||
new Date(a.deployments[0]!.createdAt).getTime()
|
||||
)
|
||||
|
||||
sortedProjects.push({
|
||||
projectId,
|
||||
deployments
|
||||
})
|
||||
// TODO: better output formatting
|
||||
logger.log(sortedProjects)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { select } from '@clack/prompts'
|
||||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
import { oraPromise } from 'ora'
|
||||
import semver from 'semver'
|
||||
|
||||
|
@ -7,7 +8,12 @@ import type { Context } from '../types'
|
|||
import { AuthStore } from '../lib/auth-store'
|
||||
import { resolveDeployment } from '../lib/resolve-deployment'
|
||||
|
||||
export function registerPublishCommand({ client, program, logger }: Context) {
|
||||
export function registerPublishCommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('publish')
|
||||
.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.'
|
||||
|
@ -24,93 +30,98 @@ export function registerPublishCommand({ client, program, logger }: Context) {
|
|||
// TODO: parseToolIdentifier
|
||||
}
|
||||
|
||||
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(
|
||||
deploymentIdentifier
|
||||
? `Deployment "${deploymentIdentifier}" is already published`
|
||||
: `Latest deployment "${deployment.identifier}" is already published`
|
||||
try {
|
||||
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'
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
const { project } = deployment
|
||||
|
||||
if (!project) {
|
||||
logger.error(
|
||||
deploymentIdentifier
|
||||
? `Deployment "${deploymentIdentifier}" failed to fetch project "${deployment.projectId}"`
|
||||
: `Latest deployment "${deployment.identifier}" failed to fetch project "${deployment.projectId}"`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const initialVersion = deployment.version
|
||||
const baseVersion =
|
||||
initialVersion || project.lastPublishedDeploymentVersion || '0.0.0'
|
||||
|
||||
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)`
|
||||
if (deployment.published) {
|
||||
logger.error(
|
||||
deploymentIdentifier
|
||||
? `Deployment "${deploymentIdentifier}" is already published`
|
||||
: `Latest deployment "${deployment.identifier}" is already published`
|
||||
)
|
||||
return gracefulExit(1)
|
||||
}
|
||||
].filter(Boolean)
|
||||
|
||||
if (project.lastPublishedDeploymentVersion) {
|
||||
if (!project) {
|
||||
logger.error(
|
||||
deploymentIdentifier
|
||||
? `Deployment "${deploymentIdentifier}" failed to fetch project "${deployment.projectId}"`
|
||||
: `Latest deployment "${deployment.identifier}" failed to fetch project "${deployment.projectId}"`
|
||||
)
|
||||
return gracefulExit(1)
|
||||
}
|
||||
|
||||
const initialVersion = deployment.version
|
||||
const baseVersion =
|
||||
initialVersion || project.lastPublishedDeploymentVersion || '0.0.0'
|
||||
|
||||
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)
|
||||
|
||||
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 gracefulExit(1)
|
||||
}
|
||||
|
||||
const publishedDeployment = await client.publishDeployment(
|
||||
{
|
||||
version
|
||||
},
|
||||
{
|
||||
deploymentId: deployment.id
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
`Project "${project.identifier}" latest published version is "${project.lastPublishedDeploymentVersion}".\n`
|
||||
`Deployment "${publishedDeployment.identifier}" published with version "${publishedDeployment.version}"`
|
||||
)
|
||||
} else {
|
||||
logger.info(`Project "${project.identifier}" is not published yet.\n`)
|
||||
logger.log(publishedDeployment)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { auth } from '../lib/auth'
|
||||
|
||||
export function registerSigninCommand({ client, program, logger }: Context) {
|
||||
export function registerSigninCommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('login')
|
||||
.alias('signin')
|
||||
.description(
|
||||
|
@ -17,20 +23,25 @@ export function registerSigninCommand({ client, program, logger }: Context) {
|
|||
'either pass email and password or neither (which will use github auth)'
|
||||
)
|
||||
program.outputHelp()
|
||||
return
|
||||
return gracefulExit(1)
|
||||
}
|
||||
|
||||
if (opts.email && opts.password) {
|
||||
await client.signInWithPassword({
|
||||
email: opts.email,
|
||||
password: opts.password
|
||||
})
|
||||
} else {
|
||||
await auth({ client, provider: 'github' })
|
||||
}
|
||||
try {
|
||||
if (opts.email && opts.password) {
|
||||
await client.signInWithPassword({
|
||||
email: opts.email,
|
||||
password: opts.password
|
||||
})
|
||||
} else {
|
||||
await auth({ client, provider: 'github' })
|
||||
}
|
||||
|
||||
const user = await client.getMe()
|
||||
logger.log(user)
|
||||
const user = await client.getMe()
|
||||
logger.log(user)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,21 +1,33 @@
|
|||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { AuthStore } from '../lib/auth-store'
|
||||
|
||||
export function registerSignoutCommand({ client, program, logger }: Context) {
|
||||
export function registerSignoutCommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('logout')
|
||||
.alias('signout')
|
||||
.description('Signs the current user out.')
|
||||
.action(async () => {
|
||||
if (!client.isAuthenticated) {
|
||||
return
|
||||
logger.log('You are already signed out')
|
||||
return gracefulExit(0)
|
||||
}
|
||||
|
||||
await client.logout()
|
||||
AuthStore.clearAuth()
|
||||
try {
|
||||
await client.logout()
|
||||
AuthStore.clearAuth()
|
||||
|
||||
logger.log('Signed out')
|
||||
logger.log('Signed out')
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { auth } from '../lib/auth'
|
||||
|
||||
export function registerSignupCommand({ client, program, logger }: Context) {
|
||||
export function registerSignupCommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('signup')
|
||||
.description(
|
||||
'Creates a new account for Agentic. If no credentials are provided, uses GitHub auth.'
|
||||
|
@ -20,21 +26,26 @@ export function registerSignupCommand({ client, program, logger }: Context) {
|
|||
'either pass email, username, and password or none of them (which will use github auth)'
|
||||
)
|
||||
program.outputHelp()
|
||||
return
|
||||
return gracefulExit(1)
|
||||
}
|
||||
|
||||
if (opts.email && opts.username && opts.password) {
|
||||
await client.signUpWithPassword({
|
||||
email: opts.email,
|
||||
username: opts.username,
|
||||
password: opts.password
|
||||
})
|
||||
} else {
|
||||
await auth({ client, provider: 'github' })
|
||||
}
|
||||
try {
|
||||
if (opts.email && opts.username && opts.password) {
|
||||
await client.signUpWithPassword({
|
||||
email: opts.email,
|
||||
username: opts.username,
|
||||
password: opts.password
|
||||
})
|
||||
} else {
|
||||
await auth({ client, provider: 'github' })
|
||||
}
|
||||
|
||||
const user = await client.getMe()
|
||||
logger.log(user)
|
||||
const user = await client.getMe()
|
||||
logger.log(user)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'exit-hook'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { AuthStore } from '../lib/auth-store'
|
||||
|
||||
export function registerWhoAmICommand({ client, program, logger }: Context) {
|
||||
export function registerWhoAmICommand({
|
||||
client,
|
||||
program,
|
||||
logger,
|
||||
handleError
|
||||
}: Context) {
|
||||
const command = new Command('whoami')
|
||||
.description('Displays info about the current user.')
|
||||
.action(async () => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
const res = await client.getMe()
|
||||
logger.log(res)
|
||||
try {
|
||||
const res = await client.getMe()
|
||||
logger.log(res)
|
||||
gracefulExit(0)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import restoreCursor from 'restore-cursor'
|
||||
|
||||
export function initExitHooks() {
|
||||
// Gracefully restore the cursor if run from a TTY
|
||||
restoreCursor()
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { gracefulExit } from 'exit-hook'
|
||||
import { HTTPError } from 'ky'
|
||||
|
||||
import type { Context } from '../types'
|
||||
|
||||
export function createErrorHandler(ctx: Omit<Context, 'handleError'>) {
|
||||
return async function handleError(error: any) {
|
||||
let message: string | undefined
|
||||
let details: Error | undefined
|
||||
|
||||
if (typeof error === 'string') {
|
||||
message = error
|
||||
} else if (error instanceof Error) {
|
||||
details = error
|
||||
message = error.message
|
||||
|
||||
if (error instanceof HTTPError) {
|
||||
if (error.response) {
|
||||
try {
|
||||
message = error.response.statusText
|
||||
|
||||
const body = await error.response.json()
|
||||
if (body.error && typeof body.error === 'string') {
|
||||
message = JSON.stringify(
|
||||
{
|
||||
...body,
|
||||
details: error.toString()
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
details = undefined
|
||||
}
|
||||
} catch {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.logger.error([message, details].filter(Boolean).join('\n'))
|
||||
gracefulExit(1)
|
||||
}
|
||||
}
|
|
@ -9,4 +9,5 @@ export type Context = {
|
|||
info: (...args: any[]) => void
|
||||
error: (...args: any[]) => void
|
||||
}
|
||||
handleError: (error: any) => void
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ catalogs:
|
|||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
exit-hook:
|
||||
specifier: ^4.0.0
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
fast-content-type-parse:
|
||||
specifier: ^3.0.0
|
||||
|
@ -242,7 +242,7 @@ catalogs:
|
|||
version: 3.2.4
|
||||
wrangler:
|
||||
specifier: ^4.21.0
|
||||
version: 4.20.3
|
||||
version: 4.21.0
|
||||
zod:
|
||||
specifier: ^3.25.67
|
||||
version: 3.25.67
|
||||
|
@ -508,7 +508,7 @@ importers:
|
|||
version: 2.46.0
|
||||
wrangler:
|
||||
specifier: 'catalog:'
|
||||
version: 4.20.3(@cloudflare/workers-types@4.20250620.0)
|
||||
version: 4.21.0(@cloudflare/workers-types@4.20250620.0)
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
|
@ -751,12 +751,18 @@ importers:
|
|||
conf:
|
||||
specifier: 'catalog:'
|
||||
version: 14.0.0
|
||||
exit-hook:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0
|
||||
get-port:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
hono:
|
||||
specifier: 'catalog:'
|
||||
version: 4.8.1
|
||||
ky:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.1
|
||||
open:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.2
|
||||
|
@ -5934,11 +5940,6 @@ packages:
|
|||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
miniflare@4.20250617.1:
|
||||
resolution: {integrity: sha512-NjwKVzPGCAUgkCOJxHBwgV2Obu3g4/wAJE0JaA9whcYip4VAJwQ1fU9TMtoKLY91jD9ANR221+CqLyqxennjqg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
miniflare@4.20250617.3:
|
||||
resolution: {integrity: sha512-j+LZycT11UdlVeNdaqD0XdNnYnqAL+wXmboz+tNPFgTq6zhD489Ujj3BfSDyEHDCA9UFBLbkc5ByGWBh+pYZ5Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
@ -7631,16 +7632,6 @@ packages:
|
|||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.20.3:
|
||||
resolution: {integrity: sha512-ugvmi43CFPbjeQFfhU7EqE1V0ek6ZFv80jzwHcPk/7jPFmOA4ahT5uUU1ga5ZP6vz6lUuG2bLnyl1T5qJah0cg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20250617.0
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
wrangler@4.21.0:
|
||||
resolution: {integrity: sha512-37xm0CG2qMvsJUNZYQKje6HbCsJFYuE8dQSnu7981iDRT4DLrEIL1DAUnZJG9HiXteKPvrSj96AkZyomi5sYHw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
@ -12687,24 +12678,6 @@ snapshots:
|
|||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
miniflare@4.20250617.1:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
acorn: 8.14.0
|
||||
acorn-walk: 8.3.2
|
||||
exit-hook: 2.2.1
|
||||
glob-to-regexp: 0.4.1
|
||||
sharp: 0.33.5
|
||||
stoppable: 1.1.0
|
||||
undici: 5.29.0
|
||||
workerd: 1.20250617.0
|
||||
ws: 8.18.0
|
||||
youch: 3.3.4
|
||||
zod: 3.22.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
miniflare@4.20250617.3:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
|
@ -14591,23 +14564,6 @@ snapshots:
|
|||
'@cloudflare/workerd-linux-arm64': 1.20250617.0
|
||||
'@cloudflare/workerd-windows-64': 1.20250617.0
|
||||
|
||||
wrangler@4.20.3(@cloudflare/workers-types@4.20250620.0):
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
'@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.25.4
|
||||
miniflare: 4.20250617.1
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.17
|
||||
workerd: 1.20250617.0
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20250620.0
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
wrangler@4.21.0(@cloudflare/workers-types@4.20250620.0):
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
|
|
|
@ -47,7 +47,7 @@ catalog:
|
|||
eslint: ^9.29.0
|
||||
eslint-plugin-drizzle: ^0.2.3
|
||||
eventid: ^2.0.1
|
||||
exit-hook: ^4.0.0
|
||||
exit-hook: 4.0.0
|
||||
fast-content-type-parse: ^3.0.0
|
||||
fast-uri: ^3.0.6
|
||||
fastmcp: ^3.4.0
|
||||
|
|
Ładowanie…
Reference in New Issue