157 lines
4.7 KiB
TypeScript
157 lines
4.7 KiB
TypeScript
import { dirname } from "path"
|
|
import {
|
|
resolveCommandsInText,
|
|
resolveFileReferencesInText,
|
|
} from "../../shared"
|
|
import { discoverAllSkills, type LoadedSkill, type LazyContentLoader } from "../../features/opencode-skill-loader"
|
|
import { discoverCommandsSync } from "../../tools/slashcommand"
|
|
import type { CommandInfo as DiscoveredCommandInfo, CommandMetadata } from "../../tools/slashcommand/types"
|
|
import type { ParsedSlashCommand } from "./types"
|
|
|
|
interface SkillCommandInfo {
|
|
name: string
|
|
path?: string
|
|
metadata: CommandMetadata
|
|
content?: string
|
|
scope: "skill"
|
|
lazyContentLoader?: LazyContentLoader
|
|
}
|
|
|
|
type CommandInfo = DiscoveredCommandInfo | SkillCommandInfo
|
|
|
|
function skillToCommandInfo(skill: LoadedSkill): SkillCommandInfo {
|
|
return {
|
|
name: skill.name,
|
|
path: skill.path,
|
|
metadata: {
|
|
name: skill.name,
|
|
description: skill.definition.description || "",
|
|
argumentHint: skill.definition.argumentHint,
|
|
model: skill.definition.model,
|
|
agent: skill.definition.agent,
|
|
subtask: skill.definition.subtask,
|
|
},
|
|
content: skill.definition.template,
|
|
scope: "skill",
|
|
lazyContentLoader: skill.lazyContent,
|
|
}
|
|
}
|
|
|
|
export interface ExecutorOptions {
|
|
skills?: LoadedSkill[]
|
|
pluginsEnabled?: boolean
|
|
enabledPluginsOverride?: Record<string, boolean>
|
|
}
|
|
|
|
function filterDiscoveredCommandsByScope(
|
|
commands: DiscoveredCommandInfo[],
|
|
scope: DiscoveredCommandInfo["scope"],
|
|
): DiscoveredCommandInfo[] {
|
|
return commands.filter(command => command.scope === scope)
|
|
}
|
|
|
|
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
|
|
const discoveredCommands = discoverCommandsSync(process.cwd(), {
|
|
pluginsEnabled: options?.pluginsEnabled,
|
|
enabledPluginsOverride: options?.enabledPluginsOverride,
|
|
})
|
|
|
|
const skills = options?.skills ?? await discoverAllSkills()
|
|
const skillCommands = skills.map(skillToCommandInfo)
|
|
|
|
return [
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "builtin"),
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "opencode-project"),
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "project"),
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "opencode"),
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "user"),
|
|
...skillCommands,
|
|
...filterDiscoveredCommandsByScope(discoveredCommands, "plugin"),
|
|
]
|
|
}
|
|
|
|
async function findCommand(commandName: string, options?: ExecutorOptions): Promise<CommandInfo | null> {
|
|
const allCommands = await discoverAllCommands(options)
|
|
return allCommands.find(
|
|
(cmd) => cmd.name.toLowerCase() === commandName.toLowerCase()
|
|
) ?? null
|
|
}
|
|
|
|
async function formatCommandTemplate(cmd: CommandInfo, args: string): Promise<string> {
|
|
const sections: string[] = []
|
|
|
|
sections.push(`# /${cmd.name} Command\n`)
|
|
|
|
if (cmd.metadata.description) {
|
|
sections.push(`**Description**: ${cmd.metadata.description}\n`)
|
|
}
|
|
|
|
if (args) {
|
|
sections.push(`**User Arguments**: ${args}\n`)
|
|
}
|
|
|
|
if (cmd.metadata.model) {
|
|
sections.push(`**Model**: ${cmd.metadata.model}\n`)
|
|
}
|
|
|
|
if (cmd.metadata.agent) {
|
|
sections.push(`**Agent**: ${cmd.metadata.agent}\n`)
|
|
}
|
|
|
|
sections.push(`**Scope**: ${cmd.scope}\n`)
|
|
sections.push("---\n")
|
|
sections.push("## Command Instructions\n")
|
|
|
|
let content = cmd.content || ""
|
|
if (!content && cmd.lazyContentLoader) {
|
|
content = await cmd.lazyContentLoader.load()
|
|
}
|
|
|
|
const commandDir = cmd.path ? dirname(cmd.path) : process.cwd()
|
|
const withFileRefs = await resolveFileReferencesInText(content, commandDir)
|
|
const resolvedContent = await resolveCommandsInText(withFileRefs)
|
|
const resolvedArguments = args
|
|
const substitutedContent = resolvedContent
|
|
.replace(/\$\{user_message\}/g, resolvedArguments)
|
|
.replace(/\$ARGUMENTS/g, resolvedArguments)
|
|
sections.push(substitutedContent.trim())
|
|
|
|
if (args) {
|
|
sections.push("\n\n---\n")
|
|
sections.push("## User Request\n")
|
|
sections.push(args)
|
|
}
|
|
|
|
return sections.join("\n")
|
|
}
|
|
|
|
export interface ExecuteResult {
|
|
success: boolean
|
|
replacementText?: string
|
|
error?: string
|
|
}
|
|
|
|
export async function executeSlashCommand(parsed: ParsedSlashCommand, options?: ExecutorOptions): Promise<ExecuteResult> {
|
|
const command = await findCommand(parsed.command, options)
|
|
|
|
if (!command) {
|
|
return {
|
|
success: false,
|
|
error: `Command "/${parsed.command}" not found. Use the skill tool to list available skills and commands.`,
|
|
}
|
|
}
|
|
|
|
try {
|
|
const template = await formatCommandTemplate(command, parsed.args)
|
|
return {
|
|
success: true,
|
|
replacementText: template,
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to load command "/${parsed.command}": ${err instanceof Error ? err.message : String(err)}`,
|
|
}
|
|
}
|
|
}
|