diff --git a/bun.lock b/bun.lock index 2b37d21ccd..4614763ee0 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,9 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", "@types/mime-types": "3.0.1", + "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", diff --git a/github/tsconfig.json b/github/tsconfig.json index bfa0fead54..d74dc69847 100644 --- a/github/tsconfig.json +++ b/github/tsconfig.json @@ -1,29 +1,8 @@ { + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + "types": ["node", "bun"] + }, + "include": ["./index.ts", "./sst-env.d.ts"] } diff --git a/nix/scripts/tsconfig.json b/nix/scripts/tsconfig.json new file mode 100644 index 0000000000..989b013a78 --- /dev/null +++ b/nix/scripts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "types": ["node", "bun"] + }, + "include": ["./**/*.ts"] +} diff --git a/package.json b/package.json index cc2d3f4c21..47fba5be15 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,9 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", "@types/mime-types": "3.0.1", + "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index ec0614729c..b0db4effb3 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -10,6 +10,8 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useGlobalSync } from "@/context/global-sync" +import { useGlobalSDK } from "@/context/global-sdk" import { monoDefault, monoFontFamily, @@ -133,6 +135,21 @@ export const SettingsGeneral: Component = () => { const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) + const globalSync = useGlobalSync() + const globalSdk = useGlobalSDK() + + const [shells] = createResource(() => globalSdk.client.pty.shells().then((res) => res.data || [])) + + const shellOptions = createMemo(() => { + const list = shells() || [] + const current = globalSync.data.config.shell + const options = list.map((s) => ({ value: s, label: s })) + if (current && !list.includes(current)) { + options.unshift({ value: current, label: current }) + } + return options + }) + const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ { value: "system", label: language.t("theme.scheme.system") }, { value: "light", label: language.t("theme.scheme.light") }, @@ -206,6 +223,32 @@ export const SettingsGeneral: Component = () => { /> + + o.id === theme.themeId())} - value={(o) => o.id} - label={(o) => o.name} + data-action="settings-shell" + options={shellOptions()} + current={ + shellOptions().find((o) => o.value === globalSync.data.config.shell) ?? { + value: "auto", + label: "Auto (Default)", + } + } + value={(o) => o.value} + label={(o) => o.label} onSelect={(option) => { - if (!option) return - theme.setTheme(option.id) - }} - onHighlight={(option) => { - if (!option) return - theme.previewTheme(option.id) - return () => theme.cancelPreview() + const value = option?.value === "auto" ? undefined : option?.value + globalSync.updateConfig({ shell: value }) }} variant="secondary" size="small" triggerVariant="settings" + triggerStyle={{ "min-width": "180px" }} /> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 39317b8d65..2ac72ae9ad 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -723,6 +723,9 @@ export const dict = { "settings.general.row.language.title": "Language", "settings.general.row.language.description": "Change the display language for OpenCode", + "settings.general.row.shell.title": "Terminal Shell", + "settings.general.row.shell.description": + "Choose the default shell used for your terminal and the agent's background processes.", "settings.general.row.appearance.title": "Appearance", "settings.general.row.appearance.description": "Customise how OpenCode looks on your device", "settings.general.row.colorScheme.title": "Color scheme", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 27618a3c36..8fe989705e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -803,6 +803,7 @@ export namespace Config { export const Info = z .object({ $schema: z.string().optional().describe("JSON schema reference for configuration validation"), + shell: z.string().optional().describe("Default shell to use for terminal and bash tool"), logLevel: Log.Level.optional().describe("Log level"), server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 72089d8441..98c3c60aaa 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -11,6 +11,7 @@ import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" import { Effect, Layer, ServiceMap } from "effect" +import { Config } from "../config/config" export namespace Pty { const log = Log.create({ service: "pty" }) @@ -172,9 +173,10 @@ export namespace Pty { const create = Effect.fn("Pty.create")(function* (input: CreateInput) { const s = yield* InstanceState.get(state) + const config = yield* Effect.promise(() => Config.get()) return yield* Effect.promise(async () => { const id = PtyID.ascending() - const command = input.command || Shell.preferred() + const command = input.command || Shell.preferred(config.shell) const args = input.args || [] if (Shell.login(command)) { args.push("-l") diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts index de79801e28..530ef67cf4 100644 --- a/packages/opencode/src/server/routes/pty.ts +++ b/packages/opencode/src/server/routes/pty.ts @@ -7,9 +7,31 @@ import { PtyID } from "@/pty/schema" import { NotFoundError } from "../../storage/db" import { errors } from "../error" import { lazy } from "../../util/lazy" +import { Shell } from "@/shell/shell" export const PtyRoutes = lazy(() => new Hono() + .get( + "/shells", + describeRoute({ + summary: "List available shells", + description: "Get a list of available shells on the system.", + operationId: "pty.shells", + responses: { + 200: { + description: "List of shells", + content: { + "application/json": { + schema: resolver(z.array(z.string())), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await Shell.available()) + }, + ) .get( "/", describeRoute({ diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index df8e8eb7eb..41f294b945 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -104,7 +104,33 @@ export namespace Shell { return POSIX.has(name(file)) } - export const preferred = lazy(() => select(process.env.SHELL)) + const defaultPreferred = lazy(() => select(process.env.SHELL)) + const defaultAcceptable = lazy(() => select(process.env.SHELL, { acceptable: true })) - export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true })) + export function preferred(configShell?: string) { + if (configShell) return select(configShell) + return defaultPreferred() + } + preferred.reset = () => defaultPreferred.reset() + + export function acceptable(configShell?: string) { + if (configShell) return select(configShell, { acceptable: true }) + return defaultAcceptable() + } + acceptable.reset = () => defaultAcceptable.reset() + + export async function available(): Promise { + if (process.platform === "win32") { + return [gitbash(), Bun.which("pwsh"), Bun.which("powershell"), process.env.COMSPEC || "cmd.exe"].filter( + Boolean, + ) as string[] + } else { + try { + const text = await import("fs/promises").then((fs) => fs.readFile("/etc/shells", "utf-8")) + return text.split("\n").filter((line) => line.trim() && !line.startsWith("#")) + } catch { + return ["/bin/bash", "/bin/zsh", "/bin/sh"] + } + } + } } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 50aa9e14ad..3e19fc2ab9 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -18,6 +18,7 @@ import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" import { Truncate } from "./truncate" import { Plugin } from "@/plugin" +import { Config } from "../config/config" const MAX_METADATA_LENGTH = 30_000 const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 @@ -441,7 +442,8 @@ const parser = lazy(async () => { // TODO: we may wanna rename this tool so it works better on other shells export const BashTool = Tool.define("bash", async () => { - const shell = Shell.acceptable() + const config = await Config.get() + const shell = Shell.acceptable(config.shell) const name = Shell.name(shell) const chain = name === "powershell" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 527584e7e2..5d4964df03 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -99,6 +99,7 @@ import type { PtyListResponses, PtyRemoveErrors, PtyRemoveResponses, + PtyShellsResponses, PtyUpdateErrors, PtyUpdateResponses, QuestionAnswer, @@ -663,6 +664,36 @@ export class Project extends HeyApiClient { } export class Pty extends HeyApiClient { + /** + * List available shells + * + * Get a list of available shells on the system. + */ + public shells( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/shells", + ...options, + ...params, + }) + } + /** * List PTY sessions * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 290c6fd5ec..73c83618ac 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1417,6 +1417,10 @@ export type Config = { * JSON schema reference for configuration validation */ $schema?: string + /** + * Default shell to use for terminal and bash tool + */ + shell?: string logLevel?: LogLevel server?: ServerConfig /** @@ -2400,6 +2404,25 @@ export type ProjectUpdateResponses = { export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] +export type PtyShellsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty/shells" +} + +export type PtyShellsResponses = { + /** + * List of shells + */ + 200: Array +} + +export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses] + export type PtyListData = { body?: never path?: never diff --git a/script/tsconfig.json b/script/tsconfig.json new file mode 100644 index 0000000000..989b013a78 --- /dev/null +++ b/script/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "types": ["node", "bun"] + }, + "include": ["./**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 65fa6c7f31..d01da02700 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,8 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", - "compilerOptions": {} + "compilerOptions": { + "types": ["node"] + }, + "include": ["sst.config.ts", "sst-env.d.ts", "infra/**/*.ts"] }