Unified · Split
| @@ -1,6 +1,9 @@ | |||
| 1 | import type { WorkspaceId } from "#schema"; | 1 | import type { WorkspaceId } from "#schema"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | import { | ||
| 5 | RuntimeLocalWorkspaceMismatchError, | ||
| 6 | } from "../runtime/local-errors"; | ||
| 4 | import { requireRuntimeLocalWorkspace } from "../runtime/local-runtime-context"; | 7 | import { requireRuntimeLocalWorkspace } from "../runtime/local-runtime-context"; |
| 5 | import { ControlPlaneForbiddenError } from "./errors"; | 8 | import { ControlPlaneForbiddenError } from "./errors"; |
| 6 | 9 | ||
| @@ -13,7 +16,10 @@ export const resolveRequestedLocalWorkspace = ( · 6 unmodified lines | |||
| 13 | new ControlPlaneForbiddenError({ | 16 | new ControlPlaneForbiddenError({ |
| 14 | operation, | 17 | operation, |
| 15 | message: "Requested workspace is not the active local workspace", | 18 | message: "Requested workspace is not the active local workspace", |
| 16 | details: cause instanceof Error ? cause.message : String(cause), | 19 | details: |
| 20 | cause instanceof RuntimeLocalWorkspaceMismatchError | ||
| 21 | ? `requestedWorkspaceId=${cause.requestedWorkspaceId} activeWorkspaceId=${cause.activeWorkspaceId}` | ||
| 22 | : "Runtime local workspace is unavailable", | ||
| 17 | }) | 23 | }) |
| 18 | ), | 24 | ), |
| 19 | ); | 25 | ); |
| @@ -17,6 +17,11 @@ import { · 16 unmodified lines | |||
| 17 | } from "#schema"; | 17 | } from "#schema"; |
| 18 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 19 | import * as Schema from "effect/Schema"; | 19 | import * as Schema from "effect/Schema"; |
| 20 | import { | ||
| 21 | LocalExecutorConfigDecodeError, | ||
| 22 | LocalFileSystemError, | ||
| 23 | unknownLocalErrorDetails, | ||
| 24 | } from "./local-errors"; | ||
| 20 | 25 | ||
| 21 | const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema); | 26 | const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema); |
| 22 | 27 | ||
| @@ -34,10 +39,13 @@ const provideNodeFileSystem = <A, E, R>( · 11 unmodified lines | |||
| 34 | Exclude<R, FileSystem.FileSystem> | 39 | Exclude<R, FileSystem.FileSystem> |
| 35 | >; | 40 | >; |
| 36 | 41 | ||
| 37 | const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => { | 42 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| 38 | const message = cause instanceof Error ? cause.message : String(cause); | 43 | new LocalFileSystemError({ |
| 39 | return new Error(`Failed to ${action} ${path}: ${message}`); | 44 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 40 | }; | 45 | action, |
| 46 | path, | ||
| 47 | details: unknownLocalErrorDetails(cause), | ||
| 48 | }); | ||
| 41 | 49 | ||
| 42 | const trimOrUndefined = (value: string | undefined | null): string | undefined => { | 50 | const trimOrUndefined = (value: string | undefined | null): string | undefined => { |
| 43 | const trimmed = value?.trim(); | 51 | const trimmed = value?.trim(); |
| @@ -106,7 +114,7 @@ export const resolveHomeConfigPath = (input: { · 62 unmodified lines | |||
| 106 | env?: NodeJS.ProcessEnv; | 114 | env?: NodeJS.ProcessEnv; |
| 107 | platform?: NodeJS.Platform; | 115 | platform?: NodeJS.Platform; |
| 108 | homeDirectory?: string; | 116 | homeDirectory?: string; |
| 109 | } = {}): Effect.Effect<string, Error> => | 117 | } = {}): Effect.Effect<string, LocalFileSystemError> => |
| 110 | provideNodeFileSystem(Effect.gen(function* () { | 118 | provideNodeFileSystem(Effect.gen(function* () { |
| 111 | const fs = yield* FileSystem.FileSystem; | 119 | const fs = yield* FileSystem.FileSystem; |
| 112 | const candidates = resolveDefaultHomeConfigCandidates(input); | 120 | const candidates = resolveDefaultHomeConfigCandidates(input); |
| @@ -152,13 +160,23 @@ const parseJsonc = (input: { path: string; content: string }): LocalExecutorConf · 39 unmodified lines | |||
| 152 | allowTrailingComma: true, | 160 | allowTrailingComma: true, |
| 153 | }); | 161 | }); |
| 154 | if (errors.length > 0) { | 162 | if (errors.length > 0) { |
| 155 | throw new Error(formatJsoncParseErrors(input.content, errors)); | 163 | throw new LocalExecutorConfigDecodeError({ |
| 164 | message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`, | ||
| 165 | path: input.path, | ||
| 166 | details: formatJsoncParseErrors(input.content, errors), | ||
| 167 | }); | ||
| 156 | } | 168 | } |
| 157 | 169 | ||
| 158 | return decodeLocalExecutorConfig(parsed); | 170 | return decodeLocalExecutorConfig(parsed); |
| 159 | } catch (cause) { | 171 | } catch (cause) { |
| 160 | const message = cause instanceof Error ? cause.message : String(cause); | 172 | if (cause instanceof LocalExecutorConfigDecodeError) { |
| 161 | throw new Error(`Invalid executor config at ${input.path}: ${message}`); | 173 | throw cause; |
| 174 | } | ||
| 175 | throw new LocalExecutorConfigDecodeError({ | ||
| 176 | message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 177 | path: input.path, | ||
| 178 | details: unknownLocalErrorDetails(cause), | ||
| 179 | }); | ||
| 162 | } | 180 | } |
| 163 | }; | 181 | }; |
| 164 | 182 | ||
| @@ -320,7 +338,7 @@ export type LoadedLocalExecutorConfig = { · 155 unmodified lines | |||
| 320 | export const resolveLocalWorkspaceContext = (input: { | 338 | export const resolveLocalWorkspaceContext = (input: { |
| 321 | cwd?: string; | 339 | cwd?: string; |
| 322 | workspaceRoot?: string; | 340 | workspaceRoot?: string; |
| 323 | } = {}): Effect.Effect<ResolvedLocalWorkspaceContext, Error> => | 341 | } = {}): Effect.Effect<ResolvedLocalWorkspaceContext, LocalFileSystemError> => |
| 324 | provideNodeFileSystem(Effect.gen(function* () { | 342 | provideNodeFileSystem(Effect.gen(function* () { |
| 325 | const cwd = resolve(input.cwd ?? process.cwd()); | 343 | const cwd = resolve(input.cwd ?? process.cwd()); |
| 326 | const workspaceRoot = resolve( | 344 | const workspaceRoot = resolve( |
| @@ -344,7 +362,7 @@ export const resolveLocalWorkspaceContext = (input: { · 17 unmodified lines | |||
| 344 | 362 | ||
| 345 | export const readOptionalLocalExecutorConfig = ( | 363 | export const readOptionalLocalExecutorConfig = ( |
| 346 | path: string, | 364 | path: string, |
| 347 | ): Effect.Effect<LocalExecutorConfig | null, Error> => | 365 | ): Effect.Effect<LocalExecutorConfig | null, LocalFileSystemError | LocalExecutorConfigDecodeError> => |
| 348 | provideNodeFileSystem(Effect.gen(function* () { | 366 | provideNodeFileSystem(Effect.gen(function* () { |
| 349 | const fs = yield* FileSystem.FileSystem; | 367 | const fs = yield* FileSystem.FileSystem; |
| 350 | const exists = yield* fs.exists(path).pipe( | 368 | const exists = yield* fs.exists(path).pipe( |
| @@ -360,13 +378,22 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines | |||
| 360 | return yield* Effect.try({ | 378 | return yield* Effect.try({ |
| 361 | try: () => parseJsonc({ path, content }), | 379 | try: () => parseJsonc({ path, content }), |
| 362 | catch: (cause) => | 380 | catch: (cause) => |
| 363 | cause instanceof Error ? cause : new Error(String(cause)), | 381 | cause instanceof LocalExecutorConfigDecodeError |
| 382 | ? cause | ||
| 383 | : new LocalExecutorConfigDecodeError({ | ||
| 384 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 385 | path, | ||
| 386 | details: unknownLocalErrorDetails(cause), | ||
| 387 | }), | ||
| 364 | }); | 388 | }); |
| 365 | })); | 389 | })); |
| 366 | 390 | ||
| 367 | export const loadLocalExecutorConfig = ( | 391 | export const loadLocalExecutorConfig = ( |
| 368 | context: ResolvedLocalWorkspaceContext, | 392 | context: ResolvedLocalWorkspaceContext, |
| 369 | ): Effect.Effect<LoadedLocalExecutorConfig, Error> => | 393 | ): Effect.Effect< |
| 394 | LoadedLocalExecutorConfig, | ||
| 395 | LocalFileSystemError | LocalExecutorConfigDecodeError | ||
| 396 | > => | ||
| 370 | Effect.gen(function* () { | 397 | Effect.gen(function* () { |
| 371 | const [homeConfig, projectConfig] = yield* Effect.all([ | 398 | const [homeConfig, projectConfig] = yield* Effect.all([ |
| 372 | readOptionalLocalExecutorConfig(context.homeConfigPath), | 399 | readOptionalLocalExecutorConfig(context.homeConfigPath), |
| @@ -388,7 +415,7 @@ export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string = · 15 unmodified lines | |||
| 388 | export const writeProjectLocalExecutorConfig = (input: { | 415 | export const writeProjectLocalExecutorConfig = (input: { |
| 389 | context: ResolvedLocalWorkspaceContext; | 416 | context: ResolvedLocalWorkspaceContext; |
| 390 | config: LocalExecutorConfig; | 417 | config: LocalExecutorConfig; |
| 391 | }): Effect.Effect<void, Error> => | 418 | }): Effect.Effect<void, LocalFileSystemError> => |
| 392 | provideNodeFileSystem(Effect.gen(function* () { | 419 | provideNodeFileSystem(Effect.gen(function* () { |
| 393 | const fs = yield* FileSystem.FileSystem; | 420 | const fs = yield* FileSystem.FileSystem; |
| 394 | yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe( | 421 | yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe( |
| @@ -0,0 +1,54 @@ | |||
| 1 | import * as Data from "effect/Data"; | ||
| 2 | |||
| 3 | export const unknownLocalErrorDetails = (cause: unknown): string => | ||
| 4 | cause instanceof Error ? cause.message : String(cause); | ||
| 5 | |||
| 6 | export class LocalFileSystemError extends Data.TaggedError("LocalFileSystemError")<{ | ||
| 7 | readonly message: string; | ||
| 8 | readonly action: string; | ||
| 9 | readonly path: string; | ||
| 10 | readonly details: string; | ||
| 11 | }> {} | ||
| 12 | |||
| 13 | export class LocalExecutorConfigDecodeError extends Data.TaggedError("LocalExecutorConfigDecodeError")<{ | ||
| 14 | readonly message: string; | ||
| 15 | readonly path: string; | ||
| 16 | readonly details: string; | ||
| 17 | }> {} | ||
| 18 | |||
| 19 | export class LocalWorkspaceStateDecodeError extends Data.TaggedError("LocalWorkspaceStateDecodeError")<{ | ||
| 20 | readonly message: string; | ||
| 21 | readonly path: string; | ||
| 22 | readonly details: string; | ||
| 23 | }> {} | ||
| 24 | |||
| 25 | export class LocalSourceArtifactDecodeError extends Data.TaggedError("LocalSourceArtifactDecodeError")<{ | ||
| 26 | readonly message: string; | ||
| 27 | readonly path: string; | ||
| 28 | readonly details: string; | ||
| 29 | }> {} | ||
| 30 | |||
| 31 | export class RuntimeLocalWorkspaceUnavailableError extends Data.TaggedError("RuntimeLocalWorkspaceUnavailableError")<{ | ||
| 32 | readonly message: string; | ||
| 33 | }> {} | ||
| 34 | |||
| 35 | export class RuntimeLocalWorkspaceMismatchError extends Data.TaggedError("RuntimeLocalWorkspaceMismatchError")<{ | ||
| 36 | readonly message: string; | ||
| 37 | readonly requestedWorkspaceId: string; | ||
| 38 | readonly activeWorkspaceId: string; | ||
| 39 | }> {} | ||
| 40 | |||
| 41 | export class LocalConfiguredSourceNotFoundError extends Data.TaggedError("LocalConfiguredSourceNotFoundError")<{ | ||
| 42 | readonly message: string; | ||
| 43 | readonly sourceId: string; | ||
| 44 | }> {} | ||
| 45 | |||
| 46 | export class LocalSourceArtifactMissingError extends Data.TaggedError("LocalSourceArtifactMissingError")<{ | ||
| 47 | readonly message: string; | ||
| 48 | readonly sourceId: string; | ||
| 49 | }> {} | ||
| 50 | |||
| 51 | export class LocalUnsupportedSourceKindError extends Data.TaggedError("LocalUnsupportedSourceKindError")<{ | ||
| 52 | readonly message: string; | ||
| 53 | readonly kind: string; | ||
| 54 | }> {} | ||
| @@ -7,6 +7,10 @@ import type { · 6 unmodified lines | |||
| 7 | LoadedLocalExecutorConfig, | 7 | LoadedLocalExecutorConfig, |
| 8 | ResolvedLocalWorkspaceContext, | 8 | ResolvedLocalWorkspaceContext, |
| 9 | } from "./local-config"; | 9 | } from "./local-config"; |
| 10 | import { | ||
| 11 | RuntimeLocalWorkspaceMismatchError, | ||
| 12 | RuntimeLocalWorkspaceUnavailableError, | ||
| 13 | } from "./local-errors"; | ||
| 10 | 14 | ||
| 11 | export type RuntimeLocalWorkspaceState = { | 15 | export type RuntimeLocalWorkspaceState = { |
| 12 | context: ResolvedLocalWorkspaceContext; | 16 | context: ResolvedLocalWorkspaceContext; |
| @@ -28,24 +32,37 @@ export const getRuntimeLocalWorkspaceOption = () => · 15 unmodified lines | |||
| 28 | Effect.map((option) => (Option.isSome(option) ? option.value : null)), | 32 | Effect.map((option) => (Option.isSome(option) ? option.value : null)), |
| 29 | ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>; | 33 | ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>; |
| 30 | 34 | ||
| 31 | export const requireRuntimeLocalWorkspace = (workspaceId?: WorkspaceId) => | 35 | export const requireRuntimeLocalWorkspace = ( |
| 32 | Effect.flatMap(getRuntimeLocalWorkspaceOption(), (runtimeLocalWorkspace) => { | 36 | workspaceId?: WorkspaceId, |
| 37 | ): Effect.Effect< | ||
| 38 | RuntimeLocalWorkspaceState, | ||
| 39 | RuntimeLocalWorkspaceUnavailableError | RuntimeLocalWorkspaceMismatchError, | ||
| 40 | never | ||
| 41 | > => | ||
| 42 | Effect.gen(function* () { | ||
| 43 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | ||
| 33 | if (runtimeLocalWorkspace === null) { | 44 | if (runtimeLocalWorkspace === null) { |
| 34 | return Effect.fail(new Error("Runtime local workspace is unavailable")); | 45 | return yield* Effect.fail( |
| 46 | new RuntimeLocalWorkspaceUnavailableError({ | ||
| 47 | message: "Runtime local workspace is unavailable", | ||
| 48 | }), | ||
| 49 | ); | ||
| 35 | } | 50 | } |
| 36 | 51 | ||
| 37 | if ( | 52 | if ( |
| 38 | workspaceId !== undefined | 53 | workspaceId !== undefined |
| 39 | && runtimeLocalWorkspace.installation.workspaceId !== workspaceId | 54 | && runtimeLocalWorkspace.installation.workspaceId !== workspaceId |
| 40 | ) { | 55 | ) { |
| 41 | return Effect.fail( | 56 | return yield* Effect.fail( |
| 42 | new Error( | 57 | new RuntimeLocalWorkspaceMismatchError({ |
| 43 | `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`, | 58 | message: `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`, |
| 44 | ), | 59 | requestedWorkspaceId: workspaceId, |
| 60 | activeWorkspaceId: runtimeLocalWorkspace.installation.workspaceId, | ||
| 61 | }), | ||
| 45 | ); | 62 | ); |
| 46 | } | 63 | } |
| 47 | 64 | ||
| 48 | return Effect.succeed(runtimeLocalWorkspace); | 65 | return runtimeLocalWorkspace; |
| 49 | }); | 66 | }); |
| 50 | 67 | ||
| 51 | export const requireRuntimeLocalAccountId = (workspaceId?: WorkspaceId) => | 68 | export const requireRuntimeLocalAccountId = (workspaceId?: WorkspaceId) => |
| @@ -17,6 +17,11 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines | |||
| 17 | import * as Schema from "effect/Schema"; | 17 | import * as Schema from "effect/Schema"; |
| 18 | 18 | ||
| 19 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; | 19 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; |
| 20 | import { | ||
| 21 | LocalFileSystemError, | ||
| 22 | LocalSourceArtifactDecodeError, | ||
| 23 | unknownLocalErrorDetails, | ||
| 24 | } from "./local-errors"; | ||
| 20 | import { | 25 | import { |
| 21 | createSourceRecipeRevisionRecord, | 26 | createSourceRecipeRevisionRecord, |
| 22 | stableSourceRecipeId, | 27 | stableSourceRecipeId, |
| @@ -52,10 +57,13 @@ const provideNodeFileSystem = <A, E, R>( · 29 unmodified lines | |||
| 52 | Exclude<R, FileSystem.FileSystem> | 57 | Exclude<R, FileSystem.FileSystem> |
| 53 | >; | 58 | >; |
| 54 | 59 | ||
| 55 | const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => { | 60 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| 56 | const message = cause instanceof Error ? cause.message : String(cause); | 61 | new LocalFileSystemError({ |
| 57 | return new Error(`Failed to ${action} ${path}: ${message}`); | 62 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 58 | }; | 63 | action, |
| 64 | path, | ||
| 65 | details: unknownLocalErrorDetails(cause), | ||
| 66 | }); | ||
| 59 | 67 | ||
| 60 | const localSourceArtifactPath = (input: { | 68 | const localSourceArtifactPath = (input: { |
| 61 | context: ResolvedLocalWorkspaceContext; | 69 | context: ResolvedLocalWorkspaceContext; |
| @@ -157,7 +165,10 @@ export const buildLocalSourceArtifact = (input: { · 95 unmodified lines | |||
| 157 | export const readLocalSourceArtifact = (input: { | 165 | export const readLocalSourceArtifact = (input: { |
| 158 | context: ResolvedLocalWorkspaceContext; | 166 | context: ResolvedLocalWorkspaceContext; |
| 159 | sourceId: string; | 167 | sourceId: string; |
| 160 | }): Effect.Effect<LocalSourceArtifact | null, Error> => | 168 | }): Effect.Effect< |
| 169 | LocalSourceArtifact | null, | ||
| 170 | LocalFileSystemError | LocalSourceArtifactDecodeError | ||
| 171 | > => | ||
| 161 | provideNodeFileSystem(Effect.gen(function* () { | 172 | provideNodeFileSystem(Effect.gen(function* () { |
| 162 | const fs = yield* FileSystem.FileSystem; | 173 | const fs = yield* FileSystem.FileSystem; |
| 163 | const path = localSourceArtifactPath(input); | 174 | const path = localSourceArtifactPath(input); |
| @@ -174,8 +185,11 @@ export const readLocalSourceArtifact = (input: { · 10 unmodified lines | |||
| 174 | return yield* Effect.try({ | 185 | return yield* Effect.try({ |
| 175 | try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown), | 186 | try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown), |
| 176 | catch: (cause) => { | 187 | catch: (cause) => { |
| 177 | const message = cause instanceof Error ? cause.message : String(cause); | 188 | return new LocalSourceArtifactDecodeError({ |
| 178 | return new Error(`Invalid local source artifact at ${path}: ${message}`); | 189 | message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 190 | path, | ||
| 191 | details: unknownLocalErrorDetails(cause), | ||
| 192 | }); | ||
| 179 | }, | 193 | }, |
| 180 | }); | 194 | }); |
| 181 | })); | 195 | })); |
| @@ -184,7 +198,7 @@ export const writeLocalSourceArtifact = (input: { · 2 unmodified lines | |||
| 184 | context: ResolvedLocalWorkspaceContext; | 198 | context: ResolvedLocalWorkspaceContext; |
| 185 | sourceId: string; | 199 | sourceId: string; |
| 186 | artifact: LocalSourceArtifact; | 200 | artifact: LocalSourceArtifact; |
| 187 | }): Effect.Effect<void, Error> => | 201 | }): Effect.Effect<void, LocalFileSystemError> => |
| 188 | provideNodeFileSystem(Effect.gen(function* () { | 202 | provideNodeFileSystem(Effect.gen(function* () { |
| 189 | const fs = yield* FileSystem.FileSystem; | 203 | const fs = yield* FileSystem.FileSystem; |
| 190 | const directory = join(input.context.artifactsDirectory, "sources"); | 204 | const directory = join(input.context.artifactsDirectory, "sources"); |
| @@ -200,7 +214,7 @@ export const writeLocalSourceArtifact = (input: { · 9 unmodified lines | |||
| 200 | export const removeLocalSourceArtifact = (input: { | 214 | export const removeLocalSourceArtifact = (input: { |
| 201 | context: ResolvedLocalWorkspaceContext; | 215 | context: ResolvedLocalWorkspaceContext; |
| 202 | sourceId: string; | 216 | sourceId: string; |
| 203 | }): Effect.Effect<void, Error> => | 217 | }): Effect.Effect<void, LocalFileSystemError> => |
| 204 | provideNodeFileSystem(Effect.gen(function* () { | 218 | provideNodeFileSystem(Effect.gen(function* () { |
| 205 | const fs = yield* FileSystem.FileSystem; | 219 | const fs = yield* FileSystem.FileSystem; |
| 206 | const path = localSourceArtifactPath(input); | 220 | const path = localSourceArtifactPath(input); |
| @@ -11,6 +11,11 @@ import * as Effect from "effect/Effect"; · 10 unmodified lines | |||
| 11 | import * as Schema from "effect/Schema"; | 11 | import * as Schema from "effect/Schema"; |
| 12 | 12 | ||
| 13 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; | 13 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; |
| 14 | import { | ||
| 15 | LocalFileSystemError, | ||
| 16 | LocalWorkspaceStateDecodeError, | ||
| 17 | unknownLocalErrorDetails, | ||
| 18 | } from "./local-errors"; | ||
| 14 | 19 | ||
| 15 | const WORKSPACE_STATE_BASENAME = "workspace-state.json"; | 20 | const WORKSPACE_STATE_BASENAME = "workspace-state.json"; |
| 16 | 21 | ||
| @@ -55,10 +60,13 @@ const provideNodeFileSystem = <A, E, R>( · 38 unmodified lines | |||
| 55 | Exclude<R, FileSystem.FileSystem> | 60 | Exclude<R, FileSystem.FileSystem> |
| 56 | >; | 61 | >; |
| 57 | 62 | ||
| 58 | const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => { | 63 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| 59 | const message = cause instanceof Error ? cause.message : String(cause); | 64 | new LocalFileSystemError({ |
| 60 | return new Error(`Failed to ${action} ${path}: ${message}`); | 65 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 61 | }; | 66 | action, |
| 67 | path, | ||
| 68 | details: unknownLocalErrorDetails(cause), | ||
| 69 | }); | ||
| 62 | 70 | ||
| 63 | const defaultLocalWorkspaceState = (): LocalWorkspaceState => ({ | 71 | const defaultLocalWorkspaceState = (): LocalWorkspaceState => ({ |
| 64 | version: 1, | 72 | version: 1, |
| @@ -72,7 +80,7 @@ export const localWorkspaceStatePath = ( · 7 unmodified lines | |||
| 72 | 80 | ||
| 73 | export const loadLocalWorkspaceState = ( | 81 | export const loadLocalWorkspaceState = ( |
| 74 | context: ResolvedLocalWorkspaceContext, | 82 | context: ResolvedLocalWorkspaceContext, |
| 75 | ): Effect.Effect<LocalWorkspaceState, Error> => | 83 | ): Effect.Effect<LocalWorkspaceState, LocalFileSystemError | LocalWorkspaceStateDecodeError> => |
| 76 | provideNodeFileSystem(Effect.gen(function* () { | 84 | provideNodeFileSystem(Effect.gen(function* () { |
| 77 | const fs = yield* FileSystem.FileSystem; | 85 | const fs = yield* FileSystem.FileSystem; |
| 78 | const path = localWorkspaceStatePath(context); | 86 | const path = localWorkspaceStatePath(context); |
| @@ -89,8 +97,11 @@ export const loadLocalWorkspaceState = ( · 10 unmodified lines | |||
| 89 | return yield* Effect.try({ | 97 | return yield* Effect.try({ |
| 90 | try: () => decodeLocalWorkspaceState(JSON.parse(content) as unknown), | 98 | try: () => decodeLocalWorkspaceState(JSON.parse(content) as unknown), |
| 91 | catch: (cause) => { | 99 | catch: (cause) => { |
| 92 | const message = cause instanceof Error ? cause.message : String(cause); | 100 | return new LocalWorkspaceStateDecodeError({ |
| 93 | return new Error(`Invalid local workspace state at ${path}: ${message}`); | 101 | message: `Invalid local workspace state at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 102 | path, | ||
| 103 | details: unknownLocalErrorDetails(cause), | ||
| 104 | }); | ||
| 94 | }, | 105 | }, |
| 95 | }); | 106 | }); |
| 96 | })); | 107 | })); |
| @@ -98,7 +109,7 @@ export const loadLocalWorkspaceState = ( · 1 unmodified line | |||
| 98 | export const writeLocalWorkspaceState = (input: { | 109 | export const writeLocalWorkspaceState = (input: { |
| 99 | context: ResolvedLocalWorkspaceContext; | 110 | context: ResolvedLocalWorkspaceContext; |
| 100 | state: LocalWorkspaceState; | 111 | state: LocalWorkspaceState; |
| 101 | }): Effect.Effect<void, Error> => | 112 | }): Effect.Effect<void, LocalFileSystemError> => |
| 102 | provideNodeFileSystem(Effect.gen(function* () { | 113 | provideNodeFileSystem(Effect.gen(function* () { |
| 103 | const fs = yield* FileSystem.FileSystem; | 114 | const fs = yield* FileSystem.FileSystem; |
| 104 | yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe( | 115 | yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe( |
| @@ -17,6 +17,7 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines | |||
| 17 | import { | 17 | import { |
| 18 | readLocalSourceArtifact, | 18 | readLocalSourceArtifact, |
| 19 | } from "./local-source-artifacts"; | 19 | } from "./local-source-artifacts"; |
| 20 | import { LocalSourceArtifactMissingError } from "./local-errors"; | ||
| 20 | import { | 21 | import { |
| 21 | requireRuntimeLocalWorkspace, | 22 | requireRuntimeLocalWorkspace, |
| 22 | } from "./local-runtime-context"; | 23 | } from "./local-runtime-context"; |
| @@ -218,7 +219,7 @@ export const loadSourceWithRecipe = (input: { · 195 unmodified lines | |||
| 218 | workspaceId: WorkspaceId; | 219 | workspaceId: WorkspaceId; |
| 219 | sourceId: Source["id"]; | 220 | sourceId: Source["id"]; |
| 220 | actorAccountId?: AccountId | null; | 221 | actorAccountId?: AccountId | null; |
| 221 | }): Effect.Effect<LoadedSourceRecipe, Error, never> => | 222 | }): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> => |
| 222 | Effect.gen(function* () { | 223 | Effect.gen(function* () { |
| 223 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId); | 224 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId); |
| 224 | const source = yield* loadSourceById(input.rows, { | 225 | const source = yield* loadSourceById(input.rows, { |
| @@ -232,7 +233,10 @@ export const loadSourceWithRecipe = (input: { · 7 unmodified lines | |||
| 232 | }); | 233 | }); |
| 233 | if (artifact === null) { | 234 | if (artifact === null) { |
| 234 | return yield* Effect.fail( | 235 | return yield* Effect.fail( |
| 235 | new Error(`Recipe artifact missing for source ${input.sourceId}`), | 236 | new LocalSourceArtifactMissingError({ |
| 237 | message: `Recipe artifact missing for source ${input.sourceId}`, | ||
| 238 | sourceId: input.sourceId, | ||
| 239 | }), | ||
| 236 | ); | 240 | ); |
| 237 | } | 241 | } |
| 238 | 242 | ||
| @@ -30,6 +30,15 @@ import { · 29 unmodified lines | |||
| 30 | readLocalSourceArtifact, | 30 | readLocalSourceArtifact, |
| 31 | removeLocalSourceArtifact, | 31 | removeLocalSourceArtifact, |
| 32 | } from "./local-source-artifacts"; | 32 | } from "./local-source-artifacts"; |
| 33 | import { | ||
| 34 | LocalConfiguredSourceNotFoundError, | ||
| 35 | LocalExecutorConfigDecodeError, | ||
| 36 | LocalFileSystemError, | ||
| 37 | LocalUnsupportedSourceKindError, | ||
| 38 | LocalWorkspaceStateDecodeError, | ||
| 39 | RuntimeLocalWorkspaceMismatchError, | ||
| 40 | RuntimeLocalWorkspaceUnavailableError, | ||
| 41 | } from "./local-errors"; | ||
| 33 | import { | 42 | import { |
| 34 | requireRuntimeLocalWorkspace, | 43 | requireRuntimeLocalWorkspace, |
| 35 | } from "./local-runtime-context"; | 44 | } from "./local-runtime-context"; |
| @@ -231,7 +240,21 @@ const configAuthFromSource = (input: { · 195 unmodified lines | |||
| 231 | return input.existingConfigAuth; | 240 | return input.existingConfigAuth; |
| 232 | }; | 241 | }; |
| 233 | 242 | ||
| 234 | const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) => | 243 | const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{ |
| 244 | context: ResolvedLocalWorkspaceContext; | ||
| 245 | installation: { | ||
| 246 | workspaceId: WorkspaceId; | ||
| 247 | accountId: AccountId; | ||
| 248 | }; | ||
| 249 | loadedConfig: LoadedLocalExecutorConfig; | ||
| 250 | workspaceState: LocalWorkspaceState; | ||
| 251 | }, | ||
| 252 | | RuntimeLocalWorkspaceUnavailableError | ||
| 253 | | RuntimeLocalWorkspaceMismatchError | ||
| 254 | | LocalFileSystemError | ||
| 255 | | LocalExecutorConfigDecodeError | ||
| 256 | | LocalWorkspaceStateDecodeError, | ||
| 257 | never> => | ||
| 235 | Effect.gen(function* () { | 258 | Effect.gen(function* () { |
| 236 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); | 259 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); |
| 237 | const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context); | 260 | const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context); |
| @@ -256,11 +279,16 @@ const buildLocalSourceRecord = (input: { · 18 unmodified lines | |||
| 256 | }): Effect.Effect<{ | 279 | }): Effect.Effect<{ |
| 257 | source: Source; | 280 | source: Source; |
| 258 | sourceId: SourceId; | 281 | sourceId: SourceId; |
| 259 | }, Error, never> => | 282 | }, LocalConfiguredSourceNotFoundError | Error, never> => |
| 260 | Effect.gen(function* () { | 283 | Effect.gen(function* () { |
| 261 | const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId]; | 284 | const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId]; |
| 262 | if (!sourceConfig) { | 285 | if (!sourceConfig) { |
| 263 | return yield* Effect.fail(new Error(`Configured source not found for id ${input.sourceId}`)); | 286 | return yield* Effect.fail( |
| 287 | new LocalConfiguredSourceNotFoundError({ | ||
| 288 | message: `Configured source not found for id ${input.sourceId}`, | ||
| 289 | sourceId: input.sourceId, | ||
| 290 | }), | ||
| 291 | ); | ||
| 264 | } | 292 | } |
| 265 | 293 | ||
| 266 | const existingState = input.workspaceState.sources[input.sourceId]; | 294 | const existingState = input.workspaceState.sources[input.sourceId]; |
| @@ -356,7 +384,17 @@ export const loadSourcesInWorkspace = ( · 89 unmodified lines | |||
| 356 | options: { | 384 | options: { |
| 357 | actorAccountId?: AccountId | null; | 385 | actorAccountId?: AccountId | null; |
| 358 | } = {}, | 386 | } = {}, |
| 359 | ) => | 387 | ): Effect.Effect< |
| 388 | readonly Source[], | ||
| 389 | | RuntimeLocalWorkspaceUnavailableError | ||
| 390 | | RuntimeLocalWorkspaceMismatchError | ||
| 391 | | LocalFileSystemError | ||
| 392 | | LocalExecutorConfigDecodeError | ||
| 393 | | LocalWorkspaceStateDecodeError | ||
| 394 | | LocalConfiguredSourceNotFoundError | ||
| 395 | | Error, | ||
| 396 | never | ||
| 397 | > => | ||
| 360 | Effect.gen(function* () { | 398 | Effect.gen(function* () { |
| 361 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId); | 399 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId); |
| 362 | const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId); | 400 | const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId); |
| @@ -382,13 +420,26 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 19 unmodified lines | |||
| 382 | workspaceId: WorkspaceId; | 420 | workspaceId: WorkspaceId; |
| 383 | sourceId: Source["id"]; | 421 | sourceId: Source["id"]; |
| 384 | actorAccountId?: AccountId | null; | 422 | actorAccountId?: AccountId | null; |
| 385 | }) => | 423 | }): Effect.Effect< |
| 424 | Source, | ||
| 425 | | RuntimeLocalWorkspaceUnavailableError | ||
| 426 | | RuntimeLocalWorkspaceMismatchError | ||
| 427 | | LocalFileSystemError | ||
| 428 | | LocalExecutorConfigDecodeError | ||
| 429 | | LocalWorkspaceStateDecodeError | ||
| 430 | | LocalConfiguredSourceNotFoundError | ||
| 431 | | Error, | ||
| 432 | never | ||
| 433 | > => | ||
| 386 | Effect.gen(function* () { | 434 | Effect.gen(function* () { |
| 387 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId); | 435 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId); |
| 388 | const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId); | 436 | const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId); |
| 389 | if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { | 437 | if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { |
| 390 | return yield* Effect.fail( | 438 | return yield* Effect.fail( |
| 391 | new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`), | 439 | new LocalConfiguredSourceNotFoundError({ |
| 440 | message: `Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | ||
| 441 | sourceId: input.sourceId, | ||
| 442 | }), | ||
| 392 | ); | 443 | ); |
| 393 | } | 444 | } |
| 394 | 445 | ||
| @@ -456,7 +507,10 @@ const configSourceFromLocalSource = (input: { · 61 unmodified lines | |||
| 456 | binding: cloneJson(input.source.binding) as Extract<LocalConfigSource, { kind: "mcp" }>["binding"], | 507 | binding: cloneJson(input.source.binding) as Extract<LocalConfigSource, { kind: "mcp" }>["binding"], |
| 457 | }; | 508 | }; |
| 458 | default: | 509 | default: |
| 459 | throw new Error(`Unsupported source kind for local config: ${input.source.kind}`); | 510 | throw new LocalUnsupportedSourceKindError({ |
| 511 | message: `Unsupported source kind for local config: ${input.source.kind}`, | ||
| 512 | kind: input.source.kind, | ||
| 513 | }); | ||
| 460 | } | 514 | } |
| 461 | }; | 515 | }; |
| 462 | 516 | ||