move-session.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. export * as MoveSession from "./move-session"
  2. import { Context, DateTime, Effect, Layer, Schema } from "effect"
  3. import { EventV2 } from "../event"
  4. import { Git } from "../git"
  5. import { Location } from "../location"
  6. import { ProjectV2 } from "../project"
  7. import { SessionV2 } from "../session"
  8. import { SessionExecution } from "../session/execution"
  9. import { SessionEvent } from "../session/event"
  10. import { SessionSchema } from "../session/schema"
  11. import { AbsolutePath, RelativePath } from "../schema"
  12. import path from "path"
  13. export const Destination = Schema.Struct({
  14. directory: AbsolutePath,
  15. }).annotate({ identifier: "MoveSession.Destination" })
  16. export type Destination = typeof Destination.Type
  17. export const Input = Schema.Struct({
  18. sessionID: SessionSchema.ID,
  19. destination: Destination,
  20. moveChanges: Schema.optional(Schema.Boolean),
  21. }).annotate({ identifier: "MoveSession.Input" })
  22. export type Input = typeof Input.Type
  23. export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
  24. "MoveSession.DestinationProjectMismatchError",
  25. {
  26. expected: ProjectV2.ID,
  27. actual: ProjectV2.ID,
  28. },
  29. ) {}
  30. export class ApplyChangesError extends Schema.TaggedErrorClass<ApplyChangesError>()("MoveSession.ApplyChangesError", {
  31. message: Schema.String,
  32. }) {}
  33. export class CaptureChangesError extends Schema.TaggedErrorClass<CaptureChangesError>()(
  34. "MoveSession.CaptureChangesError",
  35. {
  36. message: Schema.String,
  37. },
  38. ) {}
  39. export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSourceChangesError>()(
  40. "MoveSession.ResetSourceChangesError",
  41. {
  42. directory: AbsolutePath,
  43. message: Schema.String,
  44. cause: Schema.optional(Schema.Defect),
  45. },
  46. ) {}
  47. export type Error =
  48. | SessionV2.NotFoundError
  49. | DestinationProjectMismatchError
  50. | CaptureChangesError
  51. | ApplyChangesError
  52. | ResetSourceChangesError
  53. export interface Interface {
  54. readonly moveSession: (input: Input) => Effect.Effect<void, Error>
  55. }
  56. export class Service extends Context.Service<Service, Interface>()("@opencode/ControlPlaneMoveSession") {}
  57. export const layer = Layer.effect(
  58. Service,
  59. Effect.gen(function* () {
  60. const git = yield* Git.Service
  61. const events = yield* EventV2.Service
  62. const project = yield* ProjectV2.Service
  63. const session = yield* SessionV2.Service
  64. const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
  65. const current = yield* session.get(input.sessionID)
  66. const directory = AbsolutePath.make(input.destination.directory)
  67. if (current.location.directory === directory) return
  68. const source = yield* project.resolve(current.location.directory)
  69. const destination = yield* project.resolve(directory)
  70. if (current.projectID !== destination.id) {
  71. return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id })
  72. }
  73. const patch =
  74. input.moveChanges && source.directory !== destination.directory
  75. ? yield* git
  76. .patch(current.location.directory)
  77. .pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message })))
  78. : ""
  79. if (patch) {
  80. yield* git
  81. .applyPatch({ directory, patch })
  82. .pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
  83. }
  84. yield* events.publish(SessionEvent.Moved, {
  85. sessionID: input.sessionID,
  86. location: Location.Ref.make({ directory }),
  87. subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
  88. timestamp: yield* DateTime.now,
  89. })
  90. if (patch) {
  91. yield* git.softResetChanges(current.location.directory).pipe(
  92. Effect.mapError(
  93. (error) =>
  94. new ResetSourceChangesError({
  95. directory: current.location.directory,
  96. message: error.message,
  97. cause: error.cause,
  98. }),
  99. ),
  100. )
  101. }
  102. })
  103. return Service.of({ moveSession })
  104. }),
  105. )
  106. export const defaultLayer = layer.pipe(
  107. Layer.provide(Git.defaultLayer),
  108. Layer.provide(EventV2.defaultLayer),
  109. Layer.provide(ProjectV2.defaultLayer),
  110. Layer.provide(SessionExecution.noopLayer),
  111. Layer.provide(SessionV2.defaultLayer),
  112. )