Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/commands/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ netlify agents
| [`agents:list`](/commands/agents#agentslist) | List agent runs for the current site |
| [`agents:open`](/commands/agents#agentsopen) | Open the agent run preview, dashboard, or pull request in a browser |
| [`agents:pr`](/commands/agents#agentspr) | Open a pull request for an agent run |
| [`agents:redeploy`](/commands/agents#agentsredeploy) | Redeploy an agent run by reapplying its existing changes (no AI inference) |
| [`agents:show`](/commands/agents#agentsshow) | Show details of a specific agent run |
| [`agents:stop`](/commands/agents#agentsstop) | Stop a running agent run |
| [`agents:sync`](/commands/agents#agentssync) | Bring an agent run up to date with the latest code from its base branch |


**Examples**
Expand Down Expand Up @@ -254,6 +256,37 @@ netlify agents:pr
netlify agents:pr 60c7c3b3e7b4a0001f5e4b3a
```

---
## `agents:redeploy`

Redeploy an agent run by reapplying its existing changes (no AI inference)

**Usage**

```bash
netlify agents:redeploy
```

**Arguments**

- id - agent run ID

**Flags**

- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `json` (*boolean*) - output result as JSON
- `project` (*string*) - project ID or name (if not in a linked directory)
- `session` (*string*) - redeploy a specific session (defaults to the latest completed one)
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

**Examples**

```bash
netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a
netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a --session 70d8...
```

---
## `agents:show`

Expand Down Expand Up @@ -318,6 +351,37 @@ netlify agents:stop 60c7c3b3e7b4a0001f5e4b3a
netlify agents:stop 60c7c3b3e7b4a0001f5e4b3a --yes
```

---
## `agents:sync`

Bring an agent run up to date with the latest code from its base branch

**Usage**

```bash
netlify agents:sync
```

**Arguments**

- id - agent run ID

**Flags**

- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `json` (*boolean*) - output result as JSON
- `project` (*string*) - project ID or name (if not in a linked directory)
- `yes` (*boolean*) - skip confirmation prompt
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

**Examples**

```bash
netlify agents:sync 60c7c3b3e7b4a0001f5e4b3a
netlify agents:sync 60c7c3b3e7b4a0001f5e4b3a --yes
```

---

<!-- AUTO-GENERATED-CONTENT:END -->
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ Manage Netlify AI agent runs
| [`agents:list`](/commands/agents#agentslist) | List agent runs for the current site |
| [`agents:open`](/commands/agents#agentsopen) | Open the agent run preview, dashboard, or pull request in a browser |
| [`agents:pr`](/commands/agents#agentspr) | Open a pull request for an agent run |
| [`agents:redeploy`](/commands/agents#agentsredeploy) | Redeploy an agent run by reapplying its existing changes (no AI inference) |
| [`agents:show`](/commands/agents#agentsshow) | Show details of a specific agent run |
| [`agents:stop`](/commands/agents#agentsstop) | Stop a running agent run |
| [`agents:sync`](/commands/agents#agentssync) | Bring an agent run up to date with the latest code from its base branch |


### [api](/commands/api)
Expand Down
70 changes: 70 additions & 0 deletions src/commands/agents/agents-redeploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { OptionValues } from 'commander'

import { chalk, log, logAndThrowError, logJson } from '../../utils/command-helpers.js'
import { startSpinner, stopSpinner } from '../../lib/spinner.js'
import type BaseCommand from '../base-command.js'
import { createAgentsApi } from './api.js'
import { formatStatus } from './utils.js'

interface AgentRedeployOptions extends OptionValues {
session?: string
json?: boolean
}

export const agentsRedeploy = async (id: string, options: AgentRedeployOptions, command: BaseCommand) => {
if (!id) return logAndThrowError('Agent run ID is required')
await command.authenticate()
const api = createAgentsApi(command.netlify)

let sessionId = options.session
if (!sessionId) {
const lookupSpinner = startSpinner({ text: 'Finding latest completed session...' })
try {
const perPage = 100
const maxPages = 10
let page = 1
let latestDone: { id: string } | undefined
while (!latestDone && page <= maxPages) {
const sessions = await api.listAgentRunnerSessions(id, { page, per_page: perPage, order_by: 'desc' })
latestDone = sessions.find((session) => session.state === 'done')
if (latestDone || sessions.length < perPage) break
page += 1
}
stopSpinner({ spinner: lookupSpinner })
if (!latestDone) {
return logAndThrowError('No completed session found to redeploy. Pass --session <id> to target a specific one.')
}
sessionId = latestDone.id
} catch (error_) {
stopSpinner({ spinner: lookupSpinner, error: true })
const error = error_ as Error
return logAndThrowError(`Failed to list sessions: ${error.message}`)
}
}

const spinner = startSpinner({ text: 'Creating redeploy session...' })
try {
const session = await api.redeployAgentRunnerSession(id, sessionId)
stopSpinner({ spinner })

if (options.json) {
logJson(session)
return session
}

log(`${chalk.green('✓')} Redeploy session created!`)
log()
log(` Run ID: ${chalk.cyan(id)}`)
log(` Session ID: ${chalk.cyan(session.id)}`)
log(` Source Session: ${chalk.dim(sessionId)}`)
log(` Status: ${formatStatus(session.state)}`)
log()
log(`Watch progress: ${chalk.cyan(`netlify agents:show ${id} --watch`)}`)
return session
} catch (error_) {
stopSpinner({ spinner, error: true })
const error = error_ as Error & { status?: number }
if (error.status === 404) return logAndThrowError(`Agent run or session not found: ${id} / ${sessionId}`)
return logAndThrowError(`Failed to redeploy: ${error.message}`)
}
}
105 changes: 105 additions & 0 deletions src/commands/agents/agents-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { OptionValues } from 'commander'
import inquirer from 'inquirer'

import { chalk, exit, log, logAndThrowError, logJson } from '../../utils/command-helpers.js'
import { startSpinner, stopSpinner } from '../../lib/spinner.js'
import type BaseCommand from '../base-command.js'
import { createAgentsApi, type AgentsApi } from './api.js'
import type { AgentRunner } from './types.js'

interface AgentSyncOptions extends OptionValues {
json?: boolean
yes?: boolean
}

type SyncStrategy = 'sync_git_origin' | 'merge_target' | 'rebase'

const pickStrategy = (runner: AgentRunner): SyncStrategy | null => {
if (runner.needs_git_sync) return 'sync_git_origin'
if (runner.merge_target_available) return 'merge_target'
if (runner.rebase_available) return 'rebase'
return null
}

const describeStrategy = (strategy: SyncStrategy, runner: AgentRunner): string => {
const target = runner.branch ? ` (target: ${runner.branch})` : ''
switch (strategy) {
case 'sync_git_origin':
return `sync with the remote git origin${target}`
case 'merge_target':
return `merge the latest target branch into this agent run${target}`
case 'rebase':
return 'reapply changes on top of the latest production deploy'
}
}

const runStrategy = (api: AgentsApi, strategy: SyncStrategy, id: string): Promise<AgentRunner> => {
switch (strategy) {
case 'sync_git_origin':
return api.syncGitOriginAgentRunner(id)
case 'merge_target':
return api.mergeTargetAgentRunner(id)
case 'rebase':
return api.rebaseAgentRunner(id)
}
}

export const agentsSync = async (id: string, options: AgentSyncOptions, command: BaseCommand) => {
if (!id) return logAndThrowError('Agent run ID is required')
await command.authenticate()
const api = createAgentsApi(command.netlify)

const lookupSpinner = startSpinner({ text: 'Checking agent run state...' })
let runner: AgentRunner
try {
runner = await api.getAgentRunner(id)
stopSpinner({ spinner: lookupSpinner })
} catch (error_) {
stopSpinner({ spinner: lookupSpinner, error: true })
const error = error_ as Error & { status?: number }
if (error.status === 404) return logAndThrowError(`Agent run not found: ${id}`)
return logAndThrowError(`Failed to fetch agent run: ${error.message}`)
}

const strategy = pickStrategy(runner)
if (!strategy) {
log(chalk.yellow('Nothing to sync — this agent run is already up to date.'))
return runner
}

if (!options.yes && !options.json) {
if (!process.stdin.isTTY) {
return logAndThrowError('Refusing to sync without --yes when stdin is not a TTY')
}
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
{
type: 'confirm',
name: 'confirmed',
message: `Sync agent run ${id}? This will ${describeStrategy(strategy, runner)}.`,
default: false,
},
])
if (!confirmed) return exit()
}

const spinner = startSpinner({ text: 'Syncing agent run...' })
try {
const updated = await runStrategy(api, strategy, id)
stopSpinner({ spinner })

if (options.json) {
logJson(updated)
return updated
}

log(`${chalk.green('✓')} Sync started: ${describeStrategy(strategy, runner)}.`)
log(` Run ID: ${chalk.cyan(updated.id)}`)
log()
log(`Watch progress: ${chalk.cyan(`netlify agents:show ${updated.id} --watch`)}`)
return updated
} catch (error_) {
stopSpinner({ spinner, error: true })
const error = error_ as Error
return logAndThrowError(`Failed to sync: ${error.message}`)
}
}
31 changes: 31 additions & 0 deletions src/commands/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,37 @@ export const createAgentsCommand = (program: BaseCommand) => {
await agentsCommit(id, options, command)
})

program
.command('agents:redeploy')
.argument('<id>', 'agent run ID')
.description('Redeploy an agent run by reapplying its existing changes (no AI inference)')
.option('--session <sid>', 'redeploy a specific session (defaults to the latest completed one)')
.option('--json', 'output result as JSON')
.option('--project <project>', 'project ID or name (if not in a linked directory)')
.hook('preAction', requiresSiteInfoWithProject)
.addExamples([
'netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a',
'netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a --session 70d8...',
])
.action(async (id: string, options: OptionValues, command: BaseCommand) => {
const { agentsRedeploy } = await import('./agents-redeploy.js')
await agentsRedeploy(id, options, command)
})

program
.command('agents:sync')
.argument('<id>', 'agent run ID')
.description('Bring an agent run up to date with the latest code from its base branch')
.option('-y, --yes', 'skip confirmation prompt')
.option('--json', 'output result as JSON')
.option('--project <project>', 'project ID or name (if not in a linked directory)')
.hook('preAction', requiresSiteInfoWithProject)
.addExamples(['netlify agents:sync 60c7c3b3e7b4a0001f5e4b3a', 'netlify agents:sync 60c7c3b3e7b4a0001f5e4b3a --yes'])
.action(async (id: string, options: OptionValues, command: BaseCommand) => {
const { agentsSync } = await import('./agents-sync.js')
await agentsSync(id, options, command)
})

const name = chalk.greenBright('`agents`')

return program
Expand Down
Loading
Loading