mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 06:02:05 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fac3f7ce7 | |||
| bf24cf423c | |||
| b30440ec26 |
@@ -0,0 +1,23 @@
|
||||
name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Test lint rules
|
||||
run: bun lint:test
|
||||
|
||||
- name: Run lint
|
||||
run: bun lint --quiet
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"jsPlugins": ["./script/lint/opencode.mjs"],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
@@ -7,6 +8,7 @@
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"opencode/tagged-error-message": "error",
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
@@ -47,5 +49,5 @@
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/.lint-tmp-*", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"lint:test": "bun script/lint/test.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||
|
||||
@@ -123,7 +123,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
|
||||
providerID: ProviderV2.ID,
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Failed to initialize AI SDK provider: ${this.providerID}`
|
||||
}
|
||||
}
|
||||
|
||||
function initError(providerID: ProviderV2.ID) {
|
||||
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
|
||||
|
||||
@@ -30,7 +30,11 @@ export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<Des
|
||||
expected: ProjectV2.ID,
|
||||
actual: ProjectV2.ID,
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Destination project ${this.actual} does not match session project ${this.expected}`
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplyChangesError extends Schema.TaggedErrorClass<ApplyChangesError>()("MoveSession.ApplyChangesError", {
|
||||
message: Schema.String,
|
||||
|
||||
@@ -30,11 +30,19 @@ export interface RemoveInput {
|
||||
|
||||
export class StaleContentError extends Schema.TaggedErrorClass<StaleContentError>()("FileMutation.StaleContentError", {
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `File changed since it was read: ${this.path}`
|
||||
}
|
||||
}
|
||||
|
||||
export class TargetExistsError extends Schema.TaggedErrorClass<TargetExistsError>()("FileMutation.TargetExistsError", {
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `File already exists: ${this.path}`
|
||||
}
|
||||
}
|
||||
|
||||
export interface WriteResult {
|
||||
readonly operation: "write"
|
||||
|
||||
@@ -7,7 +7,11 @@ import { FileSystem } from "./filesystem"
|
||||
export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
|
||||
"Image.ResizerUnavailableError",
|
||||
{},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return "Image resizer is unavailable"
|
||||
}
|
||||
}
|
||||
|
||||
export class DecodeError extends Schema.TaggedErrorClass<DecodeError>()("Image.DecodeError", {
|
||||
resource: Schema.String,
|
||||
|
||||
@@ -169,11 +169,19 @@ export type AttemptStatus = typeof AttemptStatus.Type
|
||||
|
||||
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
|
||||
attemptID: AttemptID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Authorization code required for OAuth attempt ${this.attemptID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", {
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return "Integration authorization failed"
|
||||
}
|
||||
}
|
||||
|
||||
export type Error = CodeRequiredError | AuthorizationError
|
||||
|
||||
|
||||
@@ -23,7 +23,13 @@ export type ResolveInput = typeof ResolveInput.Type
|
||||
export class PathError extends Schema.TaggedErrorClass<PathError>()("LocationMutation.PathError", {
|
||||
path: Schema.String,
|
||||
reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
if (this.reason === "relative_escape") return `Relative path escapes the location: ${this.path}`
|
||||
if (this.reason === "location_escape") return `Path resolves outside the location: ${this.path}`
|
||||
return `Path has a non-directory ancestor: ${this.path}`
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExternalDirectoryAuthorization {
|
||||
readonly action: "external_directory"
|
||||
|
||||
@@ -16,7 +16,11 @@ export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedErr
|
||||
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
dir: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Failed to install dependencies in ${this.dir}`
|
||||
}
|
||||
}
|
||||
|
||||
export interface EntryPoint {
|
||||
readonly directory: string
|
||||
|
||||
@@ -83,19 +83,36 @@ export const Event = {
|
||||
}),
|
||||
}
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {
|
||||
override get message() {
|
||||
return "The user rejected this permission request"
|
||||
}
|
||||
}
|
||||
|
||||
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
|
||||
feedback: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `The user rejected this permission request with feedback: ${this.feedback}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
|
||||
rules: PermissionSchema.Ruleset,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
if (this.rules.length === 0) return "Permission denied by configured rules"
|
||||
return `Permission denied by configured rules: ${this.rules.map((rule) => `${rule.action} ${rule.resource}`).join(", ")}`
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
|
||||
requestID: ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Permission request not found: ${this.requestID}`
|
||||
}
|
||||
}
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
|
||||
|
||||
@@ -58,32 +58,56 @@ export type ListEntry = typeof ListEntry.Type
|
||||
export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
|
||||
"ProjectCopy.SourceDirectoryNotFoundError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Project copy source directory not found: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
|
||||
"ProjectCopy.DestinationExistsError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Project copy destination already exists: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
|
||||
"ProjectCopy.DirectoryUnavailableError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Project copy directory is unavailable: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
|
||||
"ProjectCopy.InvalidDirectoryError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Invalid project copy directory: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
|
||||
"ProjectCopy.StrategyUnavailableError",
|
||||
{ strategy: StrategyID },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Project copy strategy is unavailable: ${this.strategy}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
|
||||
"ProjectCopy.DuplicateStrategyError",
|
||||
{ strategy: StrategyID },
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Project copy strategy is already registered: ${this.strategy}`
|
||||
}
|
||||
}
|
||||
|
||||
export type Error =
|
||||
| SourceDirectoryNotFoundError
|
||||
|
||||
@@ -94,11 +94,19 @@ export type Attachment = {
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Pty.NotFoundError", {
|
||||
ptyID: PtyID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `PTY session not found: ${this.ptyID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.ExitedError", {
|
||||
ptyID: PtyID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `PTY session has exited: ${this.ptyID}`
|
||||
}
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Created: EventV2.define({ type: "pty.created", schema: { info: Info } }),
|
||||
|
||||
@@ -87,7 +87,11 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("QuestionV2.NotFoundError", {
|
||||
requestID: ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Question request not found: ${this.requestID}`
|
||||
}
|
||||
}
|
||||
|
||||
export interface AskInput {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
|
||||
@@ -83,21 +83,33 @@ type CompactInput = {
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Session.NotFoundError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Session not found: ${this.sessionID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
|
||||
"Session.OperationUnavailableError",
|
||||
{
|
||||
operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]),
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `Session ${this.operation} is not available yet`
|
||||
}
|
||||
}
|
||||
|
||||
export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error"
|
||||
|
||||
export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Prompt message ${this.messageID} conflicts with an existing durable record in session ${this.sessionID}`
|
||||
}
|
||||
}
|
||||
|
||||
export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseServic
|
||||
return row === undefined ? undefined : fromRow(row)
|
||||
})
|
||||
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- internal defect sentinel for inconsistent projections
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInput.LifecycleConflict", {
|
||||
id: SessionMessage.ID,
|
||||
}) {}
|
||||
|
||||
@@ -16,11 +16,19 @@ export namespace EffectFlock {
|
||||
|
||||
export class LockTimeoutError extends Schema.TaggedErrorClass<LockTimeoutError>()("LockTimeoutError", {
|
||||
key: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Timed out acquiring lock: ${this.key}`
|
||||
}
|
||||
}
|
||||
|
||||
export class LockCompromisedError extends Schema.TaggedErrorClass<LockCompromisedError>()("LockCompromisedError", {
|
||||
detail: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Lock was compromised: ${this.detail}`
|
||||
}
|
||||
}
|
||||
|
||||
class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", {
|
||||
detail: Schema.String,
|
||||
@@ -32,6 +40,7 @@ export namespace EffectFlock {
|
||||
}
|
||||
|
||||
/** Internal: signals "lock is held, retry later". Never leaks to callers. */
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- internal retry sentinel for lock contention
|
||||
class NotAcquired extends Schema.TaggedErrorClass<NotAcquired>()("NotAcquired", {}) {}
|
||||
|
||||
export type LockError = LockTimeoutError | LockCompromisedError
|
||||
|
||||
@@ -91,6 +91,10 @@ export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("Permiss
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
|
||||
requestID: ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Permission request not found: ${this.requestID}`
|
||||
}
|
||||
}
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
|
||||
@@ -3,50 +3,86 @@ import { Schema } from "effect"
|
||||
|
||||
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()("ACPSessionNotFoundError", {
|
||||
sessionId: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `session not found: ${this.sessionId}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidConfigOptionError extends Schema.TaggedErrorClass<InvalidConfigOptionError>()(
|
||||
"ACPInvalidConfigOptionError",
|
||||
{
|
||||
configId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `unknown config option: ${this.configId}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidModelError extends Schema.TaggedErrorClass<InvalidModelError>()("ACPInvalidModelError", {
|
||||
modelId: Schema.String,
|
||||
providerId: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `model not found: ${this.modelId}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidEffortError extends Schema.TaggedErrorClass<InvalidEffortError>()("ACPInvalidEffortError", {
|
||||
effort: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `effort not found: ${this.effort}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidModeError extends Schema.TaggedErrorClass<InvalidModeError>()("ACPInvalidModeError", {
|
||||
mode: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `mode not found: ${this.mode}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthRequiredError extends Schema.TaggedErrorClass<AuthRequiredError>()("ACPAuthRequiredError", {
|
||||
providerId: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return "provider authentication required"
|
||||
}
|
||||
}
|
||||
|
||||
export class UnknownAuthMethodError extends Schema.TaggedErrorClass<UnknownAuthMethodError>()(
|
||||
"ACPUnknownAuthMethodError",
|
||||
{
|
||||
methodId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `unknown auth method: ${this.methodId}`
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedOperationError extends Schema.TaggedErrorClass<UnsupportedOperationError>()(
|
||||
"ACPUnsupportedOperationError",
|
||||
{
|
||||
method: Schema.String,
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return `method not found: ${this.method}`
|
||||
}
|
||||
}
|
||||
|
||||
export class ServiceFailureError extends Schema.TaggedErrorClass<ServiceFailureError>()("ACPServiceFailureError", {
|
||||
safeMessage: Schema.String,
|
||||
service: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return this.safeMessage
|
||||
}
|
||||
}
|
||||
|
||||
export type Error =
|
||||
| SessionNotFoundError
|
||||
|
||||
@@ -9,6 +9,7 @@ const wordmark = [
|
||||
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
|
||||
]
|
||||
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- CLI cancellation intentionally renders without an error message.
|
||||
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("UICancelledError", {}) {}
|
||||
|
||||
export const Style = {
|
||||
|
||||
@@ -8,7 +8,9 @@ export interface Runner<A, E = never> {
|
||||
readonly cancel: Effect.Effect<void>
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- Internal coordinator signal, not a user-facing error.
|
||||
export class Cancelled extends Schema.TaggedErrorClass<Cancelled>()("RunnerCancelled", {}) {}
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- Internal coordinator signal, not a user-facing error.
|
||||
export class Busy extends Schema.TaggedErrorClass<Busy>()("RunnerBusy", {}) {}
|
||||
|
||||
interface RunHandle<A, E> {
|
||||
|
||||
@@ -29,7 +29,11 @@ export type Diagnostic = VSCodeDiagnostic
|
||||
export class InitializeError extends Schema.TaggedErrorClass<InitializeError>()("LSPInitializeError", {
|
||||
serverID: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Failed to initialize LSP server: ${this.serverID}`
|
||||
}
|
||||
}
|
||||
|
||||
type DocumentDiagnosticReport = {
|
||||
items?: Diagnostic[]
|
||||
|
||||
@@ -80,7 +80,11 @@ export const Failed = NamedError.create("MCPFailed", {
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
||||
name: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `MCP server not found: ${this.name}`
|
||||
}
|
||||
}
|
||||
|
||||
type MCPClient = Client
|
||||
|
||||
|
||||
@@ -102,7 +102,11 @@ export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePa
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Project.NotFoundError", {
|
||||
projectID: ProjectV2.ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Project not found: ${this.projectID}`
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Effect service
|
||||
|
||||
@@ -67,16 +67,28 @@ export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
|
||||
|
||||
export class OauthMissing extends Schema.TaggedErrorClass<OauthMissing>()("ProviderAuthOauthMissing", {
|
||||
providerID: ProviderV2.ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `No pending OAuth authorization for provider: ${this.providerID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class OauthCodeMissing extends Schema.TaggedErrorClass<OauthCodeMissing>()("ProviderAuthOauthCodeMissing", {
|
||||
providerID: ProviderV2.ID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `OAuth authorization code is required for provider: ${this.providerID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class OauthCallbackFailed extends Schema.TaggedErrorClass<OauthCallbackFailed>()(
|
||||
"ProviderAuthOauthCallbackFailed",
|
||||
{},
|
||||
) {}
|
||||
) {
|
||||
override get message() {
|
||||
return "OAuth callback failed"
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationFailed extends Schema.TaggedErrorClass<ValidationFailed>()("ProviderAuthValidationFailed", {
|
||||
field: Schema.String,
|
||||
|
||||
@@ -98,7 +98,11 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Question.NotFoundError", {
|
||||
requestID: QuestionID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Question request not found: ${this.requestID}`
|
||||
}
|
||||
}
|
||||
|
||||
interface PendingEntry {
|
||||
info: Request
|
||||
|
||||
@@ -454,7 +454,11 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata?
|
||||
|
||||
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("SessionBusyError", {
|
||||
sessionID: SessionID,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Session is busy: ${this.sessionID}`
|
||||
}
|
||||
}
|
||||
|
||||
export type NotFound = NotFoundError
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ function isSkillFrontmatter(data: unknown): data is { name: string; description?
|
||||
|
||||
export class InvalidError extends Schema.TaggedErrorClass<InvalidError>()("SkillInvalidError", {
|
||||
path: Schema.String,
|
||||
message: Schema.optional(Schema.String),
|
||||
message: Schema.String,
|
||||
issues: Schema.optional(Schema.Array(Issue)),
|
||||
}) {}
|
||||
|
||||
@@ -68,7 +68,11 @@ export class NameMismatchError extends Schema.TaggedErrorClass<NameMismatchError
|
||||
path: Schema.String,
|
||||
expected: Schema.String,
|
||||
actual: Schema.String,
|
||||
}) {}
|
||||
}) {
|
||||
override get message() {
|
||||
return `Skill name mismatch at ${this.path}: expected "${this.expected}", got "${this.actual}"`
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Skill.NotFoundError", {
|
||||
name: Schema.String,
|
||||
|
||||
@@ -18,6 +18,7 @@ const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
||||
const SAMPLE_BYTES = 4096
|
||||
const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"])
|
||||
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- Internal sentinel used only to terminate the read stream.
|
||||
class ReadStop extends Schema.TaggedErrorClass<ReadStop>()("ReadStop", {}) {}
|
||||
|
||||
// `offset` and `limit` were originally `z.coerce.number()` — the runtime
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
const taggedErrorMessage = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "require Effect tagged errors to expose a message",
|
||||
},
|
||||
messages: {
|
||||
missing: "Schema.TaggedErrorClass must define a message schema field or instance message implementation.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
function visitClass(node) {
|
||||
const fields = taggedErrorFields(context, node.superClass)
|
||||
if (!fields) return
|
||||
if (fields.properties.some(hasRequiredMessage)) return
|
||||
if (node.body.body.some(hasInstanceMessage)) return
|
||||
context.report({ node, messageId: "missing" })
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: visitClass,
|
||||
ClassExpression: visitClass,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function taggedErrorFields(context, superClass) {
|
||||
if (superClass?.type !== "CallExpression" || superClass.arguments.length < 2) return null
|
||||
if (superClass.arguments[1].type !== "ObjectExpression") return null
|
||||
const factory = superClass.callee
|
||||
if (factory.type !== "CallExpression" || factory.arguments.length !== 0) return null
|
||||
const taggedError = factory.callee
|
||||
if (taggedError.type !== "MemberExpression" || taggedError.computed) return null
|
||||
if (taggedError.object.type !== "Identifier" || !isEffectSchema(context, taggedError.object)) return null
|
||||
if (taggedError.property.type !== "Identifier" || taggedError.property.name !== "TaggedErrorClass") return null
|
||||
return superClass.arguments[1]
|
||||
}
|
||||
|
||||
function isEffectSchema(context, identifier) {
|
||||
const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name)
|
||||
if (!variable || variable.defs.length !== 1 || variable.defs[0].type !== "ImportBinding") return false
|
||||
const definition = variable.defs[0]
|
||||
if (definition.parent.source.value === "effect")
|
||||
return definition.node.type === "ImportSpecifier" && definition.node.imported.name === "Schema"
|
||||
return definition.parent.source.value === "effect/Schema" && definition.node.type === "ImportNamespaceSpecifier"
|
||||
}
|
||||
|
||||
function findVariable(scope, name) {
|
||||
return scope.set.get(name) ?? (scope.upper ? findVariable(scope.upper, name) : null)
|
||||
}
|
||||
|
||||
function hasRequiredMessage(property) {
|
||||
if (propertyName(property) !== "message" || property.type !== "Property") return false
|
||||
return isStringSchema(property.value) && !isOptional(property.value)
|
||||
}
|
||||
|
||||
function isStringSchema(node) {
|
||||
if (node.type === "MemberExpression")
|
||||
return (
|
||||
node.object.type === "Identifier" &&
|
||||
node.object.name === "Schema" &&
|
||||
node.property.type === "Identifier" &&
|
||||
node.property.name === "String"
|
||||
)
|
||||
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false
|
||||
return isStringSchema(node.callee.object)
|
||||
}
|
||||
|
||||
function isOptional(node) {
|
||||
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false
|
||||
if (node.callee.object.type === "Identifier" && node.callee.object.name === "Schema")
|
||||
return node.callee.property.type === "Identifier" && node.callee.property.name === "optional"
|
||||
if (node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return false
|
||||
return node.arguments.some(
|
||||
(argument) =>
|
||||
argument.type === "MemberExpression" &&
|
||||
argument.object.type === "Identifier" &&
|
||||
argument.object.name === "Schema" &&
|
||||
argument.property.type === "Identifier" &&
|
||||
argument.property.name === "optional",
|
||||
)
|
||||
}
|
||||
|
||||
function propertyName(property) {
|
||||
if (property.type !== "Property" && property.type !== "PropertyDefinition" && property.type !== "MethodDefinition")
|
||||
return null
|
||||
if (property.computed) return null
|
||||
if (property.key.type === "Identifier" || property.key.type === "Literal") return property.key.name ?? property.key.value
|
||||
return null
|
||||
}
|
||||
|
||||
function hasInstanceMessage(member) {
|
||||
if (member.static || propertyName(member) !== "message") return false
|
||||
if (member.type === "PropertyDefinition") return member.value !== null && !isEmptyMessage(member.value)
|
||||
if (member.type !== "MethodDefinition" || member.kind !== "get") return false
|
||||
if (member.value.body.body.length === 0) return false
|
||||
const direct = member.value.body.body.length === 1 ? member.value.body.body[0] : null
|
||||
return direct?.type !== "ReturnStatement" || (direct.argument !== null && !isEmptyMessage(direct.argument))
|
||||
}
|
||||
|
||||
function isEmptyMessage(node) {
|
||||
return (node.type === "Identifier" && node.name === "undefined") || (node.type === "Literal" && !node.value)
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
name: "opencode",
|
||||
},
|
||||
rules: {
|
||||
"tagged-error-message": taggedErrorMessage,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import path from "path"
|
||||
import { mkdtemp, rm } from "fs/promises"
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: "schema field",
|
||||
errors: 0,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.String }) {}`,
|
||||
},
|
||||
{
|
||||
name: "getter",
|
||||
errors: 0,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) {
|
||||
override get message() { return "Example failed" }
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "initialized property",
|
||||
errors: 0,
|
||||
source: `import { Schema } from "effect"
|
||||
const Example = class extends Schema.TaggedErrorClass<Example>()("Example", {}) {
|
||||
override message = "Example failed"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "namespace import",
|
||||
errors: 0,
|
||||
source: `import * as Schema from "effect/Schema"
|
||||
export namespace Example { export class Error extends Schema.TaggedErrorClass<Error>()("Example", { message: Schema.String }) {} }`,
|
||||
},
|
||||
{
|
||||
name: "spread fields",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
const fields = { message: Schema.String }
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { ...fields }) {}`,
|
||||
},
|
||||
{
|
||||
name: "computed fields",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
const key = "message"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { [key]: Schema.String }) {}`,
|
||||
},
|
||||
{
|
||||
name: "optional message",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.optional(Schema.String) }) {}`,
|
||||
},
|
||||
{
|
||||
name: "piped optional message",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.String.pipe(Schema.optional) }) {}`,
|
||||
},
|
||||
{
|
||||
name: "unrelated Schema binding",
|
||||
errors: 0,
|
||||
source: `const Schema = getSchema()
|
||||
class Example extends Schema.TaggedErrorClass()("Example", {}) {}`,
|
||||
},
|
||||
{
|
||||
name: "shadowed Schema binding",
|
||||
errors: 0,
|
||||
source: `import { Schema } from "effect"
|
||||
function example() {
|
||||
const Schema = getSchema()
|
||||
return class extends Schema.TaggedErrorClass()("Example", {}) {}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "missing message",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { cause: Schema.Defect }) {}`,
|
||||
},
|
||||
{
|
||||
name: "static message",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { static message = "Example failed" }`,
|
||||
},
|
||||
{
|
||||
name: "uninitialized property",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { declare message: string }`,
|
||||
},
|
||||
{
|
||||
name: "undefined property",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { override message = undefined }`,
|
||||
},
|
||||
{
|
||||
name: "empty getter",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { override get message() { return "" } }`,
|
||||
},
|
||||
{
|
||||
name: "non-string message schema",
|
||||
errors: 1,
|
||||
source: `import { Schema } from "effect"
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.Unknown }) {}`,
|
||||
},
|
||||
{
|
||||
name: "documented disable",
|
||||
errors: 0,
|
||||
source: `import { Schema } from "effect"
|
||||
// oxlint-disable-next-line opencode/tagged-error-message -- internal control-flow sentinel
|
||||
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) {}`,
|
||||
},
|
||||
]
|
||||
|
||||
const directory = await mkdtemp(path.join(import.meta.dir, "../../.lint-tmp-"))
|
||||
const config = path.join(directory, ".oxlintrc.json")
|
||||
|
||||
try {
|
||||
await Bun.write(
|
||||
config,
|
||||
JSON.stringify({
|
||||
jsPlugins: [path.join(import.meta.dir, "opencode.mjs")],
|
||||
rules: { "opencode/tagged-error-message": "error" },
|
||||
}),
|
||||
)
|
||||
await Promise.all(
|
||||
cases.map(async (fixture, index) => {
|
||||
const file = path.join(directory, `${index}.ts`)
|
||||
await Bun.write(file, fixture.source)
|
||||
const result = Bun.spawnSync([
|
||||
path.join(import.meta.dir, "../../node_modules/.bin/oxlint"),
|
||||
"--config",
|
||||
config,
|
||||
"--format",
|
||||
"json",
|
||||
file,
|
||||
])
|
||||
if (!result.stdout.length) throw new Error(`${fixture.name}: ${result.stderr.toString()}`)
|
||||
const diagnostics: unknown = JSON.parse(result.stdout.toString())
|
||||
if (!hasDiagnostics(diagnostics)) throw new Error(`${fixture.name}: invalid Oxlint output`)
|
||||
const errors = diagnostics.diagnostics.filter(isErrorDiagnostic)
|
||||
if (errors.length !== fixture.errors || !errors.every(isRuleDiagnostic))
|
||||
throw new Error(
|
||||
`${fixture.name}: expected ${fixture.errors} errors, received ${errors.length}\n${result.stdout.toString()}`,
|
||||
)
|
||||
const exitCode = fixture.errors === 0 ? 0 : 1
|
||||
if (result.exitCode !== exitCode || result.signalCode)
|
||||
throw new Error(`${fixture.name}: expected exit ${exitCode}, received ${result.exitCode}`)
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log(`Validated ${cases.length} tagged error lint fixtures`)
|
||||
|
||||
function hasDiagnostics(value: unknown): value is { diagnostics: unknown[] } {
|
||||
return typeof value === "object" && value !== null && "diagnostics" in value && Array.isArray(value.diagnostics)
|
||||
}
|
||||
|
||||
function isRuleDiagnostic(value: unknown) {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"code" in value &&
|
||||
value.code === "opencode(tagged-error-message)"
|
||||
)
|
||||
}
|
||||
|
||||
function isErrorDiagnostic(value: unknown) {
|
||||
return typeof value === "object" && value !== null && "severity" in value && value.severity === "error"
|
||||
}
|
||||
Reference in New Issue
Block a user