Skip to content

Commit 82f3668

Browse files
committed
publish multiple agents at once
1 parent 1cc4920 commit 82f3668

File tree

4 files changed

+268
-208
lines changed

4 files changed

+268
-208
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { z } from 'zod'
2+
3+
import { DynamicAgentTemplateSchema } from '../../../types/dynamic-agent-template'
4+
5+
export const publishAgentsRequestSchema = z.object({
6+
data: DynamicAgentTemplateSchema.array(),
7+
publisherId: z.string().optional(),
8+
authToken: z.string(),
9+
})
10+
export type PublishAgentsRequest = z.infer<typeof publishAgentsRequestSchema>
11+
12+
export const publishAgentsSuccessResponseSchema = z.object({
13+
success: z.literal(true),
14+
publisherId: z.string(),
15+
agents: z
16+
.object({
17+
id: z.string(),
18+
version: z.string(),
19+
displayName: z.string(),
20+
})
21+
.array(),
22+
})
23+
export type PublishAgentsSuccessResponse = z.infer<
24+
typeof publishAgentsSuccessResponseSchema
25+
>
26+
27+
export const publishAgentsErrorResponseSchema = z.object({
28+
success: z.literal(false),
29+
error: z.string(),
30+
details: z.string().optional(),
31+
availablePublishers: z
32+
.object({
33+
id: z.string(),
34+
name: z.string(),
35+
ownershipType: z.enum(['user', 'organization']),
36+
organizationName: z.string().optional(),
37+
})
38+
.array()
39+
.optional(),
40+
validationErrors: z
41+
.object({
42+
code: z.string(),
43+
message: z.string(),
44+
path: z.array(z.string()),
45+
})
46+
.array()
47+
.optional(),
48+
})
49+
export type PublishAgentsErrorResponse = z.infer<
50+
typeof publishAgentsErrorResponseSchema
51+
>
52+
53+
export const publishAgentsResponseSchema = z.discriminatedUnion('success', [
54+
publishAgentsSuccessResponseSchema,
55+
publishAgentsErrorResponseSchema,
56+
])
57+
export type PublishAgentsResponse = z.infer<typeof publishAgentsResponseSchema>

npm-app/src/cli-handlers/publish.ts

Lines changed: 112 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,18 @@ import { loadLocalAgents } from '../agents/load-agents'
77
import { websiteUrl } from '../config'
88
import { getUserCredentials } from '../credentials'
99

10+
import type {
11+
PublishAgentsErrorResponse,
12+
PublishAgentsResponse,
13+
} from '@codebuff/common/types/api/agents/publish'
1014
import type { DynamicAgentTemplate } from '@codebuff/common/types/dynamic-agent-template'
1115

12-
interface PublishResponse {
13-
success: boolean
14-
agentId?: string
15-
version?: string
16-
message?: string
17-
error?: string
18-
details?: string
19-
statusCode?: number
20-
availablePublishers?: Array<{
21-
id: string
22-
name: string
23-
ownershipType: 'user' | 'organization'
24-
organizationName?: string
25-
}>
26-
validationErrors?: Array<{
27-
code: string
28-
message: string
29-
path: (string | number)[]
30-
}>
31-
}
32-
3316
/**
3417
* Handle the publish command to upload agent templates to the backend
3518
* @param agentId The id of the agent to publish (required)
3619
* @param publisherId The id of the publisher to use (optional)
3720
*/ export async function handlePublish(
38-
agentId?: string,
21+
agentIds: string[],
3922
publisherId?: string,
4023
): Promise<void> {
4124
const user = getUserCredentials()
@@ -45,10 +28,9 @@ interface PublishResponse {
4528
return
4629
}
4730

48-
if (!agentId) {
49-
console.log(red('Agent id is required. Usage: publish <agent-id>'))
31+
if (agentIds?.length === 0) {
5032
console.log(
51-
yellow('This prevents accidentally publishing all agents at once.'),
33+
red('Agent id is required. Usage: publish <agent-id> [agent-id2] ...'),
5234
)
5335

5436
// Show available agents
@@ -84,126 +66,122 @@ interface PublishResponse {
8466
return
8567
}
8668

87-
// Find the specific agent
88-
const matchingTemplate = Object.entries(agentTemplates).find(
89-
([key, template]) =>
90-
key === agentId ||
91-
template.id === agentId ||
92-
template.displayName === agentId,
93-
)
69+
const matchingTemplates: Record<string, DynamicAgentTemplate> = {}
70+
for (const agentId of agentIds) {
71+
// Find the specific agent
72+
const matchingTemplate = Object.entries(agentTemplates).find(
73+
([key, template]) =>
74+
key === agentId ||
75+
template.id === agentId ||
76+
template.displayName === agentId,
77+
)
9478

95-
if (!matchingTemplate) {
96-
console.log(red(`Agent "${agentId}" not found. Available agents:`))
97-
Object.values(agentTemplates).forEach((template) => {
98-
console.log(` - ${template.displayName} (${template.id})`)
99-
})
100-
return
79+
if (!matchingTemplate) {
80+
console.log(red(`Agent "${agentId}" not found. Available agents:`))
81+
Object.values(agentTemplates).forEach((template) => {
82+
console.log(` - ${template.displayName} (${template.id})`)
83+
})
84+
return
85+
}
86+
87+
matchingTemplates[matchingTemplate[0]] = matchingTemplate[1]
88+
}
89+
console.log(yellow(`Publishing:`))
90+
for (const [key, template] of Object.entries(matchingTemplates)) {
91+
console.log(` - ${template.displayName} (${template.id})`)
10192
}
102-
const [key, template] = matchingTemplate
103-
console.log(
104-
yellow(`Publishing ${template.displayName} (${template.id})...`),
105-
)
10693

10794
try {
108-
const result = await publishAgentTemplate(
109-
template,
95+
const result = await publishAgentTemplates(
96+
Object.values(matchingTemplates),
11097
user.authToken!,
11198
publisherId,
11299
)
113100

114101
if (result.success) {
102+
console.log(green(`✅ Successfully published:`))
103+
for (const agent of result.agents) {
104+
console.log(
105+
cyan(
106+
` - ${agent.displayName} (${result.publisherId}/${agent.id}@${agent.version})`,
107+
),
108+
)
109+
}
110+
return
111+
}
112+
113+
console.log(red(`❌ Failed to publish agents: ${result.error}`))
114+
if (result.statusCode !== 403) {
115+
return
116+
}
117+
118+
// Check if this is a "no publisher" error vs "multiple publishers" error
119+
if (result.error?.includes('No publisher associated with user')) {
120+
console.log()
115121
console.log(
116-
green(
117-
`✅ Successfully published ${template.displayName} v${result.version}`,
118-
),
122+
cyan('Please visit the website to create your publisher profile:'),
119123
)
120-
console.log(cyan(` Agent ID: ${result.agentId}`))
121-
} else {
124+
console.log(yellow(`${websiteUrl}/publishers`))
125+
console.log()
126+
console.log('A publisher profile allows you to:')
127+
console.log(' • Publish and manage your agents')
128+
console.log(' • Build your reputation in the community')
129+
console.log(' • Organize agents under your name or organization')
130+
console.log()
131+
} else if (
132+
result.availablePublishers &&
133+
result.availablePublishers.length > 0
134+
) {
135+
// Show available publishers
122136
console.log(
123-
red(`❌ Failed to publish ${template.displayName}: ${result.error}`),
137+
cyan(
138+
'You have access to multiple publishers. Please specify which one to use:',
139+
),
124140
)
125-
// Check if the error is about missing publisher (403 status)
126-
if (result.statusCode === 403) {
127-
console.log()
128-
129-
// Check if this is a "no publisher" error vs "multiple publishers" error
130-
if (result.error?.includes('No publisher associated with user')) {
131-
console.log(
132-
cyan(
133-
'Please visit the website to create your publisher profile:',
134-
),
135-
)
136-
console.log(yellow(`${websiteUrl}/publishers`))
137-
console.log()
138-
console.log('A publisher profile allows you to:')
139-
console.log(' • Publish and manage your agents')
140-
console.log(' • Build your reputation in the community')
141-
console.log(' • Organize agents under your name or organization')
142-
console.log()
143-
} else if (
144-
result.availablePublishers &&
145-
result.availablePublishers.length > 0
146-
) {
147-
// Show available publishers
148-
console.log(
149-
cyan(
150-
'You have access to multiple publishers. Please specify which one to use:',
151-
),
152-
)
153-
console.log()
154-
console.log(cyan('Available publishers:'))
155-
result.availablePublishers.forEach((publisher) => {
156-
const orgInfo = publisher.organizationName
157-
? ` (${publisher.organizationName})`
158-
: ''
159-
const typeInfo =
160-
publisher.ownershipType === 'organization'
161-
? ' [Organization]'
162-
: ' [Personal]'
163-
console.log(
164-
` • ${yellow(publisher.id)} - ${publisher.name}${orgInfo}${typeInfo}`,
165-
)
166-
})
167-
console.log()
168-
console.log('Run one of these commands:')
169-
result.availablePublishers.forEach((publisher) => {
170-
console.log(
171-
yellow(
172-
` codebuff publish ${agentId} --publisher ${publisher.id}`,
173-
),
174-
)
175-
})
176-
console.log()
177-
console.log(cyan('Or visit the website to manage your publishers:'))
178-
console.log(yellow(`${websiteUrl}/publishers`))
179-
console.log()
180-
} else {
181-
// Generic 403 error
182-
console.log(cyan('You may need to specify which publisher to use.'))
183-
console.log()
184-
console.log('Try running:')
185-
console.log(
186-
yellow(` publish ${agentId} --publisher <publisher-id>`),
187-
)
188-
console.log()
189-
console.log(
190-
cyan('Visit the website to see your available publishers:'),
191-
)
192-
console.log(yellow(`${websiteUrl}/publishers`))
193-
console.log()
194-
}
195-
} else {
141+
console.log()
142+
console.log(cyan('Available publishers:'))
143+
result.availablePublishers.forEach((publisher) => {
144+
const orgInfo = publisher.organizationName
145+
? ` (${publisher.organizationName})`
146+
: ''
147+
const typeInfo =
148+
publisher.ownershipType === 'organization'
149+
? ' [Organization]'
150+
: ' [Personal]'
196151
console.log(
197-
red(
198-
`❌ Failed to publish ${template.displayName}: ${result.error}`,
152+
` • ${yellow(publisher.id)} - ${publisher.name}${orgInfo}${typeInfo}`,
153+
)
154+
})
155+
console.log()
156+
console.log('Run one of these commands:')
157+
result.availablePublishers.forEach((publisher) => {
158+
console.log(
159+
yellow(
160+
` codebuff publish ${agentIds.join(' ')} --publisher ${publisher.id}`,
199161
),
200162
)
201-
}
163+
})
164+
console.log()
165+
console.log(cyan('Or visit the website to manage your publishers:'))
166+
console.log(yellow(`${websiteUrl}/publishers`))
167+
console.log()
168+
} else {
169+
// Generic 403 error
170+
console.log(cyan('You may need to specify which publisher to use.'))
171+
console.log()
172+
console.log('Try running:')
173+
console.log(
174+
yellow(` publish ${agentIds.join(' ')} --publisher <publisher-id>`),
175+
)
176+
console.log()
177+
console.log(cyan('Visit the website to see your available publishers:'))
178+
console.log(yellow(`${websiteUrl}/publishers`))
179+
console.log()
202180
}
203181
} catch (error) {
204182
console.log(
205183
red(
206-
`❌ Error publishing ${template.displayName}: ${error instanceof Error ? error.message : String(error)}`,
184+
`❌ Error publishing agents: ${error instanceof Error ? error.message : String(error)}`,
207185
),
208186
)
209187
// Avoid logger.error here as it can cause sonic boom errors that mask the real error
@@ -221,13 +199,13 @@ interface PublishResponse {
221199
}
222200

223201
/**
224-
* Publish an agent template to the backend
202+
* Publish agent templates to the backend
225203
*/
226-
async function publishAgentTemplate(
227-
data: DynamicAgentTemplate,
204+
async function publishAgentTemplates(
205+
data: DynamicAgentTemplate[],
228206
authToken: string,
229207
publisherId?: string,
230-
): Promise<PublishResponse> {
208+
): Promise<PublishAgentsResponse & { statusCode?: number }> {
231209
try {
232210
const response = await fetch(`${websiteUrl}/api/agents/publish`, {
233211
method: 'POST',
@@ -241,17 +219,19 @@ async function publishAgentTemplate(
241219
}),
242220
})
243221

244-
let result: any
222+
let result: PublishAgentsResponse
245223
try {
246224
result = await response.json()
247225
} catch (jsonError) {
248226
return {
249227
success: false,
250228
error: `Failed to parse server response: ${response.status} ${response.statusText}`,
229+
statusCode: response.status,
251230
}
252231
}
253232

254233
if (!response.ok) {
234+
result = result as PublishAgentsErrorResponse
255235
// Extract detailed error information from the response
256236
let errorMessage =
257237
result.error || `HTTP ${response.status}: ${response.statusText}`
@@ -284,10 +264,8 @@ async function publishAgentTemplate(
284264
}
285265

286266
return {
287-
success: true,
288-
agentId: result.agent?.id,
289-
version: result.agent?.version,
290-
message: result.message,
267+
...result,
268+
statusCode: response.status,
291269
}
292270
} catch (error) {
293271
// Handle network errors, timeouts, etc.

0 commit comments

Comments
 (0)