mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-02 21:46:18 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd42f318e | ||
|
|
38290a67a1 |
No files matched your search
@@ -447,7 +447,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/location`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -472,7 +472,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/agent`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -484,7 +484,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/agent/${encodeURIComponent(input.agentID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -498,7 +498,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/plugin`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -511,7 +511,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input?.["location"] },
|
||||
body: { target: input?.["target"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -524,7 +524,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { targets: input["targets"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -655,7 +655,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
|
||||
body: { before: input["before"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -667,7 +667,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
|
||||
body: { agent: input["agent"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -679,7 +679,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
|
||||
body: { model: input["model"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -691,7 +691,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
|
||||
body: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -703,7 +703,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
|
||||
body: { directory: input["directory"], delivery: input["delivery"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -724,7 +724,7 @@ export function make(options: ClientOptions) {
|
||||
resume: input["resume"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -743,7 +743,7 @@ export function make(options: ClientOptions) {
|
||||
delivery: input["delivery"],
|
||||
},
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 500],
|
||||
declaredStatuses: [400, 401, 403, 404, 500],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -755,7 +755,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/skill`,
|
||||
body: { id: input["id"], resume: input["resume"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -774,7 +774,7 @@ export function make(options: ClientOptions) {
|
||||
resume: input["resume"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -786,7 +786,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`,
|
||||
body: { id: input["id"], command: input["command"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -798,7 +798,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||
body: { id: input["id"], delivery: input["delivery"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -809,7 +809,7 @@ export function make(options: ClientOptions) {
|
||||
method: "POST",
|
||||
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -822,7 +822,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||
body: { messageID: input["messageID"], files: input["files"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 409, 500],
|
||||
declaredStatuses: [400, 401, 403, 404, 409, 500],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -833,7 +833,7 @@ export function make(options: ClientOptions) {
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 409, 500],
|
||||
declaredStatuses: [400, 401, 403, 404, 409, 500],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -844,7 +844,7 @@ export function make(options: ClientOptions) {
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -868,7 +868,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/diff`,
|
||||
query: { from: input["from"], to: input["to"], context: input["context"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 500],
|
||||
declaredStatuses: [400, 401, 403, 404, 500],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -917,7 +917,7 @@ export function make(options: ClientOptions) {
|
||||
method: "GET",
|
||||
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -929,7 +929,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
|
||||
body: { value: input["value"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 413],
|
||||
declaredStatuses: [400, 401, 403, 404, 413],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -940,7 +940,7 @@ export function make(options: ClientOptions) {
|
||||
method: "DELETE",
|
||||
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -954,7 +954,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/generate`,
|
||||
body: { prompt: input["prompt"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -978,7 +978,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
|
||||
query: { resume: input["resume"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -989,7 +989,7 @@ export function make(options: ClientOptions) {
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/background`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1014,7 +1014,7 @@ export function make(options: ClientOptions) {
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1026,7 +1026,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
|
||||
body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1037,7 +1037,7 @@ export function make(options: ClientOptions) {
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1049,7 +1049,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`,
|
||||
body: { answer: input["answer"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1061,7 +1061,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
|
||||
query: { message: input["message"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404, 409],
|
||||
declaredStatuses: [400, 401, 403, 404, 409],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1114,7 +1114,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/model`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1126,7 +1126,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/model/default`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1140,7 +1140,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/generate`,
|
||||
body: { prompt: input["prompt"], model: input["model"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1154,7 +1154,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/provider`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1166,7 +1166,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/provider/${encodeURIComponent(input.providerID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1180,7 +1180,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1192,7 +1192,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1206,7 +1206,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { url: input["url"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1221,7 +1221,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { key: input["key"], answer: input["answer"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1236,7 +1236,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1248,7 +1248,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1261,7 +1261,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { code: input["code"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1273,7 +1273,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1288,7 +1288,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1300,7 +1300,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1312,7 +1312,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1327,7 +1327,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/mcp`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1340,7 +1340,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { config: input["config"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1352,7 +1352,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1364,7 +1364,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1376,7 +1376,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1389,7 +1389,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1458,7 +1458,13 @@ export function make(options: ClientOptions) {
|
||||
project: {
|
||||
list: (requestOptions?: RequestOptions) =>
|
||||
request<ProjectListOutput>(
|
||||
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/project`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: ProjectUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -1473,7 +1479,7 @@ export function make(options: ClientOptions) {
|
||||
commands: input["commands"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1487,7 +1493,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/form`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1502,7 +1508,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/permission/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1516,7 +1522,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/permission/saved`,
|
||||
query: { projectID: input?.["projectID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1527,7 +1533,7 @@ export function make(options: ClientOptions) {
|
||||
method: "DELETE",
|
||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1548,7 +1554,7 @@ export function make(options: ClientOptions) {
|
||||
agent: input["agent"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1559,7 +1565,7 @@ export function make(options: ClientOptions) {
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1570,7 +1576,7 @@ export function make(options: ClientOptions) {
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1582,7 +1588,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`,
|
||||
body: { decision: input["decision"], message: input["message"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1596,7 +1602,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/fs/read/${encodePath(input.path)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
binary: true,
|
||||
},
|
||||
@@ -1609,7 +1615,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/fs/list`,
|
||||
query: { location: input?.["location"], path: input?.["path"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1621,7 +1627,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/fs/find`,
|
||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1634,7 +1640,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"], path: input["path"] },
|
||||
body: input["payload"],
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
binaryBody: true,
|
||||
},
|
||||
@@ -1649,7 +1655,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/command`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1663,7 +1669,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/skill`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1678,7 +1684,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { input: input["input"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 500],
|
||||
declaredStatuses: [400, 401, 403, 404, 500],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1699,7 +1705,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/pty`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1718,7 +1724,7 @@ export function make(options: ClientOptions) {
|
||||
env: input?.["env"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1730,7 +1736,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1743,7 +1749,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { title: input["title"], size: input["size"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1755,7 +1761,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1909,7 +1915,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1927,7 +1933,7 @@ export function make(options: ClientOptions) {
|
||||
metadata: input["metadata"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1939,7 +1945,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell/${encodeURIComponent(input.id)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1951,7 +1957,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell/${encodeURIComponent(input.id)}/output`,
|
||||
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1963,7 +1969,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell/${encodeURIComponent(input.id)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1977,7 +1983,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/reference`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2047,7 +2053,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2059,7 +2065,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/base`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2071,7 +2077,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/status`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2084,7 +2090,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/branch`,
|
||||
query: { location: input?.["location"], search: input?.["search"], limit: input?.["limit"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2097,7 +2103,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/diff`,
|
||||
query: { location: input["location"], mode: input["mode"], base: input["base"], context: input["context"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2153,7 +2159,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/websearch/provider`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2166,7 +2172,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { query: input["query"], providerID: input["providerID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 403, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2180,14 +2186,20 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/config`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
shells: (requestOptions?: RequestOptions) =>
|
||||
request<ConfigShellsOutput>(
|
||||
{ method: "GET", path: `/api/config/shell`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/config/shell`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: ConfigUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -2197,7 +2209,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/config`,
|
||||
body: { shell: input["shell"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 403, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
|
||||
@@ -2525,6 +2525,22 @@ export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly m
|
||||
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
|
||||
|
||||
export type DirectoryAccessDeniedError = {
|
||||
readonly _tag: "DirectoryAccessDeniedError"
|
||||
readonly directory: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isDirectoryAccessDeniedError = (value: unknown): value is DirectoryAccessDeniedError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "DirectoryAccessDeniedError"
|
||||
|
||||
export type DirectoryNotFoundError = {
|
||||
readonly _tag: "DirectoryNotFoundError"
|
||||
readonly directory: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isDirectoryNotFoundError = (value: unknown): value is DirectoryNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "DirectoryNotFoundError"
|
||||
|
||||
export type ServiceUnavailableError = {
|
||||
readonly _tag: "ServiceUnavailableError"
|
||||
readonly message: string
|
||||
|
||||
@@ -26,30 +26,6 @@ export class NotFoundError extends Schema.TaggedError<NotFoundError>()("FileSyst
|
||||
path: RelativePath,
|
||||
}) {}
|
||||
|
||||
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
|
||||
"FileSystem.DirectoryNotFoundError",
|
||||
{
|
||||
directory: AbsolutePath,
|
||||
cause: Schema.Defect(),
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Directory not found: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
|
||||
"FileSystem.DirectoryAccessDeniedError",
|
||||
{
|
||||
directory: AbsolutePath,
|
||||
cause: Schema.Defect(),
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Access denied to directory: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export const Content = Schema.Struct({
|
||||
uri: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
@@ -111,24 +87,7 @@ const baseLayer = Layer.effect(
|
||||
// configured directory as canonical; local placements keep symlink
|
||||
// canonicalization. This skip is boot-only: resolve/read/list below still
|
||||
// access the host filesystem per operation (tracked in #44568).
|
||||
const root = location.workspaceID
|
||||
? location.directory
|
||||
: yield* fs.realPath(location.directory).pipe(
|
||||
Effect.catch((cause): Effect.Effect<never, DirectoryNotFoundError | DirectoryAccessDeniedError> => {
|
||||
if (cause.reason._tag === "NotFound")
|
||||
return Effect.fail(new DirectoryNotFoundError({ directory: location.directory, cause }))
|
||||
// macOS privacy denials arrive as Unknown with an EPERM cause.
|
||||
if (
|
||||
cause.reason._tag === "PermissionDenied" ||
|
||||
(cause.reason._tag === "Unknown" &&
|
||||
cause.reason.cause instanceof Error &&
|
||||
"code" in cause.reason.cause &&
|
||||
cause.reason.cause.code === "EPERM")
|
||||
)
|
||||
return Effect.fail(new DirectoryAccessDeniedError({ directory: location.directory, cause }))
|
||||
return Effect.die(cause)
|
||||
}),
|
||||
)
|
||||
const root = location.workspaceID ? location.directory : yield* fs.realDirectory(location.directory)
|
||||
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
|
||||
const absolute = path.resolve(location.directory, input ?? ".")
|
||||
if (!FSUtil.contains(location.directory, absolute))
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FileMutation } from "./file-mutation.js"
|
||||
import { Environment } from "./environment/index.js"
|
||||
import { Formatter } from "./formatter.js"
|
||||
import { FileSystem } from "./filesystem.js"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { FileSystemSearch } from "./filesystem/search.js"
|
||||
import { Generate } from "./generate.js"
|
||||
import { Form } from "./form.js"
|
||||
@@ -114,7 +115,7 @@ const nodes = [
|
||||
export const graph = LayerNode.group(nodes)
|
||||
|
||||
export type Services = LayerNode.Output<typeof graph>
|
||||
export type Error = Layer.Error<ReturnType<typeof layer>>
|
||||
export type Error = FSUtil.DirectoryError
|
||||
|
||||
export interface Options {
|
||||
// Plugins this instance is born with; empty and absent are equivalent.
|
||||
@@ -145,7 +146,7 @@ const vanillaReplacements: LayerNode.Replacements = [
|
||||
]
|
||||
|
||||
// One instance is one compiled, fresh copy of the graph standing on a directory.
|
||||
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services> {
|
||||
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services, Error> {
|
||||
const startedAt = performance.now()
|
||||
// Ordered: vanilla defaults, then caller replacements (which win over the
|
||||
// defaults), then instance bindings (which win over everything).
|
||||
@@ -156,9 +157,31 @@ export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Ser
|
||||
InstancePlugins.node.replace(InstancePlugins.bound(options.plugins ?? [])),
|
||||
]
|
||||
|
||||
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global }).pipe(
|
||||
// Instance boot failures are defects; provided operations retain their typed errors.
|
||||
Layer.orDie,
|
||||
return Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
// Validate before project/config discovery can turn directory access failures into defects.
|
||||
if (!ref.workspaceID)
|
||||
yield* FSUtil.Service.pipe(
|
||||
Effect.flatMap((fs) => fs.realDirectory(ref.directory)),
|
||||
Effect.provide(LayerNode.compile(FSUtil.node, { replacements: options.replacements })),
|
||||
)
|
||||
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global })
|
||||
}),
|
||||
).pipe(
|
||||
// Unavailable directories are expected; other instance boot failures remain defects.
|
||||
Layer.catchCause(
|
||||
(cause): Layer.Layer<Services, Error> =>
|
||||
Layer.unwrap(
|
||||
Effect.failCause(cause).pipe(
|
||||
Effect.catch(
|
||||
(error): Effect.Effect<never, Error> =>
|
||||
error instanceof FSUtil.DirectoryNotFoundError || error instanceof FSUtil.DirectoryAccessDeniedError
|
||||
? Effect.fail(error)
|
||||
: Effect.die(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Layer.tap((context) => Effect.addFinalizer(() => Context.get(context, LocationLifecycle.Service).shutdown)),
|
||||
Layer.tap(() =>
|
||||
Effect.logInfo("location services booted", {
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Context, type Effect } from "effect"
|
||||
import type { Session } from "@opencode/schema/session"
|
||||
import { Node } from "@opencode/util/effect/app-node"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import type { Services } from "../instance.js"
|
||||
import type { Services, Error } from "../instance.js"
|
||||
|
||||
/** Selects Session capabilities; implementations own caching and lifetime. */
|
||||
export interface Interface {
|
||||
readonly provide: (
|
||||
session: Session.Info,
|
||||
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Services>>
|
||||
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Error, Exclude<R, Services>>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Instance") {}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function buildLocationServiceMap(
|
||||
Effect.gen(function* () {
|
||||
const owner = yield* Effect.scope
|
||||
const builds = MutableHashMap.empty<Location.Ref, { close?: Effect.Effect<void> }>()
|
||||
const inner: LayerMap.LayerMap<Location.Ref, LocationServices> = yield* LayerMap.make(
|
||||
const inner: LayerMap.LayerMap<Location.Ref, LocationServices, Instance.Error> = yield* LayerMap.make(
|
||||
(ref: Location.Ref) => {
|
||||
const build: { close?: Effect.Effect<void> } = {}
|
||||
MutableHashMap.set(builds, ref, build)
|
||||
|
||||
@@ -145,7 +145,10 @@ export interface Interface {
|
||||
readonly from?: SessionMessage.ID
|
||||
readonly to?: SessionMessage.ID
|
||||
readonly context?: number
|
||||
}) => Effect.Effect<readonly FileDiff.Info[], NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error>
|
||||
}) => Effect.Effect<
|
||||
readonly FileDiff.Info[],
|
||||
NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error | FSUtil.DirectoryError
|
||||
>
|
||||
/**
|
||||
* Durable admitted session work not yet visible in projected history,
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs and
|
||||
@@ -195,7 +198,7 @@ export interface Interface {
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
skills?: PromptInput.Prompt["skills"]
|
||||
delivery?: SessionInbox.Delivery
|
||||
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError>
|
||||
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError | FSUtil.DirectoryError>
|
||||
readonly shell: (
|
||||
input: Parameters<Session.Handle["shell"]>[0] & { sessionID: SessionSchema.ID },
|
||||
) => ReturnType<Session.Handle["shell"]>
|
||||
@@ -218,8 +221,13 @@ export interface Interface {
|
||||
sessionID: SessionSchema.ID
|
||||
messageID: SessionMessage.ID
|
||||
files?: boolean
|
||||
}) => Effect.Effect<SessionSchema.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
|
||||
}) => Effect.Effect<
|
||||
SessionSchema.Revert,
|
||||
NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error | FSUtil.DirectoryError
|
||||
>
|
||||
readonly clear: (
|
||||
sessionID: SessionSchema.ID,
|
||||
) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error | FSUtil.DirectoryError>
|
||||
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * as SessionGenerate from "./generate.js"
|
||||
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { LLMClient, Message, type AIError } from "@opencode/ai"
|
||||
import { Effect } from "effect"
|
||||
import { Database } from "../database/database.js"
|
||||
@@ -14,7 +15,12 @@ import { SessionModelRequest } from "./model-request.js"
|
||||
import type { SessionRunnerModel } from "./runner/model.js"
|
||||
import type { SessionSchema } from "./schema.js"
|
||||
|
||||
export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
|
||||
export type Error =
|
||||
| AgentNotFoundError
|
||||
| Instructions.InitializationBlocked
|
||||
| SessionRunnerModel.Error
|
||||
| AIError
|
||||
| FSUtil.DirectoryError
|
||||
|
||||
/** Generates text from current Session context without mutating the Session. */
|
||||
export const generate = Effect.fn("SessionGenerate.generate")(function* (input: {
|
||||
|
||||
@@ -45,7 +45,11 @@ export interface Interface {
|
||||
delivery?: SessionInbox.Delivery
|
||||
}) => Effect.Effect<
|
||||
void,
|
||||
NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError | DestinationUnavailableError
|
||||
| NotFoundError
|
||||
| DestinationNotFoundError
|
||||
| DestinationNotDirectoryError
|
||||
| DestinationUnavailableError
|
||||
| FSUtil.DirectoryError
|
||||
>
|
||||
}
|
||||
|
||||
@@ -91,7 +95,8 @@ const layer = Layer.effect(
|
||||
yield* locations.contextEffect(destination.location).pipe(
|
||||
Effect.scoped,
|
||||
Effect.catchCause((cause) => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
|
||||
if (Cause.hasInterruptsOnly(cause))
|
||||
return Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
|
||||
return Effect.logWarning("session move destination unavailable", { directory, cause }).pipe(
|
||||
Effect.andThen(Effect.fail(new DestinationUnavailableError({ directory }))),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * as SessionRunner from "./index.js"
|
||||
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import type { AIError } from "@opencode/ai"
|
||||
import { Context, Data, Effect } from "effect"
|
||||
import { SessionSchema } from "../schema.js"
|
||||
@@ -15,6 +16,7 @@ export type RunError =
|
||||
| AgentNotFoundError
|
||||
| StepFailedError
|
||||
| Instructions.InitializationBlocked
|
||||
| FSUtil.DirectoryError
|
||||
|
||||
export type Continuation = { readonly step: number }
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export class StrategyUnavailableError extends Schema.TaggedError<StrategyUnavail
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| FSUtil.DirectoryError
|
||||
| Project.NotFoundError
|
||||
| SourceDirectoryNotFoundError
|
||||
| DestinationExistsError
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Bus } from "@opencode/core/bus"
|
||||
import { Image } from "@opencode/core/image"
|
||||
import { LocationServiceMap } from "@opencode/core/location-service-map"
|
||||
@@ -24,7 +25,7 @@ export const promptLocationNode = makeGlobalNode({
|
||||
Bus.node.replace(Layer.succeed(Bus.Service, bus)),
|
||||
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.void })),
|
||||
],
|
||||
}) as Layer.Layer<LocationServices>,
|
||||
}) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Effect, FileSystem, Layer, type PlatformError } from "effect"
|
||||
import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform"
|
||||
|
||||
/** Fails platform realPath for selected paths, so FSUtil's own error classification still runs. */
|
||||
export function blockRealPath(
|
||||
fs: FileSystem.FileSystem,
|
||||
failure: (target: string) => PlatformError.PlatformError | undefined,
|
||||
) {
|
||||
return LayerNodePlatform.filesystem.replace(
|
||||
Layer.succeed(FileSystem.FileSystem, {
|
||||
...fs,
|
||||
realPath: (target) => {
|
||||
const error = failure(target)
|
||||
return error ? Effect.fail(error) : fs.realPath(target)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Deferred, Duration, Effect, Fiber, Layer, LayerMap, RcMap, Schema } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
@@ -65,7 +66,7 @@ const locations = Layer.effect(
|
||||
Layer.provideMerge(Form.layer),
|
||||
Layer.provide(Layer.succeed(Bus.Service, bus)),
|
||||
Layer.fresh,
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
{ idleTimeToLive: Duration.infinity },
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FileSystem } from "@opencode/core/filesystem"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { AbsolutePath, RelativePath } from "@opencode/core/schema"
|
||||
import { Workspace } from "@opencode/core/workspace"
|
||||
import { blockRealPath } from "./fixture/realpath"
|
||||
import { location } from "./fixture/location"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { it } from "./lib/effect"
|
||||
@@ -118,12 +119,7 @@ describe("FileSystem", () => {
|
||||
Location.node.replace(
|
||||
Layer.succeed(Location.Service, location({ directory: AbsolutePath.make(directory) })),
|
||||
),
|
||||
FSUtil.node.replace(
|
||||
Layer.succeed(FSUtil.Service, {
|
||||
...filesystem,
|
||||
realPath: (target) => (target === directory ? Effect.fail(cause) : filesystem.realPath(target)),
|
||||
}),
|
||||
),
|
||||
blockRealPath(filesystem, (target) => (target === directory ? cause : undefined)),
|
||||
],
|
||||
}),
|
||||
),
|
||||
@@ -145,7 +141,7 @@ describe("FileSystem", () => {
|
||||
}
|
||||
expect(result.cause.reasons.filter(Cause.isDieReason)).toMatchObject([{ defect: cause }])
|
||||
}
|
||||
}).pipe(Effect.provide(LayerNode.compile(FSUtil.node))),
|
||||
}).pipe(Effect.provide(LayerNode.compile(FSUtil.node).pipe(Layer.fresh))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
@@ -67,7 +68,7 @@ const activityLocations = Layer.effect(
|
||||
workspaceID: ref.workspaceID,
|
||||
project: { id: Project.ID.global, directory: ref.directory, canonical: ref.directory },
|
||||
}),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
{ idleTimeToLive: Duration.infinity },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode/ai"
|
||||
import { OpenAIChat } from "@opencode/ai/protocols"
|
||||
@@ -58,7 +59,7 @@ const locations = Layer.effect(
|
||||
Layer.provide(client),
|
||||
Layer.provide(config),
|
||||
Layer.provide(models),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
),
|
||||
)
|
||||
const it = testEffect(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AIError, TransportError } from "@opencode/ai"
|
||||
import { Database } from "@opencode/core/database/database"
|
||||
@@ -1371,7 +1372,7 @@ function buildExecution(
|
||||
() =>
|
||||
// The local execution test only needs the Session runner from the Location graph.
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
|
||||
runner as unknown as Layer.Layer<LocationServices>,
|
||||
runner as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
),
|
||||
)
|
||||
return yield* Layer.buildWithScope(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { chmod, mkdir, readdir, rm } from "fs/promises"
|
||||
@@ -58,7 +59,7 @@ const itWithActiveExecution = testEffect(
|
||||
replacements: [Project.node.replace(globalProjectNode), offlineModels],
|
||||
}),
|
||||
Layer.succeed(SessionRunner.Service, { drain: () => Effect.never }),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -68,7 +69,11 @@ const itWithActiveExecution = testEffect(
|
||||
const unavailableLocations = Layer.effect(
|
||||
LocationServiceMap.Service,
|
||||
LayerMap.make(
|
||||
() => Layer.effectDiscard(Effect.fail(new Error("broken location"))) as unknown as Layer.Layer<LocationServices>,
|
||||
() =>
|
||||
Layer.effectDiscard(Effect.die(new Error("broken location"))) as unknown as Layer.Layer<
|
||||
LocationServices,
|
||||
FSUtil.DirectoryError
|
||||
>,
|
||||
),
|
||||
)
|
||||
const itWithUnavailableDestination = testEffect(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import { DateTime, Effect, Fiber, Layer, LayerMap, Schema, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
@@ -83,7 +84,7 @@ const locations = makeGlobalNode({
|
||||
restore: () => Effect.void,
|
||||
}),
|
||||
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
|
||||
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices>,
|
||||
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FSUtil } from "@opencode/util/fs-util"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, LayerMap } from "effect"
|
||||
@@ -49,7 +50,7 @@ const locations = makeGlobalNode({
|
||||
list: () => Effect.succeed([info]),
|
||||
}),
|
||||
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
|
||||
),
|
||||
),
|
||||
deps: [],
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { InvalidRequestError, SessionNotFoundError } from "./errors.js"
|
||||
import {
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
InvalidRequestError,
|
||||
SessionNotFoundError,
|
||||
} from "./errors.js"
|
||||
import { makeDefaultApi } from "./api.js"
|
||||
import type { Api } from "./api.js"
|
||||
import type { Context } from "effect"
|
||||
@@ -7,11 +12,12 @@ import type { EventGroup } from "./groups/event.js"
|
||||
|
||||
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
|
||||
"@opencode/client/LocationMiddleware",
|
||||
{ error: [DirectoryNotFoundError, DirectoryAccessDeniedError] },
|
||||
) {}
|
||||
|
||||
class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
|
||||
"@opencode/client/SessionLocationMiddleware",
|
||||
{ error: [InvalidRequestError, SessionNotFoundError] },
|
||||
{ error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError] },
|
||||
) {}
|
||||
|
||||
type ClientApiShape = Api<
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { Schema } from "effect"
|
||||
import { Skill } from "@opencode/schema/skill"
|
||||
import { AbsolutePath } from "@opencode/schema/schema"
|
||||
|
||||
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
|
||||
"DirectoryNotFoundError",
|
||||
{ directory: AbsolutePath, message: Schema.String },
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
|
||||
"DirectoryAccessDeniedError",
|
||||
{ directory: AbsolutePath, message: Schema.String },
|
||||
{ httpApiStatus: 403 },
|
||||
) {}
|
||||
|
||||
export class InvalidRequestError extends Schema.TaggedError<InvalidRequestError>()(
|
||||
"InvalidRequestError",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError, ServiceUnavailableError } from "../errors.js"
|
||||
import {
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
InvalidRequestError,
|
||||
ServiceUnavailableError,
|
||||
} from "../errors.js"
|
||||
|
||||
export const GenerateGroup = HttpApiGroup.make("server.generate")
|
||||
.add(
|
||||
@@ -13,7 +18,7 @@ export const GenerateGroup = HttpApiGroup.make("server.generate")
|
||||
success: Schema.Struct({
|
||||
data: Schema.Struct({ text: Schema.String }),
|
||||
}).annotate({ identifier: "GenerateTextResponse" }),
|
||||
error: [InvalidRequestError, ServiceUnavailableError],
|
||||
error: [InvalidRequestError, ServiceUnavailableError, DirectoryNotFoundError, DirectoryAccessDeniedError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.generate.text",
|
||||
|
||||
@@ -6,7 +6,12 @@ import { Project } from "@opencode/schema/project"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { Context, Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../errors.js"
|
||||
import {
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
PermissionNotFoundError,
|
||||
SessionNotFoundError,
|
||||
} from "../errors.js"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
|
||||
export const makePermissionGroup = <
|
||||
@@ -89,7 +94,7 @@ export const makePermissionGroup = <
|
||||
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(Permission.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
error: [SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.permission.list",
|
||||
|
||||
@@ -18,6 +18,8 @@ import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import {
|
||||
ConflictError,
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
CommandExecutionError,
|
||||
CommandNotFoundError,
|
||||
FormAlreadySettledError,
|
||||
@@ -170,12 +172,10 @@ export const SessionsQuery = Schema.Struct({
|
||||
cursor: SessionsQueryCursor.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "SessionsQuery" })
|
||||
|
||||
export const makeSessionGroup = <
|
||||
I extends HttpApiMiddleware.AnyId,
|
||||
S,
|
||||
FormI extends HttpApiMiddleware.AnyId,
|
||||
FormS,
|
||||
>(sessionLocationMiddleware: Context.Key<I, S>, formLocationMiddleware: Context.Key<FormI, FormS>) =>
|
||||
export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S, FormI extends HttpApiMiddleware.AnyId, FormS>(
|
||||
sessionLocationMiddleware: Context.Key<I, S>,
|
||||
formLocationMiddleware: Context.Key<FormI, FormS>,
|
||||
) =>
|
||||
HttpApiGroup.make("server.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.list", "/api/session", {
|
||||
@@ -382,7 +382,7 @@ export const makeSessionGroup = <
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ ...Location.PublicRef.fields, delivery: SessionInbox.Delivery.pipe(Schema.optional) }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, InvalidRequestError],
|
||||
error: [SessionNotFoundError, InvalidRequestError, DirectoryNotFoundError, DirectoryAccessDeniedError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.move",
|
||||
@@ -562,9 +562,7 @@ export const makeSessionGroup = <
|
||||
error: [SessionNotFoundError, SessionBusyError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.revert.commit", summary: "Commit staged revert" }),
|
||||
),
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "session.revert.commit", summary: "Commit staged revert" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", {
|
||||
@@ -594,7 +592,14 @@ export const makeSessionGroup = <
|
||||
}),
|
||||
}),
|
||||
success: Schema.Struct({ data: Schema.Array(FileDiff.Info) }),
|
||||
error: [InvalidRequestError, MessageNotFoundError, SessionNotFoundError, UnknownError],
|
||||
error: [
|
||||
InvalidRequestError,
|
||||
MessageNotFoundError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.diff",
|
||||
|
||||
@@ -57,7 +57,11 @@ export function layer(options: Options, replacements: () => LayerNode.Replacemen
|
||||
...LocationServiceMap.canonical(session.location),
|
||||
})
|
||||
const provide = (session: Session.Info) => Effect.provide(instances.get(key(session)))
|
||||
const instances: LayerMap.LayerMap<ReturnType<typeof key>, Instance.Services> = yield* LayerMap.make(
|
||||
const instances: LayerMap.LayerMap<
|
||||
ReturnType<typeof key>,
|
||||
Instance.Services,
|
||||
Instance.Error
|
||||
> = yield* LayerMap.make(
|
||||
(input: ReturnType<typeof key>) =>
|
||||
Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Global } from "@opencode/util/global"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { directoryErrors } from "../location"
|
||||
|
||||
export const GenerateHandler = HttpApiBuilder.group(Api, "server.generate", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -15,19 +16,23 @@ export const GenerateHandler = HttpApiBuilder.group(Api, "server.generate", (han
|
||||
const services = locations.get(Location.Ref.make({ directory: AbsolutePath.make(global.config) }))
|
||||
return handlers.handle(
|
||||
"generate.text",
|
||||
Effect.fn("server.generate.text")(function* (request) {
|
||||
const generate = yield* Generate.Service
|
||||
const text = yield* generate
|
||||
.text(request.payload)
|
||||
.pipe(
|
||||
Effect.mapError((error) =>
|
||||
error._tag === "Generate.ModelSelectionError"
|
||||
? new InvalidRequestError({ message: error.message })
|
||||
: new ServiceUnavailableError({ message: error.message, service: error.service }),
|
||||
),
|
||||
)
|
||||
return { data: { text } }
|
||||
}, Effect.provide(services)),
|
||||
Effect.fn("server.generate.text")(
|
||||
function* (request) {
|
||||
const generate = yield* Generate.Service
|
||||
const text = yield* generate
|
||||
.text(request.payload)
|
||||
.pipe(
|
||||
Effect.mapError((error) =>
|
||||
error._tag === "Generate.ModelSelectionError"
|
||||
? new InvalidRequestError({ message: error.message })
|
||||
: new ServiceUnavailableError({ message: error.message, service: error.service }),
|
||||
),
|
||||
)
|
||||
return { data: { text } }
|
||||
},
|
||||
Effect.provide(services),
|
||||
directoryErrors,
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -24,7 +24,7 @@ export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (han
|
||||
Effect.provideService(LocationServiceMap.Service, locations),
|
||||
Effect.catchCause((cause) =>
|
||||
Cause.hasInterruptsOnly(cause)
|
||||
? Effect.failCause(cause)
|
||||
? Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
|
||||
: Effect.fail(new ServiceUnavailableError({ message: Cause.pretty(cause), service: "location" })),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { PermissionNotFoundError } from "@opencode/protocol/errors"
|
||||
import { response, sessionInfo } from "../location"
|
||||
import { directoryErrors, response, sessionInfo } from "../location"
|
||||
import { missingSession } from "./session-error"
|
||||
|
||||
function missingRequest(id: Permission.ID) {
|
||||
@@ -62,7 +62,7 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
|
||||
const session = yield* sessionInfo(sessions, ctx.params.sessionID)
|
||||
const requests = yield* Permission.Service.use((permission) =>
|
||||
permission.forSession(ctx.params.sessionID),
|
||||
).pipe(instances.provide(session))
|
||||
).pipe(instances.provide(session), directoryErrors)
|
||||
return { data: requests }
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SkillNotFoundError,
|
||||
} from "@opencode/protocol/errors"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { directoryErrors } from "../location"
|
||||
import { failedMessageDecode, failedSnapshot, missingMessage, missingSession } from "./session-error"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
@@ -299,6 +300,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.catchTag("Session.DestinationUnavailableError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: `Directory is unavailable: ${error.directory}` })),
|
||||
),
|
||||
directoryErrors,
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
@@ -335,6 +337,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.catchTag("Session.SkillNotFoundError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: `Skill not found: ${error.skill}`, field: "skills" })),
|
||||
),
|
||||
directoryErrors,
|
||||
),
|
||||
}
|
||||
}),
|
||||
@@ -370,6 +373,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
}),
|
||||
),
|
||||
),
|
||||
directoryErrors,
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
@@ -388,6 +392,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.catchTag("Session.SkillNotFoundError", (error) =>
|
||||
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
|
||||
),
|
||||
directoryErrors,
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
@@ -471,6 +476,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.catchTag("Session.MessageNotFoundError", missingMessage),
|
||||
Effect.catchTag("Session.BusyError", busySession),
|
||||
Effect.catchTag("Snapshot.Error", failedSnapshot("stage session revert", ctx.params.sessionID)),
|
||||
directoryErrors,
|
||||
),
|
||||
}
|
||||
}),
|
||||
@@ -485,6 +491,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.catchTag("Session.NotFoundError", missingSession),
|
||||
Effect.catchTag("Session.BusyError", busySession),
|
||||
Effect.catchTag("Snapshot.Error", failedSnapshot("clear session revert", ctx.params.sessionID)),
|
||||
directoryErrors,
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
@@ -527,6 +534,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
(error) => new InvalidRequestError({ message: error.message, field: error.field }),
|
||||
),
|
||||
Effect.catchTag("Snapshot.Error", failedSnapshot("diff session turn", ctx.params.sessionID)),
|
||||
directoryErrors,
|
||||
),
|
||||
}
|
||||
}),
|
||||
@@ -588,15 +596,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
.handle(
|
||||
"session.generate",
|
||||
Effect.fn(function* (ctx) {
|
||||
const text = yield* session
|
||||
.generate({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt })
|
||||
.pipe(
|
||||
Effect.mapError((error) =>
|
||||
error._tag === "Session.NotFoundError"
|
||||
? missingSession(error)
|
||||
: new ServiceUnavailableError({ message: error.message, service: "session generation" }),
|
||||
),
|
||||
)
|
||||
const text = yield* session.generate({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt }).pipe(
|
||||
directoryErrors,
|
||||
Effect.mapError((error) =>
|
||||
error._tag === "Session.NotFoundError"
|
||||
? missingSession(error)
|
||||
: new ServiceUnavailableError({ message: error.message, service: "session generation" }),
|
||||
),
|
||||
)
|
||||
return { data: { text } }
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { LocationServiceMap } from "@opencode/core/location-services"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { InvalidRequestError } from "@opencode/protocol/errors"
|
||||
import { DirectoryNotFoundError, DirectoryAccessDeniedError, InvalidRequestError } from "@opencode/protocol/errors"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
@@ -12,8 +13,24 @@ export type LocationServices = Layer.Success<ReturnType<(typeof LocationServiceM
|
||||
|
||||
export class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware, { provides: LocationServices }>()(
|
||||
"@opencode/HttpApiLocation",
|
||||
{ error: [DirectoryNotFoundError, DirectoryAccessDeniedError] },
|
||||
) {}
|
||||
|
||||
export function directoryErrors<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return effect.pipe(
|
||||
Effect.catchIf(
|
||||
(error): error is Extract<E, FSUtil.DirectoryError> =>
|
||||
error instanceof FSUtil.DirectoryNotFoundError || error instanceof FSUtil.DirectoryAccessDeniedError,
|
||||
(error): Effect.Effect<never, DirectoryNotFoundError | DirectoryAccessDeniedError> =>
|
||||
Effect.fail(
|
||||
error instanceof FSUtil.DirectoryNotFoundError
|
||||
? new DirectoryNotFoundError({ directory: AbsolutePath.make(error.directory), message: error.message })
|
||||
: new DirectoryAccessDeniedError({ directory: AbsolutePath.make(error.directory), message: error.message }),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
@@ -61,7 +78,7 @@ export const layer = Layer.effect(
|
||||
return LocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))), directoryErrors)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Instance } from "@opencode/core/instance/service"
|
||||
import { LocationServiceMap } from "@opencode/core/location-services"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors"
|
||||
import {
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
InvalidRequestError,
|
||||
SessionNotFoundError,
|
||||
} from "@opencode/protocol/errors"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { requestRef, sessionInfo, type LocationServices } from "../location"
|
||||
import { directoryErrors, requestRef, sessionInfo, type LocationServices } from "../location"
|
||||
|
||||
export class FormLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
FormLocationMiddleware,
|
||||
{ provides: LocationServices }
|
||||
>()("@opencode/HttpApiFormLocation", {
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
|
||||
}) {}
|
||||
|
||||
export const formLocationLayer = Layer.effect(
|
||||
@@ -29,11 +34,11 @@ export const formLocationLayer = Layer.effect(
|
||||
// the session row lookup because some MCP elicitations cannot currently be attributed to
|
||||
// a real session. Keep this undocumented and remove once elicitations carry session ownership.
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))), directoryErrors)
|
||||
}
|
||||
|
||||
const session = yield* sessionInfo(sessions, route.params.sessionID)
|
||||
return yield* effect.pipe(instances.provide(session))
|
||||
return yield* effect.pipe(instances.provide(session), directoryErrors)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,14 +3,19 @@ import { Session } from "@opencode/core/session"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors"
|
||||
import { sessionInfo, type LocationServices } from "../location"
|
||||
import {
|
||||
DirectoryNotFoundError,
|
||||
DirectoryAccessDeniedError,
|
||||
InvalidRequestError,
|
||||
SessionNotFoundError,
|
||||
} from "@opencode/protocol/errors"
|
||||
import { directoryErrors, sessionInfo, type LocationServices } from "../location"
|
||||
|
||||
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
SessionLocationMiddleware,
|
||||
{ provides: LocationServices }
|
||||
>()("@opencode/HttpApiSessionLocation", {
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
|
||||
}) {}
|
||||
|
||||
export const sessionLocationLayer = Layer.effect(
|
||||
@@ -23,7 +28,7 @@ export const sessionLocationLayer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
const session = yield* sessionInfo(sessions, route.params.sessionID)
|
||||
return yield* effect.pipe(instances.provide(session))
|
||||
return yield* effect.pipe(instances.provide(session), directoryErrors)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,11 @@ import path from "node:path"
|
||||
import { Agent } from "@opencode/schema/agent"
|
||||
import { Integration } from "@opencode/schema/integration"
|
||||
import { ServerInfo } from "@opencode/protocol/groups/server"
|
||||
import { Effect, Schedule, Schema } from "effect"
|
||||
import { Effect, Layer, PlatformError, Schedule, Schema } from "effect"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { blockRealPath } from "../../core/test/fixture/realpath"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerFetch } from "../src/fetch"
|
||||
@@ -18,6 +22,99 @@ const options = {
|
||||
fs: { filewatcher: false },
|
||||
} as const
|
||||
|
||||
for (const input of [
|
||||
{ code: "ENOENT", reason: "NotFound", status: 404, tag: "DirectoryNotFoundError", message: "Directory not found" },
|
||||
{
|
||||
code: "EACCES",
|
||||
reason: "PermissionDenied",
|
||||
status: 403,
|
||||
tag: "DirectoryAccessDeniedError",
|
||||
message: "Access denied to directory",
|
||||
},
|
||||
{
|
||||
code: "EPERM",
|
||||
reason: "Unknown",
|
||||
status: 403,
|
||||
tag: "DirectoryAccessDeniedError",
|
||||
message: "Access denied to directory",
|
||||
},
|
||||
{ code: "EIO", reason: "Unknown", status: 500 },
|
||||
] as const) {
|
||||
it.live(`returns directory HTTP errors for ${input.code} and retries after recovery`, () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-directory-errors-")))
|
||||
const directory = path.join(config.path, "project")
|
||||
const filesystem = yield* FSUtil.Service.pipe(Effect.provide(LayerNode.compile(FSUtil.node)))
|
||||
const blocked = new Set<string>()
|
||||
const cause = PlatformError.systemError({
|
||||
_tag: input.reason,
|
||||
module: "FileSystem",
|
||||
method: "realPath",
|
||||
pathOrDescriptor: directory,
|
||||
cause: Object.assign(new Error(input.code), { code: input.code }),
|
||||
})
|
||||
const handler = yield* ServerFetch.make(
|
||||
{ ...options, config: { directory: config.path } },
|
||||
{
|
||||
overrides: [
|
||||
blockRealPath(filesystem, (target) => (blocked.has(target) && input.code !== "ENOENT" ? cause : undefined)),
|
||||
],
|
||||
},
|
||||
)
|
||||
// Session creation only resolves its placement; it does not boot the Location graph.
|
||||
const created = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-opencode-directory": encodeURIComponent(directory) },
|
||||
body: JSON.stringify({ location: { directory } }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(created.status).toBe(200)
|
||||
const session = Schema.decodeUnknownSync(Schema.Struct({ data: Session.Info }))(
|
||||
yield* Effect.promise(() => created.json()),
|
||||
).data
|
||||
blocked.add(directory)
|
||||
if (input.code !== "ENOENT") yield* Effect.promise(() => fs.mkdir(directory))
|
||||
const endpoints = [
|
||||
"/api/model",
|
||||
"/api/integration",
|
||||
`/api/session/${session.id}/permission`,
|
||||
`/api/experimental/session/${session.id}/instructions/entries`,
|
||||
`/api/session/${session.id}/form`,
|
||||
"/api/session/global/form",
|
||||
]
|
||||
for (const endpoint of endpoints) {
|
||||
const response = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request(`http://opencode.local${endpoint}`, {
|
||||
headers: { "x-opencode-directory": encodeURIComponent(directory) },
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect({ endpoint, status: response.status }).toEqual({ endpoint, status: input.status })
|
||||
if (input.status !== 500)
|
||||
expect(yield* Effect.promise(() => response.json())).toEqual({
|
||||
_tag: input.tag,
|
||||
directory,
|
||||
message: `${input.message}: ${directory}`,
|
||||
})
|
||||
}
|
||||
blocked.clear()
|
||||
if (input.code === "ENOENT") yield* Effect.promise(() => fs.mkdir(directory))
|
||||
const recovered = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/model", {
|
||||
headers: { "x-opencode-directory": encodeURIComponent(directory) },
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(recovered.status).toBe(200)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
type Handler = (request: Request) => Promise<Response>
|
||||
|
||||
function occupy(port: number, cancel = false) {
|
||||
@@ -105,7 +202,9 @@ it.live("serves the HttpApi and enforces Basic auth like the Node server", () =>
|
||||
),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
const body = yield* Effect.promise(() => response.json()).pipe(Effect.flatMap(Schema.decodeUnknownEffect(ServerInfo)))
|
||||
const body = yield* Effect.promise(() => response.json()).pipe(
|
||||
Effect.flatMap(Schema.decodeUnknownEffect(ServerInfo)),
|
||||
)
|
||||
expect(body.version).toBe("test-version")
|
||||
expect(body.paths.tmp).toEndWith("opencode")
|
||||
}),
|
||||
|
||||
@@ -22,6 +22,26 @@ export namespace FSUtil {
|
||||
|
||||
export type Error = PlatformError | FileSystemError
|
||||
|
||||
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
|
||||
"FileSystem.DirectoryNotFoundError",
|
||||
{ directory: Schema.String, cause: Schema.Defect() },
|
||||
) {
|
||||
override get message() {
|
||||
return `Directory not found: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
|
||||
"FileSystem.DirectoryAccessDeniedError",
|
||||
{ directory: Schema.String, cause: Schema.Defect() },
|
||||
) {
|
||||
override get message() {
|
||||
return `Access denied to directory: ${this.directory}`
|
||||
}
|
||||
}
|
||||
|
||||
export type DirectoryError = DirectoryNotFoundError | DirectoryAccessDeniedError
|
||||
|
||||
export interface DirEntry {
|
||||
readonly name: string
|
||||
readonly type: "file" | "directory" | "symlink" | "other"
|
||||
@@ -47,6 +67,8 @@ export namespace FSUtil {
|
||||
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
|
||||
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
|
||||
readonly resolve: (path: string) => Effect.Effect<string>
|
||||
/** realPath for a directory that must be reachable; missing and denied directories fail with typed errors. */
|
||||
readonly realDirectory: (path: string) => Effect.Effect<string, DirectoryError>
|
||||
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
||||
readonly up: (options: UpOptions) => Effect.Effect<string[], Error>
|
||||
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
||||
@@ -109,6 +131,24 @@ export namespace FSUtil {
|
||||
)
|
||||
})
|
||||
|
||||
const realDirectory = Effect.fn("FileSystem.realDirectory")(function* (directory: string) {
|
||||
return yield* fs.realPath(directory).pipe(
|
||||
Effect.catch((cause): Effect.Effect<never, DirectoryError> => {
|
||||
if (cause.reason._tag === "NotFound") return Effect.fail(new DirectoryNotFoundError({ directory, cause }))
|
||||
// macOS privacy denials arrive as Unknown with an EPERM cause.
|
||||
if (
|
||||
cause.reason._tag === "PermissionDenied" ||
|
||||
(cause.reason._tag === "Unknown" &&
|
||||
cause.reason.cause instanceof Error &&
|
||||
"code" in cause.reason.cause &&
|
||||
cause.reason.cause.code === "EPERM")
|
||||
)
|
||||
return Effect.fail(new DirectoryAccessDeniedError({ directory, cause }))
|
||||
return Effect.die(cause)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
|
||||
const text = yield* fs.readFileString(path)
|
||||
return yield* Effect.try({
|
||||
@@ -214,6 +254,7 @@ export namespace FSUtil {
|
||||
isFile,
|
||||
readDirectoryEntries,
|
||||
resolve,
|
||||
realDirectory,
|
||||
readJson,
|
||||
writeJson,
|
||||
ensureDir,
|
||||
|
||||
Reference in new issue
Block a user