mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-06 08:21:50 +00:00
feat(cli): add effectCmd wrapper + convert models command (#25429)
This commit is contained in:
parent
0d0ec7dc46
commit
146ff8ad85
4 changed files with 101 additions and 60 deletions
|
|
@ -1,19 +1,16 @@
|
|||
import type { Argv } from "yargs"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { cmd } from "./cmd"
|
||||
import { effectCmd, fail } from "../effect-cmd"
|
||||
import { UI } from "../ui"
|
||||
import { EOL } from "os"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const ModelsCommand = cmd({
|
||||
export const ModelsCommand = effectCmd({
|
||||
command: "models [provider]",
|
||||
describe: "list all available models",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("provider", {
|
||||
describe: "provider ID to filter models by",
|
||||
type: "string",
|
||||
|
|
@ -26,63 +23,45 @@ export const ModelsCommand = cmd({
|
|||
.option("refresh", {
|
||||
describe: "refresh the models cache from models.dev",
|
||||
type: "boolean",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
}),
|
||||
handler: Effect.fn("Cli.models")(function* (args) {
|
||||
if (args.refresh) {
|
||||
await ModelsDev.refresh(true)
|
||||
// followup: lift ModelsDev into an Effect Service so this drops the Effect.promise wrap.
|
||||
yield* Effect.promise(() => ModelsDev.refresh(true))
|
||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
||||
}
|
||||
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Provider.Service
|
||||
const providers = yield* svc.list()
|
||||
const provider = yield* Provider.Service
|
||||
const providers = yield* provider.list()
|
||||
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const provider = providers[providerID]
|
||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const p = providers[providerID]
|
||||
const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
const provider = providers[providerID]
|
||||
if (!provider) {
|
||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`)
|
||||
print(providerID, args.verbose)
|
||||
return
|
||||
}
|
||||
|
||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
||||
return
|
||||
}
|
||||
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsOpencode = a.startsWith("opencode")
|
||||
const bIsOpencode = b.startsWith("opencode")
|
||||
if (aIsOpencode && !bIsOpencode) return -1
|
||||
if (!aIsOpencode && bIsOpencode) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
for (const providerID of ids) {
|
||||
print(ProviderID.make(providerID), args.verbose)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsOpencode = a.startsWith("opencode")
|
||||
const bIsOpencode = b.startsWith("opencode")
|
||||
if (aIsOpencode && !bIsOpencode) return -1
|
||||
if (!aIsOpencode && bIsOpencode) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
},
|
||||
|
||||
for (const providerID of ids) print(ProviderID.make(providerID), args.verbose)
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
52
packages/opencode/src/cli/effect-cmd.ts
Normal file
52
packages/opencode/src/cli/effect-cmd.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import type { Argv } from "yargs"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { cmd } from "./cmd/cmd"
|
||||
|
||||
/**
|
||||
* User-visible command failure. Throw via `fail("...")` from an effectCmd handler
|
||||
* to surface a printed message + non-zero exit. Recognised by the global error
|
||||
* formatter in `src/cli/error.ts` (FormatError), so the existing top-level
|
||||
* catch + cleanup in `src/index.ts` runs normally.
|
||||
*/
|
||||
export class CliError extends Schema.TaggedErrorClass<CliError>()("CliError", {
|
||||
message: Schema.String,
|
||||
exitCode: Schema.optional(Schema.Number),
|
||||
}) {}
|
||||
|
||||
export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError({ message, exitCode }))
|
||||
|
||||
/**
|
||||
* Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
|
||||
* an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
|
||||
*
|
||||
* Errors propagate to the existing top-level handler in `src/index.ts`; use
|
||||
* `fail("...")` for user-visible domain failures (clean exit, formatted message).
|
||||
*
|
||||
* Handlers are typically `Effect.fn("Cli.<name>")(function*(args) { ... })`,
|
||||
* which adds a named tracing span per CLI invocation. Once all commands use
|
||||
* `effectCmd`, swapping the underlying `cmd()` factory for effect/cli's
|
||||
* `Command.make(...)` won't touch any handler bodies.
|
||||
*/
|
||||
export const effectCmd = <Args, A>(opts: {
|
||||
command: string | readonly string[]
|
||||
describe: string | false
|
||||
builder?: (yargs: Argv) => Argv<Args>
|
||||
/** Defaults to process.cwd(). Override for commands that take a directory positional. */
|
||||
directory?: (args: Args) => string
|
||||
handler: (args: Args) => Effect.Effect<A, CliError, AppServices | InstanceStore.Service>
|
||||
}) =>
|
||||
cmd<{}, Args>({
|
||||
command: opts.command,
|
||||
describe: opts.describe,
|
||||
builder: opts.builder as never,
|
||||
async handler(rawArgs) {
|
||||
// yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
|
||||
const args = rawArgs as unknown as Args
|
||||
const directory = opts.directory?.(args) ?? process.cwd()
|
||||
await AppRuntime.runPromise(
|
||||
InstanceStore.Service.use((s) => s.provide({ directory }, opts.handler(args))),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -15,6 +15,13 @@ function isTaggedError(error: unknown, tag: string): boolean {
|
|||
}
|
||||
|
||||
export function FormatError(input: unknown) {
|
||||
// CliError: domain failure surfaced from an effectCmd handler via fail("...")
|
||||
if (isTaggedError(input, "CliError")) {
|
||||
const data = input as ErrorLike & { exitCode?: number }
|
||||
if (data.exitCode != null) process.exitCode = data.exitCode
|
||||
return data.message ?? ""
|
||||
}
|
||||
|
||||
// MCPFailed: { name: string }
|
||||
if (NamedError.hasName(input, "MCPFailed")) {
|
||||
return `MCP server "${(input as ErrorLike).data?.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export const AppLayer = Layer.mergeAll(
|
|||
|
||||
const rt = ManagedRuntime.make(AppLayer, { memoMap })
|
||||
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
|
||||
|
||||
/** Services provided by AppRuntime — i.e. what an Effect run via AppRuntime.runPromise can yield. */
|
||||
export type AppServices = ManagedRuntime.ManagedRuntime.Services<typeof rt>
|
||||
const wrap = (effect: Parameters<typeof rt.runSync>[0]) => attach(effect as never) as never
|
||||
|
||||
export const AppRuntime: Runtime = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue