Skip to content
Merged
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
18 changes: 18 additions & 0 deletions src/entrypoints/cli/commandContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { RenderOptions } from 'ink'

export type CliCommandRegistrationContext = {
stdinContent: string
renderContext: RenderOptions | undefined
renderContextWithExitOnCtrlC: RenderOptions
}

export function omitKeys<T extends Record<string, any>>(
input: T,
...keys: (keyof T | string)[]
): Partial<T> {
const result = { ...input } as Partial<T>
for (const key of keys) {
delete (result as any)[key as any]
}
return result
}
71 changes: 71 additions & 0 deletions src/entrypoints/cli/commands/agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Command } from '@commander-js/extra-typings'
import { cwd } from 'process'
import { setup } from '../setup'

export function registerAgentsCommands(program: Command): void {
const agentsCmd = program
.command('agents')
.description('Agent utilities (validate templates, etc.)')

agentsCmd
.command('validate [paths...]')
.description(
'Validate agent markdown files (defaults to user+project agent dirs)',
)
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.option('--json', 'Output as JSON')
.option(
'--no-tools-check',
'Skip validating tool names against the tool registry',
)
.action(async (paths: string[] | undefined, options: any) => {
try {
const workingDir =
typeof options?.cwd === 'string' ? options.cwd : cwd()
await setup(workingDir, false)
const { validateAgentTemplates } = await import('../agentsValidate')
const report = await validateAgentTemplates({
cwd: workingDir,
paths: Array.isArray(paths) ? paths : [],
checkTools: options.toolsCheck !== false,
})

if (options.json) {
console.log(JSON.stringify(report, null, 2))
process.exitCode = report.ok ? 0 : 1
return
}

console.log(
`Validated ${report.results.length} agent file(s): ${report.errorCount} error(s), ${report.warningCount} warning(s)\n`,
)

for (const r of report.results) {
const rel = r.filePath
const title = r.agentType ? `${r.agentType}` : '(unknown agent)'
console.log(`${title} — ${rel}`)
if (r.model) {
const normalized = r.normalizedModel
? ` (normalized: ${r.normalizedModel})`
: ''
console.log(` model: ${r.model}${normalized}`)
}
if (r.issues.length === 0) {
console.log(` OK`)
} else {
for (const issue of r.issues) {
console.log(` - ${issue.level}: ${issue.message}`)
}
}
console.log('')
}

process.exitCode = report.ok ? 0 : 1
return
} catch (error) {
console.error((error as Error).message)
process.exitCode = 1
return
}
})
}
30 changes: 30 additions & 0 deletions src/entrypoints/cli/commands/approvedTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Command } from '@commander-js/extra-typings'
import { getCwd } from '@utils/state'
import {
handleListApprovedTools,
handleRemoveApprovedTool,
} from '@commands/approved-tools'

export function registerApprovedToolsCommands(program: Command): void {
const allowedTools = program
.command('approved-tools')
.description('Manage approved tools')

allowedTools
.command('list')
.description('List all approved tools')
.action(async () => {
const result = handleListApprovedTools(getCwd())
console.log(result)
process.exit(0)
})

allowedTools
.command('remove <tool>')
.description('Remove a tool from the list of approved tools')
.action(async (tool: string) => {
const result = handleRemoveApprovedTool(tool)
console.log(result.message)
process.exit(result.success ? 0 : 1)
})
}
70 changes: 70 additions & 0 deletions src/entrypoints/cli/commands/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Command } from '@commander-js/extra-typings'
import { cwd } from 'process'
import { PRODUCT_COMMAND } from '@constants/product'
import { setup } from '../setup'
import {
deleteConfigForCLI,
getConfigForCLI,
listConfigForCLI,
setConfigForCLI,
} from '@utils/config'

export function registerConfigCommands(program: Command): void {
const config = program
.command('config')
.description(
`Manage configuration (eg. ${PRODUCT_COMMAND} config set -g theme dark)`,
)

config
.command('get <key>')
.description('Get a config value')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.option('-g, --global', 'Use global config')
.action(async (key, { cwd, global }) => {
await setup(cwd, false)
console.log(getConfigForCLI(key, global ?? false))
process.exit(0)
})

config
.command('set <key> <value>')
.description('Set a config value')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.option('-g, --global', 'Use global config')
.action(async (key, value, { cwd, global }) => {
await setup(cwd, false)
setConfigForCLI(key, value, global ?? false)
console.log(`Set ${key} to ${value}`)
process.exit(0)
})

config
.command('remove <key>')
.description('Remove a config value')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.option('-g, --global', 'Use global config')
.action(async (key, { cwd, global }) => {
await setup(cwd, false)
deleteConfigForCLI(key, global ?? false)
console.log(`Removed ${key}`)
process.exit(0)
})

config
.command('list')
.description('List all config values')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.option('-g, --global', 'Use global config', false)
.action(async ({ cwd, global }) => {
await setup(cwd, false)
console.log(
JSON.stringify(
global ? listConfigForCLI(true) : listConfigForCLI(false),
null,
2,
),
)
process.exit(0)
})
}
71 changes: 71 additions & 0 deletions src/entrypoints/cli/commands/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Command } from '@commander-js/extra-typings'
import { cwd } from 'process'
import { PRODUCT_COMMAND } from '@constants/product'
import { getContext, removeContext, setContext } from '@context'
import { setup } from '../setup'
import { omitKeys } from '../commandContext'

export function registerContextCommands(program: Command): void {
const contextCmd = program
.command('context')
.description(
`Set static context (eg. ${PRODUCT_COMMAND} context add-file ./src/*.py)`,
)

contextCmd
.command('get <key>')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.description('Get a value from context')
.action(async (key, { cwd }) => {
await setup(cwd, false)

const context = omitKeys(
await getContext(),
'codeStyle',
'directoryStructure',
)
console.log(context[key])
process.exit(0)
})

contextCmd
.command('set <key> <value>')
.description('Set a value in context')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.action(async (key, value, { cwd }) => {
await setup(cwd, false)

setContext(key, value)
console.log(`Set context.${key} to "${value}"`)
process.exit(0)
})

contextCmd
.command('list')
.description('List all context values')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.action(async ({ cwd }) => {
await setup(cwd, false)

const context = omitKeys(
await getContext(),
'codeStyle',
'directoryStructure',
'gitStatus',
)
console.log(JSON.stringify(context, null, 2))
process.exit(0)
})

contextCmd
.command('remove <key>')
.description('Remove a value from context')
.option('--cwd <cwd>', 'The current working directory', String, cwd())
.action(async (key, { cwd }) => {
await setup(cwd, false)

removeContext(key)
console.log(`Removed context.${key}`)
process.exit(0)
})
}
53 changes: 53 additions & 0 deletions src/entrypoints/cli/commands/doctorUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import type { Command } from '@commander-js/extra-typings'
import { PRODUCT_NAME } from '@constants/product'
import { Doctor } from '@screens/Doctor'
import { MACRO } from '@constants/macros'

export function registerDoctorAndUpdateCommands(program: Command): void {
program
.command('doctor')
.description(`Check the health of your ${PRODUCT_NAME} installation`)
.action(async () => {
await new Promise<void>(resolve => {
;(async () => {
const { render } = await import('ink')
render(<Doctor onDone={() => resolve()} doctorMode={true} />)
})()
})
process.exit(0)
})

program
.command('update')
.description('Show manual upgrade commands (no auto-install)')
.action(async () => {
console.log(`Current version: ${MACRO.VERSION}`)
console.log('Checking for updates...')

const { getLatestVersion, getUpdateCommandSuggestions } =
await import('@utils/session/autoUpdater')
const latestVersion = await getLatestVersion()

if (!latestVersion) {
console.error('Failed to check for updates')
process.exit(1)
}

if (latestVersion === MACRO.VERSION) {
console.log(`${PRODUCT_NAME} is up to date`)
process.exit(0)
}

console.log(`New version available: ${latestVersion}`)
const cmds = await getUpdateCommandSuggestions()
console.log('\nRun one of the following commands to update:')
for (const c of cmds) console.log(` ${c}`)
if (process.platform !== 'win32') {
console.log(
'\nNote: you may need to prefix with "sudo" on macOS/Linux.',
)
}
process.exit(0)
})
}
26 changes: 26 additions & 0 deletions src/entrypoints/cli/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Command } from '@commander-js/extra-typings'
import type { CliCommandRegistrationContext } from '../commandContext'
import { registerAgentsCommands } from './agents'
import { registerApprovedToolsCommands } from './approvedTools'
import { registerConfigCommands } from './config'
import { registerContextCommands } from './context'
import { registerDoctorAndUpdateCommands } from './doctorUpdate'
import { registerMcpCommands } from './mcp'
import { registerModelsCommands } from './models'
import { registerPluginAndSkillsCommands } from './pluginSkills'
import { registerSessionLogAndErrorCommands } from './session'

export function registerCliCommands(
program: Command,
context: CliCommandRegistrationContext,
): void {
registerConfigCommands(program)
registerModelsCommands(program)
registerAgentsCommands(program)
registerPluginAndSkillsCommands(program)
registerApprovedToolsCommands(program)
registerMcpCommands(program)
registerDoctorAndUpdateCommands(program)
registerSessionLogAndErrorCommands(program, context)
registerContextCommands(program)
}
Loading
Loading