diff --git a/package.json b/package.json index 0810127669f..9da29ab6bab 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", - "lint:effect-patterns": "ast-grep scan --rule script/ast-grep/no-json-parse-cast.yml packages/core/src packages/server/src packages/protocol/src && ast-grep scan --rule script/ast-grep/no-nested-effect-service-yield.yml packages/core/src packages/server/src packages/protocol/src", + "lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src", + "test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml", "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", diff --git a/packages/cli/src/commands/handlers/debug/agents.ts b/packages/cli/src/commands/handlers/debug/agents.ts index ee241bcc1bb..1dbaf4d8013 100644 --- a/packages/cli/src/commands/handlers/debug/agents.ts +++ b/packages/cli/src/commands/handlers/debug/agents.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/mcp/list.ts b/packages/cli/src/commands/handlers/mcp/list.ts index 167509fb700..2d38a6aca01 100644 --- a/packages/cli/src/commands/handlers/mcp/list.ts +++ b/packages/cli/src/commands/handlers/mcp/list.ts @@ -1,5 +1,5 @@ import { EOL } from "node:os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/migrate.ts b/packages/cli/src/commands/handlers/migrate.ts index c73c7750df0..6ff6939aa1c 100644 --- a/packages/cli/src/commands/handlers/migrate.ts +++ b/packages/cli/src/commands/handlers/migrate.ts @@ -1,4 +1,4 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/serve.ts b/packages/cli/src/commands/handlers/serve.ts index 11483189c3d..8a6b1c2cebd 100644 --- a/packages/cli/src/commands/handlers/serve.ts +++ b/packages/cli/src/commands/handlers/serve.ts @@ -4,8 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Global } from "@opencode-ai/core/global" -import { Context, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect" -import * as Effect from "effect/Effect" +import { Context, Effect, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect" import { HttpRouter, HttpServer } from "effect/unstable/http" import { createServer } from "node:http" import { createRoutes } from "@opencode-ai/server/routes" @@ -60,7 +59,7 @@ export default Runtime.handler( if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`) const updater = yield* Updater.Service yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped) - return yield* (input.stdio ? waitForStdinClose() : Effect.never) + return yield* input.stdio ? waitForStdinClose() : Effect.never }).pipe(Effect.annotateLogs({ role: "server" })), ) }), diff --git a/packages/cli/src/commands/handlers/service/get.ts b/packages/cli/src/commands/handlers/service/get.ts index 1b85faf6e9e..fd4b9af3844 100644 --- a/packages/cli/src/commands/handlers/service/get.ts +++ b/packages/cli/src/commands/handlers/service/get.ts @@ -1,6 +1,5 @@ import { EOL } from "os" -import { Option } from "effect" -import * as Effect from "effect/Effect" +import { Effect, Option } from "effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" import { ServiceConfig } from "../../../services/service-config" diff --git a/packages/cli/src/commands/handlers/service/restart.ts b/packages/cli/src/commands/handlers/service/restart.ts index c78273ed0f4..93b8836acd3 100644 --- a/packages/cli/src/commands/handlers/service/restart.ts +++ b/packages/cli/src/commands/handlers/service/restart.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Service } from "@opencode-ai/client/effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/service/set.ts b/packages/cli/src/commands/handlers/service/set.ts index 6ecde18d41f..f761c02411f 100644 --- a/packages/cli/src/commands/handlers/service/set.ts +++ b/packages/cli/src/commands/handlers/service/set.ts @@ -1,4 +1,4 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" import { ServiceConfig } from "../../../services/service-config" diff --git a/packages/cli/src/commands/handlers/service/start.ts b/packages/cli/src/commands/handlers/service/start.ts index a3a7200bf95..602a26ecf18 100644 --- a/packages/cli/src/commands/handlers/service/start.ts +++ b/packages/cli/src/commands/handlers/service/start.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Service } from "@opencode-ai/client/effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/service/status.ts b/packages/cli/src/commands/handlers/service/status.ts index 807d7d749dc..bf58968eefb 100644 --- a/packages/cli/src/commands/handlers/service/status.ts +++ b/packages/cli/src/commands/handlers/service/status.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Service } from "@opencode-ai/client/effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/service/stop.ts b/packages/cli/src/commands/handlers/service/stop.ts index 53ad3615c3c..5bf45ccabc5 100644 --- a/packages/cli/src/commands/handlers/service/stop.ts +++ b/packages/cli/src/commands/handlers/service/stop.ts @@ -1,4 +1,4 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Service } from "@opencode-ai/client/effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/service/unset.ts b/packages/cli/src/commands/handlers/service/unset.ts index ef5fdf93307..cc738125d38 100644 --- a/packages/cli/src/commands/handlers/service/unset.ts +++ b/packages/cli/src/commands/handlers/service/unset.ts @@ -1,4 +1,4 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" import { ServiceConfig } from "../../../services/service-config" diff --git a/packages/cli/src/framework/runtime.ts b/packages/cli/src/framework/runtime.ts index 902c89d716b..2108da415db 100644 --- a/packages/cli/src/framework/runtime.ts +++ b/packages/cli/src/framework/runtime.ts @@ -1,9 +1,8 @@ -import * as Effect from "effect/Effect" -import * as Command from "effect/unstable/cli/Command" +import { Effect, FileSystem, Scope } from "effect" +import { Command } from "effect/unstable/cli" import { Spec } from "./spec" import { Global } from "@opencode-ai/core/global" import { Updater } from "../services/updater" -import { FileSystem, Scope } from "effect" export type Input = Value extends Spec.Node @@ -12,11 +11,21 @@ export type Input = ? Input : never -type RuntimeHandler = (input: unknown) => Effect.Effect +type RuntimeHandler = ( + input: unknown, +) => Effect.Effect type Loader = () => Promise<{ - default: (input: Input) => Effect.Effect + default: ( + input: Input, + ) => Effect.Effect }> -type ProvidedCommand = Command.Command +type ProvidedCommand = Command.Command< + string, + unknown, + unknown, + unknown, + FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope +> export type Handlers = keyof Node["commands"] extends never ? Loader diff --git a/packages/cli/src/framework/spec.ts b/packages/cli/src/framework/spec.ts index 3bb47e5e5ed..345a0cfa85e 100644 --- a/packages/cli/src/framework/spec.ts +++ b/packages/cli/src/framework/spec.ts @@ -1,4 +1,4 @@ -import * as Command from "effect/unstable/cli/Command" +import { Command } from "effect/unstable/cli" type Options> = { readonly description?: string diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 071fd37e1a6..e515424183c 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,10 +1,7 @@ #!/usr/bin/env bun -import * as NodeRuntime from "@effect/platform-node/NodeRuntime" -import * as NodeServices from "@effect/platform-node/NodeServices" -import { NodeFileSystem } from "@effect/platform-node" -import * as Effect from "effect/Effect" -import { Layer, Logger, References } from "effect" +import { NodeFileSystem, NodeRuntime, NodeServices } from "@effect/platform-node" +import { Effect, Layer, Logger, References } from "effect" import { Commands } from "./commands/commands" import { Runtime } from "./framework/runtime" import { Logging } from "@opencode-ai/core/observability/logging" @@ -46,7 +43,11 @@ const Handlers = Runtime.handlers(Commands, { serve: () => import("./commands/handlers/serve"), }) -Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe( +Effect.logInfo("cli starting", { + version: InstallationVersion, + channel: InstallationChannel, + local: InstallationLocal, +}).pipe( Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })), Effect.annotateLogs({ role: "cli" }), Effect.provide(Updater.layer), diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts index d364d6f3448..7e81780e95b 100644 --- a/packages/core/src/account.ts +++ b/packages/core/src/account.ts @@ -1,7 +1,7 @@ export * as AccountV2 from "./account" import { Schema } from "effect" -import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type { HttpClientError } from "effect/unstable/http" export const ID = Schema.String.pipe(Schema.brand("AccountID")) export type ID = Schema.Schema.Type diff --git a/packages/core/src/config/experimental.ts b/packages/core/src/config/experimental.ts index 12a02635db6..8b38a225b4c 100644 --- a/packages/core/src/config/experimental.ts +++ b/packages/core/src/config/experimental.ts @@ -2,17 +2,19 @@ export * as ConfigExperimental from "./experimental" import { Schema } from "effect" import { Catalog } from "../catalog" -import { Policy as PolicyV2 } from "../policy" +import { Policy } from "../policy" // Each core domain exports the policy actions it supports. Adding an action to // this union makes it valid in authored config while keeping Policy generic. export const PolicyAction = Schema.Union([Catalog.PolicyActions]) -export class Policy extends Schema.Class("ConfigV2.Experimental.Policy")({ - ...PolicyV2.Info.fields, +class PolicyConfig extends Schema.Class("ConfigV2.Experimental.Policy")({ + ...Policy.Info.fields, action: PolicyAction, }) {} +export { PolicyConfig as Policy } + export class Experimental extends Schema.Class("ConfigV2.Experimental")({ - policies: Policy.pipe(Schema.Array, Schema.optional), + policies: PolicyConfig.pipe(Schema.Array, Schema.optional), }) {} diff --git a/packages/core/src/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts index 6ea9022acf1..f6a04ffd932 100644 --- a/packages/core/src/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -1,26 +1,17 @@ -import type * as Arr from "effect/Array" -import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node" -import * as NodePath from "@effect/platform-node/NodePath" -import * as Deferred from "effect/Deferred" -import * as Effect from "effect/Effect" -import * as Exit from "effect/Exit" -import * as FileSystem from "effect/FileSystem" -import * as Layer from "effect/Layer" -import * as Path from "effect/Path" -import * as PlatformError from "effect/PlatformError" -import * as Predicate from "effect/Predicate" -import type * as Scope from "effect/Scope" -import * as Sink from "effect/Sink" -import * as Stream from "effect/Stream" -import * as ChildProcess from "effect/unstable/process/ChildProcess" -import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner" +import type { NonEmptyReadonlyArray } from "effect/Array" +import { NodeFileSystem, NodePath, NodeSink, NodeStream } from "@effect/platform-node" +import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect" +import type { Scope } from "effect" +import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner, ExitCode, - make as makeSpawner, + make, makeHandle, ProcessId, + type ChildProcessHandle, } from "effect/unstable/process/ChildProcessSpawner" +// ast-grep-ignore: no-star-import import * as NodeChildProcess from "node:child_process" import { PassThrough } from "node:stream" import launch from "cross-spawn" @@ -71,7 +62,7 @@ const flatten = (command: ChildProcess.Command) => { if (commands.length === 0) throw new Error("flatten produced empty commands array") const [head, ...tail] = commands return { - commands: [head, ...tail] as Arr.NonEmptyReadonlyArray, + commands: [head, ...tail] as NonEmptyReadonlyArray, opts, } } @@ -96,7 +87,7 @@ const toPlatformError = ( type ExitSignal = Deferred.Deferred -export const make = Effect.gen(function* () { +const makeCrossSpawnSpawner = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path @@ -494,12 +485,12 @@ export const make = Effect.gen(function* () { }, ) - return makeSpawner(spawnCommand) + return make(spawnCommand) }) const layer: Layer.Layer = Layer.effect( ChildProcessSpawner, - make, + makeCrossSpawnSpawner, ) export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] }) diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index d61adf047ea..f3cc8b3f9f9 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -1,7 +1,7 @@ export * as Database from "./database" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" -import { layer as sqliteLayer } from "#sqlite" +import { layer } from "#sqlite" import { Context, Effect, Layer } from "effect" import { Global } from "../global" import { Flag } from "../flag/flag" @@ -19,7 +19,7 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/storage/Database") {} -const layer = Layer.effect( +const databaseLayer = Layer.effect( Service, Effect.gen(function* () { const db = yield* makeDatabase @@ -37,7 +37,7 @@ const layer = Layer.effect( ) export function layerFromPath(filename: string) { - return layer.pipe(Layer.provide(sqliteLayer({ filename }))) + return databaseLayer.pipe(Layer.provide(layer({ filename }))) } export function path() { diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index 90dee8acbf3..17bf0a5c44e 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -22,7 +22,7 @@ export function apply(db: Database) { sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`, ) if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations) - if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table") + if (tables.length > 0) return yield* Effect.die(new Error("Database is not empty and has no session table")) yield* db.transaction((tx) => Effect.gen(function* () { yield* schema.up(tx) diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts index e15f4c117e4..bbb3ad6143e 100644 --- a/packages/core/src/database/sqlite.bun.ts +++ b/packages/core/src/database/sqlite.bun.ts @@ -1,18 +1,11 @@ import { Database } from "bun:sqlite" import { drizzle } from "drizzle-orm/bun-sqlite" -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import * as Fiber from "effect/Fiber" +import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect" import { identity } from "effect/Function" -import * as Layer from "effect/Layer" -import * as Scope from "effect/Scope" -import * as Semaphore from "effect/Semaphore" -import * as Stream from "effect/Stream" -import * as Reactivity from "effect/unstable/reactivity/Reactivity" -import * as Client from "effect/unstable/sql/SqlClient" +import { Reactivity } from "effect/unstable/reactivity" +import { SqlClient, Statement } from "effect/unstable/sql" import type { Connection } from "effect/unstable/sql/SqlConnection" import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" -import * as Statement from "effect/unstable/sql/Statement" import { Sqlite } from "./sqlite" const ATTR_DB_SYSTEM_NAME = "db.system.name" @@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name" const TypeId = "~@opencode-ai/core/database/SqliteBun" as const type TypeId = typeof TypeId -interface SqliteClient extends Client.SqlClient { +interface SqliteClient extends SqlClient.SqlClient { readonly [TypeId]: TypeId readonly config: Config readonly export: Effect.Effect @@ -57,7 +50,7 @@ const make = (options: Config) => Effect.withFiber>, SqlError>((fiber) => { const statement = native.query(query) // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 - statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers)) try { return Effect.succeed((statement.all(...(params as any)) ?? []) as Array>) } catch (cause) { @@ -73,7 +66,7 @@ const make = (options: Config) => Effect.withFiber, SqlError>((fiber) => { const statement = native.query(query) // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 - statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers)) try { return Effect.succeed((statement.values(...(params as any)) ?? []) as Array) } catch (cause) { @@ -130,7 +123,7 @@ const make = (options: Config) => }) const client = Object.assign( - (yield* Client.make({ + (yield* SqlClient.make({ acquirer, compiler, transactionAcquirer, @@ -166,7 +159,7 @@ const nativeLayer = (config: Config) => }), ) -const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) +const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config)) const drizzleLayer = Layer.effect( Sqlite.Drizzle, diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts index 6eaecbee26e..6fcbd76e4e7 100644 --- a/packages/core/src/database/sqlite.node.ts +++ b/packages/core/src/database/sqlite.node.ts @@ -1,18 +1,11 @@ import { DatabaseSync, type SQLInputValue } from "node:sqlite" import { drizzle } from "drizzle-orm/node-sqlite" -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import * as Fiber from "effect/Fiber" +import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect" import { identity } from "effect/Function" -import * as Layer from "effect/Layer" -import * as Scope from "effect/Scope" -import * as Semaphore from "effect/Semaphore" -import * as Stream from "effect/Stream" -import * as Reactivity from "effect/unstable/reactivity/Reactivity" -import * as Client from "effect/unstable/sql/SqlClient" +import { Reactivity } from "effect/unstable/reactivity" +import { SqlClient, Statement } from "effect/unstable/sql" import type { Connection } from "effect/unstable/sql/SqlConnection" import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" -import * as Statement from "effect/unstable/sql/Statement" import { Sqlite } from "./sqlite" const ATTR_DB_SYSTEM_NAME = "db.system.name" @@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name" const TypeId = "~@opencode-ai/core/database/SqliteNode" as const type TypeId = typeof TypeId -interface SqliteClient extends Client.SqlClient { +interface SqliteClient extends SqlClient.SqlClient { readonly [TypeId]: TypeId readonly config: Config readonly loadExtension: (path: string) => Effect.Effect @@ -56,7 +49,7 @@ const make = (options: Config) => const run = (query: string, params: ReadonlyArray = []) => Effect.withFiber>, SqlError>((fiber) => { const statement = native.prepare(query) - statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers)) try { return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array>) } catch (cause) { @@ -71,7 +64,7 @@ const make = (options: Config) => const runValues = (query: string, params: ReadonlyArray = []) => Effect.withFiber>, SqlError>((fiber) => { const statement = native.prepare(query) - statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers)) statement.setReturnArrays(true) try { return Effect.succeed( @@ -124,7 +117,7 @@ const make = (options: Config) => }) const client = Object.assign( - (yield* Client.make({ + (yield* SqlClient.make({ acquirer, compiler, transactionAcquirer, @@ -161,7 +154,7 @@ const nativeLayer = (config: Config) => }), ) -const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) +const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config)) const drizzleLayer = Layer.effect( Sqlite.Drizzle, diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index 47a72ed23a4..d49129b3b81 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -98,10 +98,14 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const events = yield* EventV2.Service - const forms = yield* Cache.makeWith(() => Effect.die("Form cache must be used via set/getSuccess, never get"), { - capacity: Number.MAX_SAFE_INTEGER, - timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION), - }) + const forms = yield* Cache.makeWith( + () => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")), + { + capacity: Number.MAX_SAFE_INTEGER, + timeToLive: (exit) => + Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION, + }, + ) const find = Effect.fn("Form.find")(function* (id: ID) { return yield* Cache.getSuccess(forms, id).pipe( @@ -131,7 +135,9 @@ export const layer = Layer.effect( ...(input.metadata === undefined ? {} : { metadata: input.metadata }), } const form: Info = - input.mode === "form" ? { ...base, mode: "form", fields: input.fields } : { ...base, mode: "url", url: input.url } + input.mode === "form" + ? { ...base, mode: "form", fields: input.fields } + : { ...base, mode: "url", url: input.url } const entry: Entry = { form, state: { status: "pending" }, @@ -149,7 +155,9 @@ export const layer = Layer.effect( Effect.gen(function* () { const form = yield* create(input) const entry = yield* find(form.id).pipe(Effect.orDie) - return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id)))) + return yield* restore(Deferred.await(entry.deferred)).pipe( + Effect.onInterrupt(() => Effect.ignore(cancel(form.id))), + ) }), ), ) @@ -301,7 +309,8 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined return `Form field has invalid pattern: ${field.key}` } } - if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}` + if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) + return `Expected email for form field: ${field.key}` if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}` if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}` if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}` @@ -324,8 +333,10 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined if (field.type === "multiselect") { if (!isStringArray(value)) return `Expected string array for form field: ${field.key}` if (field.required && value.length === 0) return `Missing required form field: ${field.key}` - if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}` - if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}` + if (field.minItems !== undefined && value.length < field.minItems) + return `Too few selections for form field: ${field.key}` + if (field.maxItems !== undefined && value.length > field.maxItems) + return `Too many selections for form field: ${field.key}` if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) { return `Invalid option for form field: ${field.key}` } diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index 124ac6d702e..b6edce6657d 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -1,7 +1,7 @@ import { NodeFileSystem } from "@effect/platform-node" -import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path" +import path, { dirname, isAbsolute, join, relative, sep } from "path" import { realpathSync } from "fs" -import * as NFS from "fs/promises" +import { readdir } from "fs/promises" import { lookup } from "mime-types" import { Context, Effect, FileSystem, Layer, Schema } from "effect" import type { PlatformError } from "effect/PlatformError" @@ -78,7 +78,7 @@ export namespace FSUtil { const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) { return yield* Effect.tryPromise({ try: async () => { - const entries = await NFS.readdir(dirPath, { withFileTypes: true }) + const entries = await readdir(dirPath, { withFileTypes: true }) return entries.map( (e): DirEntry => ({ name: e.name, @@ -90,8 +90,8 @@ export namespace FSUtil { }) }) - const resolve = Effect.fn("FileSystem.resolve")(function* (path: string) { - const resolved = pathResolve(windowsPath(path)) + const resolve = Effect.fn("FileSystem.resolve")(function* (input: string) { + const resolved = path.resolve(windowsPath(input)) return yield* fs.realPath(resolved).pipe( Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)), Effect.orDie, @@ -219,7 +219,7 @@ export namespace FSUtil { export function normalizePath(p: string): string { if (process.platform !== "win32") return p - const resolved = pathResolve(windowsPath(p)) + const resolved = path.resolve(windowsPath(p)) try { return realpathSync.native(resolved) } catch { @@ -237,7 +237,7 @@ export namespace FSUtil { } export function resolve(p: string): string { - const resolved = pathResolve(windowsPath(p)) + const resolved = path.resolve(windowsPath(p)) try { return normalizePath(realpathSync(resolved)) } catch (e: any) { diff --git a/packages/core/src/id/id.ts b/packages/core/src/id/id.ts index be1efc446a6..b4d6e496ea6 100644 --- a/packages/core/src/id/id.ts +++ b/packages/core/src/id/id.ts @@ -1,4 +1,4 @@ -import { create as createIdentifier } from "@opencode-ai/schema/identifier" +import { create } from "@opencode-ai/schema/identifier" const prefixes = { job: "job", @@ -23,7 +23,7 @@ export function descending(prefix: keyof typeof prefixes, given?: string) { function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { if (!given) { - return create(prefixes[prefix], direction) + return createID(prefixes[prefix], direction) } if (!given.startsWith(prefixes[prefix])) { @@ -32,10 +32,12 @@ function generateID(prefix: keyof typeof prefixes, direction: "descending" | "as return given } -export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { - return prefix + "_" + createIdentifier(direction === "descending", timestamp) +function createID(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { + return prefix + "_" + create(direction === "descending", timestamp) } +export { createID as create } + /** Extract timestamp from an ascending ID. Does not work with descending IDs. */ export function timestamp(id: string): number { const prefix = id.split("_")[0] diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 9acbbe93eb8..12dad07dd34 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -406,7 +406,7 @@ const layer = Layer.effect( .get() .integrations.get(input.integrationID) ?.methods.some((method) => method.type === "key") - if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`) + if (!method) return yield* Effect.die(new Error(`Key method not found: ${input.integrationID}`)) yield* credentials.create({ integrationID: input.integrationID, label: input.label, @@ -418,7 +418,7 @@ const layer = Layer.effect( oauth: Effect.fn("Integration.connection.oauth")(function* (input) { const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID) if (!method) { - return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`) + return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`)) } const attemptScope = yield* Scope.fork(scope) const authorization = yield* authorize(method.authorize(input.inputs)).pipe( @@ -475,7 +475,7 @@ const layer = Layer.effect( attempt: { status: Effect.fn("Integration.attempt.status")(function* (attemptID) { const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) - if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`) + if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`)) if (attempt.status === "failed") { return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } } @@ -488,12 +488,13 @@ const layer = Layer.effect( if (match.authorization.mode === "code" && input.code === undefined) return [match, current] return [match, new Map(current).set(input.attemptID, { ...match, completing: true })] }) - if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`) + if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`)) if (attempt.status !== "pending") return if (attempt.authorization.mode === "code" && input.code === undefined) { return yield* new CodeRequiredError({ attemptID: input.attemptID }) } - if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`) + if (attempt.completing) + return yield* Effect.die(new Error(`OAuth attempt already completing: ${input.attemptID}`)) const callback = attempt.authorization.mode === "auto" ? attempt.authorization.callback diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 1af35e61a12..ffb7db2cf10 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -29,7 +29,7 @@ import { QuestionV2 } from "./question" import { Shell } from "./shell" import { Reference } from "./reference" import { ReferenceGuidance } from "./reference/guidance" -import * as SessionRunnerLLM from "./session/runner/llm" +import { SessionRunnerLLM } from "./session/runner/llm" import { SessionRunnerModel } from "./session/runner/model" import { SessionCompaction } from "./session/compaction" import { SessionTitle } from "./session/title" diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index 791d0a9bcd5..3a8dc12b25e 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -1,7 +1,7 @@ export * as PermissionV2 from "./permission" import { makeLocationNode } from "./effect/app-node" -import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Permission } from "@opencode-ai/schema/permission" import { EventV2 } from "./event" import { Location } from "./location" @@ -11,7 +11,9 @@ import { SessionStore } from "./session/store" import { Wildcard } from "./util/wildcard" import { PermissionSaved } from "./permission/saved" -export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" +const PermissionEffect = Permission.Effect +export { PermissionEffect as Effect } +export { Rule, Ruleset } from "@opencode-ai/schema/permission" const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] export const ID = Permission.ID @@ -90,12 +92,12 @@ export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset { } export interface Interface { - readonly ask: (input: AssertInput) => EffectRuntime.Effect - readonly assert: (input: AssertInput) => EffectRuntime.Effect - readonly reply: (input: ReplyInput) => EffectRuntime.Effect - readonly get: (id: ID) => EffectRuntime.Effect - readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect> - readonly list: () => EffectRuntime.Effect> + readonly ask: (input: AssertInput) => Effect.Effect + readonly assert: (input: AssertInput) => Effect.Effect + readonly reply: (input: ReplyInput) => Effect.Effect + readonly get: (id: ID) => Effect.Effect + readonly forSession: (sessionID: SessionV2.ID) => Effect.Effect> + readonly list: () => Effect.Effect> } export class Service extends Context.Service()("@opencode/v2/Permission") {} @@ -108,7 +110,7 @@ interface Pending { const layer = Layer.effect( Service, - EffectRuntime.gen(function* () { + Effect.gen(function* () { const events = yield* EventV2.Service const location = yield* Location.Service const agents = yield* AgentV2.Service @@ -116,28 +118,25 @@ const layer = Layer.effect( const saved = yield* PermissionSaved.Service const pending = new Map() - yield* EffectRuntime.addFinalizer(() => - EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { + yield* Effect.addFinalizer(() => + Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { discard: true, }).pipe( - EffectRuntime.ensuring( - EffectRuntime.sync(() => { + Effect.ensuring( + Effect.sync(() => { pending.clear() }), ), ), ) - const savedRules = EffectRuntime.fnUntraced(function* () { + const savedRules = Effect.fnUntraced(function* () { return (yield* saved.list({ projectID: location.project.id })).map( (item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }), ) }) - const configured = EffectRuntime.fn("PermissionV2.configured")(function* ( - sessionID: SessionV2.ID, - agentID?: AgentV2.ID, - ) { + const configured = Effect.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID, agentID?: AgentV2.ID) { const session = yield* sessions.get(sessionID) if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) const agent = yield* agents.resolve(agentID ?? session.agent) @@ -152,7 +151,7 @@ const layer = Layer.effect( return rules.filter((rule) => Wildcard.match(input.action, rule.action)) } - const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) { + const evaluateInput = Effect.fnUntraced(function* (input: AssertInput) { const rules = yield* configured(input.sessionID, input.agent) if (denied(input, rules)) return { effect: "deny" as const, rules } const all = [...rules, ...(yield* savedRules())] @@ -174,29 +173,30 @@ const layer = Layer.effect( } const create = (request: Request, agent?: AgentV2.ID) => - EffectRuntime.uninterruptible( - EffectRuntime.gen(function* () { + Effect.uninterruptible( + Effect.gen(function* () { const deferred = yield* Deferred.make() const item = { request, agent, deferred } - if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) + if (pending.has(request.id)) + return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`)) pending.set(request.id, item) yield* events .publish(Event.Asked, request) - .pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id)))) + .pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id)))) return item }), ) - const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) { + const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) { const result = yield* evaluateInput(input) const value = request(input) if (result.effect === "ask") yield* create(value, input.agent) return { id: value.id, effect: result.effect } }) - const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) => - EffectRuntime.uninterruptibleMask((restore) => - EffectRuntime.gen(function* () { + const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { const result = yield* evaluateInput(input) if (result.effect === "deny") { return yield* new DeniedError({ @@ -206,8 +206,8 @@ const layer = Layer.effect( if (result.effect === "allow") return const item = yield* create(request(input), input.agent) return yield* restore(Deferred.await(item.deferred)).pipe( - EffectRuntime.ensuring( - EffectRuntime.sync(() => { + Effect.ensuring( + Effect.sync(() => { pending.delete(item.request.id) }), ), @@ -216,9 +216,9 @@ const layer = Layer.effect( ), ) - const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => - EffectRuntime.uninterruptible( - EffectRuntime.gen(function* () { + const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) => + Effect.uninterruptible( + Effect.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) yield* events.publish(Event.Replied, { @@ -261,7 +261,7 @@ const layer = Layer.effect( for (const [id, item] of pending) { const input = { ...item.request } const rules = yield* configured(item.request.sessionID, item.agent).pipe( - EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)), + Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)), ) if (!rules) continue if (denied(input, rules)) continue @@ -284,15 +284,15 @@ const layer = Layer.effect( ), ) - const list = EffectRuntime.fn("PermissionV2.list")(function* () { + const list = Effect.fn("PermissionV2.list")(function* () { return Array.from(pending.values(), (item) => item.request) }) - const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) { + const get = Effect.fn("PermissionV2.get")(function* (id: ID) { return pending.get(id)?.request }) - const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { + const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) }) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index dd80647a732..a6ff337c62b 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -48,7 +48,7 @@ const layer = Layer.effect( let host: Parameters[0] const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) { - if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`) + if (loading.has(id)) return yield* Effect.die(new Error(`Plugin load cycle detected for ${id}`)) yield* locks.withLock(id)( Effect.sync(() => { @@ -90,7 +90,7 @@ const layer = Layer.effect( }) const remove = Effect.fn("Plugin.remove")(function* (id: ID) { - if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`) + if (loading.has(id)) return yield* Effect.die(new Error(`Cannot remove plugin ${id} while it is loading`)) yield* locks.withLock(id)( State.batch( diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 00ff7c82af0..d07ba6f0fd9 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -1,6 +1,6 @@ export * as PluginHost from "./host" -import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { Effect, Schema } from "effect" import { AgentV2 } from "../agent" import { AISDK } from "../aisdk" @@ -40,7 +40,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int workspaceID: location.workspaceID, project: location.project, }) - const locationRef = (input?: Parameters[0]) => + const locationRef = (input?: Parameters[0]) => input?.location === undefined ? undefined : Location.Ref.make({ @@ -305,5 +305,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int command: runtime.session.command, interrupt: (input) => runtime.session.interrupt(input.sessionID), }, - } satisfies Interface + } satisfies PluginContext }) diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index cb5b40fd17b..e79b498bd42 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -31,7 +31,7 @@ export interface Cell { export const makeCell = (): Cell => ({}) -const unavailable = () => Effect.die("Plugin runtime is unavailable") as Effect.Effect +const unavailable = () => Effect.die(new Error("Plugin runtime is unavailable")) as Effect.Effect const require = (cell: Cell, f: (runtime: Interface) => Effect.Effect) => Effect.suspend(() => { const runtime = cell.runtime diff --git a/packages/core/src/policy.ts b/packages/core/src/policy.ts index 782f182b0e4..d1dbb3e37c8 100644 --- a/packages/core/src/policy.ts +++ b/packages/core/src/policy.ts @@ -1,22 +1,23 @@ export * as Policy from "./policy" import { makeLocationNode } from "./effect/app-node" -import { Context, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Context, Effect, Layer, Schema } from "effect" import { Wildcard } from "./util/wildcard" import { Location } from "./location" -export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" }) -export type Effect = typeof Effect.Type +const PolicyEffect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" }) +export { PolicyEffect as Effect } +export type Effect = typeof PolicyEffect.Type export class Info extends Schema.Class("Policy.Info")({ action: Schema.String, - effect: Effect, + effect: PolicyEffect, resource: Schema.String, }) {} export interface Interface { - readonly load: (statements: Info[]) => EffectRuntime.Effect - readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect + readonly load: (statements: Info[]) => Effect.Effect + readonly evaluate: (action: string, resource: string, fallback: Effect) => Effect.Effect readonly hasStatements: () => boolean } @@ -24,16 +25,16 @@ export class Service extends Context.Service()("@opencode/v2 const layer = Layer.effect( Service, - EffectRuntime.gen(function* () { + Effect.gen(function* () { let statements: Info[] = [] yield* Location.Service return Service.of({ - load: EffectRuntime.fn("Policy.load")(function* (input) { + load: Effect.fn("Policy.load")(function* (input) { statements = input }), hasStatements: () => statements.length > 0, - evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) { + evaluate: Effect.fn("Policy.evaluate")(function* (action, resource, fallback) { return ( statements.findLast( (statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource), diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts index ab05fdac4a1..6a7e5a9d549 100644 --- a/packages/core/src/project/sql.ts +++ b/packages/core/src/project/sql.ts @@ -1,11 +1,11 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" -import * as DatabasePath from "../database/path" +import { absoluteArrayColumn, absoluteColumn } from "../database/path" import { Timestamps } from "../database/schema.sql" import { ProjectSchema } from "./schema" export const ProjectTable = sqliteTable("project", { id: text().$type().primaryKey(), - worktree: DatabasePath.absoluteColumn().notNull(), + worktree: absoluteColumn().notNull(), vcs: text(), name: text(), icon_url: text(), @@ -13,7 +13,7 @@ export const ProjectTable = sqliteTable("project", { icon_color: text(), ...Timestamps, time_initialized: integer(), - sandboxes: DatabasePath.absoluteArrayColumn().notNull(), + sandboxes: absoluteArrayColumn().notNull(), commands: text({ mode: "json" }).$type<{ start?: string }>(), }) @@ -24,7 +24,7 @@ export const ProjectDirectoryTable = sqliteTable( .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), - directory: DatabasePath.absoluteColumn().notNull(), + directory: absoluteColumn().notNull(), type: text().$type<"main" | "root" | "git_worktree">(), strategy: text(), time_created: integer() diff --git a/packages/core/src/pty/pty.bun.ts b/packages/core/src/pty/pty.bun.ts index 1f8ce8e4546..b92ba4409b4 100644 --- a/packages/core/src/pty/pty.bun.ts +++ b/packages/core/src/pty/pty.bun.ts @@ -1,10 +1,10 @@ -import { spawn as create } from "bun-pty" +import { spawn } from "bun-pty" import type { Opts, Proc } from "./pty" export type { Disp, Exit, Opts, Proc } from "./pty" -export function spawn(file: string, args: string[], opts: Opts): Proc { - const pty = create(file, args, opts) +function spawnPty(file: string, args: string[], opts: Opts): Proc { + const pty = spawn(file, args, opts) return { pid: pty.pid, onData(listener) { @@ -24,3 +24,5 @@ export function spawn(file: string, args: string[], opts: Opts): Proc { }, } } + +export { spawnPty as spawn } diff --git a/packages/core/src/pty/pty.node.ts b/packages/core/src/pty/pty.node.ts index 76f415f4cdd..cc775c8fda2 100644 --- a/packages/core/src/pty/pty.node.ts +++ b/packages/core/src/pty/pty.node.ts @@ -1,3 +1,4 @@ +// ast-grep-ignore: no-star-import import * as pty from "@lydell/node-pty" import type { Opts, Proc } from "./pty" diff --git a/packages/core/src/pty/ticket.ts b/packages/core/src/pty/ticket.ts index 07838b14157..2f227aa1427 100644 --- a/packages/core/src/pty/ticket.ts +++ b/packages/core/src/pty/ticket.ts @@ -32,7 +32,7 @@ function matches(record: Scope, input: Scope) { // Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is // never invoked; it dies if it ever is, which would signal a misuse of the Service interface. -const noLookup = () => Effect.die("PtyTicket cache must be used via set/invalidateWhen, never get") +const noLookup = () => Effect.die(new Error("PtyTicket cache must be used via set/invalidateWhen, never get")) // Visible for tests so the TTL can be shortened. Production uses `layer` with the default TTL. export const make = (ttl: Duration.Input = DEFAULT_TTL) => diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 9ff953be82d..fdb81c50458 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -2,10 +2,8 @@ export * as SessionCompaction from "./compaction" import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/llm" import { Context, DateTime, Effect, Layer, Stream } from "effect" -import type { Config } from "../config" -import { Config as ConfigV2 } from "../config" -import type { EventV2 } from "../event" -import { EventV2 as EventV2Service } from "../event" +import { Config } from "../config" +import { EventV2 } from "../event" import { makeLocationNode } from "../effect/app-node" import { llmClient } from "../effect/app-node-platform" import { SessionEvent } from "./event" @@ -311,9 +309,9 @@ const make = (dependencies: Dependencies) => { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2Service.Service + const events = yield* EventV2.Service const llm = yield* LLMClient.Service - const config = yield* ConfigV2.Service + const config = yield* Config.Service const models = yield* SessionRunnerModel.Service const compaction = make({ events, llm, config: yield* config.entries() }) @@ -336,5 +334,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2Service.node, llmClient, ConfigV2.node, SessionRunnerModel.node], + deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node], }) diff --git a/packages/core/src/session/context-checkpoint.ts b/packages/core/src/session/context-checkpoint.ts index c2036e726a2..8ea1f46513e 100644 --- a/packages/core/src/session/context-checkpoint.ts +++ b/packages/core/src/session/context-checkpoint.ts @@ -112,7 +112,7 @@ const rewrite = Effect.fnUntraced(function* ( .returning({ sessionID: SessionContextCheckpointTable.session_id }) .get() .pipe(Effect.orDie) - if (!updated) return yield* Effect.die("Context checkpoint not found") + if (!updated) return yield* Effect.die(new Error("Context checkpoint not found")) }) const advance = Effect.fnUntraced(function* ( @@ -127,5 +127,5 @@ const advance = Effect.fnUntraced(function* ( .returning({ sessionID: SessionContextCheckpointTable.session_id }) .get() .pipe(Effect.orDie) - if (!updated) return yield* Effect.die("Context checkpoint not found") + if (!updated) return yield* Effect.die(new Error("Context checkpoint not found")) }) diff --git a/packages/core/src/session/execution/local.ts b/packages/core/src/session/execution/local.ts index 8609162c82d..60e32b7c2cf 100644 --- a/packages/core/src/session/execution/local.ts +++ b/packages/core/src/session/execution/local.ts @@ -19,7 +19,7 @@ const layer = Layer.effect( const coordinator = yield* SessionRunCoordinator.make({ drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) { const session = yield* store.get(sessionID) - if (!session) return yield* Effect.die(`Session not found: ${sessionID}`) + if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) return yield* SessionRunner.Service.use((runner) => runner.run({ sessionID, force })).pipe( Effect.provide(locations.get(session.location)), Effect.tapCause((cause) => diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index 2c6aeda460b..8d895635431 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -62,7 +62,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* ( .pipe( Effect.flatMap((event) => event.durable === undefined - ? Effect.die("Prompt admission event is missing aggregate sequence") + ? Effect.die(new Error("Prompt admission event is missing aggregate sequence")) : Effect.succeed( Admitted.make({ admittedSeq: event.durable.seq, diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 5e2d112229d..7b087336849 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -157,7 +157,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( .where(eq(SessionTable.id, event.data.parentID)) .get() .pipe(Effect.orDie) - if (!parent) return yield* Effect.die(`Fork parent session not found: ${event.data.parentID}`) + if (!parent) return yield* Effect.die(new Error(`Fork parent session not found: ${event.data.parentID}`)) const boundary = event.data.messageID ? yield* db .select({ seq: SessionMessageTable.seq }) @@ -172,7 +172,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( .pipe(Effect.orDie) : undefined if (event.data.messageID && !boundary) - return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`) + return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.messageID}`)) const copied = yield* db .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) @@ -341,7 +341,8 @@ function run(db: DatabaseService, event: MessageEvent) { const decodeRow = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }) const updateMessage = (message: SessionMessage.Message) => { - if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence") + if (event.durable === undefined) + return Effect.die(new Error("Durable Session event is missing aggregate sequence")) const encoded = encodeMessage(message) const { id, type, ...data } = encoded return db @@ -418,7 +419,7 @@ function run(db: DatabaseService, event: MessageEvent) { } function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) { - if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence") + if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence")) const encoded = encodeMessage(message) const { id, type, ...data } = encoded return db @@ -585,7 +586,8 @@ const layer = Layer.effectDiscard( yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event)) yield* events.project(SessionEvent.Prompted, (event) => Effect.gen(function* () { - if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence") + if (event.durable === undefined) + return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) yield* SessionInput.projectPrompted(db, { id: event.data.messageID, sessionID: event.data.sessionID, @@ -599,7 +601,8 @@ const layer = Layer.effectDiscard( ) yield* events.project(SessionEvent.PromptAdmitted, (event) => Effect.gen(function* () { - if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence") + if (event.durable === undefined) + return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) yield* SessionInput.projectAdmitted(db, { admittedSeq: event.durable.seq, id: event.data.messageID, @@ -670,7 +673,7 @@ const layer = Layer.effectDiscard( ) .get() .pipe(Effect.orDie) - if (!boundary) return yield* Effect.die(`Revert boundary message not found: ${event.data.messageID}`) + if (!boundary) return yield* Effect.die(new Error(`Revert boundary message not found: ${event.data.messageID}`)) yield* db .delete(SessionMessageTable) .where( diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 42e96749043..f76ee10a3f0 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,3 +1,5 @@ +export * as SessionRunnerLLM from "./llm" + import { LLM, LLMClient, @@ -119,7 +121,7 @@ const layer = Layer.effect( const forkTitle = yield* FiberSet.makeRuntime() const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) - if (!session) return yield* Effect.die(`Session not found: ${sessionID}`) + if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) return session }) diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index b21492d18db..2b81e609ea5 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -2,8 +2,11 @@ export * as SessionRunnerModel from "./model" import { makeLocationNode } from "../../effect/app-node" import { type Model } from "@opencode-ai/llm" +// ast-grep-ignore: no-star-import import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages" +// ast-grep-ignore: no-star-import import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat" +// ast-grep-ignore: no-star-import import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses" import { Auth, type AnyRoute } from "@opencode-ai/llm/route" import { Context, Effect, Layer, Schema } from "effect" diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 33652a618c9..04c6437ef48 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -85,7 +85,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) }) const currentAssistantMessageID = () => assistantMessageID === undefined - ? Effect.die("Tool event before assistant step start") + ? Effect.die(new Error("Tool event before assistant step start")) : Effect.succeed(assistantMessageID) const fragments = ( @@ -95,20 +95,20 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const chunks = new Map() const start = (id: string) => Effect.suspend(() => { - if (chunks.has(id)) return Effect.die(`Duplicate ${name} start: ${id}`) + if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`)) chunks.set(id, []) return Effect.void }) const append = (id: string, value: string) => Effect.suspend(() => { const current = chunks.get(id) - if (!current) return Effect.die(`${name} delta before start: ${id}`) + if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`)) current.push(value) return Effect.void }) const end = Effect.fnUntraced(function* (id: string, providerMetadata?: ProviderMetadata) { const current = chunks.get(id) - if (!current) return yield* Effect.die(`${name} end before start: ${id}`) + if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`)) yield* ended(id, current.join(""), providerMetadata) chunks.delete(id) }) @@ -144,7 +144,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const toolInput = fragments("tool input", (callID, value) => Effect.gen(function* () { const tool = tools.get(callID) - if (!tool) return yield* Effect.die(`Tool input end before start: ${callID}`) + if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`)) yield* events.publish(SessionEvent.Tool.Input.Ended, { sessionID: input.sessionID, timestamp: yield* timestamp, @@ -163,7 +163,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) }) const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) { - if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`) + if (tools.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input start: ${event.id}`)) const assistantMessageID = yield* startAssistant() tools.set(event.id, { assistantMessageID, @@ -185,10 +185,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) { const tool = tools.get(event.id) - if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`) + if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`)) if (tool.name !== event.name) - return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`) - if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`) + return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)) + if (tool.inputEnded) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`)) yield* toolInput.end(event.id) }) @@ -233,7 +233,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const assistantMessageIDForTool = (callID: string) => { const tool = tools.get(callID) - return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(`Unknown tool call: ${callID}`) + return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`)) } const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* ( @@ -293,10 +293,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) return case "tool-input-delta": { const tool = tools.get(event.id) - if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`) + if (!tool) return yield* Effect.die(new Error(`Tool input delta before start: ${event.id}`)) if (tool.name !== event.name) - return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`) - if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`) + return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)) + if (tool.inputEnded) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`)) yield* toolInput.append(event.id, event.text) yield* events.publish(SessionEvent.Tool.Input.Delta, { sessionID: input.sessionID, @@ -315,8 +315,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const tool = tools.get(event.id)! if (!tool.inputEnded) yield* endToolInput(event) if (tool.name !== event.name) - return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`) - if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`) + return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)) + if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`)) tool.called = true tool.providerExecuted = event.providerExecuted === true tool.providerMetadata = event.providerMetadata @@ -336,12 +336,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) } case "tool-result": { const tool = tools.get(event.id) - if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`) + if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`)) if (tool.name !== event.name) - return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`) + return yield* Effect.die(new Error(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)) if (tool.settled) { if (event.result.type === "error") return - return yield* Effect.die(`Duplicate tool result: ${event.id}`) + return yield* Effect.die(new Error(`Duplicate tool result: ${event.id}`)) } tool.settled = true const result = settledOutput(event.output, event.result) @@ -375,10 +375,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) } case "tool-error": { const tool = tools.get(event.id) - if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`) + if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`)) if (tool.name !== event.name) - return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`) - if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`) + return yield* Effect.die(new Error(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)) + if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`)) tool.settled = true yield* events.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, @@ -396,7 +396,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) case "step-finish": yield* flush() assistantActive = false - if (stepSettlement) return yield* Effect.die("Duplicate step finish") + if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish")) stepSettlement = { finish: event.reason, tokens: tokens(event.usage) } return case "finish": diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index d1e923b40f5..739d3f18880 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from "drizzle-orm/sqlite-core" -import * as DatabasePath from "../database/path" +import { directoryColumn, pathColumn } from "../database/path" import { ProjectTable } from "../project/sql" import type { SessionMessage } from "./message" import type { Prompt } from "./prompt" @@ -31,8 +31,8 @@ export const SessionTable = sqliteTable( workspace_id: text().$type(), parent_id: text().$type(), slug: text().notNull(), - directory: DatabasePath.directoryColumn().notNull(), - path: DatabasePath.pathColumn(), + directory: directoryColumn().notNull(), + path: pathColumn(), title: text().notNull(), version: text().notNull(), share_url: text(), diff --git a/packages/core/src/shell/select.ts b/packages/core/src/shell/select.ts index 110697421da..311fb7c9a14 100644 --- a/packages/core/src/shell/select.ts +++ b/packages/core/src/shell/select.ts @@ -4,7 +4,7 @@ import path from "path" import { spawn, type ChildProcess } from "child_process" import { readFile } from "fs/promises" import { statSync } from "fs" -import { setTimeout as sleep } from "node:timers/promises" +import { setTimeout } from "node:timers/promises" import { Flag } from "../flag/flag" import { FSUtil } from "../fs-util" import { which } from "../util/which" @@ -46,13 +46,13 @@ export async function killTree(proc: ChildProcess, opts?: { exited?: () => boole try { process.kill(-pid, "SIGTERM") - await sleep(SIGKILL_TIMEOUT_MS) + await setTimeout(SIGKILL_TIMEOUT_MS) if (!opts?.exited?.()) { process.kill(-pid, "SIGKILL") } } catch { proc.kill("SIGTERM") - await sleep(SIGKILL_TIMEOUT_MS) + await setTimeout(SIGKILL_TIMEOUT_MS) if (!opts?.exited?.()) { proc.kill("SIGKILL") } diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 45699e7112e..4c9446394c4 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -3424,9 +3424,10 @@ describe("SessionRunnerLLM", () => { streamStarted = undefined response = [LLMEvent.textStart({ id: "text-1" }), LLMEvent.textStart({ id: "text-1" })] - expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe( - "Duplicate text start: text-1", - ) + const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed)) + expect(defect).toBeInstanceOf(Error) + if (!(defect instanceof Error)) return + expect(defect.message).toBe("Duplicate text start: text-1") }), ) @@ -3468,9 +3469,10 @@ describe("SessionRunnerLLM", () => { streamStarted = undefined response = [LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: "{}" })] - expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe( - "Tool input delta before start: call-1", - ) + const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed)) + expect(defect).toBeInstanceOf(Error) + if (!(defect instanceof Error)) return + expect(defect.message).toBe("Tool input delta before start: call-1") }), ) }) diff --git a/packages/server/src/auth.ts b/packages/server/src/auth.ts index c71536518d7..239eb0d2371 100644 --- a/packages/server/src/auth.ts +++ b/packages/server/src/auth.ts @@ -1,6 +1,7 @@ export * as ServerAuth from "./auth" -import { Config as EffectConfig, Context, Effect, Layer, Option, Redacted } from "effect" +import { Context, Effect, Layer, Option, Redacted } from "effect" +import { all, option, string, withDefault } from "effect/Config" export type Credentials = { password?: string @@ -27,9 +28,9 @@ export class Config extends Context.Service()("@opencode/ServerAut this, Effect.gen(function* () { return Config.of( - yield* EffectConfig.all({ - password: EffectConfig.string("OPENCODE_SERVER_PASSWORD").pipe(EffectConfig.option), - username: EffectConfig.string("OPENCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")), + yield* all({ + password: string("OPENCODE_SERVER_PASSWORD").pipe(option), + username: string("OPENCODE_SERVER_USERNAME").pipe(withDefault("opencode")), }), ) }), diff --git a/packages/server/src/handlers/event.ts b/packages/server/src/handlers/event.ts index f412137abaa..ca9c02620d4 100644 --- a/packages/server/src/handlers/event.ts +++ b/packages/server/src/handlers/event.ts @@ -1,9 +1,9 @@ import { EventV2 } from "@opencode-ai/core/event" import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { Effect, Schema, Stream } from "effect" +import { Sse } from "effect/unstable/encoding" import { HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" -import * as Sse from "effect/unstable/encoding/Sse" import { Api } from "../api" const subscriberCapacity = 256 diff --git a/packages/server/src/handlers/pty.ts b/packages/server/src/handlers/pty.ts index e2c5fcc5685..7f8a9ea64e9 100644 --- a/packages/server/src/handlers/pty.ts +++ b/packages/server/src/handlers/pty.ts @@ -5,7 +5,7 @@ import { Location } from "@opencode-ai/core/location" import { Effect, Queue } from "effect" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import * as Socket from "effect/unstable/socket/Socket" +import { Socket } from "effect/unstable/socket" import { Api } from "../api" import { CorsConfig, isAllowedRequestOrigin } from "../cors" import { ForbiddenError, PtyNotFoundError } from "@opencode-ai/protocol/errors" diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 30c3d961b7a..db69ba6c7ff 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -23,7 +23,7 @@ import { handlers } from "./handlers" import { authorizationLayer } from "./middleware/authorization" import { schemaErrorLayer } from "./middleware/schema-error" import { PtyEnvironment } from "./pty-environment" -import { layer as locationLayer } from "./location" +import { layer } from "./location" import { formLocationLayer } from "./middleware/form-location" import { sessionLocationLayer } from "./middleware/session-location" @@ -82,7 +82,7 @@ function makeRoutes( Layer.provide(handlers), Layer.provide(formLocationLayer), Layer.provide(sessionLocationLayer), - Layer.provide(locationLayer), + Layer.provide(layer), Layer.provide(authorizationLayer), Layer.provide(schemaErrorLayer), Layer.provide(auth), diff --git a/script/ast-grep/rule-tests/__snapshots__/no-drizzle-column-name-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-drizzle-column-name-snapshot.yml new file mode 100644 index 00000000000..aae0c580ee7 --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-drizzle-column-name-snapshot.yml @@ -0,0 +1,12 @@ +id: no-drizzle-column-name +snapshots: + ? | + const table = sqliteTable("session", { + projectID: text("project_id").notNull(), + createdAt: integer("time_created").notNull(), + }) + : labels: + - source: text("project_id") + style: primary + start: 52 + end: 70 diff --git a/script/ast-grep/rule-tests/__snapshots__/no-effect-die-string-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-effect-die-string-snapshot.yml new file mode 100644 index 00000000000..a1bb4641644 --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-effect-die-string-snapshot.yml @@ -0,0 +1,30 @@ +id: no-effect-die-string +snapshots: + Effect.die("boom"): + labels: + - source: Effect.die("boom") + style: primary + start: 0 + end: 18 + - source: '"boom"' + style: secondary + start: 11 + end: 17 + - source: ("boom") + style: secondary + start: 10 + end: 18 + Effect.die(`boom ${value}`): + labels: + - source: Effect.die(`boom ${value}`) + style: primary + start: 0 + end: 27 + - source: '`boom ${value}`' + style: secondary + start: 11 + end: 26 + - source: (`boom ${value}`) + style: secondary + start: 10 + end: 27 diff --git a/script/ast-grep/rule-tests/__snapshots__/no-import-alias-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-import-alias-snapshot.yml new file mode 100644 index 00000000000..6336c0bd01d --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-import-alias-snapshot.yml @@ -0,0 +1,42 @@ +id: no-import-alias +snapshots: + import { baz, foo as bar } from "./foo": + labels: + - source: foo as bar + style: primary + start: 14 + end: 24 + - source: bar + style: secondary + start: 21 + end: 24 + import { foo as bar } from "./foo": + labels: + - source: foo as bar + style: primary + start: 9 + end: 19 + - source: bar + style: secondary + start: 16 + end: 19 + import { foo as bar, baz } from "./foo": + labels: + - source: foo as bar + style: primary + start: 9 + end: 19 + - source: bar + style: secondary + start: 16 + end: 19 + import { type Foo as Bar, baz } from "./foo": + labels: + - source: type Foo as Bar + style: primary + start: 9 + end: 24 + - source: Bar + style: secondary + start: 21 + end: 24 diff --git a/script/ast-grep/rule-tests/__snapshots__/no-json-parse-cast-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-json-parse-cast-snapshot.yml new file mode 100644 index 00000000000..0aa2a274eac --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-json-parse-cast-snapshot.yml @@ -0,0 +1,8 @@ +id: no-json-parse-cast +snapshots: + const value = JSON.parse(input) as Record: + labels: + - source: JSON.parse(input) as Record + style: primary + start: 14 + end: 58 diff --git a/script/ast-grep/rule-tests/__snapshots__/no-nested-effect-service-yield-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-nested-effect-service-yield-snapshot.yml new file mode 100644 index 00000000000..30619694f8e --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-nested-effect-service-yield-snapshot.yml @@ -0,0 +1,20 @@ +id: no-nested-effect-service-yield +snapshots: + ? | + Effect.gen(function* () { + yield* (yield* Foo.Service).client.run() + }) + : labels: + - source: (yield* Foo.Service).client.run() + style: primary + start: 35 + end: 68 + ? | + Effect.gen(function* () { + yield* (yield* Foo.Service).run() + }) + : labels: + - source: (yield* Foo.Service).run() + style: primary + start: 35 + end: 61 diff --git a/script/ast-grep/rule-tests/__snapshots__/no-star-import-snapshot.yml b/script/ast-grep/rule-tests/__snapshots__/no-star-import-snapshot.yml new file mode 100644 index 00000000000..dab38e82f6a --- /dev/null +++ b/script/ast-grep/rule-tests/__snapshots__/no-star-import-snapshot.yml @@ -0,0 +1,22 @@ +id: no-star-import +snapshots: + import * as Foo from "./foo": + labels: + - source: '* as Foo' + style: primary + start: 7 + end: 15 + - source: import * as Foo from "./foo" + style: secondary + start: 0 + end: 28 + import type * as Foo from "./foo": + labels: + - source: '* as Foo' + style: primary + start: 12 + end: 20 + - source: import type * as Foo from "./foo" + style: secondary + start: 0 + end: 33 diff --git a/script/ast-grep/rule-tests/no-drizzle-column-name-test.yml b/script/ast-grep/rule-tests/no-drizzle-column-name-test.yml new file mode 100644 index 00000000000..00e1a7bb286 --- /dev/null +++ b/script/ast-grep/rule-tests/no-drizzle-column-name-test.yml @@ -0,0 +1,14 @@ +id: no-drizzle-column-name +valid: + - | + const table = sqliteTable("session", { + project_id: text().notNull(), + time_created: integer().notNull(), + payload: text({ mode: "json" }), + }) +invalid: + - | + const table = sqliteTable("session", { + projectID: text("project_id").notNull(), + createdAt: integer("time_created").notNull(), + }) diff --git a/script/ast-grep/rule-tests/no-effect-die-string-test.yml b/script/ast-grep/rule-tests/no-effect-die-string-test.yml new file mode 100644 index 00000000000..4ca9e737c01 --- /dev/null +++ b/script/ast-grep/rule-tests/no-effect-die-string-test.yml @@ -0,0 +1,7 @@ +id: no-effect-die-string +valid: + - Effect.die(new Error("boom")) + - Effect.fail("boom") +invalid: + - Effect.die("boom") + - Effect.die(`boom ${value}`) diff --git a/script/ast-grep/rule-tests/no-import-alias-test.yml b/script/ast-grep/rule-tests/no-import-alias-test.yml new file mode 100644 index 00000000000..7c3ab9235b6 --- /dev/null +++ b/script/ast-grep/rule-tests/no-import-alias-test.yml @@ -0,0 +1,13 @@ +id: no-import-alias +valid: + - import { foo } from "./foo" + - import type { Foo } from "./foo" + - import foo from "./foo" + - export { foo as bar } from "./foo" + - import type { Plugin as EffectPlugin } from "./foo" + - import type { Foo as Bar, Baz } from "./foo" +invalid: + - import { foo as bar } from "./foo" + - import { baz, foo as bar } from "./foo" + - import { foo as bar, baz } from "./foo" + - import { type Foo as Bar, baz } from "./foo" diff --git a/script/ast-grep/rule-tests/no-json-parse-cast-test.yml b/script/ast-grep/rule-tests/no-json-parse-cast-test.yml new file mode 100644 index 00000000000..ee44819ce8b --- /dev/null +++ b/script/ast-grep/rule-tests/no-json-parse-cast-test.yml @@ -0,0 +1,6 @@ +id: no-json-parse-cast +valid: + - const value = JSON.parse(input) + - const value = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(input) +invalid: + - const value = JSON.parse(input) as Record diff --git a/script/ast-grep/rule-tests/no-nested-effect-service-yield-test.yml b/script/ast-grep/rule-tests/no-nested-effect-service-yield-test.yml new file mode 100644 index 00000000000..e126f2d68a6 --- /dev/null +++ b/script/ast-grep/rule-tests/no-nested-effect-service-yield-test.yml @@ -0,0 +1,21 @@ +id: no-nested-effect-service-yield +valid: + - | + Effect.gen(function* () { + const service = yield* Foo.Service + yield* service.run() + }) + - | + Effect.gen(function* () { + const db = (yield* Database.Service).db + yield* db.run() + }) +invalid: + - | + Effect.gen(function* () { + yield* (yield* Foo.Service).run() + }) + - | + Effect.gen(function* () { + yield* (yield* Foo.Service).client.run() + }) diff --git a/script/ast-grep/rule-tests/no-star-import-test.yml b/script/ast-grep/rule-tests/no-star-import-test.yml new file mode 100644 index 00000000000..6006f02140e --- /dev/null +++ b/script/ast-grep/rule-tests/no-star-import-test.yml @@ -0,0 +1,8 @@ +id: no-star-import +valid: + - import { Foo } from "./foo" + - import Foo from "./foo" + - export * as Foo from "./foo" +invalid: + - import * as Foo from "./foo" + - import type * as Foo from "./foo" diff --git a/script/ast-grep/rules/no-drizzle-column-name.yml b/script/ast-grep/rules/no-drizzle-column-name.yml new file mode 100644 index 00000000000..5b798b81abd --- /dev/null +++ b/script/ast-grep/rules/no-drizzle-column-name.yml @@ -0,0 +1,22 @@ +id: no-drizzle-column-name +language: TypeScript +message: Use snake_case object keys instead of explicit drizzle column names. +severity: error +files: + - packages/core/src/**/sql.ts + - packages/core/src/**/*.sql.ts +rule: + any: + - pattern: text($NAME) + - pattern: text($NAME, $$$ARGS) + - pattern: integer($NAME) + - pattern: integer($NAME, $$$ARGS) + - pattern: blob($NAME) + - pattern: blob($NAME, $$$ARGS) + - pattern: real($NAME) + - pattern: real($NAME, $$$ARGS) + - pattern: numeric($NAME) + - pattern: numeric($NAME, $$$ARGS) +constraints: + NAME: + kind: string diff --git a/script/ast-grep/rules/no-effect-die-string.yml b/script/ast-grep/rules/no-effect-die-string.yml new file mode 100644 index 00000000000..77265afb291 --- /dev/null +++ b/script/ast-grep/rules/no-effect-die-string.yml @@ -0,0 +1,22 @@ +id: no-effect-die-string +language: TypeScript +message: die with `new Error(...)`. +severity: error +rule: + any: + - all: + - pattern: Effect.die($MESSAGE) + - has: + field: arguments + all: + - kind: arguments + - has: + kind: string + - all: + - pattern: Effect.die($MESSAGE) + - has: + field: arguments + all: + - kind: arguments + - has: + kind: template_string diff --git a/script/ast-grep/rules/no-import-alias.yml b/script/ast-grep/rules/no-import-alias.yml new file mode 100644 index 00000000000..1c6c4c5298c --- /dev/null +++ b/script/ast-grep/rules/no-import-alias.yml @@ -0,0 +1,14 @@ +id: no-import-alias +language: TypeScript +message: Do not alias value imports. For type name collisions, alias inside a dedicated `import type` statement. +severity: error +rule: + all: + - kind: import_specifier + - has: + field: alias + kind: identifier + - not: + inside: + pattern: import type { $$$SPECS } from "$MOD" + stopBy: end diff --git a/script/ast-grep/no-json-parse-cast.yml b/script/ast-grep/rules/no-json-parse-cast.yml similarity index 100% rename from script/ast-grep/no-json-parse-cast.yml rename to script/ast-grep/rules/no-json-parse-cast.yml diff --git a/script/ast-grep/no-nested-effect-service-yield.yml b/script/ast-grep/rules/no-nested-effect-service-yield.yml similarity index 100% rename from script/ast-grep/no-nested-effect-service-yield.yml rename to script/ast-grep/rules/no-nested-effect-service-yield.yml diff --git a/script/ast-grep/rules/no-star-import.yml b/script/ast-grep/rules/no-star-import.yml new file mode 100644 index 00000000000..e8042231081 --- /dev/null +++ b/script/ast-grep/rules/no-star-import.yml @@ -0,0 +1,10 @@ +id: no-star-import +language: TypeScript +message: Do not use star imports. +severity: error +rule: + all: + - kind: namespace_import + - inside: + kind: import_statement + stopBy: end diff --git a/script/ast-grep/sgconfig.yml b/script/ast-grep/sgconfig.yml new file mode 100644 index 00000000000..098ecb2af48 --- /dev/null +++ b/script/ast-grep/sgconfig.yml @@ -0,0 +1,4 @@ +ruleDirs: + - rules +testConfigs: + - testDir: rule-tests