@neurocode-ai/server 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "@neurocode-ai/server",
4
+ "version": "1.18.8",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ "./*": "./src/*.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsgo --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@neurocode-ai/core": "1.18.8",
15
+ "@neurocode-ai/protocol": "1.18.8",
16
+ "drizzle-orm": "1.0.0-rc.2",
17
+ "effect": "4.0.0-beta.83"
18
+ },
19
+ "devDependencies": {
20
+ "@tsconfig/bun": "1.0.9",
21
+ "@types/bun": "1.3.13",
22
+ "@typescript/native-preview": "7.0.0-dev.20251207.1"
23
+ }
24
+ }
package/src/api.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { makeDefaultApi } from "@neurocode-ai/protocol/api"
2
+ import { LocationMiddleware } from "./location"
3
+ import { SessionLocationMiddleware } from "./middleware/session-location"
4
+
5
+ export const Api = makeDefaultApi({
6
+ locationMiddleware: LocationMiddleware,
7
+ sessionLocationMiddleware: SessionLocationMiddleware,
8
+ })
package/src/auth.ts ADDED
@@ -0,0 +1,63 @@
1
+ export * as ServerAuth from "./auth"
2
+
3
+ import { Config as EffectConfig, Context, Effect, Layer, Option, Redacted } from "effect"
4
+
5
+ export type Credentials = {
6
+ password?: string
7
+ username?: string
8
+ }
9
+
10
+ export type DecodedCredentials = {
11
+ readonly username: string
12
+ readonly password: Redacted.Redacted
13
+ }
14
+
15
+ export type Info = {
16
+ readonly password: Option.Option<string>
17
+ readonly username: string
18
+ }
19
+
20
+ export class Config extends Context.Service<Config, Info>()("@neurocode/ServerAuthConfig") {
21
+ static configLayer(input: Info) {
22
+ return Layer.succeed(this, this.of(input))
23
+ }
24
+
25
+ static get layer() {
26
+ return Layer.effect(
27
+ this,
28
+ Effect.gen(function* () {
29
+ return Config.of(
30
+ yield* EffectConfig.all({
31
+ password: EffectConfig.string("NEUROCODE_SERVER_PASSWORD").pipe(EffectConfig.option),
32
+ username: EffectConfig.string("NEUROCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")),
33
+ }),
34
+ )
35
+ }),
36
+ )
37
+ }
38
+ }
39
+
40
+ export function required(config: Info) {
41
+ return Option.isSome(config.password) && config.password.value !== ""
42
+ }
43
+
44
+ export function authorized(credentials: DecodedCredentials, config: Info) {
45
+ return (
46
+ Option.isSome(config.password) &&
47
+ credentials.username === config.username &&
48
+ Redacted.value(credentials.password) === config.password.value
49
+ )
50
+ }
51
+
52
+ export function header(credentials?: Credentials) {
53
+ const password = credentials?.password ?? process.env.NEUROCODE_SERVER_PASSWORD
54
+ if (!password) return undefined
55
+
56
+ return `Basic ${Buffer.from(`${credentials?.username ?? process.env.NEUROCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}`
57
+ }
58
+
59
+ export function headers(credentials?: Credentials) {
60
+ const authorization = header(credentials)
61
+ if (!authorization) return undefined
62
+ return { Authorization: authorization }
63
+ }
package/src/cors.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Context } from "effect"
2
+
3
+ const opencodeOrigin = /^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/
4
+
5
+ export type CorsOptions = { readonly cors?: ReadonlyArray<string> }
6
+
7
+ export const CorsConfig = Context.Reference<CorsOptions | undefined>("@neurocode/ServerCorsConfig", {
8
+ defaultValue: () => undefined,
9
+ })
10
+
11
+ export function isAllowedCorsOrigin(input: string | undefined, opts?: CorsOptions) {
12
+ if (!input) return true
13
+ if (input.startsWith("http://localhost:")) return true
14
+ if (input.startsWith("http://127.0.0.1:")) return true
15
+ if (input.startsWith("oc://renderer")) return true
16
+ if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
17
+ return true
18
+ if (opencodeOrigin.test(input)) return true
19
+ return opts?.cors?.includes(input) ?? false
20
+ }
21
+
22
+ export function isAllowedRequestOrigin(input: string | undefined, host: string | undefined, opts?: CorsOptions) {
23
+ if (!input) return true
24
+ if (host && sameHost(input, host)) return true
25
+ return isAllowedCorsOrigin(input, opts)
26
+ }
27
+
28
+ function sameHost(origin: string, host: string) {
29
+ try {
30
+ return new URL(origin).host === host
31
+ } catch {
32
+ return false
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ import { AgentV2 } from "@neurocode-ai/core/agent"
2
+ import { Effect } from "effect"
3
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
4
+ import { Api } from "../api"
5
+ import { response } from "../location"
6
+
7
+ export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
8
+ handlers.handle("agent.list", () =>
9
+ Effect.gen(function* () {
10
+ return yield* response(AgentV2.Service.use((agent) => agent.all()))
11
+ }),
12
+ ),
13
+ )
@@ -0,0 +1,8 @@
1
+ import { CommandV2 } from "@neurocode-ai/core/command"
2
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
3
+ import { Api } from "../api"
4
+ import { response } from "../location"
5
+
6
+ export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) =>
7
+ handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))),
8
+ )
@@ -0,0 +1,22 @@
1
+ import { Integration } from "@neurocode-ai/core/integration"
2
+ import { Effect } from "effect"
3
+ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
4
+ import { Api } from "../api"
5
+
6
+ export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential", (handlers) =>
7
+ handlers
8
+ .handle(
9
+ "credential.update",
10
+ Effect.fn(function* (ctx) {
11
+ yield* (yield* Integration.Service).connection.update(ctx.params.credentialID, { label: ctx.payload.label })
12
+ return HttpApiSchema.NoContent.make()
13
+ }),
14
+ )
15
+ .handle(
16
+ "credential.remove",
17
+ Effect.fn(function* (ctx) {
18
+ yield* (yield* Integration.Service).connection.remove(ctx.params.credentialID)
19
+ return HttpApiSchema.NoContent.make()
20
+ }),
21
+ ),
22
+ )
@@ -0,0 +1,52 @@
1
+ import { EventV2 } from "@neurocode-ai/core/event"
2
+ import { OpenCodeEvent } from "@neurocode-ai/protocol/groups/event"
3
+ import { Effect, Schema, Stream } from "effect"
4
+ import { HttpServerResponse } from "effect/unstable/http"
5
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
6
+ import * as Sse from "effect/unstable/encoding/Sse"
7
+ import { Api } from "../api"
8
+
9
+ const subscriberCapacity = 256
10
+
11
+ function eventData(data: unknown): Sse.Event {
12
+ return {
13
+ _tag: "Event",
14
+ event: "message",
15
+ id: undefined,
16
+ data: JSON.stringify(Schema.encodeUnknownSync(OpenCodeEvent)(data)),
17
+ }
18
+ }
19
+
20
+ export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
21
+ Effect.gen(function* () {
22
+ const events = yield* EventV2.Service
23
+ return handlers.handleRaw("event.subscribe", () =>
24
+ Effect.gen(function* () {
25
+ const connected = {
26
+ id: EventV2.ID.create(),
27
+ type: "server.connected",
28
+ data: {},
29
+ }
30
+ const output = Stream.unwrap(
31
+ Effect.gen(function* () {
32
+ // Acquiring the bounded stream installs its listener before readiness is observable.
33
+ const live = yield* EventV2.allBounded(events, subscriberCapacity)
34
+ return Stream.make(connected).pipe(Stream.concat(live))
35
+ }),
36
+ ).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
37
+ const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
38
+ return HttpServerResponse.stream(
39
+ output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
40
+ {
41
+ contentType: "text/event-stream",
42
+ headers: {
43
+ "Cache-Control": "no-cache, no-transform",
44
+ "X-Accel-Buffering": "no",
45
+ "X-Content-Type-Options": "nosniff",
46
+ },
47
+ },
48
+ )
49
+ }),
50
+ )
51
+ }),
52
+ )
@@ -0,0 +1,39 @@
1
+ import { FileSystem } from "@neurocode-ai/core/filesystem"
2
+ import { RelativePath } from "@neurocode-ai/core/schema"
3
+ import { Effect } from "effect"
4
+ import { HttpServerResponse } from "effect/unstable/http"
5
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
6
+ import { Api } from "../api"
7
+ import { response } from "../location"
8
+
9
+ export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
10
+ Effect.gen(function* () {
11
+ return handlers
12
+ .handleRaw("fs.read", (ctx) =>
13
+ Effect.gen(function* () {
14
+ const file = yield* (yield* FileSystem.Service).read({
15
+ path: RelativePath.make(
16
+ decodeURIComponent(new URL(ctx.request.url, "http://localhost").pathname.slice(13)),
17
+ ),
18
+ })
19
+ return HttpServerResponse.uint8Array(file.content, { contentType: file.mime })
20
+ }),
21
+ )
22
+ .handle("fs.list", (ctx) =>
23
+ response(
24
+ Effect.gen(function* () {
25
+ const fs = yield* FileSystem.Service
26
+ return yield* fs.list(ctx.query)
27
+ }),
28
+ ),
29
+ )
30
+ .handle("fs.find", (ctx) =>
31
+ response(
32
+ Effect.gen(function* () {
33
+ const fs = yield* FileSystem.Service
34
+ return yield* fs.find(ctx.query)
35
+ }),
36
+ ),
37
+ )
38
+ }),
39
+ )
@@ -0,0 +1,7 @@
1
+ import { Effect } from "effect"
2
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
3
+ import { Api } from "../api"
4
+
5
+ export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handlers) =>
6
+ handlers.handle("health.get", () => Effect.succeed({ healthy: true as const })),
7
+ )
@@ -0,0 +1,104 @@
1
+ import { Integration } from "@neurocode-ai/core/integration"
2
+ import { Effect } from "effect"
3
+ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
4
+ import { Api } from "../api"
5
+ import { InvalidRequestError } from "@neurocode-ai/protocol/errors"
6
+ import { response } from "../location"
7
+
8
+ const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
9
+ effect.pipe(
10
+ Effect.mapError(
11
+ () =>
12
+ new InvalidRequestError({
13
+ message: "Authentication failed",
14
+ kind: "integration_authorization",
15
+ }),
16
+ ),
17
+ )
18
+
19
+ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration", (handlers) =>
20
+ Effect.gen(function* () {
21
+ return handlers
22
+ .handle(
23
+ "integration.list",
24
+ Effect.fn(function* () {
25
+ const service = yield* Integration.Service
26
+ return yield* response(service.list())
27
+ }),
28
+ )
29
+ .handle(
30
+ "integration.get",
31
+ Effect.fn(function* (ctx) {
32
+ const service = yield* Integration.Service
33
+ return yield* response(service.get(ctx.params.integrationID))
34
+ }),
35
+ )
36
+ .handle(
37
+ "integration.connect.key",
38
+ Effect.fn(function* (ctx) {
39
+ const service = yield* Integration.Service
40
+ yield* authorize(
41
+ service.connection.key({
42
+ integrationID: ctx.params.integrationID,
43
+ key: ctx.payload.key,
44
+ label: ctx.payload.label,
45
+ }),
46
+ )
47
+ return HttpApiSchema.NoContent.make()
48
+ }),
49
+ )
50
+ .handle(
51
+ "integration.connect.oauth",
52
+ Effect.fn(function* (ctx) {
53
+ const service = yield* Integration.Service
54
+ return yield* response(
55
+ authorize(
56
+ service.connection.oauth({
57
+ integrationID: ctx.params.integrationID,
58
+ methodID: ctx.payload.methodID,
59
+ inputs: ctx.payload.inputs,
60
+ label: ctx.payload.label,
61
+ }),
62
+ ),
63
+ )
64
+ }),
65
+ )
66
+ .handle(
67
+ "integration.attempt.status",
68
+ Effect.fn(function* (ctx) {
69
+ const service = yield* Integration.Service
70
+ return yield* response(service.attempt.status(ctx.params.attemptID))
71
+ }),
72
+ )
73
+ .handle(
74
+ "integration.attempt.complete",
75
+ Effect.fn(function* (ctx) {
76
+ const service = yield* Integration.Service
77
+ yield* service.attempt.complete({ attemptID: ctx.params.attemptID, code: ctx.payload.code }).pipe(
78
+ Effect.mapError(
79
+ (error) =>
80
+ new InvalidRequestError({
81
+ message:
82
+ error._tag === "Integration.CodeRequired"
83
+ ? "Authorization code is required"
84
+ : "Authentication failed",
85
+ kind:
86
+ error._tag === "Integration.CodeRequired"
87
+ ? "integration_code_required"
88
+ : "integration_authorization",
89
+ }),
90
+ ),
91
+ )
92
+ return HttpApiSchema.NoContent.make()
93
+ }),
94
+ )
95
+ .handle(
96
+ "integration.attempt.cancel",
97
+ Effect.fn(function* (ctx) {
98
+ const service = yield* Integration.Service
99
+ yield* service.attempt.cancel(ctx.params.attemptID)
100
+ return HttpApiSchema.NoContent.make()
101
+ }),
102
+ )
103
+ }),
104
+ )
@@ -0,0 +1,18 @@
1
+ import { Location } from "@neurocode-ai/core/location"
2
+ import { Effect } from "effect"
3
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
4
+ import { Api } from "../api"
5
+
6
+ export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (handlers) =>
7
+ handlers.handle(
8
+ "location.get",
9
+ Effect.fn(function* () {
10
+ const location = yield* Location.Service
11
+ return new Location.Info({
12
+ directory: location.directory,
13
+ workspaceID: location.workspaceID,
14
+ project: location.project,
15
+ })
16
+ }),
17
+ ),
18
+ )
@@ -0,0 +1,81 @@
1
+ import { SessionMessage } from "@neurocode-ai/core/session/message"
2
+ import { SessionV2 } from "@neurocode-ai/core/session"
3
+ import { Effect, Schema } from "effect"
4
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
5
+ import { Api } from "../api"
6
+ import { InvalidCursorError, SessionNotFoundError, UnknownError } from "@neurocode-ai/protocol/errors"
7
+
8
+ const DefaultMessagesLimit = 50
9
+
10
+ const Cursor = Schema.Struct({
11
+ id: SessionMessage.ID,
12
+ order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
13
+ direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
14
+ })
15
+
16
+ const decodeCursor = Schema.decodeUnknownSync(Cursor)
17
+
18
+ const cursor = {
19
+ encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
20
+ return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
21
+ },
22
+ decode(input: string) {
23
+ return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
24
+ },
25
+ }
26
+
27
+ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) =>
28
+ Effect.gen(function* () {
29
+ const session = yield* SessionV2.Service
30
+
31
+ return handlers.handle(
32
+ "session.messages",
33
+ Effect.fn(function* (ctx) {
34
+ if (ctx.query.cursor && ctx.query.order !== undefined)
35
+ return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" })
36
+ const decoded = yield* Effect.try({
37
+ try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
38
+ catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
39
+ })
40
+ const order = decoded?.order ?? ctx.query.order ?? "desc"
41
+ const messages = yield* session
42
+ .messages({
43
+ sessionID: ctx.params.sessionID,
44
+ limit: ctx.query.limit ?? DefaultMessagesLimit,
45
+ order,
46
+ cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
47
+ })
48
+ .pipe(
49
+ Effect.catchTag("Session.NotFoundError", (error) =>
50
+ Effect.fail(
51
+ new SessionNotFoundError({
52
+ sessionID: error.sessionID,
53
+ message: `Session not found: ${error.sessionID}`,
54
+ }),
55
+ ),
56
+ ),
57
+ Effect.catchTag("Session.MessageDecodeError", (error) => {
58
+ const ref = `err_${crypto.randomUUID().slice(0, 8)}`
59
+ return Effect.logError("failed to decode session message").pipe(
60
+ Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
61
+ Effect.andThen(
62
+ Effect.fail(
63
+ new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
64
+ ),
65
+ ),
66
+ )
67
+ }),
68
+ )
69
+ const first = messages[0]
70
+ const last = messages.at(-1)
71
+ return {
72
+ data: messages,
73
+ cursor: {
74
+ previous: first ? cursor.encode(first, order, "previous") : undefined,
75
+ next: last ? cursor.encode(last, order, "next") : undefined,
76
+ },
77
+ }
78
+ }),
79
+ )
80
+ }),
81
+ )
@@ -0,0 +1,17 @@
1
+ import { Catalog } from "@neurocode-ai/core/catalog"
2
+ import { Effect } from "effect"
3
+ import { HttpApiBuilder } from "effect/unstable/httpapi"
4
+ import { Api } from "../api"
5
+ import { response } from "../location"
6
+
7
+ export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
8
+ Effect.gen(function* () {
9
+ return handlers.handle(
10
+ "model.list",
11
+ Effect.fn(function* () {
12
+ const catalog = yield* Catalog.Service
13
+ return yield* response(catalog.model.available())
14
+ }),
15
+ )
16
+ }),
17
+ )
@@ -0,0 +1,98 @@
1
+ import { Location } from "@neurocode-ai/core/location"
2
+ import { PermissionV2 } from "@neurocode-ai/core/permission"
3
+ import { PermissionSaved } from "@neurocode-ai/core/permission/saved"
4
+ import { Effect } from "effect"
5
+ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
6
+ import { Api } from "../api"
7
+ import { PermissionNotFoundError, SessionNotFoundError } from "@neurocode-ai/protocol/errors"
8
+ import { response } from "../location"
9
+
10
+ function missingRequest(id: PermissionV2.ID) {
11
+ return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
12
+ }
13
+
14
+ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", (handlers) =>
15
+ Effect.gen(function* () {
16
+ return handlers
17
+ .handle(
18
+ "permission.request.list",
19
+ Effect.fn(function* () {
20
+ return yield* response((yield* PermissionV2.Service).list())
21
+ }),
22
+ )
23
+ .handle(
24
+ "session.permission.create",
25
+ Effect.fn(function* (ctx) {
26
+ const permission = yield* PermissionV2.Service
27
+ return {
28
+ data: yield* permission
29
+ .ask({
30
+ id: ctx.payload.id,
31
+ sessionID: ctx.params.sessionID,
32
+ action: ctx.payload.action,
33
+ resources: ctx.payload.resources,
34
+ save: ctx.payload.save,
35
+ metadata: ctx.payload.metadata,
36
+ source: ctx.payload.source,
37
+ agent: ctx.payload.agent,
38
+ })
39
+ .pipe(
40
+ Effect.catchTag(
41
+ "Session.NotFoundError",
42
+ (error) =>
43
+ new SessionNotFoundError({
44
+ sessionID: error.sessionID,
45
+ message: `Session not found: ${error.sessionID}`,
46
+ }),
47
+ ),
48
+ ),
49
+ }
50
+ }),
51
+ )
52
+ .handle(
53
+ "session.permission.list",
54
+ Effect.fn(function* (ctx) {
55
+ const permission = yield* PermissionV2.Service
56
+ return { data: yield* permission.forSession(ctx.params.sessionID) }
57
+ }),
58
+ )
59
+ .handle(
60
+ "session.permission.get",
61
+ Effect.fn(function* (ctx) {
62
+ const request = yield* (yield* PermissionV2.Service).get(ctx.params.requestID)
63
+ if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
64
+ return { data: request }
65
+ }),
66
+ )
67
+ .handle(
68
+ "session.permission.reply",
69
+ Effect.fn(function* (ctx) {
70
+ const permission = yield* PermissionV2.Service
71
+ const request = yield* permission.get(ctx.params.requestID)
72
+ if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
73
+ yield* permission
74
+ .reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
75
+ .pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
76
+ return HttpApiSchema.NoContent.make()
77
+ }),
78
+ )
79
+ .handle(
80
+ "permission.saved.list",
81
+ Effect.fn(function* (ctx) {
82
+ const location = yield* Location.Service
83
+ return {
84
+ data: yield* (yield* PermissionSaved.Service).list({
85
+ projectID: ctx.query.projectID ?? location.project.id,
86
+ }),
87
+ }
88
+ }),
89
+ )
90
+ .handle(
91
+ "permission.saved.remove",
92
+ Effect.fn(function* (ctx) {
93
+ yield* (yield* PermissionSaved.Service).remove(ctx.params.id)
94
+ return HttpApiSchema.NoContent.make()
95
+ }),
96
+ )
97
+ }),
98
+ )
@@ -0,0 +1,68 @@
1
+ import { Location } from "@neurocode-ai/core/location"
2
+ import { ProjectCopy } from "@neurocode-ai/core/project/copy"
3
+ import { Git } from "@neurocode-ai/core/git"
4
+ import { Effect } from "effect"
5
+ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
6
+ import { Api } from "../api"
7
+ import { ProjectCopyError } from "@neurocode-ai/protocol/groups/project-copy"
8
+
9
+ export const ProjectCopyHandler = HttpApiBuilder.group(Api, "server.projectCopy", (handlers) =>
10
+ Effect.succeed(
11
+ handlers
12
+ .handle("projectCopy.create", (ctx) =>
13
+ Effect.gen(function* () {
14
+ const copies = yield* ProjectCopy.Service
15
+ const location = yield* Location.Service
16
+ return yield* badRequest(
17
+ copies.create({
18
+ ...ctx.payload,
19
+ projectID: ctx.params.projectID,
20
+ sourceDirectory: location.project.directory,
21
+ }),
22
+ )
23
+ }),
24
+ )
25
+ .handle("projectCopy.remove", (ctx) =>
26
+ ProjectCopy.Service.use((copies) =>
27
+ badRequest(copies.remove({ ...ctx.payload, projectID: ctx.params.projectID })).pipe(
28
+ Effect.as(HttpApiSchema.NoContent.make()),
29
+ ),
30
+ ),
31
+ )
32
+ .handle("projectCopy.refresh", (ctx) =>
33
+ ProjectCopy.Service.use((copies) =>
34
+ badRequest(copies.refresh({ projectID: ctx.params.projectID })).pipe(
35
+ Effect.as(HttpApiSchema.NoContent.make()),
36
+ ),
37
+ ),
38
+ ),
39
+ ),
40
+ )
41
+
42
+ function badRequest<A, R>(effect: Effect.Effect<A, ProjectCopy.Error, R>) {
43
+ return effect.pipe(
44
+ Effect.mapError(
45
+ (error) =>
46
+ new ProjectCopyError({
47
+ name: "ProjectCopyError",
48
+ data: {
49
+ message: message(error),
50
+ forceRequired: error instanceof Git.WorktreeError ? error.forceRequired : undefined,
51
+ },
52
+ }),
53
+ ),
54
+ )
55
+ }
56
+
57
+ function message(error: ProjectCopy.Error) {
58
+ if (error instanceof ProjectCopy.SourceDirectoryNotFoundError)
59
+ return `Project copy source not found: ${error.directory}`
60
+ if (error instanceof ProjectCopy.DestinationExistsError)
61
+ return `Project copy destination already exists: ${error.directory}`
62
+ if (error instanceof ProjectCopy.DirectoryUnavailableError)
63
+ return `Project copy directory unavailable: ${error.directory}`
64
+ if (error instanceof ProjectCopy.InvalidDirectoryError) return `Invalid project copy directory: ${error.directory}`
65
+ if (error instanceof ProjectCopy.StrategyUnavailableError)
66
+ return `Project copy strategy unavailable: ${error.strategy}`
67
+ return error.message
68
+ }