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
66 changes: 66 additions & 0 deletions docs/commands/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ 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:publish`](/commands/agents#agentspublish) | Publish an agent run’s changes to production |
| [`agents:redeploy`](/commands/agents#agentsredeploy) | Redeploy an agent run by reapplying its existing changes (no AI inference) |
| [`agents:revert`](/commands/agents#agentsrevert) | Revert an agent run to a specific session (sessions after it are discarded) |
| [`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 |
Expand Down Expand Up @@ -256,6 +258,39 @@ netlify agents:pr
netlify agents:pr 60c7c3b3e7b4a0001f5e4b3a
```

---
## `agents:publish`

Publish an agent run’s changes to production

**Usage**

```bash
netlify agents:publish
```

**Arguments**

- id - agent run ID

**Flags**

- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `force` (*boolean*) - publish even when the run is out of sync with production
- `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:publish 60c7c3b3e7b4a0001f5e4b3a
netlify agents:publish 60c7c3b3e7b4a0001f5e4b3a --yes
netlify agents:publish 60c7c3b3e7b4a0001f5e4b3a --force
```

---
## `agents:redeploy`

Expand Down Expand Up @@ -287,6 +322,37 @@ netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a
netlify agents:redeploy 60c7c3b3e7b4a0001f5e4b3a --session 70d8...
```

---
## `agents:revert`

Revert an agent run to a specific session (sessions after it are discarded)

**Usage**

```bash
netlify agents:revert
```

**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*) - session ID to revert to
- `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:revert 60c7c3b3e7b4a0001f5e4b3a --session 70d8...
```

---
## `agents:show`

Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ 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:publish`](/commands/agents#agentspublish) | Publish an agent run’s changes to production |
| [`agents:redeploy`](/commands/agents#agentsredeploy) | Redeploy an agent run by reapplying its existing changes (no AI inference) |
| [`agents:revert`](/commands/agents#agentsrevert) | Revert an agent run to a specific session (sessions after it are discarded) |
| [`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 |
Expand Down
126 changes: 126 additions & 0 deletions src/commands/agents/agents-publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 } from './api.js'
import type { AgentRunner } from './types.js'
import { buildAgentDashboardUrl } from './utils.js'

interface AgentPublishOptions extends OptionValues {
json?: boolean
yes?: boolean
force?: boolean
}

const isOutOfSync = (runner: AgentRunner): boolean =>
Boolean(runner.needs_git_sync || runner.rebase_available || runner.merge_target_available)

const describeOutOfSync = (runner: AgentRunner): string => {
if (runner.needs_git_sync) return 'the code origin has changed since this run started'
if (runner.merge_target_available) return 'the target branch has new commits'
return 'production has moved on since this run started'
}

export const agentsPublish = async (id: string, options: AgentPublishOptions, command: BaseCommand) => {
if (!id) return logAndThrowError('Agent run ID is required')
await command.authenticate()
const { siteInfo } = command.netlify
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 outOfSync = isOutOfSync(runner)
if (outOfSync && !options.force) {
if (options.json) {
return logAndThrowError(
`Refusing to publish: ${describeOutOfSync(runner)}. Run netlify agents:sync ${id} first, or pass --force.`,
)
}
log(chalk.yellow(`! This agent run is out of date: ${describeOutOfSync(runner)}.`))
log(` Sync first: ${chalk.cyan(`netlify agents:sync ${id}`)}`)
log(` Or override: pass ${chalk.cyan('--force')} to publish the existing diff as-is`)
if (!options.yes) {
if (!process.stdin.isTTY) return logAndThrowError('Refusing to publish out-of-date run without --force')
const { action } = await inquirer.prompt<{ action: 'sync' | 'publish' | 'cancel' }>([
{
type: 'list',
name: 'action',
message: 'How would you like to proceed?',
choices: [
{
name: 'Sync with production now, then re-run publish manually (recommended)',
value: 'sync',
},
{ name: 'Publish anyway (use the current diff as-is)', value: 'publish' },
{ name: 'Cancel', value: 'cancel' },
],
default: 'sync',
},
])
if (action === 'cancel') return exit()
if (action === 'sync') {
const { agentsSync } = await import('./agents-sync.js')
await agentsSync(id, { yes: true }, command)
log()
log(chalk.dim(`After the sync completes, re-run: ${chalk.cyan(`netlify agents:publish ${id}`)}`))
return
}
// action === 'publish' falls through to the publish call below
} else {
return logAndThrowError('Refusing to publish out-of-date run without --force')
}
}

if (!options.yes && !options.json && !outOfSync) {
if (!process.stdin.isTTY) {
return logAndThrowError('Refusing to publish without --yes when stdin is not a TTY')
}
log(chalk.redBright('Warning'), 'You are about to publish agent changes to production.')
log(` Site: ${chalk.bold(siteInfo.name)}`)
log(` Run: ${chalk.bold(id)}`)
log()
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
{
type: 'confirm',
name: 'confirmed',
message: `Publish agent run ${id} to production?`,
default: false,
},
])
if (!confirmed) return exit()
}

const spinner = startSpinner({ text: 'Publishing to production...' })
try {
const updated = await api.agentRunnerPublishToProduction(id)
stopSpinner({ spinner })

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

log(`${chalk.green('✓')} Published agent run to production!`)
log()
log(` Run ID: ${chalk.cyan(updated.id)}`)
if (updated.merge_commit_sha) log(` Commit: ${chalk.cyan(updated.merge_commit_sha)}`)
log(` Browser: ${chalk.blue(buildAgentDashboardUrl(siteInfo.name, updated.id))}`)
return updated
} catch (error_) {
stopSpinner({ spinner, error: true })
const error = error_ as Error
return logAndThrowError(`Failed to publish: ${error.message}`)
}
}
56 changes: 56 additions & 0 deletions src/commands/agents/agents-revert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 } from './api.js'

interface AgentRevertOptions extends OptionValues {
json?: boolean
yes?: boolean
session?: string
}

export const agentsRevert = async (id: string, options: AgentRevertOptions, command: BaseCommand) => {
if (!id) return logAndThrowError('Agent run ID is required')
if (!options.session) return logAndThrowError('--session <id> is required: revert targets a specific session')
await command.authenticate()
const api = createAgentsApi(command.netlify)

if (!options.yes && !options.json) {
if (!process.stdin.isTTY) {
return logAndThrowError('Refusing to revert without --yes when stdin is not a TTY')
}
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
{
type: 'confirm',
name: 'confirmed',
message: `Revert agent run ${id} to session ${options.session}? Sessions after that will be discarded.`,
default: false,
},
])
if (!confirmed) return exit()
}

const spinner = startSpinner({ text: 'Reverting agent run...' })
try {
const runner = await api.revertAgentRunner(id, options.session)
stopSpinner({ spinner })

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

log(`${chalk.green('✓')} Agent run reverted!`)
log(` Run ID: ${chalk.cyan(runner.id)}`)
log(` Reverted to session: ${chalk.cyan(options.session)}`)
return runner
} 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} / ${options.session}`)
return logAndThrowError(`Failed to revert: ${error.message}`)
}
}
34 changes: 34 additions & 0 deletions src/commands/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,40 @@ export const createAgentsCommand = (program: BaseCommand) => {
await agentsCommit(id, options, command)
})

program
.command('agents:publish')
.argument('<id>', 'agent run ID')
.description('Publish an agent run’s changes to production')
.option('-y, --yes', 'skip confirmation prompt')
.option('--force', 'publish even when the run is out of sync with production')
.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:publish 60c7c3b3e7b4a0001f5e4b3a',
'netlify agents:publish 60c7c3b3e7b4a0001f5e4b3a --yes',
'netlify agents:publish 60c7c3b3e7b4a0001f5e4b3a --force',
])
.action(async (id: string, options: OptionValues, command: BaseCommand) => {
const { agentsPublish } = await import('./agents-publish.js')
await agentsPublish(id, options, command)
})

program
.command('agents:revert')
.argument('<id>', 'agent run ID')
.description('Revert an agent run to a specific session (sessions after it are discarded)')
.requiredOption('--session <sid>', 'session ID to revert to')
.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:revert 60c7c3b3e7b4a0001f5e4b3a --session 70d8...'])
.action(async (id: string, options: OptionValues, command: BaseCommand) => {
const { agentsRevert } = await import('./agents-revert.js')
await agentsRevert(id, options, command)
})

program
.command('agents:redeploy')
.argument('<id>', 'agent run ID')
Expand Down
Loading
Loading