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
43 changes: 11 additions & 32 deletions packages/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@ export const PrintLogs = GlobalFlag.setting("print-logs")({

declare const OPENCODE_CLI_NAME: string | undefined

const ServerParams = {
standalone: Flag.boolean("standalone").pipe(
Flag.withDescription("Run with a private server instead of the background service"),
Flag.withDefault(false),
),
server: Flag.string("server").pipe(
Flag.withDescription("Connect to a server URL instead of the background service"),
Flag.optional,
),
}

const PermissionParams = {
auto: Flag.boolean("auto").pipe(
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
Expand All @@ -38,7 +27,6 @@ const PermissionParams = {
const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "shuvcode", {
description: "Shuvcode command line interface",
params: {
...ServerParams,
...PermissionParams,
directory: Argument.string("directory").pipe(
Argument.withDescription("Directory to start Shuvcode in"),
Expand Down Expand Up @@ -96,11 +84,10 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
),
},
}),
Spec.make("acp", { description: "Start an Agent Client Protocol server" }),
Spec.make("acp", { description: "Start an Agent Client Protocol server", connectionFlags: "unsupported" }),
Spec.make("api", {
description: "Make a request to the running server",
params: {
...ServerParams,
request: Argument.string("operation | method path").pipe(
Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"),
Argument.variadic({ min: 1, max: 2 }),
Expand Down Expand Up @@ -134,7 +121,9 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
"log",
"repos",
]).pipe(
Argument.withDescription("Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos"),
Argument.withDescription(
"Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos",
),
Argument.optional,
),
},
Expand All @@ -147,7 +136,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("list", {
description: "list integrations and credentials",
params: {
...ServerParams,
format: Flag.choice("format", ["default", "json"]).pipe(
Flag.withDescription("Output format"),
Flag.withDefault("default"),
Expand All @@ -157,7 +145,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("login", {
description: "connect an integration",
params: {
...ServerParams,
target: Argument.string("target").pipe(
Argument.withDescription("Integration ID, name, or well-known provider URL"),
Argument.optional,
Expand All @@ -172,7 +159,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("logout", {
description: "log out of a saved account",
params: {
...ServerParams,
target: Argument.string("target").pipe(
Argument.withDescription("Integration ID or name"),
Argument.optional,
Expand All @@ -186,7 +172,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("export", {
description: "print stored credentials, including secrets, as JSON",
params: {
...ServerParams,
target: Argument.string("target").pipe(
Argument.withDescription("Integration ID or name (exports every integration when omitted)"),
Argument.optional,
Expand All @@ -196,7 +181,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("import", {
description: "import credentials exported by auth export",
params: {
...ServerParams,
file: Argument.string("file").pipe(
Argument.withDescription("JSON file to import (reads stdin when omitted)"),
Argument.optional,
Expand All @@ -206,7 +190,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("switch", {
description: "switch the active account for an integration",
params: {
...ServerParams,
target: Argument.string("target").pipe(
Argument.withDescription("Integration ID or name"),
Argument.optional,
Expand All @@ -225,6 +208,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("list", { description: "List configured MCP servers and their status" }),
Spec.make("add", {
description: "Add an MCP server to your configuration",
connectionFlags: "unsupported",
params: {
name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")),
command: Argument.string("command").pipe(
Expand Down Expand Up @@ -272,6 +256,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("add", {
description: "Install a plugin and add it to the global configuration",
connectionFlags: "unsupported",
params: {
package: Argument.string("package").pipe(Argument.withDescription("npm registry or Git package specifier")),
},
Expand All @@ -287,6 +272,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("update", {
description: "Update package plugins",
connectionFlags: "unsupported",
params: {
target: Argument.string("target").pipe(
Argument.withDescription("Configured package target; omit to update all outdated plugins"),
Expand All @@ -296,6 +282,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("remove", {
description: "Remove a plugin from global configuration",
connectionFlags: "unsupported",
params: {
package: Argument.string("package").pipe(Argument.withDescription("configured package specifier")),
},
Expand All @@ -304,12 +291,10 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("models", {
description: "List all available models",
params: ServerParams,
}),
Spec.make("stats", {
description: "Show shareable usage statistics",
params: {
...ServerParams,
days: Flag.integer("days").pipe(
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
Flag.withDescription("Show the last N days; 0 means today"),
Expand Down Expand Up @@ -340,7 +325,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("mini", {
description: "Start the minimal interactive interface",
params: {
...ServerParams,
continue: Flag.boolean("continue").pipe(
Flag.withAlias("c"),
Flag.withDescription("Continue the last session"),
Expand Down Expand Up @@ -376,7 +360,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("run", {
description: "Run Shuvcode with a message",
params: {
...ServerParams,
message: Argument.string("message").pipe(
Argument.withDescription("Message to send"),
Argument.variadic({ min: 0 }),
Expand Down Expand Up @@ -421,7 +404,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("list", {
description: "List top-level sessions in the current project, newest first",
params: {
...ServerParams,
maxCount: Flag.integer("max-count").pipe(
Flag.withAlias("n"),
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
Expand All @@ -437,14 +419,12 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("delete", {
description: "Delete a session and its child sessions",
params: {
...ServerParams,
sessionID: Argument.string("sessionID").pipe(Argument.withDescription("Session ID to delete")),
},
}),
Spec.make("export", {
description: "Export session data as JSON",
params: {
...ServerParams,
session: Argument.string("session").pipe(
Argument.withDescription("Session ID to export"),
Argument.optional,
Expand All @@ -458,7 +438,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("import", {
description: "Import session data from a JSON file or URL",
params: {
...ServerParams,
file: Argument.string("file").pipe(Argument.withDescription("JSON file or URL to import")),
directory: Flag.string("directory").pipe(
Flag.withDescription("Directory in which to import the session"),
Expand All @@ -470,6 +449,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("service", {
description: "Manage the background server",
connectionFlags: "unsupported",
commands: [
Spec.make("start", { description: "Start the background server" }),
Spec.make("restart", { description: "Restart the background server" }),
Expand Down Expand Up @@ -512,12 +492,10 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("reload", {
description: "Reload configuration",
params: {
...ServerParams,
},
}),
Spec.make("pair", {
description: "Print one-time links to connect a browser or app",
connectionFlags: "unsupported",
params: {
url: Flag.string("url").pipe(
Flag.withDescription("Use an external HTTP(S) server URL in pairing links"),
Expand All @@ -536,6 +514,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("serve", {
description: "Start the v2 API and web server",
connectionFlags: "unsupported",
params: {
hostname: Flag.string("hostname").pipe(Flag.optional),
port: Flag.integer("port").pipe(Flag.optional),
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/commands/handlers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export default Runtime.handler(
Commands.commands.api,
Effect.fn("cli.api")(function* (input) {
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
mismatch: "ignore",
})
const endpoint = server.endpoint
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/handlers/auth/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default Runtime.handler(
Commands.commands.auth.commands.export,
Effect.fn("cli.auth.export")(
function* (input) {
const client = yield* createClient({ server: Option.getOrUndefined(input.server), standalone: input.standalone })
const client = yield* createClient()
const target = Option.getOrUndefined(input.target)
const integrationID = target ? (yield* resolveIntegration(yield* loadIntegrations(client), target)).id : undefined
const credentials = (yield* request((signal) => client.credential.list({ signal }))).filter(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/handlers/auth/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default Runtime.handler(
return yield* Effect.fail(new Error("Pipe auth export output into stdin or pass a file to import"))
const text = yield* request(() => (file ? Bun.file(file).text() : readStdin()))
const credentials = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Credential.Entry)))(text)
const client = yield* createClient({ server: Option.getOrUndefined(input.server), standalone: input.standalone })
const client = yield* createClient()
const existing = yield* request((signal) => client.credential.list({ signal }))
const ids = new Set(existing.map((credential) => credential.id))
const integrations = new Set(existing.map((credential) => credential.integrationID))
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/auth/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EOL } from "node:os"
import { Effect, Option } from "effect"
import { Effect } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { createClient, loadIntegrations } from "./shared"
Expand All @@ -17,7 +17,7 @@ export default Runtime.handler(Commands.commands.auth.commands.list, (input) =>
)

const list = Effect.fn("cli.auth.list")(function* (input) {
const client = yield* createClient({ server: Option.getOrUndefined(input.server), standalone: input.standalone })
const client = yield* createClient()
const integrations = (yield* loadIntegrations(client)).filter((integration) => integration.connections.length > 0)
if (input.format === "json") {
process.stdout.write(
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/commands/handlers/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export default Runtime.handler(
target: Option.getOrUndefined(input.target),
method: Option.getOrUndefined(input.method),
answer: input.answer,
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
}).pipe(handlePromptErrors),
),
)
Expand All @@ -43,13 +41,11 @@ const login = Effect.fn("cli.auth.login.run")(function* (input: {
target?: string
method?: string
answer?: ReadonlyArray<string>
server?: string
standalone: boolean
}) {
if (!input.target)
yield* requireInteractive("Pass an integration ID or name when running without an interactive terminal")
intro("Connect an integration")
const client = yield* createClient({ server: input.server, standalone: input.standalone })
const client = yield* createClient()
const integration = yield* findIntegration(client, input.target)
const methods = connectMethods(integration)
if (methods.length === 0) yield* Effect.fail(new Error(`${integration.name} has no interactive login methods`))
Expand Down
11 changes: 2 additions & 9 deletions packages/cli/src/commands/handlers/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,17 @@ export default Runtime.handler(
logout({
target: Option.getOrUndefined(input.target),
credential: Option.getOrUndefined(input.credential),
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
}).pipe(handlePromptErrors),
),
)

const logout = Effect.fn("cli.auth.logout.run")(function* (input: {
target?: string
credential?: string
server?: string
standalone: boolean
}) {
const logout = Effect.fn("cli.auth.logout.run")(function* (input: { target?: string; credential?: string }) {
if (!input.target)
yield* requireInteractive("Pass an integration ID or name when running without an interactive terminal")
if (!input.credential)
yield* requireInteractive("Pass a credential ID or label when running without an interactive terminal")
intro("Log out of an account")
const client = yield* createClient({ server: input.server, standalone: input.standalone })
const client = yield* createClient()
const integrations = yield* loadIntegrations(client)
const integration = yield* chooseIntegration(integrations, input.target)
const credentialID = yield* chooseCredential(integration, "log out", input.credential)
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/handlers/auth/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ServerConnection } from "../../../services/server-connection"

export const location = { directory: process.cwd() }

export const createClient = Effect.fn("cli.auth.client")(function* (input: ServerConnection.Args) {
export const createClient = Effect.fn("cli.auth.client")(function* (input: ServerConnection.Args = {}) {
const server = yield* ServerConnection.resolve(input)
return OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) })
})
Expand Down
11 changes: 2 additions & 9 deletions packages/cli/src/commands/handlers/auth/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,17 @@ export default Runtime.handler(
switchAccount({
target: Option.getOrUndefined(input.target),
credential: Option.getOrUndefined(input.credential),
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
}).pipe(handlePromptErrors),
),
)

const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: {
target?: string
credential?: string
server?: string
standalone: boolean
}) {
const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: { target?: string; credential?: string }) {
if (!input.target)
yield* requireInteractive("Pass an integration ID or name when running without an interactive terminal")
if (!input.credential)
yield* requireInteractive("Pass a credential ID or label when running without an interactive terminal")
intro("Switch account")
const client = yield* createClient({ server: input.server, standalone: input.standalone })
const client = yield* createClient()
const integrations = yield* loadIntegrations(client)
const integration = yield* chooseIntegration(integrations, input.target)
const credentialID = yield* chooseCredential(integration, "switch to", input.credential)
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/commands/handlers/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Runtime } from "../../framework/runtime"
import { Config } from "../../config"
import { Context, Effect, FileSystem, Option, Queue, Schedule, Semaphore } from "effect"
import { ServerConnection } from "../../services/server-connection"
import { ServerFlags } from "../../services/server-flags"
import { Updater } from "../../services/updater"
import { UpdatePreflight } from "../../services/update-preflight"
import { Npm } from "@opencode/util/npm"
Expand All @@ -19,7 +20,7 @@ import { errorMessage } from "../../util/error"
export default Runtime.handler(Commands, (input) =>
Effect.gen(function* () {
const requestedDirectory = Option.getOrUndefined(input.directory)
const requestedServer = Option.getOrUndefined(input.server)
const requestedServer = (yield* ServerFlags.read()).server
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
Expand All @@ -33,8 +34,6 @@ export default Runtime.handler(Commands, (input) =>
Effect.forkScoped,
)
const server = yield* ServerConnection.resolve({
server: requestedServer,
standalone: input.standalone,
mismatch: "replace",
onStart: (reason, previousVersion) => {
Queue.offerUnsafe(serviceStarts, { reason, previousVersion })
Expand All @@ -56,7 +55,10 @@ export default Runtime.handler(Commands, (input) =>
session !== undefined &&
(yield* Effect.tryPromise({
try: () =>
findSession(OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) }), session),
findSession(
OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) }),
session,
),
catch: (cause) => new Error(errorMessage(cause)),
})) !== undefined
const updater = yield* Updater.Service
Expand Down
3 changes: 0 additions & 3 deletions packages/cli/src/commands/handlers/mini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ export default Runtime.handler(Commands.commands.mini, (input) =>
Effect.gen(function* () {
const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini"))
yield* Effect.promise(async () => validateMiniTerminal())
const serverURL = Option.getOrUndefined(input.server)
const server = yield* ServerConnection.resolve({
server: serverURL,
standalone: input.standalone,
mismatch: "replace",
})
const config = yield* Config.Service
Expand Down
Loading
Loading