@neuralnomads/codenomad-dev 0.10.3-dev-20260213-ba418a85 → 0.10.3-dev-20260213-e9f281a6

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.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/public/apple-touch-icon-180x180.png +0 -0
  3. package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
  4. package/public/index.html +1 -1
  5. package/public/sw.js +1 -1
  6. package/public/ui-version.json +1 -1
  7. package/dist/integrations/github/bot-signature.js +0 -11
  8. package/dist/integrations/github/git-ops.js +0 -133
  9. package/dist/integrations/github/github-types.js +0 -1
  10. package/dist/integrations/github/job-runner.js +0 -608
  11. package/dist/integrations/github/octokit.js +0 -58
  12. package/dist/integrations/github/sanitize-webhook.js +0 -42
  13. package/dist/integrations/github/webhook-verify.js +0 -21
  14. package/dist/integrations/github/workspace-context.js +0 -10
  15. package/dist/integrations/github/worktree-context.js +0 -15
  16. package/dist/opencode/request-context.js +0 -39
  17. package/dist/opencode/worktree-directory.js +0 -42
  18. package/dist/opencode-config-template/README.md +0 -32
  19. package/dist/opencode-config-template/opencode.jsonc +0 -3
  20. package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
  21. package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
  22. package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
  23. package/dist/server/routes/github-plugin.js +0 -215
  24. package/dist/server/routes/github-webhook.js +0 -32
  25. package/scripts/copy-auth-pages.mjs +0 -22
  26. package/scripts/copy-opencode-config.mjs +0 -61
  27. package/scripts/copy-ui-dist.mjs +0 -21
  28. package/src/api-types.ts +0 -326
  29. package/src/auth/auth-store.ts +0 -175
  30. package/src/auth/http-auth.ts +0 -38
  31. package/src/auth/manager.ts +0 -163
  32. package/src/auth/password-hash.ts +0 -49
  33. package/src/auth/session-manager.ts +0 -23
  34. package/src/auth/token-manager.ts +0 -32
  35. package/src/background-processes/manager.ts +0 -519
  36. package/src/bin.ts +0 -29
  37. package/src/config/binaries.ts +0 -192
  38. package/src/config/location.ts +0 -78
  39. package/src/config/schema.ts +0 -104
  40. package/src/config/store.ts +0 -244
  41. package/src/events/bus.ts +0 -45
  42. package/src/filesystem/__tests__/search-cache.test.ts +0 -61
  43. package/src/filesystem/browser.ts +0 -353
  44. package/src/filesystem/search-cache.ts +0 -66
  45. package/src/filesystem/search.ts +0 -184
  46. package/src/index.ts +0 -540
  47. package/src/launcher.ts +0 -177
  48. package/src/loader.ts +0 -21
  49. package/src/logger.ts +0 -133
  50. package/src/opencode-config.ts +0 -31
  51. package/src/plugins/channel.ts +0 -55
  52. package/src/plugins/handlers.ts +0 -36
  53. package/src/releases/dev-release-monitor.ts +0 -118
  54. package/src/releases/release-monitor.ts +0 -149
  55. package/src/server/http-server.ts +0 -693
  56. package/src/server/network-addresses.ts +0 -75
  57. package/src/server/routes/auth-pages/login.html +0 -134
  58. package/src/server/routes/auth-pages/token.html +0 -93
  59. package/src/server/routes/auth.ts +0 -164
  60. package/src/server/routes/background-processes.ts +0 -85
  61. package/src/server/routes/config.ts +0 -76
  62. package/src/server/routes/events.ts +0 -61
  63. package/src/server/routes/filesystem.ts +0 -54
  64. package/src/server/routes/meta.ts +0 -58
  65. package/src/server/routes/plugin.ts +0 -75
  66. package/src/server/routes/storage.ts +0 -66
  67. package/src/server/routes/workspaces.ts +0 -113
  68. package/src/server/routes/worktrees.ts +0 -195
  69. package/src/server/tls.ts +0 -283
  70. package/src/storage/instance-store.ts +0 -64
  71. package/src/ui/__tests__/remote-ui.test.ts +0 -58
  72. package/src/ui/remote-ui.ts +0 -571
  73. package/src/workspaces/git-worktrees.ts +0 -241
  74. package/src/workspaces/instance-events.ts +0 -226
  75. package/src/workspaces/manager.ts +0 -493
  76. package/src/workspaces/opencode-auth.ts +0 -22
  77. package/src/workspaces/runtime.ts +0 -428
  78. package/src/workspaces/worktree-map.ts +0 -129
  79. package/tsconfig.json +0 -17
@@ -1,54 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { z } from "zod"
3
- import { FileSystemBrowser } from "../../filesystem/browser"
4
-
5
- interface RouteDeps {
6
- fileSystemBrowser: FileSystemBrowser
7
- }
8
-
9
- const FilesystemQuerySchema = z.object({
10
- path: z.string().optional(),
11
- includeFiles: z.coerce.boolean().optional(),
12
- })
13
-
14
- const FilesystemCreateFolderSchema = z.object({
15
- parentPath: z.string().optional(),
16
- name: z.string(),
17
- })
18
-
19
- export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps) {
20
- app.get("/api/filesystem", async (request, reply) => {
21
- const query = FilesystemQuerySchema.parse(request.query ?? {})
22
-
23
- try {
24
- return deps.fileSystemBrowser.browse(query.path, {
25
- includeFiles: query.includeFiles,
26
- })
27
- } catch (error) {
28
- reply.code(400)
29
- return { error: (error as Error).message }
30
- }
31
- })
32
-
33
- app.post("/api/filesystem/folders", async (request, reply) => {
34
- const body = FilesystemCreateFolderSchema.parse(request.body ?? {})
35
-
36
- try {
37
- const created = deps.fileSystemBrowser.createFolder(body.parentPath, body.name)
38
- reply.code(201)
39
- return created
40
- } catch (error) {
41
- const err = error as NodeJS.ErrnoException
42
- if (err?.code === "EEXIST") {
43
- reply.code(409).type("text/plain").send("Folder already exists")
44
- return
45
- }
46
- if (err?.code === "EACCES" || err?.code === "EPERM") {
47
- reply.code(403).type("text/plain").send("Permission denied")
48
- return
49
- }
50
-
51
- reply.code(400).type("text/plain").send((error as Error).message)
52
- }
53
- })
54
- }
@@ -1,58 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { ServerMeta } from "../../api-types"
3
- import { resolveNetworkAddresses } from "../network-addresses"
4
-
5
- interface RouteDeps {
6
- serverMeta: ServerMeta
7
- }
8
-
9
- export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
10
- app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta))
11
- }
12
-
13
- function buildMetaResponse(meta: ServerMeta): ServerMeta {
14
- const localPort = resolveLocalPort(meta)
15
- const remote = resolveRemote(meta)
16
- const addresses = remote && remote.port > 0 ? resolveNetworkAddresses({ host: meta.host, protocol: remote.protocol, port: remote.port }) : []
17
-
18
- return {
19
- ...meta,
20
- localPort,
21
- remotePort: remote?.port,
22
- listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
23
- addresses,
24
- }
25
- }
26
-
27
- function resolveLocalPort(meta: ServerMeta): number {
28
- if (Number.isInteger(meta.localPort) && meta.localPort > 0) {
29
- return meta.localPort
30
- }
31
- try {
32
- const parsed = new URL(meta.localUrl)
33
- const port = Number(parsed.port)
34
- return Number.isInteger(port) && port > 0 ? port : 0
35
- } catch {
36
- return 0
37
- }
38
- }
39
-
40
- function resolveRemote(meta: ServerMeta): { protocol: "http" | "https"; port: number } | null {
41
- if (!meta.remoteUrl) {
42
- return null
43
- }
44
- try {
45
- const parsed = new URL(meta.remoteUrl)
46
- const protocol = parsed.protocol === "https:" ? "https" : "http"
47
- const port = Number(parsed.port)
48
- return { protocol, port: Number.isInteger(port) && port > 0 ? port : 0 }
49
- } catch {
50
- return null
51
- }
52
- }
53
-
54
- function isLoopbackHost(host: string): boolean {
55
- return host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
56
- }
57
-
58
- // NetworkAddress shape is resolved in ../network-addresses
@@ -1,75 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { z } from "zod"
3
- import type { WorkspaceManager } from "../../workspaces/manager"
4
- import type { EventBus } from "../../events/bus"
5
- import type { Logger } from "../../logger"
6
- import { PluginChannelManager } from "../../plugins/channel"
7
- import { buildPingEvent, handlePluginEvent } from "../../plugins/handlers"
8
-
9
- interface RouteDeps {
10
- workspaceManager: WorkspaceManager
11
- eventBus: EventBus
12
- logger: Logger
13
- }
14
-
15
- const PluginEventSchema = z.object({
16
- type: z.string().min(1),
17
- properties: z.record(z.unknown()).optional(),
18
- })
19
-
20
- export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
21
- const channel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
22
-
23
- app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/events", (request, reply) => {
24
- const workspace = deps.workspaceManager.get(request.params.id)
25
- if (!workspace) {
26
- reply.code(404).send({ error: "Workspace not found" })
27
- return
28
- }
29
-
30
- reply.raw.setHeader("Content-Type", "text/event-stream")
31
- reply.raw.setHeader("Cache-Control", "no-cache")
32
- reply.raw.setHeader("Connection", "keep-alive")
33
- reply.raw.flushHeaders?.()
34
- reply.hijack()
35
-
36
- const registration = channel.register(request.params.id, reply)
37
-
38
- const heartbeat = setInterval(() => {
39
- channel.send(request.params.id, buildPingEvent())
40
- }, 15000)
41
-
42
- const close = () => {
43
- clearInterval(heartbeat)
44
- registration.close()
45
- reply.raw.end?.()
46
- }
47
-
48
- request.raw.on("close", close)
49
- request.raw.on("error", close)
50
- })
51
-
52
- const handleWildcard = async (request: any, reply: any) => {
53
- const workspaceId = request.params.id as string
54
- const workspace = deps.workspaceManager.get(workspaceId)
55
- if (!workspace) {
56
- reply.code(404).send({ error: "Workspace not found" })
57
- return
58
- }
59
-
60
- const suffix = (request.params["*"] as string | undefined) ?? ""
61
- const normalized = suffix.replace(/^\/+/, "")
62
-
63
- if (normalized === "event" && request.method === "POST") {
64
- const parsed = PluginEventSchema.parse(request.body ?? {})
65
- handlePluginEvent(workspaceId, parsed, { workspaceManager: deps.workspaceManager, eventBus: deps.eventBus, logger: deps.logger })
66
- reply.code(204).send()
67
- return
68
- }
69
-
70
- reply.code(404).send({ error: "Unknown plugin endpoint" })
71
- }
72
-
73
- app.all("/workspaces/:id/plugin/*", handleWildcard)
74
- app.all("/workspaces/:id/plugin", handleWildcard)
75
- }
@@ -1,66 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { z } from "zod"
3
- import { InstanceStore } from "../../storage/instance-store"
4
- import { EventBus } from "../../events/bus"
5
- import { ModelPreferenceSchema } from "../../config/schema"
6
- import type { InstanceData } from "../../api-types"
7
- import { WorkspaceManager } from "../../workspaces/manager"
8
-
9
- interface RouteDeps {
10
- instanceStore: InstanceStore
11
- eventBus: EventBus
12
- workspaceManager: WorkspaceManager
13
- }
14
-
15
- const InstanceDataSchema = z.object({
16
- messageHistory: z.array(z.string()).default([]),
17
- agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}),
18
- })
19
-
20
- const EMPTY_INSTANCE_DATA: InstanceData = {
21
- messageHistory: [],
22
- agentModelSelections: {},
23
- }
24
-
25
- export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) {
26
- const resolveStorageKey = (instanceId: string): string => {
27
- const workspace = deps.workspaceManager.get(instanceId)
28
- return workspace?.path ?? instanceId
29
- }
30
-
31
- app.get<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
32
- try {
33
- const storageId = resolveStorageKey(request.params.id)
34
- const data = await deps.instanceStore.read(storageId)
35
- return data
36
- } catch (error) {
37
- reply.code(500)
38
- return { error: error instanceof Error ? error.message : "Failed to read instance data" }
39
- }
40
- })
41
-
42
- app.put<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
43
- try {
44
- const body = InstanceDataSchema.parse(request.body ?? {})
45
- const storageId = resolveStorageKey(request.params.id)
46
- await deps.instanceStore.write(storageId, body)
47
- deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: body })
48
- reply.code(204)
49
- } catch (error) {
50
- reply.code(400)
51
- return { error: error instanceof Error ? error.message : "Failed to save instance data" }
52
- }
53
- })
54
-
55
- app.delete<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
56
- try {
57
- const storageId = resolveStorageKey(request.params.id)
58
- await deps.instanceStore.delete(storageId)
59
- deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: EMPTY_INSTANCE_DATA })
60
- reply.code(204)
61
- } catch (error) {
62
- reply.code(500)
63
- return { error: error instanceof Error ? error.message : "Failed to delete instance data" }
64
- }
65
- })
66
- }
@@ -1,113 +0,0 @@
1
- import { FastifyInstance, FastifyReply } from "fastify"
2
- import { z } from "zod"
3
- import { WorkspaceManager } from "../../workspaces/manager"
4
-
5
- interface RouteDeps {
6
- workspaceManager: WorkspaceManager
7
- }
8
-
9
- const WorkspaceCreateSchema = z.object({
10
- path: z.string(),
11
- name: z.string().optional(),
12
- })
13
-
14
- const WorkspaceFilesQuerySchema = z.object({
15
- path: z.string().optional(),
16
- })
17
-
18
- const WorkspaceFileContentQuerySchema = z.object({
19
- path: z.string(),
20
- })
21
-
22
- const WorkspaceFileSearchQuerySchema = z.object({
23
- q: z.string().trim().min(1, "Query is required"),
24
- limit: z.coerce.number().int().positive().max(200).optional(),
25
- type: z.enum(["all", "file", "directory"]).optional(),
26
- refresh: z
27
- .string()
28
- .optional()
29
- .transform((value) => (value === undefined ? undefined : value === "true")),
30
- })
31
-
32
- export function registerWorkspaceRoutes(app: FastifyInstance, deps: RouteDeps) {
33
- app.get("/api/workspaces", async () => {
34
- return deps.workspaceManager.list()
35
- })
36
-
37
- app.post("/api/workspaces", async (request, reply) => {
38
- try {
39
- const body = WorkspaceCreateSchema.parse(request.body ?? {})
40
- const workspace = await deps.workspaceManager.create(body.path, body.name)
41
- reply.code(201)
42
- return workspace
43
- } catch (error) {
44
- request.log.error({ err: error }, "Failed to create workspace")
45
- const message = error instanceof Error ? error.message : "Failed to create workspace"
46
- reply.code(400).type("text/plain").send(message)
47
- }
48
- })
49
-
50
- app.get<{ Params: { id: string } }>("/api/workspaces/:id", async (request, reply) => {
51
- const workspace = deps.workspaceManager.get(request.params.id)
52
- if (!workspace) {
53
- reply.code(404)
54
- return { error: "Workspace not found" }
55
- }
56
- return workspace
57
- })
58
-
59
- app.delete<{ Params: { id: string } }>("/api/workspaces/:id", async (request, reply) => {
60
- await deps.workspaceManager.delete(request.params.id)
61
- reply.code(204)
62
- })
63
-
64
- app.get<{
65
- Params: { id: string }
66
- Querystring: { path?: string }
67
- }>("/api/workspaces/:id/files", async (request, reply) => {
68
- try {
69
- const query = WorkspaceFilesQuerySchema.parse(request.query ?? {})
70
- return deps.workspaceManager.listFiles(request.params.id, query.path ?? ".")
71
- } catch (error) {
72
- return handleWorkspaceError(error, reply)
73
- }
74
- })
75
-
76
- app.get<{
77
- Params: { id: string }
78
- Querystring: { q?: string; limit?: string; type?: "all" | "file" | "directory"; refresh?: string }
79
- }>("/api/workspaces/:id/files/search", async (request, reply) => {
80
- try {
81
- const query = WorkspaceFileSearchQuerySchema.parse(request.query ?? {})
82
- return deps.workspaceManager.searchFiles(request.params.id, query.q, {
83
- limit: query.limit,
84
- type: query.type,
85
- refresh: query.refresh,
86
- })
87
- } catch (error) {
88
- return handleWorkspaceError(error, reply)
89
- }
90
- })
91
-
92
- app.get<{
93
- Params: { id: string }
94
- Querystring: { path?: string }
95
- }>("/api/workspaces/:id/files/content", async (request, reply) => {
96
- try {
97
- const query = WorkspaceFileContentQuerySchema.parse(request.query ?? {})
98
- return deps.workspaceManager.readFile(request.params.id, query.path)
99
- } catch (error) {
100
- return handleWorkspaceError(error, reply)
101
- }
102
- })
103
- }
104
-
105
-
106
- function handleWorkspaceError(error: unknown, reply: FastifyReply) {
107
- if (error instanceof Error && error.message === "Workspace not found") {
108
- reply.code(404)
109
- return { error: "Workspace not found" }
110
- }
111
- reply.code(400)
112
- return { error: error instanceof Error ? error.message : "Unable to fulfill request" }
113
- }
@@ -1,195 +0,0 @@
1
- import type { FastifyInstance, FastifyReply } from "fastify"
2
- import { z } from "zod"
3
- import { WorkspaceManager } from "../../workspaces/manager"
4
- import {
5
- resolveRepoRoot,
6
- listWorktrees,
7
- isValidWorktreeSlug,
8
- createManagedWorktree,
9
- removeWorktree,
10
- } from "../../workspaces/git-worktrees"
11
- import type { WorktreeListResponse, WorktreeMap } from "../../api-types"
12
- import { ensureCodenomadGitExclude, readWorktreeMap, writeWorktreeMap } from "../../workspaces/worktree-map"
13
-
14
- interface RouteDeps {
15
- workspaceManager: WorkspaceManager
16
- }
17
-
18
- const WorktreeMapSchema = z.object({
19
- version: z.literal(1),
20
- defaultWorktreeSlug: z.string().min(1).default("root"),
21
- parentSessionWorktreeSlug: z.record(z.string(), z.string()).default({}),
22
- })
23
-
24
- const WorktreeCreateSchema = z.object({
25
- slug: z.string().trim().min(1),
26
- branch: z.string().trim().min(1).optional(),
27
- })
28
-
29
- export function registerWorktreeRoutes(app: FastifyInstance, deps: RouteDeps) {
30
- app.get<{ Params: { id: string } }>("/api/workspaces/:id/worktrees", async (request, reply) => {
31
- const workspace = deps.workspaceManager.get(request.params.id)
32
- if (!workspace) {
33
- reply.code(404)
34
- return { error: "Workspace not found" }
35
- }
36
-
37
- const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)
38
- const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })
39
- const response: WorktreeListResponse = { worktrees, isGitRepo }
40
- return response
41
- })
42
-
43
- app.post<{ Params: { id: string } }>("/api/workspaces/:id/worktrees", async (request, reply) => {
44
- const workspace = deps.workspaceManager.get(request.params.id)
45
- if (!workspace) {
46
- reply.code(404)
47
- return { error: "Workspace not found" }
48
- }
49
-
50
- try {
51
- const body = WorktreeCreateSchema.parse(request.body ?? {})
52
- const slug = body.slug
53
- if (!isValidWorktreeSlug(slug) || slug === "root") {
54
- reply.code(400)
55
- return { error: "Invalid worktree slug" }
56
- }
57
- if (body.branch) {
58
- if (!isValidWorktreeSlug(body.branch) || body.branch === "root") {
59
- reply.code(400)
60
- return { error: "Invalid worktree branch" }
61
- }
62
- if (body.branch !== slug) {
63
- reply.code(400)
64
- return { error: "Branch must match slug" }
65
- }
66
- }
67
-
68
- const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)
69
- if (!isGitRepo) {
70
- reply.code(400)
71
- return { error: "Workspace is not a Git repository" }
72
- }
73
-
74
- await ensureCodenomadGitExclude(workspace.path, request.log).catch(() => undefined)
75
-
76
- const created = await createManagedWorktree({
77
- repoRoot,
78
- workspaceFolder: workspace.path,
79
- slug,
80
- logger: request.log,
81
- })
82
-
83
- reply.code(201)
84
- return created
85
- } catch (error) {
86
- return handleError(error, reply)
87
- }
88
- })
89
-
90
- app.delete<{ Params: { id: string; slug: string }; Querystring: { force?: string } }>(
91
- "/api/workspaces/:id/worktrees/:slug",
92
- async (request, reply) => {
93
- const workspace = deps.workspaceManager.get(request.params.id)
94
- if (!workspace) {
95
- reply.code(404)
96
- return { error: "Workspace not found" }
97
- }
98
-
99
- const slug = (request.params.slug ?? "").trim()
100
- if (!isValidWorktreeSlug(slug) || slug === "root") {
101
- reply.code(400)
102
- return { error: "Invalid worktree slug" }
103
- }
104
-
105
- const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)
106
- if (!isGitRepo) {
107
- reply.code(400)
108
- return { error: "Workspace is not a Git repository" }
109
- }
110
-
111
- const force = (request.query?.force ?? "").toString().toLowerCase() === "true"
112
-
113
- try {
114
- const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })
115
- const match = worktrees.find((wt) => wt.slug === slug)
116
- if (!match || match.kind === "root") {
117
- reply.code(404)
118
- return { error: "Worktree not found" }
119
- }
120
-
121
- await removeWorktree({ workspaceFolder: workspace.path, directory: match.directory, force, logger: request.log })
122
-
123
- // Best-effort: prune any mappings that point at the deleted worktree.
124
- const current = await readWorktreeMap(workspace.path, request.log)
125
- let changed = false
126
- const nextMapping: Record<string, string> = { ...(current.parentSessionWorktreeSlug ?? {}) }
127
- for (const [sessionId, mapped] of Object.entries(nextMapping)) {
128
- if (mapped === slug) {
129
- delete nextMapping[sessionId]
130
- changed = true
131
- }
132
- }
133
- const nextDefault = current.defaultWorktreeSlug === slug ? "root" : current.defaultWorktreeSlug
134
- if (nextDefault !== current.defaultWorktreeSlug) {
135
- changed = true
136
- }
137
- if (changed) {
138
- await writeWorktreeMap(
139
- workspace.path,
140
- {
141
- version: 1,
142
- defaultWorktreeSlug: nextDefault,
143
- parentSessionWorktreeSlug: nextMapping,
144
- },
145
- request.log,
146
- )
147
- }
148
-
149
- reply.code(204)
150
- } catch (error) {
151
- return handleError(error, reply)
152
- }
153
- },
154
- )
155
-
156
- app.get<{ Params: { id: string } }>("/api/workspaces/:id/worktrees/map", async (request, reply) => {
157
- const workspace = deps.workspaceManager.get(request.params.id)
158
- if (!workspace) {
159
- reply.code(404)
160
- return { error: "Workspace not found" }
161
- }
162
- return await readWorktreeMap(workspace.path, request.log)
163
- })
164
-
165
- app.put<{ Params: { id: string } }>("/api/workspaces/:id/worktrees/map", async (request, reply) => {
166
- const workspace = deps.workspaceManager.get(request.params.id)
167
- if (!workspace) {
168
- reply.code(404)
169
- return { error: "Workspace not found" }
170
- }
171
-
172
- try {
173
- const parsed = WorktreeMapSchema.parse(request.body ?? {}) as WorktreeMap
174
- if (!isValidWorktreeSlug(parsed.defaultWorktreeSlug)) {
175
- reply.code(400)
176
- return { error: "Invalid defaultWorktreeSlug" }
177
- }
178
- for (const slug of Object.values(parsed.parentSessionWorktreeSlug ?? {})) {
179
- if (!isValidWorktreeSlug(slug)) {
180
- reply.code(400)
181
- return { error: "Invalid worktree slug in mapping" }
182
- }
183
- }
184
- await writeWorktreeMap(workspace.path, parsed, request.log)
185
- reply.code(204)
186
- } catch (error) {
187
- return handleError(error, reply)
188
- }
189
- })
190
- }
191
-
192
- function handleError(error: unknown, reply: FastifyReply) {
193
- reply.code(400)
194
- return { error: error instanceof Error ? error.message : "Unable to fulfill request" }
195
- }