Skip to content

Commit 3fe906f

Browse files
authored
refactor: collapse command barrel into command/index.ts (#22903)
1 parent a8d8a35 commit 3fe906f

2 files changed

Lines changed: 188 additions & 187 deletions

File tree

packages/opencode/src/command/command.ts

Lines changed: 0 additions & 186 deletions
This file was deleted.
Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,188 @@
1-
export * as Command from "./command"
1+
import { BusEvent } from "@/bus/bus-event"
2+
import { InstanceState } from "@/effect"
3+
import { EffectBridge } from "@/effect"
4+
import type { InstanceContext } from "@/project/instance"
5+
import { SessionID, MessageID } from "@/session/schema"
6+
import { Effect, Layer, Context } from "effect"
7+
import z from "zod"
8+
import { Config } from "../config"
9+
import { MCP } from "../mcp"
10+
import { Skill } from "../skill"
11+
import PROMPT_INITIALIZE from "./template/initialize.txt"
12+
import PROMPT_REVIEW from "./template/review.txt"
13+
14+
type State = {
15+
commands: Record<string, Info>
16+
}
17+
18+
export const Event = {
19+
Executed: BusEvent.define(
20+
"command.executed",
21+
z.object({
22+
name: z.string(),
23+
sessionID: SessionID.zod,
24+
arguments: z.string(),
25+
messageID: MessageID.zod,
26+
}),
27+
),
28+
}
29+
30+
export const Info = z
31+
.object({
32+
name: z.string(),
33+
description: z.string().optional(),
34+
agent: z.string().optional(),
35+
model: z.string().optional(),
36+
source: z.enum(["command", "mcp", "skill"]).optional(),
37+
// workaround for zod not supporting async functions natively so we use getters
38+
// https://zod.dev/v4/changelog?id=zfunction
39+
template: z.promise(z.string()).or(z.string()),
40+
subtask: z.boolean().optional(),
41+
hints: z.array(z.string()),
42+
})
43+
.meta({
44+
ref: "Command",
45+
})
46+
47+
// for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it
48+
export type Info = Omit<z.infer<typeof Info>, "template"> & { template: Promise<string> | string }
49+
50+
export function hints(template: string) {
51+
const result: string[] = []
52+
const numbered = template.match(/\$\d+/g)
53+
if (numbered) {
54+
for (const match of [...new Set(numbered)].sort()) result.push(match)
55+
}
56+
if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS")
57+
return result
58+
}
59+
60+
export const Default = {
61+
INIT: "init",
62+
REVIEW: "review",
63+
} as const
64+
65+
export interface Interface {
66+
readonly get: (name: string) => Effect.Effect<Info | undefined>
67+
readonly list: () => Effect.Effect<Info[]>
68+
}
69+
70+
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
71+
72+
export const layer = Layer.effect(
73+
Service,
74+
Effect.gen(function* () {
75+
const config = yield* Config.Service
76+
const mcp = yield* MCP.Service
77+
const skill = yield* Skill.Service
78+
79+
const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
80+
const cfg = yield* config.get()
81+
const bridge = yield* EffectBridge.make()
82+
const commands: Record<string, Info> = {}
83+
84+
commands[Default.INIT] = {
85+
name: Default.INIT,
86+
description: "guided AGENTS.md setup",
87+
source: "command",
88+
get template() {
89+
return PROMPT_INITIALIZE.replace("${path}", ctx.worktree)
90+
},
91+
hints: hints(PROMPT_INITIALIZE),
92+
}
93+
commands[Default.REVIEW] = {
94+
name: Default.REVIEW,
95+
description: "review changes [commit|branch|pr], defaults to uncommitted",
96+
source: "command",
97+
get template() {
98+
return PROMPT_REVIEW.replace("${path}", ctx.worktree)
99+
},
100+
subtask: true,
101+
hints: hints(PROMPT_REVIEW),
102+
}
103+
104+
for (const [name, command] of Object.entries(cfg.command ?? {})) {
105+
commands[name] = {
106+
name,
107+
agent: command.agent,
108+
model: command.model,
109+
description: command.description,
110+
source: "command",
111+
get template() {
112+
return command.template
113+
},
114+
subtask: command.subtask,
115+
hints: hints(command.template),
116+
}
117+
}
118+
119+
for (const [name, prompt] of Object.entries(yield* mcp.prompts())) {
120+
commands[name] = {
121+
name,
122+
source: "mcp",
123+
description: prompt.description,
124+
get template() {
125+
return bridge.promise(
126+
mcp
127+
.getPrompt(
128+
prompt.client,
129+
prompt.name,
130+
prompt.arguments
131+
? Object.fromEntries(prompt.arguments.map((argument, i) => [argument.name, `$${i + 1}`]))
132+
: {},
133+
)
134+
.pipe(
135+
Effect.map(
136+
(template) =>
137+
template?.messages
138+
.map((message) => (message.content.type === "text" ? message.content.text : ""))
139+
.join("\n") || "",
140+
),
141+
),
142+
)
143+
},
144+
hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
145+
}
146+
}
147+
148+
for (const item of yield* skill.all()) {
149+
if (commands[item.name]) continue
150+
commands[item.name] = {
151+
name: item.name,
152+
description: item.description,
153+
source: "skill",
154+
get template() {
155+
return item.content
156+
},
157+
hints: [],
158+
}
159+
}
160+
161+
return {
162+
commands,
163+
}
164+
})
165+
166+
const state = yield* InstanceState.make<State>((ctx) => init(ctx))
167+
168+
const get = Effect.fn("Command.get")(function* (name: string) {
169+
const s = yield* InstanceState.get(state)
170+
return s.commands[name]
171+
})
172+
173+
const list = Effect.fn("Command.list")(function* () {
174+
const s = yield* InstanceState.get(state)
175+
return Object.values(s.commands)
176+
})
177+
178+
return Service.of({ get, list })
179+
}),
180+
)
181+
182+
export const defaultLayer = layer.pipe(
183+
Layer.provide(Config.defaultLayer),
184+
Layer.provide(MCP.defaultLayer),
185+
Layer.provide(Skill.defaultLayer),
186+
)
187+
188+
export * as Command from "."

0 commit comments

Comments
 (0)