feat(cli): add agent update command for name and labels
This commit is contained in:
@@ -16,6 +16,7 @@ import { runSendCommand } from './commands/agent/send.js'
|
||||
import { runInspectCommand } from './commands/agent/inspect.js'
|
||||
import { runWaitCommand } from './commands/agent/wait.js'
|
||||
import { runAttachCommand } from './commands/agent/attach.js'
|
||||
import { runUpdateCommand } from './commands/agent/update.js'
|
||||
import { withOutput } from './output/index.js'
|
||||
|
||||
const VERSION = '0.1.0'
|
||||
@@ -125,6 +126,21 @@ export function createCli(): Command {
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runWaitCommand))
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Update an agent (alias for "paseo agent update")')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--name <name>', "Update the agent's display name")
|
||||
.option(
|
||||
'--label <label>',
|
||||
'Add/set label(s) on the agent (can be used multiple times or comma-separated)',
|
||||
collectMultiple,
|
||||
[]
|
||||
)
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runUpdateCommand))
|
||||
|
||||
// Top-level local daemon shortcuts
|
||||
program.addCommand(daemonStartCommand())
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { runSendCommand } from './send.js'
|
||||
import { runInspectCommand } from './inspect.js'
|
||||
import { runWaitCommand } from './wait.js'
|
||||
import { runAttachCommand } from './attach.js'
|
||||
import { runUpdateCommand } from './update.js'
|
||||
import { withOutput } from '../../output/index.js'
|
||||
|
||||
export function createAgentCommand(): Command {
|
||||
@@ -120,5 +121,20 @@ export function createAgentCommand(): Command {
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runArchiveCommand))
|
||||
|
||||
agent
|
||||
.command('update')
|
||||
.description("Update an agent's metadata")
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--name <name>', "Update the agent's display name")
|
||||
.option(
|
||||
'--label <label>',
|
||||
'Add/set label(s) on the agent (can be used multiple times or comma-separated)',
|
||||
collectMultiple,
|
||||
[]
|
||||
)
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runUpdateCommand))
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { Command } from 'commander'
|
||||
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
|
||||
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
|
||||
|
||||
/** Result type for agent update command */
|
||||
export interface AgentUpdateResult {
|
||||
agentId: string
|
||||
name: string | null
|
||||
labels: string
|
||||
}
|
||||
|
||||
/** Schema for update command output */
|
||||
export const updateSchema: OutputSchema<AgentUpdateResult> = {
|
||||
idField: 'agentId',
|
||||
columns: [
|
||||
{ header: 'AGENT ID', field: 'agentId' },
|
||||
{ header: 'NAME', field: 'name' },
|
||||
{ header: 'LABELS', field: 'labels' },
|
||||
],
|
||||
}
|
||||
|
||||
export interface AgentUpdateOptions extends CommandOptions {
|
||||
name?: string
|
||||
label?: string[]
|
||||
host?: string
|
||||
}
|
||||
|
||||
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>
|
||||
|
||||
function parseLabelOptions(labels: string[] | undefined): Record<string, string> {
|
||||
const parsed: Record<string, string> = {}
|
||||
if (!labels) {
|
||||
return parsed
|
||||
}
|
||||
|
||||
for (const rawLabel of labels) {
|
||||
for (const segment of rawLabel.split(',')) {
|
||||
const label = segment.trim()
|
||||
if (!label) {
|
||||
continue
|
||||
}
|
||||
|
||||
const eqIndex = label.indexOf('=')
|
||||
if (eqIndex === -1) {
|
||||
const error: CommandError = {
|
||||
code: 'INVALID_LABEL',
|
||||
message: `Invalid label format: ${label}`,
|
||||
details: 'Labels must be in key=value format',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const key = label.slice(0, eqIndex).trim()
|
||||
const value = label.slice(eqIndex + 1)
|
||||
if (!key) {
|
||||
const error: CommandError = {
|
||||
code: 'INVALID_LABEL',
|
||||
message: `Invalid label format: ${label}`,
|
||||
details: 'Labels must include a non-empty key in key=value format',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
parsed[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
function formatLabels(labels: Record<string, string>): string {
|
||||
const entries = Object.entries(labels)
|
||||
if (entries.length === 0) {
|
||||
return '-'
|
||||
}
|
||||
return entries.map(([key, value]) => `${key}=${value}`).join(',')
|
||||
}
|
||||
|
||||
export async function runUpdateCommand(
|
||||
agentIdArg: string,
|
||||
options: AgentUpdateOptions,
|
||||
_command: Command
|
||||
): Promise<AgentUpdateCommandResult> {
|
||||
const host = getDaemonHost({ host: options.host as string | undefined })
|
||||
|
||||
// Validate arguments
|
||||
if (!agentIdArg || agentIdArg.trim().length === 0) {
|
||||
const error: CommandError = {
|
||||
code: 'MISSING_AGENT_ID',
|
||||
message: 'Agent ID is required',
|
||||
details: 'Usage: paseo agent update <id> [--name <name>] [--label <key=value>]',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const name = options.name?.trim()
|
||||
if (options.name !== undefined && !name) {
|
||||
const error: CommandError = {
|
||||
code: 'INVALID_NAME',
|
||||
message: 'Name cannot be empty',
|
||||
details: 'Use --name <name> with a non-empty value',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const labels = parseLabelOptions(options.label)
|
||||
if (!name && Object.keys(labels).length === 0) {
|
||||
const error: CommandError = {
|
||||
code: 'NO_CHANGES_PROVIDED',
|
||||
message: 'Nothing to update',
|
||||
details: 'Provide at least one of: --name <name>, --label <key=value>',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
let client
|
||||
try {
|
||||
client = await connectToDaemon({ host: options.host as string | undefined })
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const error: CommandError = {
|
||||
code: 'DAEMON_NOT_RUNNING',
|
||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||
details: 'Start the daemon with: paseo daemon start',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
const agent = await client.fetchAgent(agentIdArg)
|
||||
if (!agent) {
|
||||
const error: CommandError = {
|
||||
code: 'AGENT_NOT_FOUND',
|
||||
message: `Agent not found: ${agentIdArg}`,
|
||||
details: 'Use "paseo ls" to list available agents',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const agentId = agent.id
|
||||
|
||||
await client.updateAgent(agentId, {
|
||||
...(name ? { name } : {}),
|
||||
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
||||
})
|
||||
|
||||
const updated = await client.fetchAgent(agentId)
|
||||
if (!updated) {
|
||||
throw new Error(`Agent not found after update: ${agentId}`)
|
||||
}
|
||||
|
||||
await client.close()
|
||||
|
||||
return {
|
||||
type: 'single',
|
||||
data: {
|
||||
agentId,
|
||||
name: updated.title,
|
||||
labels: formatLabels(updated.labels),
|
||||
},
|
||||
schema: updateSchema,
|
||||
}
|
||||
} catch (err) {
|
||||
await client.close().catch(() => {})
|
||||
|
||||
// Re-throw CommandError as-is
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const error: CommandError = {
|
||||
code: 'UPDATE_FAILED',
|
||||
message: `Failed to update agent: ${message}`,
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user