| ... | @@ -9,11 +9,9 @@ import { | ... | @@ -9,11 +9,9 @@ import { |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import * as Console from "effect/Console"; | 10 | import * as Console from "effect/Console"; |
| 11 | import * as Crypto from "effect/Crypto"; | 11 | import * as Crypto from "effect/Crypto"; |
| 12 | import * as Data from "effect/Data"; | | |
| 13 | import * as DateTime from "effect/DateTime"; | 12 | import * as DateTime from "effect/DateTime"; |
| 14 | import * as Duration from "effect/Duration"; | 13 | import * as Duration from "effect/Duration"; |
| 15 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 16 | import * as Exit from "effect/Exit"; | | |
| 17 | import * as FileSystem from "effect/FileSystem"; | 15 | import * as FileSystem from "effect/FileSystem"; |
| 18 | import * as Layer from "effect/Layer"; | 16 | import * as Layer from "effect/Layer"; |
| 19 | import * as Option from "effect/Option"; | 17 | import * as Option from "effect/Option"; |
| ... | @@ -52,16 +50,64 @@ type ProjectCliDispatchCommand = Extract< | ... | @@ -52,16 +50,64 @@ type ProjectCliDispatchCommand = Extract< |
| 52 | { type: "project.create" | "project.meta.update" | "project.delete" } | 50 | { type: "project.create" | "project.meta.update" | "project.delete" } |
| 53 | >; | 51 | >; |
| 54 | | 52 | |
| 55 | class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{ | 53 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); |
| 56 | readonly message: string; | 54 | const ProjectCommandOperation = Schema.Literals([ |
| 57 | }> {} | 55 | "generateProjectCommandId", |
| | 56 | "callLiveServer", |
| | 57 | "validateProjectTitle", |
| | 58 | "resolveProjectTarget", |
| | 59 | "addProject", |
| | 60 | ]); |
| | 61 | |
| | 62 | export class ProjectCommandError extends Schema.TaggedErrorClass<ProjectCommandError>()( |
| | 63 | "ProjectCommandError", |
| | 64 | { |
| | 65 | operation: ProjectCommandOperation, |
| | 66 | detail: Schema.String, |
| | 67 | code: Schema.optional(Schema.String), |
| | 68 | traceId: Schema.optional(Schema.String), |
| | 69 | status: Schema.optional(Schema.Number), |
| | 70 | cause: Schema.optional(Schema.Defect()), |
| | 71 | }, |
| | 72 | ) { |
| | 73 | static fromLiveServerRequest(cause: unknown): ProjectCommandError { |
| | 74 | if (isEnvironmentHttpCommonError(cause)) { |
| | 75 | return new ProjectCommandError({ |
| | 76 | operation: "callLiveServer", |
| | 77 | detail: `Server request failed (${cause.code}, trace ${cause.traceId}).`, |
| | 78 | code: cause.code, |
| | 79 | traceId: cause.traceId, |
| | 80 | cause, |
| | 81 | }); |
| | 82 | } |
| | 83 | if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) { |
| | 84 | return new ProjectCommandError({ |
| | 85 | operation: "callLiveServer", |
| | 86 | detail: `Server request failed with undeclared status ${cause.response.status}.`, |
| | 87 | status: cause.response.status, |
| | 88 | cause, |
| | 89 | }); |
| | 90 | } |
| | 91 | return new ProjectCommandError({ |
| | 92 | operation: "callLiveServer", |
| | 93 | detail: "Failed to call the running server.", |
| | 94 | cause, |
| | 95 | }); |
| | 96 | } |
| | 97 | |
| | 98 | override get message(): string { |
| | 99 | return this.detail; |
| | 100 | } |
| | 101 | } |
| 58 | | 102 | |
| 59 | const projectCommandUuid = Crypto.Crypto.pipe( | 103 | const projectCommandUuid = Crypto.Crypto.pipe( |
| 60 | Effect.flatMap((crypto) => crypto.randomUUIDv4), | 104 | Effect.flatMap((crypto) => crypto.randomUUIDv4), |
| 61 | Effect.mapError( | 105 | Effect.mapError( |
| 62 | () => | 106 | (cause) => |
| 63 | new ProjectCommandError({ | 107 | new ProjectCommandError({ |
| 64 | message: "Failed to generate a project command identifier.", | 108 | operation: "generateProjectCommandId", |
| | 109 | detail: "Failed to generate a project command identifier.", |
| | 110 | cause, |
| 65 | }), | 111 | }), |
| 66 | ), | 112 | ), |
| 67 | ); | 113 | ); |
| ... | @@ -75,7 +121,6 @@ const ProjectCliRuntimeLive = Layer.mergeAll( | ... | @@ -75,7 +121,6 @@ const ProjectCliRuntimeLive = Layer.mergeAll( |
| 75 | ); | 121 | ); |
| 76 | | 122 | |
| 77 | const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1); | 123 | const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1); |
| 78 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); | | |
| 79 | const withProjectCliSessionToken = <A, E, R>( | 124 | const withProjectCliSessionToken = <A, E, R>( |
| 80 | environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], | 125 | environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], |
| 81 | run: (token: string) => Effect.Effect<A, E, R>, | 126 | run: (token: string) => Effect.Effect<A, E, R>, |
| ... | @@ -92,28 +137,6 @@ const withProjectCliSessionToken = <A, E, R>( | ... | @@ -92,28 +137,6 @@ const withProjectCliSessionToken = <A, E, R>( |
| 92 | const withProjectCliLiveServerTimeout = <A, E, R>(effect: Effect.Effect<A, E, R>) => | 137 | const withProjectCliLiveServerTimeout = <A, E, R>(effect: Effect.Effect<A, E, R>) => |
| 93 | effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT)); | 138 | effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT)); |
| 94 | | 139 | |
| 95 | const failLiveServerRequest = (cause: unknown) => { | | |
| 96 | if (isEnvironmentHttpCommonError(cause)) { | | |
| 97 | return Effect.fail( | | |
| 98 | new ProjectCommandError({ | | |
| 99 | message: `Server request failed (${cause.code}, trace ${cause.traceId}).`, | | |
| 100 | }), | | |
| 101 | ); | | |
| 102 | } | | |
| 103 | if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) { | | |
| 104 | return Effect.fail( | | |
| 105 | new ProjectCommandError({ | | |
| 106 | message: `Server request failed with undeclared status ${cause.response.status}.`, | | |
| 107 | }), | | |
| 108 | ); | | |
| 109 | } | | |
| 110 | return Effect.fail( | | |
| 111 | new ProjectCommandError({ | | |
| 112 | message: `Failed to call running server: ${String(cause)}.`, | | |
| 113 | }), | | |
| 114 | ); | | |
| 115 | }; | | |
| 116 | | | |
| 117 | const makeLiveServerClient = (origin: string) => | 140 | const makeLiveServerClient = (origin: string) => |
| 118 | HttpApiClient.make(EnvironmentHttpApi, { | 141 | HttpApiClient.make(EnvironmentHttpApi, { |
| 119 | baseUrl: origin, | 142 | baseUrl: origin, |
| ... | @@ -135,7 +158,10 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* ( | ... | @@ -135,7 +158,10 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* ( |
| 135 | if (trimmed.length > 0) { | 158 | if (trimmed.length > 0) { |
| 136 | return trimmed; | 159 | return trimmed; |
| 137 | } | 160 | } |
| 138 | return yield* new ProjectCommandError({ message: "Project title cannot be empty." }); | 161 | return yield* new ProjectCommandError({ |
| | 162 | operation: "validateProjectTitle", |
| | 163 | detail: "Project title cannot be empty.", |
| | 164 | }); |
| 139 | } | 165 | } |
| 140 | | 166 | |
| 141 | const path = yield* Path.Path; | 167 | const path = yield* Path.Path; |
| ... | @@ -149,7 +175,10 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( | ... | @@ -149,7 +175,10 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 149 | }) { | 175 | }) { |
| 150 | const trimmedIdentifier = input.identifier.trim(); | 176 | const trimmedIdentifier = input.identifier.trim(); |
| 151 | if (trimmedIdentifier.length === 0) { | 177 | if (trimmedIdentifier.length === 0) { |
| 152 | return yield* new ProjectCommandError({ message: "Project identifier cannot be empty." }); | 178 | return yield* new ProjectCommandError({ |
| | 179 | operation: "resolveProjectTarget", |
| | 180 | detail: "Project identifier cannot be empty.", |
| | 181 | }); |
| 153 | } | 182 | } |
| 154 | | 183 | |
| 155 | const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null); | 184 | const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null); |
| ... | @@ -162,12 +191,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( | ... | @@ -162,12 +191,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 162 | } satisfies ProjectMutationTarget; | 191 | } satisfies ProjectMutationTarget; |
| 163 | } | 192 | } |
| 164 | | 193 | |
| 165 | const normalizedWorkspaceRootResult = yield* Effect.exit( | 194 | const normalizedWorkspaceRootResult = yield* Effect.result( |
| 166 | normalizeWorkspaceRootForProjectCommand(trimmedIdentifier), | 195 | normalizeWorkspaceRootForProjectCommand(trimmedIdentifier), |
| 167 | ); | 196 | ); |
| 168 | const normalizedWorkspaceRoot = Exit.isSuccess(normalizedWorkspaceRootResult) | 197 | const normalizedWorkspaceRoot = |
| 169 | ? normalizedWorkspaceRootResult.value | 198 | normalizedWorkspaceRootResult._tag === "Success" ? normalizedWorkspaceRootResult.success : null; |
| 170 | : null; | | |
| 171 | | 199 | |
| 172 | const exactWorkspaceMatch = | 200 | const exactWorkspaceMatch = |
| 173 | normalizedWorkspaceRoot === null | 201 | normalizedWorkspaceRoot === null |
| ... | @@ -177,7 +205,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( | ... | @@ -177,7 +205,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 177 | const resolved = exactWorkspaceMatch; | 205 | const resolved = exactWorkspaceMatch; |
| 178 | if (!resolved) { | 206 | if (!resolved) { |
| 179 | return yield* new ProjectCommandError({ | 207 | return yield* new ProjectCommandError({ |
| 180 | message: `No active project found for '${trimmedIdentifier}'.`, | 208 | operation: "resolveProjectTarget", |
| | 209 | detail: `No active project found for '${trimmedIdentifier}'.`, |
| | 210 | ...(normalizedWorkspaceRootResult._tag === "Failure" |
| | 211 | ? { cause: normalizedWorkspaceRootResult.failure } |
| | 212 | : {}), |
| 181 | }); | 213 | }); |
| 182 | } | 214 | } |
| 183 | | 215 | |
| ... | @@ -194,7 +226,10 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) => | ... | @@ -194,7 +226,10 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) => |
| 194 | return yield* client.orchestration.snapshot({ | 226 | return yield* client.orchestration.snapshot({ |
| 195 | headers: { authorization: `Bearer ${bearerToken}` }, | 227 | headers: { authorization: `Bearer ${bearerToken}` }, |
| 196 | }); | 228 | }); |
| 197 | }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest)); | 229 | }).pipe( |
| | 230 | withProjectCliLiveServerTimeout, |
| | 231 | Effect.mapError(ProjectCommandError.fromLiveServerRequest), |
| | 232 | ); |
| 198 | | 233 | |
| 199 | const dispatchLiveOrchestrationCommand = ( | 234 | const dispatchLiveOrchestrationCommand = ( |
| 200 | origin: string, | 235 | origin: string, |
| ... | @@ -207,7 +242,10 @@ const dispatchLiveOrchestrationCommand = ( | ... | @@ -207,7 +242,10 @@ const dispatchLiveOrchestrationCommand = ( |
| 207 | headers: { authorization: `Bearer ${bearerToken}` }, | 242 | headers: { authorization: `Bearer ${bearerToken}` }, |
| 208 | payload: command, | 243 | payload: command, |
| 209 | } as Parameters<typeof client.orchestration.dispatch>[0]); | 244 | } as Parameters<typeof client.orchestration.dispatch>[0]); |
| 210 | }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest)); | 245 | }).pipe( |
| | 246 | withProjectCliLiveServerTimeout, |
| | 247 | Effect.mapError(ProjectCommandError.fromLiveServerRequest), |
| | 248 | ); |
| 211 | | 249 | |
| 212 | const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { | 250 | const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { |
| 213 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; | 251 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; |
| ... | @@ -232,11 +270,15 @@ const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecu | ... | @@ -232,11 +270,15 @@ const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecu |
| 232 | ), | 270 | ), |
| 233 | ); | 271 | ); |
| 234 | | 272 | |
| 235 | const attempted = yield* Effect.exit(attempt); | 273 | const attempted = yield* Effect.result(attempt); |
| 236 | if (Exit.isSuccess(attempted)) { | 274 | if (attempted._tag === "Success") { |
| 237 | return Option.some(attempted.value); | 275 | return Option.some(attempted.success); |
| 238 | } | 276 | } |
| 239 | | 277 | |
| | 278 | yield* Effect.logDebug("Failed to connect to the persisted project CLI server.", { |
| | 279 | origin: runtimeState.value.origin, |
| | 280 | cause: attempted.failure, |
| | 281 | }); |
| 240 | yield* clearPersistedServerRuntimeState(config.serverRuntimeStatePath); | 282 | yield* clearPersistedServerRuntimeState(config.serverRuntimeStatePath); |
| 241 | return Option.none<{ readonly origin: string }>(); | 283 | return Option.none<{ readonly origin: string }>(); |
| 242 | }, | 284 | }, |
| ... | @@ -335,7 +377,8 @@ const projectAddCommand = Command.make("add", { | ... | @@ -335,7 +377,8 @@ const projectAddCommand = Command.make("add", { |
| 335 | ); | 377 | ); |
| 336 | if (existingProject) { | 378 | if (existingProject) { |
| 337 | return yield* new ProjectCommandError({ | 379 | return yield* new ProjectCommandError({ |
| 338 | message: `An active project already exists for '${workspaceRoot}'.`, | 380 | operation: "addProject", |
| | 381 | detail: `An active project already exists for '${workspaceRoot}'.`, |
| 339 | }); | 382 | }); |
| 340 | } | 383 | } |
| 341 | | 384 | |