diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100755 index 00000000..400f3390 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "Opencode Dev Environment", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "customizations": { + "vscode": { + "extensions": [ + "Oven.bun-vscode" + ] + } + }, + "postCreateCommand": "curl -fsSL https://bun.sh/install | bash", + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 47550938..d1c7c07d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -11,6 +11,7 @@ }, "scripts": { "typecheck": "tsgo -b", + "test": "bun test src/main/sidecar-command.test.ts", "predev": "bun ./scripts/predev.ts", "dev": "electron-vite dev", "prebuild": "bun ./scripts/prebuild.ts", diff --git a/packages/desktop/src/main/sidecar-command.test.ts b/packages/desktop/src/main/sidecar-command.test.ts new file mode 100644 index 00000000..7e801dfd --- /dev/null +++ b/packages/desktop/src/main/sidecar-command.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test" +import { parseCommand } from "./sidecar-command" + + +describe("parseCommand", () => { + const valid = { + type: "start", + hostname: "127.0.0.1", + port: 4096, + password: "secret", + userDataPath: "/tmp/data", + } + + test("parses a valid start command", () => { + expect(parseCommand(valid)).toEqual(valid) + }) + + test("parses a stop command", () => { + expect(parseCommand({ type: "stop" })).toEqual({ type: "stop" }) + }) + + test("ignores extra fields on a start command", () => { + expect(parseCommand({ ...valid, extra: "nope" })).toEqual(valid) + }) + + test("rejects non-object values", () => { + expect(parseCommand(undefined)).toBeUndefined() + expect(parseCommand(null)).toBeUndefined() + expect(parseCommand("start")).toBeUndefined() + expect(parseCommand(42)).toBeUndefined() + }) + + test("rejects unknown command types", () => { + expect(parseCommand({ type: "restart" })).toBeUndefined() + expect(parseCommand({})).toBeUndefined() + }) + + test("rejects a start command with a missing field", () => { + for (const key of ["hostname", "port", "password", "userDataPath"]) { + const { [key]: _, ...partial } = valid as Record + expect(parseCommand(partial)).toBeUndefined() + } + }) + + test("rejects a start command with wrong field types", () => { + expect(parseCommand({ ...valid, port: "4096" })).toBeUndefined() + expect(parseCommand({ ...valid, hostname: 123 })).toBeUndefined() + expect(parseCommand({ ...valid, password: null })).toBeUndefined() + expect(parseCommand({ ...valid, userDataPath: [] })).toBeUndefined() + }) +}) \ No newline at end of file diff --git a/packages/desktop/src/main/sidecar-command.ts b/packages/desktop/src/main/sidecar-command.ts new file mode 100644 index 00000000..78fc8a1f --- /dev/null +++ b/packages/desktop/src/main/sidecar-command.ts @@ -0,0 +1,25 @@ +export type StartCommand = { + type: "start" + hostname: string + port: number + password: string + userDataPath: string +} +export type StopCommand = { type: "stop" } +export type SidecarCommand = StartCommand | StopCommand + +export function parseCommand(value: unknown): SidecarCommand | undefined { + if (!value || typeof value !== "object") return + const command = value as Partial + if (command.type === "stop") return { type: "stop" } + if (command.type !== "start") return + const { hostname, port, password, userDataPath } = command + if ( + typeof hostname !== "string" || + typeof port !== "number" || + typeof password !== "string" || + typeof userDataPath !== "string" + ) + return + return { type: "start", hostname, port, password, userDataPath } +} \ No newline at end of file diff --git a/packages/desktop/src/main/sidecar.ts b/packages/desktop/src/main/sidecar.ts index 246871fb..2b9e980d 100644 --- a/packages/desktop/src/main/sidecar.ts +++ b/packages/desktop/src/main/sidecar.ts @@ -1,5 +1,6 @@ import * as http from "node:http" import * as tls from "node:tls" +import { parseCommand, type StartCommand } from "./sidecar-command" type NodeHttpWithEnvProxy = typeof http & { setGlobalProxyFromEnv: () => void @@ -9,18 +10,6 @@ type NodeTlsWithSystemCertificates = typeof tls & { getCACertificates: (type: "default" | "system") => string[] setDefaultCACertificates: (certificates: string[]) => void } - -type StartCommand = { - type: "start" - hostname: string - port: number - password: string - userDataPath: string -} - -type StopCommand = { type: "stop" } -type SidecarCommand = StartCommand | StopCommand - type SidecarMessage = | { type: "ready" } | { type: "stopped" } @@ -127,23 +116,6 @@ function useEnvProxy() { } } -function parseCommand(value: unknown): SidecarCommand | undefined { - if (!value || typeof value !== "object") return - const command = value as Partial - if (command.type === "stop") return { type: "stop" } - if (command.type !== "start") return - if (typeof command.hostname !== "string") return - if (typeof command.port !== "number") return - if (typeof command.password !== "string") return - if (typeof command.userDataPath !== "string") return - return { - type: "start", - hostname: command.hostname, - port: command.port, - password: command.password, - userDataPath: command.userDataPath, - } -} function serializeError(error: unknown) { if (error instanceof Error) return { message: error.message, stack: error.stack } diff --git a/turbo.json b/turbo.json index f59711fa..d11249a6 100644 --- a/turbo.json +++ b/turbo.json @@ -42,6 +42,10 @@ "@opencode-ai/session-ui#test": { "dependsOn": ["^build"], "outputs": [] - } + }, + "@opencode-ai/desktop#test": { + "dependsOn": ["^build"], + "outputs": [] } } +}