2026-01-01 20:59:36 +09:00
import { existsSync , readdirSync , readFileSync } from "fs"
import { join , basename , dirname } from "path"
import {
parseFrontmatter ,
resolveCommandsInText ,
resolveFileReferencesInText ,
sanitizeModelField ,
getClaudeConfigDir ,
2026-01-22 12:15:09 +07:00
getOpenCodeConfigDir ,
2026-01-01 20:59:36 +09:00
} from "../../shared"
2026-02-03 14:33:53 +09:00
import { loadBuiltinCommands } from "../../features/builtin-commands"
2026-01-02 15:11:14 +09:00
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
2026-01-01 20:59:36 +09:00
import { isMarkdownFile } from "../../shared/file-utils"
2026-01-07 02:55:32 +09:00
import { discoverAllSkills , type LoadedSkill , type LazyContentLoader } from "../../features/opencode-skill-loader"
2026-01-01 20:59:36 +09:00
import type { ParsedSlashCommand } from "./types"
interface CommandScope {
2026-02-03 14:33:53 +09:00
type : "user" | "project" | "opencode" | "opencode-project" | "skill" | "builtin"
2026-01-01 20:59:36 +09:00
}
interface CommandMetadata {
name : string
description : string
argumentHint? : string
model? : string
agent? : string
subtask? : boolean
}
interface CommandInfo {
name : string
path? : string
metadata : CommandMetadata
content? : string
scope : CommandScope [ "type" ]
2026-01-07 02:55:32 +09:00
lazyContentLoader? : LazyContentLoader
2026-01-01 20:59:36 +09:00
}
function discoverCommandsFromDir ( commandsDir : string , scope : CommandScope [ "type" ] ) : CommandInfo [ ] {
if ( ! existsSync ( commandsDir ) ) {
return [ ]
}
const entries = readdirSync ( commandsDir , { withFileTypes : true } )
const commands : CommandInfo [ ] = [ ]
for ( const entry of entries ) {
if ( ! isMarkdownFile ( entry ) ) continue
const commandPath = join ( commandsDir , entry . name )
const commandName = basename ( entry . name , ".md" )
try {
const content = readFileSync ( commandPath , "utf-8" )
2026-01-02 15:11:14 +09:00
const { data , body } = parseFrontmatter < CommandFrontmatter > ( content )
2026-01-01 20:59:36 +09:00
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const metadata : CommandMetadata = {
name : commandName ,
description : data.description || "" ,
argumentHint : data [ "argument-hint" ] ,
model : sanitizeModelField ( data . model , isOpencodeSource ? "opencode" : "claude-code" ) ,
agent : data.agent ,
subtask : Boolean ( data . subtask ) ,
}
commands . push ( {
name : commandName ,
path : commandPath ,
metadata ,
content : body ,
scope ,
} )
} catch {
continue
}
}
return commands
}
function skillToCommandInfo ( skill : LoadedSkill ) : CommandInfo {
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" ,
2026-01-07 02:55:32 +09:00
lazyContentLoader : skill.lazyContent ,
2026-01-01 20:59:36 +09:00
}
}
2026-01-07 02:55:32 +09:00
export interface ExecutorOptions {
skills? : LoadedSkill [ ]
}
async function discoverAllCommands ( options? : ExecutorOptions ) : Promise < CommandInfo [ ] > {
2026-01-22 12:15:09 +07:00
const configDir = getOpenCodeConfigDir ( { binary : "opencode" } )
2026-01-01 20:59:36 +09:00
const userCommandsDir = join ( getClaudeConfigDir ( ) , "commands" )
const projectCommandsDir = join ( process . cwd ( ) , ".claude" , "commands" )
2026-01-22 12:15:09 +07:00
const opencodeGlobalDir = join ( configDir , "command" )
2026-01-01 20:59:36 +09:00
const opencodeProjectDir = join ( process . cwd ( ) , ".opencode" , "command" )
const userCommands = discoverCommandsFromDir ( userCommandsDir , "user" )
const opencodeGlobalCommands = discoverCommandsFromDir ( opencodeGlobalDir , "opencode" )
const projectCommands = discoverCommandsFromDir ( projectCommandsDir , "project" )
const opencodeProjectCommands = discoverCommandsFromDir ( opencodeProjectDir , "opencode-project" )
2026-02-03 14:33:53 +09:00
const builtinCommandsMap = loadBuiltinCommands ( )
const builtinCommands : CommandInfo [ ] = Object . values ( builtinCommandsMap ) . map ( cmd = > ( {
name : cmd.name ,
metadata : {
name : cmd.name ,
description : cmd.description || "" ,
model : cmd.model ,
agent : cmd.agent ,
subtask : cmd.subtask ,
} ,
content : cmd.template ,
scope : "builtin" ,
} ) )
2026-01-01 20:59:36 +09:00
2026-01-07 02:55:32 +09:00
const skills = options ? . skills ? ? await discoverAllSkills ( )
2026-01-01 20:59:36 +09:00
const skillCommands = skills . map ( skillToCommandInfo )
return [
2026-02-03 14:33:53 +09:00
. . . builtinCommands ,
2026-01-01 20:59:36 +09:00
. . . opencodeProjectCommands ,
. . . projectCommands ,
. . . opencodeGlobalCommands ,
. . . userCommands ,
. . . skillCommands ,
]
}
2026-01-07 02:55:32 +09:00
async function findCommand ( commandName : string , options? : ExecutorOptions ) : Promise < CommandInfo | null > {
const allCommands = await discoverAllCommands ( options )
2026-01-01 20:59:36 +09:00
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" )
2026-01-07 02:55:32 +09:00
let content = cmd . content || ""
if ( ! content && cmd . lazyContentLoader ) {
content = await cmd . lazyContentLoader . load ( )
}
2026-01-01 20:59:36 +09:00
const commandDir = cmd . path ? dirname ( cmd . path ) : process . cwd ( )
2026-01-07 02:55:32 +09:00
const withFileRefs = await resolveFileReferencesInText ( content , commandDir )
2026-01-01 20:59:36 +09:00
const resolvedContent = await resolveCommandsInText ( withFileRefs )
sections . push ( resolvedContent . 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
}
2026-01-07 02:55:32 +09:00
export async function executeSlashCommand ( parsed : ParsedSlashCommand , options? : ExecutorOptions ) : Promise < ExecuteResult > {
const command = await findCommand ( parsed . command , options )
2026-01-01 20:59:36 +09:00
if ( ! command ) {
return {
success : false ,
2026-02-11 19:13:29 +09:00
error : parsed.command.includes ( ":" ) ? ` Marketplace plugin commands like "/ ${ parsed . command } " are not supported. Use .claude/commands/ for custom commands. ` : ` Command "/ ${ parsed . command } " not found. Use the slashcommand tool to list available commands. ` ,
2026-01-01 20:59:36 +09:00
}
}
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 ) } ` ,
}
}
}