| author | |
| committer | |
| log | 42b510e8c876d97bf505c3e637e521f7c59ef73c |
| tree | f1ea5b8d84977ed2ad671a0b06d9aea5f501c260 |
| parent | ce841a7ef10543478823d44c73da3820b9990829 |
20 files changed, 840 insertions(+), 640 deletions(-)
apps/server/package.json-1| ... | ... | @@ -18,7 +18,6 @@ |
| 18 | 18 | }, |
| 19 | 19 | "dependencies": { |
| 20 | 20 | "@effect/platform-node": "4.0.0-beta.10", |
| 21 | "@effect/sql-sqlite-node": "4.0.0-beta.10", | |
| 22 | 21 | "effect": "4.0.0-beta.10", |
| 23 | 22 | "node-pty": "^1.1.0", |
| 24 | 23 | "open": "^10.1.0", |
apps/server/src/checkpointing/Layers/CheckpointService.test.ts+147-112| ... | ... | @@ -1,7 +1,3 @@ |
| 1 | import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | 1 | import type { |
| 6 | 2 | ProviderApprovalDecision, |
| 7 | 3 | ProviderEvent, |
| ... | ... | @@ -13,7 +9,7 @@ import type { |
| 13 | 9 | } from "@t3tools/contracts"; |
| 14 | 10 | import { NodeServices } from "@effect/platform-node"; |
| 15 | 11 | import { it, assert } from "@effect/vitest"; |
| 16 | import { Effect, Layer } from "effect"; | |
| 12 | import { Effect, FileSystem, Layer, Path } from "effect"; | |
| 17 | 13 | |
| 18 | 14 | import { runGit as runGitProcess } from "../../git/Process.ts"; |
| 19 | 15 | import { CheckpointServiceLive } from "./CheckpointService.ts"; |
| ... | ... | @@ -34,14 +30,9 @@ import type { |
| 34 | 30 | } from "../../provider/Services/ProviderAdapter.ts"; |
| 35 | 31 | import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts"; |
| 36 | 32 | import { ProviderSessionDirectoryLive } from "../../provider/Layers/ProviderSessionDirectory.ts"; |
| 37 | import { makeSqliteCheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts"; | |
| 33 | import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; | |
| 38 | 34 | import { CheckpointRepository } from "../../persistence/Services/Checkpoints.ts"; |
| 39 | ||
| 40 | const makeTemporaryDirectoryScoped = (prefix: string) => | |
| 41 | Effect.acquireRelease( | |
| 42 | Effect.promise(() => mkdtemp(path.join(os.tmpdir(), prefix))), | |
| 43 | (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })), | |
| 44 | ); | |
| 35 | import { CheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts"; | |
| 45 | 36 | |
| 46 | 37 | const runGit = (cwd: string, args: readonly string[]) => |
| 47 | 38 | runGitProcess({ |
| ... | ... | @@ -53,23 +44,24 @@ const runGit = (cwd: string, args: readonly string[]) => |
| 53 | 44 | Effect.provide(NodeServices.layer), |
| 54 | 45 | ); |
| 55 | 46 | |
| 56 | function createRepo() { | |
| 57 | return Effect.gen(function* () { | |
| 58 | const cwd = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-repo-"); | |
| 59 | yield* runGit(cwd, ["init", "--initial-branch=main"]); | |
| 60 | yield* runGit(cwd, ["config", "user.email", "test@example.com"]); | |
| 61 | yield* runGit(cwd, ["config", "user.name", "Test User"]); | |
| 62 | yield* Effect.promise(() => writeFile(path.join(cwd, "README.md"), "v1\n")); | |
| 63 | yield* runGit(cwd, ["add", "."]); | |
| 64 | yield* runGit(cwd, ["commit", "-m", "Initial"]); | |
| 65 | return cwd; | |
| 66 | }); | |
| 67 | } | |
| 47 | const makeGitRepository = Effect.gen(function* () { | |
| 48 | const fs = yield* FileSystem.FileSystem; | |
| 49 | const path = yield* Path.Path; | |
| 50 | const cwd = yield* fs.makeTempDirectory(); | |
| 51 | yield* runGit(cwd, ["init", "--initial-branch=main"]); | |
| 52 | yield* runGit(cwd, ["config", "user.email", "test@example.com"]); | |
| 53 | yield* runGit(cwd, ["config", "user.name", "Test User"]); | |
| 54 | yield* fs.writeFileString(path.join(cwd, "README.md"), "v1\n"); | |
| 55 | yield* runGit(cwd, ["add", "."]); | |
| 56 | yield* runGit(cwd, ["commit", "-m", "Initial"]); | |
| 57 | return cwd; | |
| 58 | }); | |
| 68 | 59 | |
| 69 | 60 | class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterError> { |
| 70 | 61 | readonly provider = "codex" as const; |
| 71 | 62 | |
| 72 | 63 | private active = true; |
| 64 | private readThreadFailure: ProviderAdapterError | undefined; | |
| 73 | 65 | private snapshot: ProviderThreadSnapshot; |
| 74 | 66 | private readonly session: ProviderSession; |
| 75 | 67 | |
| ... | ... | @@ -105,6 +97,10 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr |
| 105 | 97 | return this.snapshot.turns; |
| 106 | 98 | } |
| 107 | 99 | |
| 100 | setReadThreadFailure(error: ProviderAdapterError | undefined): void { | |
| 101 | this.readThreadFailure = error; | |
| 102 | } | |
| 103 | ||
| 108 | 104 | readonly startSession = ( |
| 109 | 105 | _input: ProviderSessionStartInput, |
| 110 | 106 | ): Effect.Effect<ProviderSession, ProviderAdapterError> => |
| ... | ... | @@ -157,7 +153,9 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr |
| 157 | 153 | this.hasSession(sessionId).pipe( |
| 158 | 154 | Effect.flatMap((hasSession) => |
| 159 | 155 | hasSession |
| 160 | ? Effect.succeed(this.snapshot) | |
| 156 | ? this.readThreadFailure | |
| 157 | ? Effect.fail(this.readThreadFailure) | |
| 158 | : Effect.succeed(this.snapshot) | |
| 161 | 159 | : Effect.fail( |
| 162 | 160 | new ProviderAdapterSessionNotFoundError({ |
| 163 | 161 | provider: this.provider, |
| ... | ... | @@ -203,9 +201,7 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr |
| 203 | 201 | |
| 204 | 202 | function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) { |
| 205 | 203 | return Effect.gen(function* () { |
| 206 | const cwd = yield* createRepo(); | |
| 207 | const stateDir = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-state-"); | |
| 208 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 204 | const cwd = yield* makeGitRepository; | |
| 209 | 205 | |
| 210 | 206 | const sessionId = "sess-1"; |
| 211 | 207 | const adapter = new InMemoryProviderAdapter(sessionId, cwd, turns); |
| ... | ... | @@ -220,9 +216,10 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) { |
| 220 | 216 | |
| 221 | 217 | const dependencies = Layer.mergeAll( |
| 222 | 218 | Layer.provide(CheckpointStoreLive, NodeServices.layer), |
| 223 | makeSqliteCheckpointRepositoryLive(dbPath), | |
| 219 | Layer.provide(CheckpointRepositoryLive, SqlitePersistenceMemory), | |
| 224 | 220 | Layer.succeed(ProviderAdapterRegistry, registry), |
| 225 | 221 | ProviderSessionDirectoryLive, |
| 222 | NodeServices.layer, | |
| 226 | 223 | ); |
| 227 | 224 | |
| 228 | 225 | const layer = CheckpointServiceLive.pipe(Layer.provideMerge(dependencies)); |
| ... | ... | @@ -236,87 +233,84 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) { |
| 236 | 233 | }); |
| 237 | 234 | } |
| 238 | 235 | |
| 239 | it.effect( | |
| 240 | "CheckpointServiceLive captures checkpoints, diffs refs, and reverts workspace + turns", | |
| 241 | () => | |
| 242 | Effect.gen(function* () { | |
| 243 | const firstTurn: ProviderThreadTurnSnapshot = { | |
| 244 | id: "turn-1", | |
| 245 | items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }], | |
| 246 | }; | |
| 247 | const secondTurn: ProviderThreadTurnSnapshot = { | |
| 248 | id: "turn-2", | |
| 249 | items: [{ type: "agentMessage", text: "second" }], | |
| 250 | }; | |
| 251 | ||
| 252 | const fixture = yield* makeFixture([]); | |
| 253 | ||
| 254 | yield* Effect.gen(function* () { | |
| 255 | const service = yield* CheckpointService; | |
| 256 | const directory = yield* ProviderSessionDirectory; | |
| 257 | ||
| 258 | yield* directory.upsert({ | |
| 259 | sessionId: fixture.sessionId, | |
| 260 | provider: "codex", | |
| 261 | threadId: "thread-1", | |
| 262 | }); | |
| 263 | ||
| 264 | yield* service.initializeForSession({ | |
| 265 | providerSessionId: fixture.sessionId, | |
| 266 | cwd: fixture.cwd, | |
| 267 | }); | |
| 268 | ||
| 269 | yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v2\n")); | |
| 270 | fixture.adapter.setTurns([firstTurn]); | |
| 271 | ||
| 272 | yield* service.captureCurrentTurn({ | |
| 273 | providerSessionId: fixture.sessionId, | |
| 274 | }); | |
| 275 | ||
| 276 | const listed = yield* service.listCheckpoints({ sessionId: fixture.sessionId }); | |
| 277 | assert.equal(listed.checkpoints.length, 2); | |
| 278 | assert.equal(listed.checkpoints[1]?.id, "turn-1"); | |
| 279 | assert.equal(listed.checkpoints[1]?.isCurrent, true); | |
| 280 | ||
| 281 | const diff = yield* service.getCheckpointDiff({ | |
| 282 | sessionId: fixture.sessionId, | |
| 283 | fromTurnCount: 0, | |
| 284 | toTurnCount: 1, | |
| 285 | }); | |
| 286 | assert.equal(diff.threadId, "thread-1"); | |
| 287 | assert.equal(diff.diff.includes("diff --git a/README.md b/README.md"), true); | |
| 288 | assert.equal(diff.diff.includes("-v1"), true); | |
| 289 | assert.equal(diff.diff.includes("+v2"), true); | |
| 290 | ||
| 291 | yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v3\n")); | |
| 292 | fixture.adapter.setTurns([firstTurn, secondTurn]); | |
| 293 | ||
| 294 | yield* service.captureCurrentTurn({ | |
| 295 | providerSessionId: fixture.sessionId, | |
| 296 | }); | |
| 297 | ||
| 298 | const beforeRevert = yield* Effect.promise(() => | |
| 299 | readFile(path.join(fixture.cwd, "README.md"), "utf8"), | |
| 300 | ); | |
| 301 | assert.equal(beforeRevert, "v3\n"); | |
| 302 | ||
| 303 | const reverted = yield* service.revertToCheckpoint({ | |
| 304 | sessionId: fixture.sessionId, | |
| 305 | turnCount: 1, | |
| 306 | }); | |
| 236 | it.effect("captures checkpoints, diffs refs, and reverts workspace + turns", () => | |
| 237 | Effect.gen(function* () { | |
| 238 | const firstTurn: ProviderThreadTurnSnapshot = { | |
| 239 | id: "turn-1", | |
| 240 | items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }], | |
| 241 | }; | |
| 242 | const secondTurn: ProviderThreadTurnSnapshot = { | |
| 243 | id: "turn-2", | |
| 244 | items: [{ type: "agentMessage", text: "second" }], | |
| 245 | }; | |
| 307 | 246 | |
| 308 | const afterRevert = yield* Effect.promise(() => | |
| 309 | readFile(path.join(fixture.cwd, "README.md"), "utf8"), | |
| 310 | ); | |
| 311 | assert.equal(afterRevert, "v2\n"); | |
| 312 | assert.equal(reverted.turnCount, 1); | |
| 313 | assert.equal(reverted.rolledBackTurns, 1); | |
| 314 | assert.equal(reverted.checkpoints.length, 2); | |
| 315 | assert.equal(reverted.checkpoints[1]?.id, "turn-1"); | |
| 316 | assert.equal(reverted.checkpoints[1]?.isCurrent, true); | |
| 317 | assert.equal(fixture.adapter.getTurns().length, 1); | |
| 318 | }).pipe(Effect.provide(fixture.layer)); | |
| 319 | }), | |
| 247 | const fixture = yield* makeFixture([]); | |
| 248 | ||
| 249 | yield* Effect.gen(function* () { | |
| 250 | const fs = yield* FileSystem.FileSystem; | |
| 251 | const path = yield* Path.Path; | |
| 252 | ||
| 253 | const service = yield* CheckpointService; | |
| 254 | const directory = yield* ProviderSessionDirectory; | |
| 255 | ||
| 256 | yield* directory.upsert({ | |
| 257 | sessionId: fixture.sessionId, | |
| 258 | provider: "codex", | |
| 259 | threadId: "thread-1", | |
| 260 | }); | |
| 261 | ||
| 262 | yield* service.initializeForSession({ | |
| 263 | providerSessionId: fixture.sessionId, | |
| 264 | cwd: fixture.cwd, | |
| 265 | }); | |
| 266 | ||
| 267 | yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v2\n"); | |
| 268 | fixture.adapter.setTurns([firstTurn]); | |
| 269 | ||
| 270 | yield* service.captureCurrentTurn({ | |
| 271 | providerSessionId: fixture.sessionId, | |
| 272 | }); | |
| 273 | ||
| 274 | const listed = yield* service.listCheckpoints({ sessionId: fixture.sessionId }); | |
| 275 | assert.equal(listed.checkpoints.length, 2); | |
| 276 | assert.equal(listed.checkpoints[1]?.id, "turn-1"); | |
| 277 | assert.equal(listed.checkpoints[1]?.isCurrent, true); | |
| 278 | ||
| 279 | const diff = yield* service.getCheckpointDiff({ | |
| 280 | sessionId: fixture.sessionId, | |
| 281 | fromTurnCount: 0, | |
| 282 | toTurnCount: 1, | |
| 283 | }); | |
| 284 | assert.equal(diff.threadId, "thread-1"); | |
| 285 | assert.equal(diff.diff.includes("diff --git a/README.md b/README.md"), true); | |
| 286 | assert.equal(diff.diff.includes("-v1"), true); | |
| 287 | assert.equal(diff.diff.includes("+v2"), true); | |
| 288 | ||
| 289 | yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v3\n"); | |
| 290 | fixture.adapter.setTurns([firstTurn, secondTurn]); | |
| 291 | ||
| 292 | yield* service.captureCurrentTurn({ | |
| 293 | providerSessionId: fixture.sessionId, | |
| 294 | }); | |
| 295 | ||
| 296 | const beforeRevert = yield* fs.readFileString(path.join(fixture.cwd, "README.md")); | |
| 297 | assert.equal(beforeRevert, "v3\n"); | |
| 298 | ||
| 299 | const reverted = yield* service.revertToCheckpoint({ | |
| 300 | sessionId: fixture.sessionId, | |
| 301 | turnCount: 1, | |
| 302 | }); | |
| 303 | ||
| 304 | const afterRevert = yield* fs.readFileString(path.join(fixture.cwd, "README.md")); | |
| 305 | assert.equal(afterRevert, "v2\n"); | |
| 306 | assert.equal(reverted.turnCount, 1); | |
| 307 | assert.equal(reverted.rolledBackTurns, 1); | |
| 308 | assert.equal(reverted.checkpoints.length, 2); | |
| 309 | assert.equal(reverted.checkpoints[1]?.id, "turn-1"); | |
| 310 | assert.equal(reverted.checkpoints[1]?.isCurrent, true); | |
| 311 | assert.equal(fixture.adapter.getTurns().length, 1); | |
| 312 | }).pipe(Effect.provide(fixture.layer)); | |
| 313 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 320 | 314 | ); |
| 321 | 315 | |
| 322 | 316 | it.effect( |
| ... | ... | @@ -338,7 +332,7 @@ it.effect( |
| 338 | 332 | assert.equal(result.failure._tag, "CheckpointSessionNotFoundError"); |
| 339 | 333 | } |
| 340 | 334 | }).pipe(Effect.provide(fixture.layer)); |
| 341 | }), | |
| 335 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 342 | 336 | ); |
| 343 | 337 | |
| 344 | 338 | it.effect( |
| ... | ... | @@ -353,6 +347,8 @@ it.effect( |
| 353 | 347 | const fixture = yield* makeFixture([]); |
| 354 | 348 | |
| 355 | 349 | yield* Effect.gen(function* () { |
| 350 | const fs = yield* FileSystem.FileSystem; | |
| 351 | const path = yield* Path.Path; | |
| 356 | 352 | const service = yield* CheckpointService; |
| 357 | 353 | const store = yield* CheckpointStore; |
| 358 | 354 | const checkpointRepository = yield* CheckpointRepository; |
| ... | ... | @@ -369,7 +365,7 @@ it.effect( |
| 369 | 365 | cwd: fixture.cwd, |
| 370 | 366 | }); |
| 371 | 367 | |
| 372 | yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v2\n")); | |
| 368 | yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v2\n"); | |
| 373 | 369 | fixture.adapter.setTurns([firstTurn]); |
| 374 | 370 | yield* service.captureCurrentTurn({ |
| 375 | 371 | providerSessionId: fixture.sessionId, |
| ... | ... | @@ -400,5 +396,44 @@ it.effect( |
| 400 | 396 | assert.equal(result.failure._tag, "CheckpointUnavailableError"); |
| 401 | 397 | } |
| 402 | 398 | }).pipe(Effect.provide(fixture.layer)); |
| 403 | }), | |
| 399 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 400 | ); | |
| 401 | ||
| 402 | it.effect("CheckpointServiceLive initializes root checkpoints without requiring thread/read", () => | |
| 403 | Effect.gen(function* () { | |
| 404 | const fixture = yield* makeFixture([]); | |
| 405 | ||
| 406 | yield* Effect.gen(function* () { | |
| 407 | const service = yield* CheckpointService; | |
| 408 | const checkpointRepository = yield* CheckpointRepository; | |
| 409 | const directory = yield* ProviderSessionDirectory; | |
| 410 | ||
| 411 | yield* directory.upsert({ | |
| 412 | sessionId: fixture.sessionId, | |
| 413 | provider: "codex", | |
| 414 | threadId: "thread-1", | |
| 415 | }); | |
| 416 | fixture.adapter.setReadThreadFailure( | |
| 417 | new ProviderAdapterValidationError({ | |
| 418 | provider: "codex", | |
| 419 | operation: "readThread", | |
| 420 | issue: "thread/read is temporarily unavailable", | |
| 421 | }), | |
| 422 | ); | |
| 423 | ||
| 424 | yield* service.initializeForSession({ | |
| 425 | providerSessionId: fixture.sessionId, | |
| 426 | cwd: fixture.cwd, | |
| 427 | }); | |
| 428 | ||
| 429 | const root = yield* checkpointRepository.getCheckpoint({ | |
| 430 | providerSessionId: fixture.sessionId, | |
| 431 | turnCount: 0, | |
| 432 | }); | |
| 433 | assert.equal(root._tag, "Some"); | |
| 434 | if (root._tag === "Some") { | |
| 435 | assert.equal(root.value.threadId, "thread-1"); | |
| 436 | } | |
| 437 | }).pipe(Effect.provide(fixture.layer)); | |
| 438 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 404 | 439 | ); |
apps/server/src/checkpointing/Layers/CheckpointService.ts+40-12| ... | ... | @@ -441,6 +441,43 @@ const makeCheckpointService = Effect.gen(function* () { |
| 441 | 441 | ), |
| 442 | 442 | ); |
| 443 | 443 | |
| 444 | const resolveThreadIdForSession = ( | |
| 445 | sessionId: string, | |
| 446 | operation: string, | |
| 447 | ): Effect.Effect<string, CheckpointServiceError> => | |
| 448 | directory.getThreadId(sessionId).pipe( | |
| 449 | Effect.mapError( | |
| 450 | (cause) => | |
| 451 | new CheckpointSessionNotFoundError({ | |
| 452 | sessionId, | |
| 453 | cause, | |
| 454 | }), | |
| 455 | ), | |
| 456 | Effect.flatMap((threadId) => | |
| 457 | Option.match(threadId, { | |
| 458 | onSome: (value) => { | |
| 459 | const normalizedThreadId = value.trim(); | |
| 460 | if (normalizedThreadId.length > 0) { | |
| 461 | return Effect.succeed(normalizedThreadId); | |
| 462 | } | |
| 463 | return Effect.fail( | |
| 464 | invariantError( | |
| 465 | operation, | |
| 466 | `Resolved empty thread id for session ${sessionId}.`, | |
| 467 | ), | |
| 468 | ); | |
| 469 | }, | |
| 470 | onNone: () => | |
| 471 | Effect.fail( | |
| 472 | invariantError( | |
| 473 | operation, | |
| 474 | `Missing thread id in session directory for session ${sessionId}.`, | |
| 475 | ), | |
| 476 | ), | |
| 477 | }), | |
| 478 | ), | |
| 479 | ); | |
| 480 | ||
| 444 | 481 | const rollbackThreadSnapshot = ( |
| 445 | 482 | sessionId: string, |
| 446 | 483 | adapter: ProviderAdapterShape<ProviderAdapterError>, |
| ... | ... | @@ -633,19 +670,10 @@ const makeCheckpointService = Effect.gen(function* () { |
| 633 | 670 | Effect.gen(function* () { |
| 634 | 671 | const adapter = yield* resolveAdapterForSession(input.providerSessionId); |
| 635 | 672 | yield* ensureSessionIsActive(input.providerSessionId, adapter); |
| 636 | const snapshot = yield* readThreadSnapshot( | |
| 673 | const threadId = yield* resolveThreadIdForSession( | |
| 637 | 674 | input.providerSessionId, |
| 638 | adapter, | |
| 639 | "CheckpointService.initializeForSession:readThread", | |
| 675 | "CheckpointService.initializeForSession", | |
| 640 | 676 | ); |
| 641 | if (snapshot.turns.length !== 0) { | |
| 642 | return yield* Effect.fail( | |
| 643 | invariantError( | |
| 644 | "CheckpointService.initializeForSession", | |
| 645 | `Expected empty thread snapshot during initialization, received ${snapshot.turns.length} turn(s).`, | |
| 646 | ), | |
| 647 | ); | |
| 648 | } | |
| 649 | 677 | |
| 650 | 678 | const supportsGit = yield* store.isGitRepository(input.cwd); |
| 651 | 679 | if (!supportsGit) { |
| ... | ... | @@ -686,7 +714,7 @@ const makeCheckpointService = Effect.gen(function* () { |
| 686 | 714 | yield* captureAndPersistCheckpoint({ |
| 687 | 715 | providerSessionId: input.providerSessionId, |
| 688 | 716 | cwd: input.cwd, |
| 689 | threadId: snapshot.threadId, | |
| 717 | threadId, | |
| 690 | 718 | checkpoint: rootCheckpoint(), |
| 691 | 719 | }); |
| 692 | 720 | yield* setTrackedCheckpointCwd(input.providerSessionId, input.cwd); |
apps/server/src/orchestration/Layer.test.ts+34-90| ... | ... | @@ -1,40 +1,21 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | 1 | import type { OrchestrationEvent } from "@t3tools/contracts"; |
| 6 | 2 | import { Effect, Layer, ManagedRuntime } from "effect"; |
| 7 | import { afterEach, describe, expect, it } from "vitest"; | |
| 3 | import { describe, expect, it } from "vitest"; | |
| 8 | 4 | |
| 9 | 5 | import { |
| 10 | 6 | OrchestrationEventRepository, |
| 11 | 7 | type OrchestrationEventRepositoryShape, |
| 12 | 8 | } from "../persistence/Services/OrchestrationEvents.ts"; |
| 13 | 9 | import { PersistenceSqlError } from "../persistence/Errors.ts"; |
| 14 | import { makeSqlitePersistenceLive } from "../persistence/Layers/Sqlite.ts"; | |
| 10 | import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; | |
| 15 | 11 | import { OrchestrationEngineLive } from "./Layer.ts"; |
| 16 | 12 | import { OrchestrationEngineService } from "./Service.ts"; |
| 17 | 13 | import { OrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents.ts"; |
| 18 | 14 | |
| 19 | const tempDirs: string[] = []; | |
| 20 | ||
| 21 | function makeTempDir(prefix: string): string { | |
| 22 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); | |
| 23 | tempDirs.push(dir); | |
| 24 | return dir; | |
| 25 | } | |
| 26 | ||
| 27 | afterEach(() => { | |
| 28 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | |
| 29 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 30 | } | |
| 31 | }); | |
| 32 | ||
| 33 | export async function createOrchestrationSystem(stateDir: string) { | |
| 34 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 15 | export async function createOrchestrationSystem() { | |
| 35 | 16 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 36 | 17 | Layer.provide(OrchestrationEventRepositoryLive), |
| 37 | Layer.provide(makeSqlitePersistenceLive(dbPath)), | |
| 18 | Layer.provide(SqlitePersistenceMemory), | |
| 38 | 19 | ); |
| 39 | 20 | const runtime = ManagedRuntime.make(orchestrationLayer); |
| 40 | 21 | const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); |
| ... | ... | @@ -46,16 +27,15 @@ export async function createOrchestrationSystem(stateDir: string) { |
| 46 | 27 | } |
| 47 | 28 | |
| 48 | 29 | describe("OrchestrationEngine", () => { |
| 49 | it("replays to the same deterministic snapshot", async () => { | |
| 50 | const stateDir = makeTempDir("t3code-orchestration-"); | |
| 30 | it("returns deterministic snapshots for repeated reads", async () => { | |
| 51 | 31 | const createdAt = new Date().toISOString(); |
| 52 | 32 | const projectId = "project-1"; |
| 53 | 33 | const threadId = "thread-1"; |
| 54 | 34 | |
| 55 | const firstSystem = await createOrchestrationSystem(stateDir); | |
| 56 | const engineA = firstSystem.engine; | |
| 57 | await firstSystem.run( | |
| 58 | engineA.dispatch({ | |
| 35 | const system = await createOrchestrationSystem(); | |
| 36 | const engine = system.engine; | |
| 37 | await system.run( | |
| 38 | engine.dispatch({ | |
| 59 | 39 | type: "thread.create", |
| 60 | 40 | commandId: "cmd-1", |
| 61 | 41 | threadId, |
| ... | ... | @@ -67,8 +47,8 @@ describe("OrchestrationEngine", () => { |
| 67 | 47 | createdAt, |
| 68 | 48 | }), |
| 69 | 49 | ); |
| 70 | await firstSystem.run( | |
| 71 | engineA.dispatch({ | |
| 50 | await system.run( | |
| 51 | engine.dispatch({ | |
| 72 | 52 | type: "message.send", |
| 73 | 53 | commandId: "cmd-3", |
| 74 | 54 | threadId, |
| ... | ... | @@ -79,19 +59,14 @@ describe("OrchestrationEngine", () => { |
| 79 | 59 | createdAt, |
| 80 | 60 | }), |
| 81 | 61 | ); |
| 82 | const snapshotA = await firstSystem.run(engineA.getSnapshot()); | |
| 83 | await firstSystem.dispose(); | |
| 84 | ||
| 85 | const secondSystem = await createOrchestrationSystem(stateDir); | |
| 86 | const engineB = secondSystem.engine; | |
| 87 | const snapshotB = await secondSystem.run(engineB.getSnapshot()); | |
| 62 | const snapshotA = await system.run(engine.getSnapshot()); | |
| 63 | const snapshotB = await system.run(engine.getSnapshot()); | |
| 88 | 64 | expect(snapshotB).toEqual(snapshotA); |
| 89 | await secondSystem.dispose(); | |
| 65 | await system.dispose(); | |
| 90 | 66 | }); |
| 91 | 67 | |
| 92 | 68 | it("fans out read-model updates to subscribers", async () => { |
| 93 | const stateDir = makeTempDir("t3code-orchestration-fanout-"); | |
| 94 | const system = await createOrchestrationSystem(stateDir); | |
| 69 | const system = await createOrchestrationSystem(); | |
| 95 | 70 | const engine = system.engine; |
| 96 | 71 | const updates: number[] = []; |
| 97 | 72 | const unsubscribe = await system.run( |
| ... | ... | @@ -118,8 +93,7 @@ describe("OrchestrationEngine", () => { |
| 118 | 93 | }); |
| 119 | 94 | |
| 120 | 95 | it("replays append-only events from sequence", async () => { |
| 121 | const stateDir = makeTempDir("t3code-orchestration-replay-"); | |
| 122 | const system = await createOrchestrationSystem(stateDir); | |
| 96 | const system = await createOrchestrationSystem(); | |
| 123 | 97 | const engine = system.engine; |
| 124 | 98 | await system.run( |
| 125 | 99 | engine.dispatch({ |
| ... | ... | @@ -150,11 +124,10 @@ describe("OrchestrationEngine", () => { |
| 150 | 124 | }); |
| 151 | 125 | |
| 152 | 126 | it("stores completed turn summaries even when no files changed", async () => { |
| 153 | const stateDir = makeTempDir("t3code-orchestration-turn-diff-"); | |
| 154 | const firstSystem = await createOrchestrationSystem(stateDir); | |
| 155 | const engine = firstSystem.engine; | |
| 127 | const system = await createOrchestrationSystem(); | |
| 128 | const engine = system.engine; | |
| 156 | 129 | const completedAt = new Date().toISOString(); |
| 157 | await firstSystem.run( | |
| 130 | await system.run( | |
| 158 | 131 | engine.dispatch({ |
| 159 | 132 | type: "thread.create", |
| 160 | 133 | commandId: "cmd-thread-turn-diff", |
| ... | ... | @@ -167,7 +140,7 @@ describe("OrchestrationEngine", () => { |
| 167 | 140 | createdAt: completedAt, |
| 168 | 141 | }), |
| 169 | 142 | ); |
| 170 | await firstSystem.run( | |
| 143 | await system.run( | |
| 171 | 144 | engine.dispatch({ |
| 172 | 145 | type: "thread.turnDiff.complete", |
| 173 | 146 | commandId: "cmd-turn-diff-complete", |
| ... | ... | @@ -181,7 +154,7 @@ describe("OrchestrationEngine", () => { |
| 181 | 154 | }), |
| 182 | 155 | ); |
| 183 | 156 | |
| 184 | const firstThread = (await firstSystem.run(engine.getSnapshot())).threads.find( | |
| 157 | const firstThread = (await system.run(engine.getSnapshot())).threads.find( | |
| 185 | 158 | (thread) => thread.id === "thread-turn-diff", |
| 186 | 159 | ); |
| 187 | 160 | expect(firstThread?.turnDiffSummaries).toEqual([ |
| ... | ... | @@ -193,31 +166,14 @@ describe("OrchestrationEngine", () => { |
| 193 | 166 | checkpointTurnCount: 1, |
| 194 | 167 | }, |
| 195 | 168 | ]); |
| 196 | await firstSystem.dispose(); | |
| 197 | ||
| 198 | const secondSystem = await createOrchestrationSystem(stateDir); | |
| 199 | const restartedEngine = secondSystem.engine; | |
| 200 | const restartedThread = (await secondSystem.run(restartedEngine.getSnapshot())).threads.find( | |
| 201 | (thread) => thread.id === "thread-turn-diff", | |
| 202 | ); | |
| 203 | expect(restartedThread?.turnDiffSummaries).toEqual([ | |
| 204 | { | |
| 205 | turnId: "turn-1", | |
| 206 | completedAt, | |
| 207 | status: "completed", | |
| 208 | files: [], | |
| 209 | checkpointTurnCount: 1, | |
| 210 | }, | |
| 211 | ]); | |
| 212 | await secondSystem.dispose(); | |
| 169 | await system.dispose(); | |
| 213 | 170 | }); |
| 214 | 171 | |
| 215 | 172 | it("reverts thread messages and turn summaries to a checkpoint", async () => { |
| 216 | const stateDir = makeTempDir("t3code-orchestration-revert-"); | |
| 217 | const firstSystem = await createOrchestrationSystem(stateDir); | |
| 218 | const engine = firstSystem.engine; | |
| 173 | const system = await createOrchestrationSystem(); | |
| 174 | const engine = system.engine; | |
| 219 | 175 | const createdAt = new Date().toISOString(); |
| 220 | await firstSystem.run( | |
| 176 | await system.run( | |
| 221 | 177 | engine.dispatch({ |
| 222 | 178 | type: "thread.create", |
| 223 | 179 | commandId: "cmd-thread-revert", |
| ... | ... | @@ -230,7 +186,7 @@ describe("OrchestrationEngine", () => { |
| 230 | 186 | createdAt, |
| 231 | 187 | }), |
| 232 | 188 | ); |
| 233 | await firstSystem.run( | |
| 189 | await system.run( | |
| 234 | 190 | engine.dispatch({ |
| 235 | 191 | type: "message.send", |
| 236 | 192 | commandId: "cmd-msg-1-user", |
| ... | ... | @@ -242,7 +198,7 @@ describe("OrchestrationEngine", () => { |
| 242 | 198 | createdAt, |
| 243 | 199 | }), |
| 244 | 200 | ); |
| 245 | await firstSystem.run( | |
| 201 | await system.run( | |
| 246 | 202 | engine.dispatch({ |
| 247 | 203 | type: "message.send", |
| 248 | 204 | commandId: "cmd-msg-1-assistant", |
| ... | ... | @@ -254,7 +210,7 @@ describe("OrchestrationEngine", () => { |
| 254 | 210 | createdAt, |
| 255 | 211 | }), |
| 256 | 212 | ); |
| 257 | await firstSystem.run( | |
| 213 | await system.run( | |
| 258 | 214 | engine.dispatch({ |
| 259 | 215 | type: "thread.turnDiff.complete", |
| 260 | 216 | commandId: "cmd-turn-1-complete", |
| ... | ... | @@ -270,7 +226,7 @@ describe("OrchestrationEngine", () => { |
| 270 | 226 | ); |
| 271 | 227 | |
| 272 | 228 | const createdAtSecond = new Date(Date.now() + 1_000).toISOString(); |
| 273 | await firstSystem.run( | |
| 229 | await system.run( | |
| 274 | 230 | engine.dispatch({ |
| 275 | 231 | type: "message.send", |
| 276 | 232 | commandId: "cmd-msg-2-user", |
| ... | ... | @@ -282,7 +238,7 @@ describe("OrchestrationEngine", () => { |
| 282 | 238 | createdAt: createdAtSecond, |
| 283 | 239 | }), |
| 284 | 240 | ); |
| 285 | await firstSystem.run( | |
| 241 | await system.run( | |
| 286 | 242 | engine.dispatch({ |
| 287 | 243 | type: "message.send", |
| 288 | 244 | commandId: "cmd-msg-2-assistant", |
| ... | ... | @@ -294,7 +250,7 @@ describe("OrchestrationEngine", () => { |
| 294 | 250 | createdAt: createdAtSecond, |
| 295 | 251 | }), |
| 296 | 252 | ); |
| 297 | await firstSystem.run( | |
| 253 | await system.run( | |
| 298 | 254 | engine.dispatch({ |
| 299 | 255 | type: "thread.turnDiff.complete", |
| 300 | 256 | commandId: "cmd-turn-2-complete", |
| ... | ... | @@ -309,7 +265,7 @@ describe("OrchestrationEngine", () => { |
| 309 | 265 | }), |
| 310 | 266 | ); |
| 311 | 267 | |
| 312 | await firstSystem.run( | |
| 268 | await system.run( | |
| 313 | 269 | engine.dispatch({ |
| 314 | 270 | type: "thread.revert", |
| 315 | 271 | commandId: "cmd-thread-revert-apply", |
| ... | ... | @@ -320,25 +276,13 @@ describe("OrchestrationEngine", () => { |
| 320 | 276 | }), |
| 321 | 277 | ); |
| 322 | 278 | |
| 323 | const thread = (await firstSystem.run(engine.getSnapshot())).threads.find( | |
| 279 | const thread = (await system.run(engine.getSnapshot())).threads.find( | |
| 324 | 280 | (entry) => entry.id === "thread-revert", |
| 325 | 281 | ); |
| 326 | 282 | expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]); |
| 327 | 283 | expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); |
| 328 | 284 | expect(thread?.latestTurnId).toBe("turn-1"); |
| 329 | await firstSystem.dispose(); | |
| 330 | ||
| 331 | const secondSystem = await createOrchestrationSystem(stateDir); | |
| 332 | const restarted = secondSystem.engine; | |
| 333 | const restartedThread = (await secondSystem.run(restarted.getSnapshot())).threads.find( | |
| 334 | (entry) => entry.id === "thread-revert", | |
| 335 | ); | |
| 336 | expect(restartedThread?.messages.map((message) => message.id)).toEqual([ | |
| 337 | "user-1", | |
| 338 | "assistant:turn-1", | |
| 339 | ]); | |
| 340 | expect(restartedThread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); | |
| 341 | await secondSystem.dispose(); | |
| 285 | await system.dispose(); | |
| 342 | 286 | }); |
| 343 | 287 | |
| 344 | 288 | it("allows stop to be called multiple times", async () => { |
apps/server/src/persistence/Layers/Checkpoints.test.ts+4-17| ... | ... | @@ -1,29 +1,16 @@ |
| 1 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | 1 | import { it, assert } from "@effect/vitest"; |
| 6 | 2 | import { Effect, Layer, Option } from "effect"; |
| 7 | 3 | |
| 8 | 4 | import { CheckpointRepository } from "../Services/Checkpoints.ts"; |
| 9 | 5 | import { CheckpointRepositoryLive } from "./Checkpoints.ts"; |
| 10 | import { makeSqlitePersistenceLive } from "./Sqlite.ts"; | |
| 11 | ||
| 12 | const makeTemporaryDirectoryScoped = (prefix: string) => | |
| 13 | Effect.acquireRelease( | |
| 14 | Effect.promise(() => mkdtemp(path.join(os.tmpdir(), prefix))), | |
| 15 | (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })), | |
| 16 | ); | |
| 6 | import { SqlitePersistenceMemory } from "./Sqlite.ts"; | |
| 17 | 7 | |
| 18 | function makeCheckpointRepositoryLayer(dbPath: string) { | |
| 19 | return CheckpointRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath))); | |
| 8 | function makeCheckpointRepositoryLayer() { | |
| 9 | return CheckpointRepositoryLive.pipe(Layer.provide(SqlitePersistenceMemory)); | |
| 20 | 10 | } |
| 21 | 11 | |
| 22 | 12 | it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint metadata", () => |
| 23 | 13 | Effect.gen(function* () { |
| 24 | const stateDir = yield* makeTemporaryDirectoryScoped("t3code-checkpoint-repository-"); | |
| 25 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 26 | ||
| 27 | 14 | yield* Effect.gen(function* () { |
| 28 | 15 | const repository = yield* CheckpointRepository; |
| 29 | 16 | |
| ... | ... | @@ -90,6 +77,6 @@ it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint |
| 90 | 77 | yield* repository.deleteAllForSession({ providerSessionId: "sess-1" }); |
| 91 | 78 | const empty = yield* repository.listCheckpoints({ providerSessionId: "sess-1" }); |
| 92 | 79 | assert.equal(empty.length, 0); |
| 93 | }).pipe(Effect.provide(makeCheckpointRepositoryLayer(dbPath))); | |
| 80 | }).pipe(Effect.provide(makeCheckpointRepositoryLayer())); | |
| 94 | 81 | }), |
| 95 | 82 | ); |
apps/server/src/persistence/Layers/Checkpoints.ts-5| ... | ... | @@ -20,7 +20,6 @@ import { |
| 20 | 20 | CheckpointRepositoryValidationError, |
| 21 | 21 | type CheckpointRepositoryError, |
| 22 | 22 | } from "../Errors.ts"; |
| 23 | import { makeSqlitePersistenceLive } from "./Sqlite.ts"; | |
| 24 | 23 | import { |
| 25 | 24 | CheckpointRepository, |
| 26 | 25 | type CheckpointRepositoryEntry, |
| ... | ... | @@ -357,7 +356,3 @@ export const CheckpointRepositoryLive = Layer.effect( |
| 357 | 356 | CheckpointRepository, |
| 358 | 357 | makeCheckpointRepository, |
| 359 | 358 | ); |
| 360 | ||
| 361 | export function makeSqliteCheckpointRepositoryLive(dbPath: string) { | |
| 362 | return CheckpointRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath))); | |
| 363 | } |
apps/server/src/persistence/Layers/OrchestrationEvents.test.ts+26-86| ... | ... | @@ -1,39 +1,25 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | import { DatabaseSync } from "node:sqlite"; | |
| 5 | ||
| 6 | import { ManagedRuntime, Effect } from "effect"; | |
| 7 | import { afterEach, describe, expect, it } from "vitest"; | |
| 1 | import { Effect, Layer } from "effect"; | |
| 2 | import { assert, it } from "@effect/vitest"; | |
| 8 | 3 | |
| 9 | 4 | import { OrchestrationEventRepository } from "../Services/OrchestrationEvents.ts"; |
| 10 | import { makeSqliteOrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts"; | |
| 11 | ||
| 12 | const tempDirs: string[] = []; | |
| 13 | ||
| 14 | function makeTempDir(prefix: string): string { | |
| 15 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); | |
| 16 | tempDirs.push(dir); | |
| 17 | return dir; | |
| 18 | } | |
| 19 | ||
| 20 | afterEach(() => { | |
| 21 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | |
| 22 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 23 | } | |
| 24 | }); | |
| 25 | ||
| 26 | describe("OrchestrationEventRepository", () => { | |
| 27 | it("persists and replays events across restarts", async () => { | |
| 28 | const stateDir = makeTempDir("t3code-event-store-"); | |
| 29 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 30 | ||
| 31 | const createdAt = new Date().toISOString(); | |
| 32 | ||
| 33 | const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); | |
| 34 | const first = await firstRuntime.runPromise(Effect.service(OrchestrationEventRepository)); | |
| 35 | const saved = await firstRuntime.runPromise( | |
| 36 | first.append({ | |
| 5 | import { SqlitePersistenceMemory } from "./Sqlite.ts"; | |
| 6 | import { OrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts"; | |
| 7 | import { NodeServices } from "@effect/platform-node"; | |
| 8 | ||
| 9 | const layer = it.layer( | |
| 10 | Layer.mergeAll( | |
| 11 | Layer.provide(OrchestrationEventRepositoryLive, SqlitePersistenceMemory), | |
| 12 | NodeServices.layer, | |
| 13 | ), | |
| 14 | ); | |
| 15 | ||
| 16 | layer("OrchestrationEventRepository", (it) => { | |
| 17 | it.effect("stores and replays events", () => | |
| 18 | Effect.gen(function* () { | |
| 19 | const createdAt = new Date().toISOString(); | |
| 20 | ||
| 21 | const repository = yield* OrchestrationEventRepository; | |
| 22 | const saved = yield* repository.append({ | |
| 37 | 23 | eventId: "event-1", |
| 38 | 24 | type: "thread.created", |
| 39 | 25 | aggregateType: "thread", |
| ... | ... | @@ -41,57 +27,11 @@ describe("OrchestrationEventRepository", () => { |
| 41 | 27 | occurredAt: createdAt, |
| 42 | 28 | commandId: "cmd-1", |
| 43 | 29 | payload: { id: "thread-1", projectId: "project-1", title: "demo" }, |
| 44 | }), | |
| 45 | ); | |
| 46 | expect(saved.sequence).toBe(1); | |
| 47 | await firstRuntime.dispose(); | |
| 48 | ||
| 49 | const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); | |
| 50 | const second = await secondRuntime.runPromise(Effect.service(OrchestrationEventRepository)); | |
| 51 | const replayed = await secondRuntime.runPromise(second.readFromSequence(0)); | |
| 52 | expect(replayed).toEqual([saved]); | |
| 53 | await secondRuntime.dispose(); | |
| 54 | }); | |
| 55 | ||
| 56 | it("creates and reuses the migrator tracking table", async () => { | |
| 57 | const stateDir = makeTempDir("t3code-event-store-migrations-"); | |
| 58 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 59 | ||
| 60 | const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); | |
| 61 | const first = await firstRuntime.runPromise(Effect.service(OrchestrationEventRepository)); | |
| 62 | await firstRuntime.runPromise(first.readAll()); | |
| 63 | await firstRuntime.dispose(); | |
| 64 | ||
| 65 | const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); | |
| 66 | const second = await secondRuntime.runPromise(Effect.service(OrchestrationEventRepository)); | |
| 67 | await secondRuntime.runPromise(second.readAll()); | |
| 68 | await secondRuntime.dispose(); | |
| 69 | ||
| 70 | const db = new DatabaseSync(dbPath, { readOnly: true }); | |
| 71 | const tableRow = db | |
| 72 | .prepare( | |
| 73 | ` | |
| 74 | SELECT name | |
| 75 | FROM sqlite_master | |
| 76 | WHERE type = 'table' AND name IN ('effect_sql_migrations', 'orchestration_sql_migrations') | |
| 77 | ORDER BY name ASC | |
| 78 | `, | |
| 79 | ) | |
| 80 | .get() as { name: string } | undefined; | |
| 81 | expect(tableRow?.name).toBeDefined(); | |
| 82 | expect(tableRow).toBeDefined(); | |
| 83 | if (!tableRow) return; | |
| 30 | }); | |
| 31 | assert.equal(saved.sequence, 1); | |
| 84 | 32 | |
| 85 | const migrationRows = db | |
| 86 | .prepare( | |
| 87 | ` | |
| 88 | SELECT * | |
| 89 | FROM ${tableRow.name} | |
| 90 | `, | |
| 91 | ) | |
| 92 | .all() as Array<Record<string, unknown>>; | |
| 93 | expect(migrationRows.length).toBeGreaterThanOrEqual(1); | |
| 94 | expect(Object.values(migrationRows[0] ?? {}).length).toBeGreaterThan(0); | |
| 95 | db.close(); | |
| 96 | }); | |
| 33 | const replayed = yield* repository.readFromSequence(0); | |
| 34 | assert.deepEqual(replayed, [saved]); | |
| 35 | }), | |
| 36 | ); | |
| 97 | 37 | }); |
apps/server/src/persistence/Layers/OrchestrationEvents.ts-5| ... | ... | @@ -11,7 +11,6 @@ import { |
| 11 | 11 | toPersistenceSqlError, |
| 12 | 12 | type OrchestrationEventRepositoryError, |
| 13 | 13 | } from "../Errors.ts"; |
| 14 | import { makeSqlitePersistenceLive } from "./Sqlite.ts"; | |
| 15 | 14 | import { |
| 16 | 15 | OrchestrationEventRepository, |
| 17 | 16 | type OrchestrationEventRepositoryShape, |
| ... | ... | @@ -177,7 +176,3 @@ export const OrchestrationEventRepositoryLive = Layer.effect( |
| 177 | 176 | OrchestrationEventRepository, |
| 178 | 177 | makeRepository, |
| 179 | 178 | ); |
| 180 | ||
| 181 | export function makeSqliteOrchestrationEventRepositoryLive(dbPath: string) { | |
| 182 | return OrchestrationEventRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath))); | |
| 183 | } |
apps/server/src/persistence/Layers/Projects.test.ts+40-67| ... | ... | @@ -1,83 +1,56 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | import { Effect, Layer, ManagedRuntime } from "effect"; | |
| 6 | import { afterEach, describe, expect, it } from "vitest"; | |
| 1 | import { Effect, Layer, FileSystem } from "effect"; | |
| 2 | import { assert, it } from "@effect/vitest"; | |
| 7 | 3 | |
| 8 | 4 | import { ProjectRepository } from "../Services/Projects.ts"; |
| 9 | 5 | import { ProjectRepositoryLive } from "./Projects.ts"; |
| 10 | import { makeSqlitePersistenceLive } from "./Sqlite.ts"; | |
| 11 | ||
| 12 | function makeProjectRepositoryTest(dbPath: string) { | |
| 13 | return ProjectRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath))); | |
| 14 | } | |
| 15 | ||
| 16 | const tempDirs: string[] = []; | |
| 17 | ||
| 18 | function makeTempDir(prefix: string): string { | |
| 19 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); | |
| 20 | tempDirs.push(dir); | |
| 21 | return dir; | |
| 22 | } | |
| 23 | ||
| 24 | afterEach(() => { | |
| 25 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | |
| 26 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 27 | } | |
| 28 | }); | |
| 29 | ||
| 30 | describe("ProjectRepository", () => { | |
| 31 | it("persists projects and deduplicates adds by cwd", async () => { | |
| 32 | const stateDir = makeTempDir("t3code-project-repo-state-"); | |
| 33 | const projectDir = makeTempDir("t3code-project-repo-project-"); | |
| 34 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 35 | ||
| 36 | const firstRuntime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath)); | |
| 37 | const first = await firstRuntime.runPromise(Effect.service(ProjectRepository)); | |
| 38 | ||
| 39 | const created = await firstRuntime.runPromise(first.add({ cwd: projectDir })); | |
| 40 | expect(created.created).toBe(true); | |
| 41 | ||
| 42 | const duplicate = await firstRuntime.runPromise(first.add({ cwd: projectDir })); | |
| 43 | expect(duplicate.created).toBe(false); | |
| 44 | expect(duplicate.project.id).toBe(created.project.id); | |
| 6 | import { SqlitePersistenceMemory } from "./Sqlite.ts"; | |
| 7 | import { NodeServices } from "@effect/platform-node"; | |
| 45 | 8 | |
| 46 | await firstRuntime.dispose(); | |
| 9 | const layer = it.layer( | |
| 10 | Layer.mergeAll(Layer.provide(ProjectRepositoryLive, SqlitePersistenceMemory), NodeServices.layer), | |
| 11 | ); | |
| 47 | 12 | |
| 48 | const secondRuntime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath)); | |
| 49 | const second = await secondRuntime.runPromise(Effect.service(ProjectRepository)); | |
| 50 | const listed = await secondRuntime.runPromise(second.list()); | |
| 13 | layer("ProjectRepository", (it) => { | |
| 14 | it.effect("stores projects and deduplicates adds by cwd", () => | |
| 15 | Effect.gen(function* () { | |
| 16 | const fs = yield* FileSystem.FileSystem; | |
| 17 | const projectDir = yield* fs.makeTempDirectoryScoped(); | |
| 51 | 18 | |
| 52 | expect(listed).toHaveLength(1); | |
| 53 | expect(listed[0]?.cwd).toBe(projectDir); | |
| 19 | const repository = yield* ProjectRepository; | |
| 54 | 20 | |
| 55 | await secondRuntime.dispose(); | |
| 56 | }); | |
| 21 | const created = yield* repository.add({ cwd: projectDir }); | |
| 22 | assert.equal(created.created, true); | |
| 57 | 23 | |
| 58 | it("prunes missing project paths", async () => { | |
| 59 | const stateDir = makeTempDir("t3code-project-repo-prune-state-"); | |
| 60 | const existing = makeTempDir("t3code-project-repo-prune-existing-"); | |
| 61 | const missing = makeTempDir("t3code-project-repo-prune-missing-"); | |
| 62 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | |
| 24 | const duplicate = yield* repository.add({ cwd: projectDir }); | |
| 25 | assert.equal(duplicate.created, false); | |
| 26 | assert.equal(duplicate.project.id, created.project.id); | |
| 63 | 27 | |
| 64 | const runtime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath)); | |
| 65 | const repository = await runtime.runPromise(Effect.service(ProjectRepository)); | |
| 28 | const listed = yield* repository.list(); | |
| 29 | assert.equal(listed.length, 1); | |
| 30 | assert.equal(listed[0]?.cwd, projectDir); | |
| 31 | }), | |
| 32 | ); | |
| 66 | 33 | |
| 67 | const existingProject = await runtime.runPromise(repository.add({ cwd: existing })); | |
| 68 | const missingProject = await runtime.runPromise(repository.add({ cwd: missing })); | |
| 34 | it.effect("prunes missing project paths", () => | |
| 35 | Effect.gen(function* () { | |
| 36 | const fs = yield* FileSystem.FileSystem; | |
| 37 | const existing = yield* fs.makeTempDirectoryScoped(); | |
| 38 | const missing = yield* fs.makeTempDirectory(); | |
| 69 | 39 | |
| 70 | expect(existingProject.created).toBe(true); | |
| 71 | expect(missingProject.created).toBe(true); | |
| 40 | const repository = yield* ProjectRepository; | |
| 72 | 41 | |
| 73 | fs.rmSync(missing, { recursive: true, force: true }); | |
| 42 | const existingProject = yield* repository.add({ cwd: existing }); | |
| 43 | const missingProject = yield* repository.add({ cwd: missing }); | |
| 74 | 44 | |
| 75 | await runtime.runPromise(repository.pruneMissing()); | |
| 76 | const listed = await runtime.runPromise(repository.list()); | |
| 45 | assert.isTrue(existingProject.created); | |
| 46 | assert.isTrue(missingProject.created); | |
| 77 | 47 | |
| 78 | expect(listed).toHaveLength(1); | |
| 79 | expect(listed[0]?.cwd).toBe(existing); | |
| 48 | yield* fs.remove(missing, { recursive: true, force: true }); | |
| 80 | 49 | |
| 81 | await runtime.dispose(); | |
| 82 | }); | |
| 50 | yield* repository.pruneMissing(); | |
| 51 | const listed = yield* repository.list(); | |
| 52 | assert.equal(listed.length, 1); | |
| 53 | assert.equal(listed[0]?.cwd, existing); | |
| 54 | }), | |
| 55 | ); | |
| 83 | 56 | }); |
apps/server/src/persistence/Layers/Sqlite.ts+18-35| ... | ... | @@ -1,41 +1,24 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import path from "node:path"; | |
| 3 | ||
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 5 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; | |
| 6 | import { Effect, Layer, ServiceMap } from "effect"; | |
| 1 | import { Effect, Layer, FileSystem, Path } from "effect"; | |
| 7 | 2 | |
| 8 | 3 | import { runMigrations } from "../Migrations.ts"; |
| 4 | import * as SqliteClient from "../SqliteClient.ts"; | |
| 9 | 5 | |
| 10 | export interface PersistenceConfigShape { | |
| 11 | readonly dbPath: string; | |
| 12 | } | |
| 13 | ||
| 14 | export class PersistenceConfig extends ServiceMap.Service< | |
| 15 | PersistenceConfig, | |
| 16 | PersistenceConfigShape | |
| 17 | >()("persistence/Config") {} | |
| 6 | const setup = Layer.effectDiscard( | |
| 7 | Effect.gen(function* () { | |
| 8 | const sql = yield* SqliteClient.SqliteClient; | |
| 9 | yield* sql`PRAGMA journal_mode = WAL;`; | |
| 10 | yield* sql`PRAGMA foreign_keys = ON;`; | |
| 11 | yield* runMigrations; | |
| 12 | }), | |
| 13 | ); | |
| 18 | 14 | |
| 19 | export function makeSqlitePersistenceLive(dbPath: string) { | |
| 20 | const SqliteClientLive = Layer.unwrap( | |
| 21 | Effect.sync(() => { | |
| 22 | fs.mkdirSync(path.dirname(dbPath), { recursive: true }); | |
| 23 | return SqliteClient.layer({ filename: dbPath }); | |
| 24 | }), | |
| 25 | ); | |
| 15 | export const makeSqlitePersistenceLive = (dbPath: string) => | |
| 16 | Effect.gen(function* () { | |
| 17 | const fs = yield* FileSystem.FileSystem; | |
| 18 | const path = yield* Path.Path; | |
| 19 | yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true }); | |
| 26 | 20 | |
| 27 | return Layer.effect( | |
| 28 | SqlClient.SqlClient, | |
| 29 | Effect.gen(function* () { | |
| 30 | const sql = yield* SqliteClient.SqliteClient; | |
| 31 | yield* sql`PRAGMA journal_mode = WAL;`; | |
| 32 | yield* sql`PRAGMA foreign_keys = ON;`; | |
| 33 | yield* runMigrations; | |
| 34 | return sql; | |
| 35 | }), | |
| 36 | ).pipe(Layer.provide(SqliteClientLive)); | |
| 37 | } | |
| 21 | return Layer.provideMerge(setup, SqliteClient.layer({ filename: dbPath })); | |
| 22 | }).pipe(Layer.unwrap); | |
| 38 | 23 | |
| 39 | export const SqlitePersistenceLive = Layer.unwrap( | |
| 40 | Effect.map(Effect.service(PersistenceConfig), ({ dbPath }) => makeSqlitePersistenceLive(dbPath)), | |
| 41 | ); | |
| 24 | export const SqlitePersistenceMemory = Layer.provideMerge(setup, SqliteClient.layerMemory()); |
apps/server/src/persistence/Migrations.ts+1-1| ... | ... | @@ -59,7 +59,7 @@ export const runMigrations = run({ loader }); |
| 59 | 59 | * @example |
| 60 | 60 | * ```typescript |
| 61 | 61 | * import { MigrationsLive } from "@acme/db/Migrations" |
| 62 | * import { SqliteClient } from "@effect/sql-sqlite-node" | |
| 62 | * import * as SqliteClient from "@acme/db/SqliteClient" | |
| 63 | 63 | * |
| 64 | 64 | * // Migrations run automatically when SqliteClient is provided |
| 65 | 65 | * const AppLayer = MigrationsLive.pipe( |
apps/server/src/persistence/SqliteClient.test.ts created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | import { assert, it } from "@effect/vitest"; | |
| 2 | import { Effect } from "effect"; | |
| 3 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 4 | ||
| 5 | import * as SqliteClient from "./SqliteClient.ts"; | |
| 6 | ||
| 7 | it.effect("runs prepared queries and returns positional values", () => | |
| 8 | Effect.gen(function* () { | |
| 9 | const sql = yield* SqlClient.SqlClient; | |
| 10 | ||
| 11 | yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`; | |
| 12 | yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`; | |
| 13 | ||
| 14 | const rows = yield* sql<{ readonly id: number; readonly name: string }>` | |
| 15 | SELECT id, name FROM entries ORDER BY id | |
| 16 | `; | |
| 17 | assert.equal(rows.length, 2); | |
| 18 | assert.equal(rows[0]?.name, "alpha"); | |
| 19 | assert.equal(rows[1]?.name, "beta"); | |
| 20 | ||
| 21 | const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values; | |
| 22 | assert.equal(values.length, 2); | |
| 23 | assert.equal(values[0]?.[1], "alpha"); | |
| 24 | assert.equal(values[1]?.[1], "beta"); | |
| 25 | }).pipe(Effect.provide(SqliteClient.layerMemory())), | |
| 26 | ); |
apps/server/src/persistence/SqliteClient.ts created+242| ... | ... | @@ -0,0 +1,242 @@ |
| 1 | /** | |
| 2 | * Port of `@effect/sql-sqlite-node` that uses the native `node:sqlite` | |
| 3 | * bindings instead of `better-sqlite3`. | |
| 4 | */ | |
| 5 | import { DatabaseSync, type StatementSync } from "node:sqlite"; | |
| 6 | ||
| 7 | import * as Cache from "effect/Cache"; | |
| 8 | import * as Config from "effect/Config"; | |
| 9 | import * as Duration from "effect/Duration"; | |
| 10 | import * as Effect from "effect/Effect"; | |
| 11 | import * as Fiber from "effect/Fiber"; | |
| 12 | import { identity } from "effect/Function"; | |
| 13 | import * as Layer from "effect/Layer"; | |
| 14 | import * as Scope from "effect/Scope"; | |
| 15 | import * as Semaphore from "effect/Semaphore"; | |
| 16 | import * as ServiceMap from "effect/ServiceMap"; | |
| 17 | import * as Stream from "effect/Stream"; | |
| 18 | import * as Reactivity from "effect/unstable/reactivity/Reactivity"; | |
| 19 | import * as Client from "effect/unstable/sql/SqlClient"; | |
| 20 | import type { Connection } from "effect/unstable/sql/SqlConnection"; | |
| 21 | import { SqlError } from "effect/unstable/sql/SqlError"; | |
| 22 | import * as Statement from "effect/unstable/sql/Statement"; | |
| 23 | ||
| 24 | const ATTR_DB_SYSTEM_NAME = "db.system.name"; | |
| 25 | ||
| 26 | export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; | |
| 27 | ||
| 28 | export type TypeId = "~local/sqlite-node/SqliteClient"; | |
| 29 | ||
| 30 | export const SqliteClient = ServiceMap.Service<Client.SqlClient>("@local/sqlite-node/SqliteClient"); | |
| 31 | ||
| 32 | export interface SqliteClientConfig { | |
| 33 | readonly filename: string; | |
| 34 | readonly readonly?: boolean | undefined; | |
| 35 | readonly allowExtension?: boolean | undefined; | |
| 36 | readonly prepareCacheSize?: number | undefined; | |
| 37 | readonly prepareCacheTTL?: Duration.Input | undefined; | |
| 38 | readonly disableWAL?: boolean | undefined; | |
| 39 | readonly spanAttributes?: Record<string, unknown> | undefined; | |
| 40 | readonly transformResultNames?: ((str: string) => string) | undefined; | |
| 41 | readonly transformQueryNames?: ((str: string) => string) | undefined; | |
| 42 | } | |
| 43 | ||
| 44 | export interface SqliteMemoryClientConfig extends Omit< | |
| 45 | SqliteClientConfig, | |
| 46 | "filename" | "readonly" | |
| 47 | > {} | |
| 48 | ||
| 49 | const makeWithDatabase = ( | |
| 50 | options: SqliteClientConfig, | |
| 51 | openDatabase: () => DatabaseSync, | |
| 52 | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => | |
| 53 | Effect.gen(function* () { | |
| 54 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); | |
| 55 | const transformRows = options.transformResultNames | |
| 56 | ? Statement.defaultTransforms(options.transformResultNames).array | |
| 57 | : undefined; | |
| 58 | ||
| 59 | const makeConnection = Effect.gen(function* () { | |
| 60 | const scope = yield* Effect.scope; | |
| 61 | const db = openDatabase(); | |
| 62 | yield* Scope.addFinalizer( | |
| 63 | scope, | |
| 64 | Effect.sync(() => db.close()), | |
| 65 | ); | |
| 66 | ||
| 67 | if (options.disableWAL !== true) { | |
| 68 | db.exec("PRAGMA journal_mode = WAL"); | |
| 69 | } | |
| 70 | ||
| 71 | const statementReaderCache = new WeakMap<StatementSync, boolean>(); | |
| 72 | const hasRows = (statement: StatementSync): boolean => { | |
| 73 | const cached = statementReaderCache.get(statement); | |
| 74 | if (cached !== undefined) { | |
| 75 | return cached; | |
| 76 | } | |
| 77 | const value = statement.columns().length > 0; | |
| 78 | statementReaderCache.set(statement, value); | |
| 79 | return value; | |
| 80 | }; | |
| 81 | ||
| 82 | const prepareCache = yield* Cache.make({ | |
| 83 | capacity: options.prepareCacheSize ?? 200, | |
| 84 | timeToLive: options.prepareCacheTTL ?? Duration.minutes(10), | |
| 85 | lookup: (sql: string) => | |
| 86 | Effect.try({ | |
| 87 | try: () => db.prepare(sql), | |
| 88 | catch: (cause) => new SqlError({ cause, message: "Failed to prepare statement" }), | |
| 89 | }), | |
| 90 | }); | |
| 91 | ||
| 92 | const runStatement = ( | |
| 93 | statement: StatementSync, | |
| 94 | params: ReadonlyArray<unknown>, | |
| 95 | raw: boolean, | |
| 96 | ) => | |
| 97 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { | |
| 98 | statement.setReadBigInts(Boolean(ServiceMap.get(fiber.services, Client.SafeIntegers))); | |
| 99 | try { | |
| 100 | if (hasRows(statement)) { | |
| 101 | return Effect.succeed(statement.all(...(params as any))); | |
| 102 | } | |
| 103 | const result = statement.run(...(params as any)); | |
| 104 | return Effect.succeed(raw ? (result as unknown as ReadonlyArray<any>) : []); | |
| 105 | } catch (cause) { | |
| 106 | return Effect.fail(new SqlError({ cause, message: "Failed to execute statement" })); | |
| 107 | } | |
| 108 | }); | |
| 109 | ||
| 110 | const run = (sql: string, params: ReadonlyArray<unknown>, raw = false) => | |
| 111 | Effect.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw)); | |
| 112 | ||
| 113 | const runValues = (sql: string, params: ReadonlyArray<unknown>) => | |
| 114 | Effect.acquireUseRelease( | |
| 115 | Cache.get(prepareCache, sql), | |
| 116 | (statement) => | |
| 117 | Effect.try({ | |
| 118 | try: () => { | |
| 119 | if (hasRows(statement)) { | |
| 120 | statement.setReturnArrays(true); | |
| 121 | // Safe to cast to array after we've setReturnArrays(true) | |
| 122 | return statement.all(...(params as any)) as unknown as ReadonlyArray< | |
| 123 | ReadonlyArray<unknown> | |
| 124 | >; | |
| 125 | } | |
| 126 | statement.run(...(params as any)); | |
| 127 | return []; | |
| 128 | }, | |
| 129 | catch: (cause) => new SqlError({ cause, message: "Failed to execute statement" }), | |
| 130 | }), | |
| 131 | (statement) => | |
| 132 | Effect.sync(() => { | |
| 133 | if (hasRows(statement)) { | |
| 134 | statement.setReturnArrays(false); | |
| 135 | } | |
| 136 | }), | |
| 137 | ); | |
| 138 | ||
| 139 | return identity<Connection>({ | |
| 140 | execute(sql, params, rowTransform) { | |
| 141 | return rowTransform ? Effect.map(run(sql, params), rowTransform) : run(sql, params); | |
| 142 | }, | |
| 143 | executeRaw(sql, params) { | |
| 144 | return run(sql, params, true); | |
| 145 | }, | |
| 146 | executeValues(sql, params) { | |
| 147 | return runValues(sql, params); | |
| 148 | }, | |
| 149 | executeUnprepared(sql, params, rowTransform) { | |
| 150 | const effect = runStatement(db.prepare(sql), params ?? [], false); | |
| 151 | return rowTransform ? Effect.map(effect, rowTransform) : effect; | |
| 152 | }, | |
| 153 | executeStream(_sql, _params) { | |
| 154 | return Stream.die("executeStream not implemented"); | |
| 155 | }, | |
| 156 | }); | |
| 157 | }); | |
| 158 | ||
| 159 | const semaphore = yield* Semaphore.make(1); | |
| 160 | const connection = yield* makeConnection; | |
| 161 | ||
| 162 | const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)); | |
| 163 | const transactionAcquirer = Effect.uninterruptibleMask((restore) => { | |
| 164 | const fiber = Fiber.getCurrent()!; | |
| 165 | const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope); | |
| 166 | return Effect.as( | |
| 167 | Effect.tap(restore(semaphore.take(1)), () => | |
| 168 | Scope.addFinalizer(scope, semaphore.release(1)), | |
| 169 | ), | |
| 170 | connection, | |
| 171 | ); | |
| 172 | }); | |
| 173 | ||
| 174 | return yield* Client.make({ | |
| 175 | acquirer, | |
| 176 | compiler, | |
| 177 | transactionAcquirer, | |
| 178 | spanAttributes: [ | |
| 179 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), | |
| 180 | [ATTR_DB_SYSTEM_NAME, "sqlite"], | |
| 181 | ], | |
| 182 | transformRows, | |
| 183 | }); | |
| 184 | }); | |
| 185 | ||
| 186 | const make = ( | |
| 187 | options: SqliteClientConfig, | |
| 188 | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => | |
| 189 | makeWithDatabase( | |
| 190 | options, | |
| 191 | () => | |
| 192 | new DatabaseSync(options.filename, { | |
| 193 | readOnly: options.readonly ?? false, | |
| 194 | allowExtension: options.allowExtension ?? false, | |
| 195 | }), | |
| 196 | ); | |
| 197 | ||
| 198 | const makeMemory = ( | |
| 199 | config: SqliteMemoryClientConfig = {}, | |
| 200 | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => | |
| 201 | makeWithDatabase( | |
| 202 | { | |
| 203 | ...config, | |
| 204 | filename: ":memory:", | |
| 205 | readonly: false, | |
| 206 | disableWAL: config.disableWAL ?? true, | |
| 207 | }, | |
| 208 | () => { | |
| 209 | const database = new DatabaseSync(":memory:", { | |
| 210 | allowExtension: config.allowExtension ?? false, | |
| 211 | }); | |
| 212 | return database; | |
| 213 | }, | |
| 214 | ); | |
| 215 | ||
| 216 | export const layerConfig = ( | |
| 217 | config: Config.Wrap<SqliteClientConfig>, | |
| 218 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => | |
| 219 | Layer.effectServices( | |
| 220 | Config.unwrap(config) | |
| 221 | .asEffect() | |
| 222 | .pipe( | |
| 223 | Effect.flatMap(make), | |
| 224 | Effect.map((client) => | |
| 225 | ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)), | |
| 226 | ), | |
| 227 | ), | |
| 228 | ).pipe(Layer.provide(Reactivity.layer)); | |
| 229 | ||
| 230 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => | |
| 231 | Layer.effectServices( | |
| 232 | Effect.map(make(config), (client) => | |
| 233 | ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)), | |
| 234 | ), | |
| 235 | ).pipe(Layer.provide(Reactivity.layer)); | |
| 236 | ||
| 237 | export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> => | |
| 238 | Layer.effectServices( | |
| 239 | Effect.map(makeMemory(config), (client) => | |
| 240 | ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)), | |
| 241 | ), | |
| 242 | ).pipe(Layer.provide(Reactivity.layer)); |
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+11-7| ... | ... | @@ -6,8 +6,9 @@ import { Effect, Layer } from "effect"; |
| 6 | 6 | |
| 7 | 7 | import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; |
| 8 | 8 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; |
| 9 | import { makeProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; | |
| 9 | import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; | |
| 10 | 10 | import { ProviderUnsupportedError } from "../Errors.ts"; |
| 11 | import { NodeServices } from "@effect/platform-node"; | |
| 11 | 12 | |
| 12 | 13 | const fakeCodexAdapter: CodexAdapterShape = { |
| 13 | 14 | provider: "codex", |
| ... | ... | @@ -25,13 +26,14 @@ const fakeCodexAdapter: CodexAdapterShape = { |
| 25 | 26 | }; |
| 26 | 27 | |
| 27 | 28 | const layer = it.layer( |
| 28 | makeProviderAdapterRegistryLive().pipe( | |
| 29 | Layer.provide(Layer.succeed(CodexAdapter, fakeCodexAdapter)), | |
| 29 | Layer.mergeAll( | |
| 30 | Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)), | |
| 31 | NodeServices.layer, | |
| 30 | 32 | ), |
| 31 | 33 | ); |
| 32 | 34 | |
| 33 | 35 | layer("ProviderAdapterRegistryLive", (it) => { |
| 34 | it("resolves a registered provider adapter", () => | |
| 36 | it.effect("resolves a registered provider adapter", () => | |
| 35 | 37 | Effect.gen(function* () { |
| 36 | 38 | const registry = yield* ProviderAdapterRegistry; |
| 37 | 39 | const adapter = yield* registry.getByProvider("codex"); |
| ... | ... | @@ -39,12 +41,14 @@ layer("ProviderAdapterRegistryLive", (it) => { |
| 39 | 41 | |
| 40 | 42 | const providers = yield* registry.listProviders(); |
| 41 | 43 | assert.deepEqual(providers, ["codex"]); |
| 42 | })); | |
| 44 | }), | |
| 45 | ); | |
| 43 | 46 | |
| 44 | it("fails with ProviderUnsupportedError for unknown providers", () => | |
| 47 | it.effect("fails with ProviderUnsupportedError for unknown providers", () => | |
| 45 | 48 | Effect.gen(function* () { |
| 46 | 49 | const registry = yield* ProviderAdapterRegistry; |
| 47 | 50 | const adapter = yield* registry.getByProvider("unknown" as ProviderKind).pipe(Effect.result); |
| 48 | 51 | assertFailure(adapter, new ProviderUnsupportedError({ provider: "unknown" })); |
| 49 | })); | |
| 52 | }), | |
| 53 | ); | |
| 50 | 54 | }); |
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts-4| ... | ... | @@ -51,7 +51,3 @@ export const ProviderAdapterRegistryLive = Layer.effect( |
| 51 | 51 | ProviderAdapterRegistry, |
| 52 | 52 | makeProviderAdapterRegistry(), |
| 53 | 53 | ); |
| 54 | ||
| 55 | export function makeProviderAdapterRegistryLive(options?: ProviderAdapterRegistryLiveOptions) { | |
| 56 | return Layer.effect(ProviderAdapterRegistry, makeProviderAdapterRegistry(options)); | |
| 57 | } |
apps/server/src/provider/Layers/ProviderService.test.ts+46-7| ... | ... | @@ -31,6 +31,7 @@ import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts" |
| 31 | 31 | import { ProviderService } from "../Services/ProviderService.ts"; |
| 32 | 32 | import { ProviderServiceLive } from "./ProviderService.ts"; |
| 33 | 33 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 34 | import { NodeServices } from "@effect/platform-node"; | |
| 34 | 35 | |
| 35 | 36 | function makeFakeCodexAdapter() { |
| 36 | 37 | const sessions = new Map<string, ProviderSession>(); |
| ... | ... | @@ -253,15 +254,17 @@ function makeProviderServiceLayer() { |
| 253 | 254 | listProviders: () => Effect.succeed(["codex"]), |
| 254 | 255 | }; |
| 255 | 256 | |
| 257 | const checkpointLayer = Layer.succeed(CheckpointService, checkpoint.service); | |
| 258 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | |
| 259 | ||
| 256 | 260 | const layer = it.layer( |
| 257 | ProviderServiceLive.pipe( | |
| 258 | Layer.provide( | |
| 259 | Layer.mergeAll( | |
| 260 | Layer.succeed(ProviderAdapterRegistry, registry), | |
| 261 | ProviderSessionDirectoryLive, | |
| 262 | Layer.succeed(CheckpointService, checkpoint.service), | |
| 263 | ), | |
| 261 | Layer.mergeAll( | |
| 262 | ProviderServiceLive.pipe( | |
| 263 | Layer.provide(providerAdapterLayer), | |
| 264 | Layer.provide(ProviderSessionDirectoryLive), | |
| 265 | Layer.provide(checkpointLayer), | |
| 264 | 266 | ), |
| 267 | NodeServices.layer, | |
| 265 | 268 | ), |
| 266 | 269 | ); |
| 267 | 270 | |
| ... | ... | @@ -424,4 +427,40 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 424 | 427 | ); |
| 425 | 428 | }), |
| 426 | 429 | ); |
| 430 | ||
| 431 | it.effect("fails startSession when adapter returns no threadId", () => | |
| 432 | Effect.gen(function* () { | |
| 433 | const provider = yield* ProviderService; | |
| 434 | ||
| 435 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => | |
| 436 | Effect.sync(() => { | |
| 437 | const now = new Date().toISOString(); | |
| 438 | return { | |
| 439 | sessionId: "sess-missing-thread", | |
| 440 | provider: "codex", | |
| 441 | status: "ready", | |
| 442 | cwd: input.cwd ?? process.cwd(), | |
| 443 | createdAt: now, | |
| 444 | updatedAt: now, | |
| 445 | } satisfies ProviderSession; | |
| 446 | }), | |
| 447 | ); | |
| 448 | ||
| 449 | const failure = yield* Effect.result( | |
| 450 | provider.startSession({ | |
| 451 | provider: "codex", | |
| 452 | cwd: "/tmp/project", | |
| 453 | }), | |
| 454 | ); | |
| 455 | ||
| 456 | assertFailure( | |
| 457 | failure, | |
| 458 | new ProviderValidationError({ | |
| 459 | operation: "ProviderService.startSession", | |
| 460 | issue: | |
| 461 | "Provider 'codex' returned a session without threadId. threadId is required for checkpoint initialization.", | |
| 462 | }), | |
| 463 | ); | |
| 464 | }), | |
| 465 | ); | |
| 427 | 466 | }); |
apps/server/src/provider/Layers/ProviderService.ts+11-1| ... | ... | @@ -161,10 +161,20 @@ const makeProviderService = Effect.gen(function* () { |
| 161 | 161 | ); |
| 162 | 162 | } |
| 163 | 163 | |
| 164 | const threadId = session.threadId?.trim(); | |
| 165 | if (!threadId) { | |
| 166 | return yield* Effect.fail( | |
| 167 | toValidationError( | |
| 168 | "ProviderService.startSession", | |
| 169 | `Provider '${adapter.provider}' returned a session without threadId. threadId is required for checkpoint initialization.`, | |
| 170 | ), | |
| 171 | ); | |
| 172 | } | |
| 173 | ||
| 164 | 174 | yield* directory.upsert({ |
| 165 | 175 | sessionId: session.sessionId, |
| 166 | 176 | provider: session.provider, |
| 167 | ...(session.threadId !== undefined ? { threadId: session.threadId } : {}), | |
| 177 | threadId, | |
| 168 | 178 | }); |
| 169 | 179 | |
| 170 | 180 | const checkpointCwd = session.cwd ?? input.cwd ?? process.cwd(); |
apps/server/src/wsServer.test.ts+13-5| ... | ... | @@ -30,7 +30,9 @@ import type { |
| 30 | 30 | } from "@t3tools/contracts"; |
| 31 | 31 | import type { TerminalManager } from "./terminalManager"; |
| 32 | 32 | import { ProjectRepositoryLive } from "./persistence/Layers/Projects"; |
| 33 | import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite"; | |
| 33 | import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite"; | |
| 34 | import { NodeServices } from "@effect/platform-node"; | |
| 35 | import { SqlClient } from "effect/unstable/sql"; | |
| 34 | 36 | |
| 35 | 37 | interface PendingMessages { |
| 36 | 38 | queue: unknown[]; |
| ... | ... | @@ -237,6 +239,7 @@ describe("WebSocket Server", () => { |
| 237 | 239 | |
| 238 | 240 | function createTestServer( |
| 239 | 241 | options: { |
| 242 | persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>; | |
| 240 | 243 | cwd?: string; |
| 241 | 244 | devUrl?: string; |
| 242 | 245 | authToken?: string; |
| ... | ... | @@ -253,6 +256,7 @@ describe("WebSocket Server", () => { |
| 253 | 256 | port: 0, |
| 254 | 257 | cwd: options.cwd ?? "/test/project", |
| 255 | 258 | stateDir, |
| 259 | persistenceLayer: options.persistenceLayer ?? SqlitePersistenceMemory, | |
| 256 | 260 | ...(options.devUrl ? { devUrl: options.devUrl } : {}), |
| 257 | 261 | ...(options.authToken ? { authToken: options.authToken } : {}), |
| 258 | 262 | ...(options.gitManager ? { gitManager: options.gitManager as never } : {}), |
| ... | ... | @@ -1033,11 +1037,11 @@ describe("WebSocket Server", () => { |
| 1033 | 1037 | const stateDir = makeTempDir("t3code-ws-prune-state-"); |
| 1034 | 1038 | const existing = makeTempDir("t3code-ws-existing-project-"); |
| 1035 | 1039 | const missing = makeTempDir("t3code-ws-missing-project-"); |
| 1040 | const dbPath = path.join(stateDir, "test-prune.sqlite"); | |
| 1041 | const persistenceLayer = Layer.provide(makeSqlitePersistenceLive(dbPath), NodeServices.layer); | |
| 1036 | 1042 | |
| 1037 | 1043 | const projectRuntime = ManagedRuntime.make( |
| 1038 | ProjectRepositoryLive.pipe( | |
| 1039 | Layer.provide(makeSqlitePersistenceLive(path.join(stateDir, "orchestration.sqlite"))), | |
| 1040 | ), | |
| 1044 | ProjectRepositoryLive.pipe(Layer.provide(persistenceLayer)), | |
| 1041 | 1045 | ); |
| 1042 | 1046 | const projectRepository = await projectRuntime.runPromise(Effect.service(ProjectRepository)); |
| 1043 | 1047 | await projectRuntime.runPromise(projectRepository.add({ cwd: existing })); |
| ... | ... | @@ -1045,7 +1049,11 @@ describe("WebSocket Server", () => { |
| 1045 | 1049 | await projectRuntime.dispose(); |
| 1046 | 1050 | fs.rmSync(missing, { recursive: true, force: true }); |
| 1047 | 1051 | |
| 1048 | server = createTestServer({ stateDir, cwd: "/test" }); | |
| 1052 | server = createTestServer({ | |
| 1053 | stateDir, | |
| 1054 | cwd: "/test", | |
| 1055 | persistenceLayer, | |
| 1056 | }); | |
| 1049 | 1057 | await server.start(); |
| 1050 | 1058 | const addr = server.httpServer.address(); |
| 1051 | 1059 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
apps/server/src/wsServer.ts+181-124| ... | ... | @@ -18,11 +18,11 @@ import { |
| 18 | 18 | type WsResponse, |
| 19 | 19 | wsRequestSchema, |
| 20 | 20 | } from "@t3tools/contracts"; |
| 21 | import { NodeServices } from "@effect/platform-node"; | |
| 21 | 22 | import { Effect, Layer, ManagedRuntime } from "effect"; |
| 22 | 23 | import { WebSocketServer, type WebSocket } from "ws"; |
| 23 | 24 | |
| 24 | 25 | import { createLogger } from "./logger"; |
| 25 | import { ProviderManager } from "./providerManager"; | |
| 26 | 26 | import { GitManager } from "./gitManager"; |
| 27 | 27 | import { |
| 28 | 28 | checkoutGitBranch, |
| ... | ... | @@ -43,6 +43,17 @@ import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite"; |
| 43 | 43 | import { ProjectRepository, type ProjectRepositoryShape } from "./persistence/Services/Projects"; |
| 44 | 44 | import assert from "node:assert"; |
| 45 | 45 | import { OrchestrationEventRepositoryLive } from "./persistence/Layers/OrchestrationEvents"; |
| 46 | import { CodexAdapterLive } from "./provider/Layers/CodexAdapter"; | |
| 47 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; | |
| 48 | import { ProviderServiceLive } from "./provider/Layers/ProviderService"; | |
| 49 | import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory"; | |
| 50 | import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService"; | |
| 51 | import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore"; | |
| 52 | import { CheckpointServiceLive } from "./checkpointing/Layers/CheckpointService"; | |
| 53 | import { CheckpointRepositoryLive } from "./persistence/Layers/Checkpoints"; | |
| 54 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 55 | import * as SqlError from "effect/unstable/sql/SqlError"; | |
| 56 | import * as Migrator from "effect/unstable/sql/Migrator"; | |
| 46 | 57 | |
| 47 | 58 | const MIME_TYPES: Record<string, string> = { |
| 48 | 59 | ".html": "text/html; charset=utf-8", |
| ... | ... | @@ -64,6 +75,9 @@ export interface ServerOptions { |
| 64 | 75 | host?: string | undefined; |
| 65 | 76 | cwd: string; |
| 66 | 77 | stateDir?: string | undefined; |
| 78 | persistenceLayer?: | |
| 79 | | Layer.Layer<SqlClient.SqlClient, SqlError.SqlError | Migrator.MigrationError> | |
| 80 | | undefined; | |
| 67 | 81 | staticDir?: string | undefined; |
| 68 | 82 | devUrl?: string | undefined; |
| 69 | 83 | logWebSocketEvents?: boolean | undefined; |
| ... | ... | @@ -121,6 +135,7 @@ export function createServer(options: ServerOptions) { |
| 121 | 135 | host, |
| 122 | 136 | cwd, |
| 123 | 137 | stateDir, |
| 138 | persistenceLayer: customPersistenceLayer, | |
| 124 | 139 | staticDir, |
| 125 | 140 | devUrl, |
| 126 | 141 | logWebSocketEvents: explicitLogWsEvents, |
| ... | ... | @@ -128,15 +143,15 @@ export function createServer(options: ServerOptions) { |
| 128 | 143 | terminalManager = new TerminalManager(), |
| 129 | 144 | authToken, |
| 130 | 145 | } = options; |
| 131 | const providerManager = new ProviderManager(); | |
| 132 | 146 | |
| 133 | 147 | const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata"); |
| 134 | 148 | let effectRuntime: ManagedRuntime.ManagedRuntime< |
| 135 | ProjectRepository | OrchestrationEngineService, | |
| 149 | ProjectRepository | OrchestrationEngineService | ProviderService, | |
| 136 | 150 | unknown |
| 137 | 151 | > | null = null; |
| 138 | 152 | let projectRepository: ProjectRepositoryShape | null = null; |
| 139 | 153 | let orchestrationEngine: OrchestrationEngineShape | null = null; |
| 154 | let providerService: ProviderServiceShape | null = null; | |
| 140 | 155 | const clients = new Set<WebSocket>(); |
| 141 | 156 | const logger = createLogger("ws"); |
| 142 | 157 | const logWebSocketEvents = |
| ... | ... | @@ -144,6 +159,7 @@ export function createServer(options: ServerOptions) { |
| 144 | 159 | let keybindingsConfig = loadResolvedKeybindingsConfig(logger); |
| 145 | 160 | let unsubscribeReadModel = noop; |
| 146 | 161 | let unsubscribeDomainEvents = noop; |
| 162 | let unsubscribeProviderEvents = noop; | |
| 147 | 163 | |
| 148 | 164 | function logOutgoingPush(push: WsPush, recipients: number) { |
| 149 | 165 | if (!logWebSocketEvents) return; |
| ... | ... | @@ -169,8 +185,16 @@ export function createServer(options: ServerOptions) { |
| 169 | 185 | return effectRuntime; |
| 170 | 186 | }; |
| 171 | 187 | |
| 188 | const getProviderService = () => { | |
| 189 | assert(providerService, "Provider service is not started"); | |
| 190 | return providerService; | |
| 191 | }; | |
| 192 | ||
| 172 | 193 | async function attachOrchestrationSubscriptions( |
| 173 | runtime: ManagedRuntime.ManagedRuntime<ProjectRepository | OrchestrationEngineService, unknown>, | |
| 194 | runtime: ManagedRuntime.ManagedRuntime< | |
| 195 | ProjectRepository | OrchestrationEngineService | ProviderService, | |
| 196 | unknown | |
| 197 | >, | |
| 174 | 198 | orchestrationEngine: OrchestrationEngineShape, |
| 175 | 199 | ) { |
| 176 | 200 | unsubscribeReadModel = await runtime.runPromise( |
| ... | ... | @@ -215,114 +239,121 @@ export function createServer(options: ServerOptions) { |
| 215 | 239 | ); |
| 216 | 240 | } |
| 217 | 241 | |
| 218 | // Forward provider events to all connected WebSocket clients | |
| 219 | providerManager.on("event", (event) => { | |
| 220 | void (async () => { | |
| 221 | const liveOrchestrationEngine = orchestrationEngine; | |
| 222 | const runtime = effectRuntime; | |
| 223 | if (!liveOrchestrationEngine || !runtime) { | |
| 224 | return; | |
| 225 | } | |
| 226 | const snapshot = await runtime.runPromise(liveOrchestrationEngine.getSnapshot()); | |
| 227 | const thread = snapshot.threads.find((entry) => entry.session?.sessionId === event.sessionId); | |
| 228 | if (!thread) return; | |
| 229 | const now = event.createdAt; | |
| 230 | if (event.method === "turn/started" || event.method === "turn/completed") { | |
| 231 | await runtime.runPromise( | |
| 232 | liveOrchestrationEngine.dispatch({ | |
| 233 | type: "thread.session", | |
| 234 | commandId: crypto.randomUUID(), | |
| 235 | threadId: thread.id, | |
| 236 | session: { | |
| 237 | sessionId: event.sessionId, | |
| 238 | provider: event.provider, | |
| 239 | status: event.method === "turn/started" ? "running" : "ready", | |
| 240 | threadId: thread.id, | |
| 241 | activeTurnId: event.method === "turn/started" ? (event.turnId ?? null) : null, | |
| 242 | createdAt: thread.session?.createdAt ?? now, | |
| 243 | updatedAt: now, | |
| 244 | lastError: null, | |
| 245 | }, | |
| 246 | createdAt: now, | |
| 247 | }), | |
| 248 | ); | |
| 249 | } | |
| 250 | if (event.textDelta && event.textDelta.length > 0) { | |
| 251 | const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`; | |
| 252 | await runtime.runPromise( | |
| 253 | liveOrchestrationEngine.dispatch({ | |
| 254 | type: "message.send", | |
| 255 | commandId: crypto.randomUUID(), | |
| 256 | threadId: thread.id, | |
| 257 | messageId: assistantMessageId, | |
| 258 | role: "assistant", | |
| 259 | text: event.textDelta, | |
| 260 | streaming: true, | |
| 261 | createdAt: now, | |
| 262 | }), | |
| 263 | ); | |
| 264 | } | |
| 265 | if (event.method === "turn/completed") { | |
| 266 | const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`; | |
| 267 | await runtime.runPromise( | |
| 268 | liveOrchestrationEngine.dispatch({ | |
| 269 | type: "message.send", | |
| 270 | commandId: crypto.randomUUID(), | |
| 271 | threadId: thread.id, | |
| 272 | messageId: assistantMessageId, | |
| 273 | role: "assistant", | |
| 274 | text: "", | |
| 275 | streaming: false, | |
| 276 | createdAt: now, | |
| 277 | }), | |
| 278 | ); | |
| 279 | } | |
| 280 | if (event.method === "checkpoint/captured") { | |
| 281 | const payload = asObject(event.payload); | |
| 282 | const turnId = event.turnId ?? asString(payload?.turnId); | |
| 283 | if (turnId) { | |
| 284 | await runtime.runPromise( | |
| 285 | liveOrchestrationEngine.dispatch({ | |
| 286 | type: "thread.turnDiff.complete", | |
| 287 | commandId: crypto.randomUUID(), | |
| 288 | threadId: thread.id, | |
| 289 | turnId, | |
| 290 | completedAt: now, | |
| 291 | ...(asString(payload?.status) !== null | |
| 292 | ? { status: asString(payload?.status) ?? undefined } | |
| 293 | : {}), | |
| 294 | files: [], | |
| 295 | assistantMessageId: `assistant:${turnId}`, | |
| 296 | ...(asNumber(payload?.turnCount) !== null | |
| 297 | ? { checkpointTurnCount: asNumber(payload?.turnCount) ?? undefined } | |
| 298 | : {}), | |
| 299 | createdAt: now, | |
| 300 | }), | |
| 242 | async function attachProviderSubscriptions( | |
| 243 | runtime: ManagedRuntime.ManagedRuntime< | |
| 244 | ProjectRepository | OrchestrationEngineService | ProviderService, | |
| 245 | unknown | |
| 246 | >, | |
| 247 | providerService: ProviderServiceShape, | |
| 248 | orchestrationEngine: OrchestrationEngineShape, | |
| 249 | ) { | |
| 250 | unsubscribeProviderEvents = await runtime.runPromise( | |
| 251 | providerService.subscribeToEvents((event) => { | |
| 252 | void (async () => { | |
| 253 | const snapshot = await runtime.runPromise(orchestrationEngine.getSnapshot()); | |
| 254 | const thread = snapshot.threads.find( | |
| 255 | (entry) => entry.session?.sessionId === event.sessionId, | |
| 301 | 256 | ); |
| 302 | } | |
| 303 | } | |
| 304 | if (event.kind === "error" && event.message) { | |
| 305 | await runtime.runPromise( | |
| 306 | liveOrchestrationEngine.dispatch({ | |
| 307 | type: "thread.session", | |
| 308 | commandId: crypto.randomUUID(), | |
| 309 | threadId: thread.id, | |
| 310 | session: { | |
| 311 | sessionId: event.sessionId, | |
| 312 | provider: event.provider, | |
| 313 | status: "error", | |
| 314 | threadId: thread.id, | |
| 315 | activeTurnId: event.turnId ?? null, | |
| 316 | createdAt: thread.session?.createdAt ?? now, | |
| 317 | updatedAt: now, | |
| 318 | lastError: event.message, | |
| 319 | }, | |
| 320 | createdAt: now, | |
| 321 | }), | |
| 322 | ); | |
| 323 | } | |
| 324 | })().catch(() => undefined); | |
| 325 | }); | |
| 257 | if (!thread) return; | |
| 258 | const now = event.createdAt; | |
| 259 | if (event.method === "turn/started" || event.method === "turn/completed") { | |
| 260 | await runtime.runPromise( | |
| 261 | orchestrationEngine.dispatch({ | |
| 262 | type: "thread.session", | |
| 263 | commandId: crypto.randomUUID(), | |
| 264 | threadId: thread.id, | |
| 265 | session: { | |
| 266 | sessionId: event.sessionId, | |
| 267 | provider: event.provider, | |
| 268 | status: event.method === "turn/started" ? "running" : "ready", | |
| 269 | threadId: thread.id, | |
| 270 | activeTurnId: event.method === "turn/started" ? (event.turnId ?? null) : null, | |
| 271 | createdAt: thread.session?.createdAt ?? now, | |
| 272 | updatedAt: now, | |
| 273 | lastError: null, | |
| 274 | }, | |
| 275 | createdAt: now, | |
| 276 | }), | |
| 277 | ); | |
| 278 | } | |
| 279 | if (event.textDelta && event.textDelta.length > 0) { | |
| 280 | const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`; | |
| 281 | await runtime.runPromise( | |
| 282 | orchestrationEngine.dispatch({ | |
| 283 | type: "message.send", | |
| 284 | commandId: crypto.randomUUID(), | |
| 285 | threadId: thread.id, | |
| 286 | messageId: assistantMessageId, | |
| 287 | role: "assistant", | |
| 288 | text: event.textDelta, | |
| 289 | streaming: true, | |
| 290 | createdAt: now, | |
| 291 | }), | |
| 292 | ); | |
| 293 | } | |
| 294 | if (event.method === "turn/completed") { | |
| 295 | const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`; | |
| 296 | await runtime.runPromise( | |
| 297 | orchestrationEngine.dispatch({ | |
| 298 | type: "message.send", | |
| 299 | commandId: crypto.randomUUID(), | |
| 300 | threadId: thread.id, | |
| 301 | messageId: assistantMessageId, | |
| 302 | role: "assistant", | |
| 303 | text: "", | |
| 304 | streaming: false, | |
| 305 | createdAt: now, | |
| 306 | }), | |
| 307 | ); | |
| 308 | } | |
| 309 | if (event.method === "checkpoint/captured") { | |
| 310 | const payload = asObject(event.payload); | |
| 311 | const turnId = event.turnId ?? asString(payload?.turnId); | |
| 312 | if (turnId) { | |
| 313 | await runtime.runPromise( | |
| 314 | orchestrationEngine.dispatch({ | |
| 315 | type: "thread.turnDiff.complete", | |
| 316 | commandId: crypto.randomUUID(), | |
| 317 | threadId: thread.id, | |
| 318 | turnId, | |
| 319 | completedAt: now, | |
| 320 | ...(asString(payload?.status) !== null | |
| 321 | ? { status: asString(payload?.status) ?? undefined } | |
| 322 | : {}), | |
| 323 | files: [], | |
| 324 | assistantMessageId: `assistant:${turnId}`, | |
| 325 | ...(asNumber(payload?.turnCount) !== null | |
| 326 | ? { checkpointTurnCount: asNumber(payload?.turnCount) ?? undefined } | |
| 327 | : {}), | |
| 328 | createdAt: now, | |
| 329 | }), | |
| 330 | ); | |
| 331 | } | |
| 332 | } | |
| 333 | if (event.kind === "error" && event.message) { | |
| 334 | await runtime.runPromise( | |
| 335 | orchestrationEngine.dispatch({ | |
| 336 | type: "thread.session", | |
| 337 | commandId: crypto.randomUUID(), | |
| 338 | threadId: thread.id, | |
| 339 | session: { | |
| 340 | sessionId: event.sessionId, | |
| 341 | provider: event.provider, | |
| 342 | status: "error", | |
| 343 | threadId: thread.id, | |
| 344 | activeTurnId: event.turnId ?? null, | |
| 345 | createdAt: thread.session?.createdAt ?? now, | |
| 346 | updatedAt: now, | |
| 347 | lastError: event.message, | |
| 348 | }, | |
| 349 | createdAt: now, | |
| 350 | }), | |
| 351 | ); | |
| 352 | } | |
| 353 | })().catch(() => undefined); | |
| 354 | }), | |
| 355 | ); | |
| 356 | } | |
| 326 | 357 | |
| 327 | 358 | const onTerminalEvent = (event: TerminalEvent) => { |
| 328 | 359 | const push: WsPush = { |
| ... | ... | @@ -504,33 +535,36 @@ export function createServer(options: ServerOptions) { |
| 504 | 535 | const orchestrationEngine = getOrchestrationEngine(); |
| 505 | 536 | const projectRepository = getProjectRepository(); |
| 506 | 537 | const effectRuntime = getEffectRuntime(); |
| 538 | const providerService = getProviderService(); | |
| 507 | 539 | |
| 508 | 540 | switch (request.method) { |
| 509 | 541 | case WS_METHODS.providersStartSession: |
| 510 | return providerManager.startSession(request.params as never); | |
| 542 | return effectRuntime.runPromise(providerService.startSession(request.params as never)); | |
| 511 | 543 | |
| 512 | 544 | case WS_METHODS.providersSendTurn: |
| 513 | return providerManager.sendTurn(request.params as never); | |
| 545 | return effectRuntime.runPromise(providerService.sendTurn(request.params as never)); | |
| 514 | 546 | |
| 515 | 547 | case WS_METHODS.providersInterruptTurn: |
| 516 | return providerManager.interruptTurn(request.params as never); | |
| 548 | return effectRuntime.runPromise(providerService.interruptTurn(request.params as never)); | |
| 517 | 549 | |
| 518 | 550 | case WS_METHODS.providersRespondToRequest: |
| 519 | return providerManager.respondToRequest(request.params as never); | |
| 551 | return effectRuntime.runPromise(providerService.respondToRequest(request.params as never)); | |
| 520 | 552 | |
| 521 | 553 | case WS_METHODS.providersStopSession: { |
| 522 | providerManager.stopSession(request.params as never); | |
| 554 | await effectRuntime.runPromise(providerService.stopSession(request.params as never)); | |
| 523 | 555 | return undefined; |
| 524 | 556 | } |
| 525 | 557 | |
| 526 | 558 | case WS_METHODS.providersListCheckpoints: |
| 527 | return providerManager.listCheckpoints(request.params as never); | |
| 559 | return effectRuntime.runPromise(providerService.listCheckpoints(request.params as never)); | |
| 528 | 560 | |
| 529 | 561 | case WS_METHODS.providersGetCheckpointDiff: |
| 530 | return providerManager.getCheckpointDiff(request.params as never); | |
| 562 | return effectRuntime.runPromise(providerService.getCheckpointDiff(request.params as never)); | |
| 531 | 563 | |
| 532 | 564 | case WS_METHODS.providersRevertToCheckpoint: { |
| 533 | const result = await providerManager.revertToCheckpoint(request.params as never); | |
| 565 | const result = await effectRuntime.runPromise( | |
| 566 | providerService.revertToCheckpoint(request.params as never), | |
| 567 | ); | |
| 534 | 568 | const params = request.params as { sessionId?: string }; |
| 535 | 569 | const sessionId = params.sessionId; |
| 536 | 570 | if (typeof sessionId === "string") { |
| ... | ... | @@ -735,25 +769,42 @@ export function createServer(options: ServerOptions) { |
| 735 | 769 | |
| 736 | 770 | async function createEffectRuntime() { |
| 737 | 771 | const dbPath = path.join(resolvedStateDir, "orchestration.sqlite"); |
| 738 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | |
| 772 | const persistenceLayer = customPersistenceLayer ?? makeSqlitePersistenceLive(dbPath); | |
| 739 | 773 | const orchestrationLayer = Layer.provide( |
| 740 | 774 | OrchestrationEngineLive, |
| 741 | 775 | OrchestrationEventRepositoryLive, |
| 742 | 776 | ); |
| 743 | const layer = Layer.mergeAll(orchestrationLayer, ProjectRepositoryLive).pipe( | |
| 777 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(Layer.provide(CodexAdapterLive)); | |
| 778 | const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(NodeServices.layer)); | |
| 779 | const checkpointDependenciesLayer = Layer.mergeAll( | |
| 780 | checkpointStoreLayer, | |
| 781 | CheckpointRepositoryLive, | |
| 782 | adapterRegistryLayer, | |
| 783 | ProviderSessionDirectoryLive, | |
| 784 | ); | |
| 785 | const checkpointServiceLayer = CheckpointServiceLive.pipe( | |
| 786 | Layer.provideMerge(checkpointDependenciesLayer), | |
| 787 | ); | |
| 788 | const providerLayer = ProviderServiceLive.pipe(Layer.provideMerge(checkpointServiceLayer)); | |
| 789 | ||
| 790 | const layer = Layer.mergeAll(orchestrationLayer, ProjectRepositoryLive, providerLayer).pipe( | |
| 744 | 791 | Layer.provide(persistenceLayer), |
| 792 | Layer.provideMerge(NodeServices.layer), | |
| 745 | 793 | ); |
| 746 | 794 | const runtime = ManagedRuntime.make(layer); |
| 747 | 795 | |
| 748 | 796 | try { |
| 749 | const [nextOrchestrationEngine, repository] = await Promise.all([ | |
| 797 | const [nextOrchestrationEngine, repository, nextProviderService] = await Promise.all([ | |
| 750 | 798 | runtime.runPromise(Effect.service(OrchestrationEngineService)), |
| 751 | 799 | runtime.runPromise(Effect.service(ProjectRepository)), |
| 800 | runtime.runPromise(Effect.service(ProviderService)), | |
| 752 | 801 | ]); |
| 753 | 802 | orchestrationEngine = nextOrchestrationEngine; |
| 754 | 803 | projectRepository = repository; |
| 804 | providerService = nextProviderService; | |
| 755 | 805 | await runtime.runPromise(repository.pruneMissing()); |
| 756 | 806 | await attachOrchestrationSubscriptions(runtime, nextOrchestrationEngine); |
| 807 | await attachProviderSubscriptions(runtime, nextProviderService, nextOrchestrationEngine); | |
| 757 | 808 | } catch (error) { |
| 758 | 809 | await runtime.dispose().catch(() => undefined); |
| 759 | 810 | throw error; |
| ... | ... | @@ -765,18 +816,26 @@ export function createServer(options: ServerOptions) { |
| 765 | 816 | async function disposeEffectRuntime() { |
| 766 | 817 | unsubscribeReadModel(); |
| 767 | 818 | unsubscribeDomainEvents(); |
| 819 | unsubscribeProviderEvents(); | |
| 768 | 820 | unsubscribeReadModel = noop; |
| 769 | 821 | unsubscribeDomainEvents = noop; |
| 822 | unsubscribeProviderEvents = noop; | |
| 770 | 823 | |
| 771 | 824 | const runtime = effectRuntime; |
| 825 | const liveProviderService = providerService; | |
| 772 | 826 | effectRuntime = null; |
| 773 | 827 | orchestrationEngine = null; |
| 774 | 828 | projectRepository = null; |
| 829 | providerService = null; | |
| 775 | 830 | |
| 776 | 831 | if (!runtime) { |
| 777 | 832 | return; |
| 778 | 833 | } |
| 779 | 834 | |
| 835 | if (liveProviderService) { | |
| 836 | await runtime.runPromise(liveProviderService.stopAll()).catch(() => undefined); | |
| 837 | } | |
| 838 | ||
| 780 | 839 | await runtime.dispose(); |
| 781 | 840 | } |
| 782 | 841 | |
| ... | ... | @@ -804,8 +863,6 @@ export function createServer(options: ServerOptions) { |
| 804 | 863 | async function stop(): Promise<void> { |
| 805 | 864 | await disposeEffectRuntime(); |
| 806 | 865 | terminalManager.off("event", onTerminalEvent); |
| 807 | providerManager.stopAll(); | |
| 808 | providerManager.dispose(); | |
| 809 | 866 | terminalManager.dispose(); |
| 810 | 867 | |
| 811 | 868 | for (const client of clients) { |
bun.lock-61| ... | ... | @@ -35,7 +35,6 @@ |
| 35 | 35 | }, |
| 36 | 36 | "dependencies": { |
| 37 | 37 | "@effect/platform-node": "4.0.0-beta.10", |
| 38 | "@effect/sql-sqlite-node": "4.0.0-beta.10", | |
| 39 | 38 | "effect": "4.0.0-beta.10", |
| 40 | 39 | "node-pty": "^1.1.0", |
| 41 | 40 | "open": "^10.1.0", |
| ... | ... | @@ -154,8 +153,6 @@ |
| 154 | 153 | |
| 155 | 154 | "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.10", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.10" } }, "sha512-gBSeOG2bfchXFdgdi5oipHKynOzSd7BlzTy5QcjFOoCUTtLP9igqohPB9hTkV3a6seWE2JJqsgsQ79VuLb7fAg=="], |
| 156 | 155 | |
| 157 | "@effect/sql-sqlite-node": ["@effect/sql-sqlite-node@4.0.0-beta.10", "", { "dependencies": { "better-sqlite3": "^12.6.2" }, "peerDependencies": { "effect": "^4.0.0-beta.10" } }, "sha512-oQl+gF31HaVM9R8YEpf96CIJypP1Roa5z1VPj8s7ZP+3aCtRVor1K+eMxYNAb8ph7R68Tz0LpmVkefq0mEI3CA=="], | |
| 158 | ||
| 159 | 156 | "@effect/vitest": ["@effect/vitest@4.0.0-beta.10", "", { "peerDependencies": { "effect": "^4.0.0-beta.10", "vitest": "^3.0.0" } }, "sha512-WmE8vCNw+eDoAq9CwwymrAdIBw1f+8hDP3AUF7jyC7/Cj/kyAYOFwkbtKmDXIFaTlf7G/2fmulEXeCO5W8j0kQ=="], |
| 160 | 157 | |
| 161 | 158 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], |
| ... | ... | @@ -610,28 +607,18 @@ |
| 610 | 607 | |
| 611 | 608 | "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], |
| 612 | 609 | |
| 613 | "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], | |
| 614 | ||
| 615 | 610 | "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], |
| 616 | 611 | |
| 617 | "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="], | |
| 618 | ||
| 619 | 612 | "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], |
| 620 | 613 | |
| 621 | "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], | |
| 622 | ||
| 623 | 614 | "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], |
| 624 | 615 | |
| 625 | "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], | |
| 626 | ||
| 627 | 616 | "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], |
| 628 | 617 | |
| 629 | 618 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
| 630 | 619 | |
| 631 | 620 | "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], |
| 632 | 621 | |
| 633 | "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], | |
| 634 | ||
| 635 | 622 | "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], |
| 636 | 623 | |
| 637 | 624 | "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], |
| ... | ... | @@ -662,8 +649,6 @@ |
| 662 | 649 | |
| 663 | 650 | "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], |
| 664 | 651 | |
| 665 | "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], | |
| 666 | ||
| 667 | 652 | "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], |
| 668 | 653 | |
| 669 | 654 | "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], |
| ... | ... | @@ -698,8 +683,6 @@ |
| 698 | 683 | |
| 699 | 684 | "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], |
| 700 | 685 | |
| 701 | "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], | |
| 702 | ||
| 703 | 686 | "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], |
| 704 | 687 | |
| 705 | 688 | "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], |
| ... | ... | @@ -774,8 +757,6 @@ |
| 774 | 757 | |
| 775 | 758 | "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], |
| 776 | 759 | |
| 777 | "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], | |
| 778 | ||
| 779 | 760 | "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], |
| 780 | 761 | |
| 781 | 762 | "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], |
| ... | ... | @@ -788,8 +769,6 @@ |
| 788 | 769 | |
| 789 | 770 | "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], |
| 790 | 771 | |
| 791 | "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], | |
| 792 | ||
| 793 | 772 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], |
| 794 | 773 | |
| 795 | 774 | "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], |
| ... | ... | @@ -798,8 +777,6 @@ |
| 798 | 777 | |
| 799 | 778 | "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], |
| 800 | 779 | |
| 801 | "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], | |
| 802 | ||
| 803 | 780 | "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], |
| 804 | 781 | |
| 805 | 782 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], |
| ... | ... | @@ -818,8 +795,6 @@ |
| 818 | 795 | |
| 819 | 796 | "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], |
| 820 | 797 | |
| 821 | "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], | |
| 822 | ||
| 823 | 798 | "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |
| 824 | 799 | |
| 825 | 800 | "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], |
| ... | ... | @@ -868,14 +843,10 @@ |
| 868 | 843 | |
| 869 | 844 | "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], |
| 870 | 845 | |
| 871 | "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], | |
| 872 | ||
| 873 | 846 | "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="], |
| 874 | 847 | |
| 875 | 848 | "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="], |
| 876 | 849 | |
| 877 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], | |
| 878 | ||
| 879 | 850 | "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], |
| 880 | 851 | |
| 881 | 852 | "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], |
| ... | ... | @@ -1082,8 +1053,6 @@ |
| 1082 | 1053 | |
| 1083 | 1054 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], |
| 1084 | 1055 | |
| 1085 | "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], | |
| 1086 | ||
| 1087 | 1056 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| 1088 | 1057 | |
| 1089 | 1058 | "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], |
| ... | ... | @@ -1098,10 +1067,6 @@ |
| 1098 | 1067 | |
| 1099 | 1068 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| 1100 | 1069 | |
| 1101 | "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], | |
| 1102 | ||
| 1103 | "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], | |
| 1104 | ||
| 1105 | 1070 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], |
| 1106 | 1071 | |
| 1107 | 1072 | "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], |
| ... | ... | @@ -1150,8 +1115,6 @@ |
| 1150 | 1115 | |
| 1151 | 1116 | "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], |
| 1152 | 1117 | |
| 1153 | "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], | |
| 1154 | ||
| 1155 | 1118 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], |
| 1156 | 1119 | |
| 1157 | 1120 | "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], |
| ... | ... | @@ -1168,8 +1131,6 @@ |
| 1168 | 1131 | |
| 1169 | 1132 | "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], |
| 1170 | 1133 | |
| 1171 | "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], | |
| 1172 | ||
| 1173 | 1134 | "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], |
| 1174 | 1135 | |
| 1175 | 1136 | "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], |
| ... | ... | @@ -1178,8 +1139,6 @@ |
| 1178 | 1139 | |
| 1179 | 1140 | "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], |
| 1180 | 1141 | |
| 1181 | "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], | |
| 1182 | ||
| 1183 | 1142 | "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], |
| 1184 | 1143 | |
| 1185 | 1144 | "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], |
| ... | ... | @@ -1234,8 +1193,6 @@ |
| 1234 | 1193 | |
| 1235 | 1194 | "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], |
| 1236 | 1195 | |
| 1237 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], | |
| 1238 | ||
| 1239 | 1196 | "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], |
| 1240 | 1197 | |
| 1241 | 1198 | "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], |
| ... | ... | @@ -1254,10 +1211,6 @@ |
| 1254 | 1211 | |
| 1255 | 1212 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |
| 1256 | 1213 | |
| 1257 | "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], | |
| 1258 | ||
| 1259 | "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], | |
| 1260 | ||
| 1261 | 1214 | "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], |
| 1262 | 1215 | |
| 1263 | 1216 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| ... | ... | @@ -1278,14 +1231,10 @@ |
| 1278 | 1231 | |
| 1279 | 1232 | "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], |
| 1280 | 1233 | |
| 1281 | "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], | |
| 1282 | ||
| 1283 | 1234 | "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], |
| 1284 | 1235 | |
| 1285 | 1236 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| 1286 | 1237 | |
| 1287 | "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], | |
| 1288 | ||
| 1289 | 1238 | "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], |
| 1290 | 1239 | |
| 1291 | 1240 | "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], |
| ... | ... | @@ -1306,10 +1255,6 @@ |
| 1306 | 1255 | |
| 1307 | 1256 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| 1308 | 1257 | |
| 1309 | "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], | |
| 1310 | ||
| 1311 | "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], | |
| 1312 | ||
| 1313 | 1258 | "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], |
| 1314 | 1259 | |
| 1315 | 1260 | "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], |
| ... | ... | @@ -1346,8 +1291,6 @@ |
| 1346 | 1291 | |
| 1347 | 1292 | "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], |
| 1348 | 1293 | |
| 1349 | "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], | |
| 1350 | ||
| 1351 | 1294 | "turbo": ["turbo@2.8.7", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.7", "turbo-darwin-arm64": "2.8.7", "turbo-linux-64": "2.8.7", "turbo-linux-arm64": "2.8.7", "turbo-windows-64": "2.8.7", "turbo-windows-arm64": "2.8.7" }, "bin": { "turbo": "bin/turbo" } }, "sha512-RBLh5caMAu1kFdTK1jgH2gH/z+jFsvX5rGbhgJ9nlIAWXSvxlzwId05uDlBA1+pBd3wO/UaKYzaQZQBXDd7kcA=="], |
| 1352 | 1295 | |
| 1353 | 1296 | "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="], |
| ... | ... | @@ -1400,8 +1343,6 @@ |
| 1400 | 1343 | |
| 1401 | 1344 | "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], |
| 1402 | 1345 | |
| 1403 | "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], | |
| 1404 | ||
| 1405 | 1346 | "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], |
| 1406 | 1347 | |
| 1407 | 1348 | "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], |
| ... | ... | @@ -1514,8 +1455,6 @@ |
| 1514 | 1455 | |
| 1515 | 1456 | "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], |
| 1516 | 1457 | |
| 1517 | "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], | |
| 1518 | ||
| 1519 | 1458 | "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| 1520 | 1459 | |
| 1521 | 1460 | "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], |