@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.
- package/package.json +1 -1
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
- package/public/index.html +1 -1
- package/public/sw.js +1 -1
- package/public/ui-version.json +1 -1
- package/dist/integrations/github/bot-signature.js +0 -11
- package/dist/integrations/github/git-ops.js +0 -133
- package/dist/integrations/github/github-types.js +0 -1
- package/dist/integrations/github/job-runner.js +0 -608
- package/dist/integrations/github/octokit.js +0 -58
- package/dist/integrations/github/sanitize-webhook.js +0 -42
- package/dist/integrations/github/webhook-verify.js +0 -21
- package/dist/integrations/github/workspace-context.js +0 -10
- package/dist/integrations/github/worktree-context.js +0 -15
- package/dist/opencode/request-context.js +0 -39
- package/dist/opencode/worktree-directory.js +0 -42
- package/dist/opencode-config-template/README.md +0 -32
- package/dist/opencode-config-template/opencode.jsonc +0 -3
- package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
- package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
- package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
- package/dist/server/routes/github-plugin.js +0 -215
- package/dist/server/routes/github-webhook.js +0 -32
- package/scripts/copy-auth-pages.mjs +0 -22
- package/scripts/copy-opencode-config.mjs +0 -61
- package/scripts/copy-ui-dist.mjs +0 -21
- package/src/api-types.ts +0 -326
- package/src/auth/auth-store.ts +0 -175
- package/src/auth/http-auth.ts +0 -38
- package/src/auth/manager.ts +0 -163
- package/src/auth/password-hash.ts +0 -49
- package/src/auth/session-manager.ts +0 -23
- package/src/auth/token-manager.ts +0 -32
- package/src/background-processes/manager.ts +0 -519
- package/src/bin.ts +0 -29
- package/src/config/binaries.ts +0 -192
- package/src/config/location.ts +0 -78
- package/src/config/schema.ts +0 -104
- package/src/config/store.ts +0 -244
- package/src/events/bus.ts +0 -45
- package/src/filesystem/__tests__/search-cache.test.ts +0 -61
- package/src/filesystem/browser.ts +0 -353
- package/src/filesystem/search-cache.ts +0 -66
- package/src/filesystem/search.ts +0 -184
- package/src/index.ts +0 -540
- package/src/launcher.ts +0 -177
- package/src/loader.ts +0 -21
- package/src/logger.ts +0 -133
- package/src/opencode-config.ts +0 -31
- package/src/plugins/channel.ts +0 -55
- package/src/plugins/handlers.ts +0 -36
- package/src/releases/dev-release-monitor.ts +0 -118
- package/src/releases/release-monitor.ts +0 -149
- package/src/server/http-server.ts +0 -693
- package/src/server/network-addresses.ts +0 -75
- package/src/server/routes/auth-pages/login.html +0 -134
- package/src/server/routes/auth-pages/token.html +0 -93
- package/src/server/routes/auth.ts +0 -164
- package/src/server/routes/background-processes.ts +0 -85
- package/src/server/routes/config.ts +0 -76
- package/src/server/routes/events.ts +0 -61
- package/src/server/routes/filesystem.ts +0 -54
- package/src/server/routes/meta.ts +0 -58
- package/src/server/routes/plugin.ts +0 -75
- package/src/server/routes/storage.ts +0 -66
- package/src/server/routes/workspaces.ts +0 -113
- package/src/server/routes/worktrees.ts +0 -195
- package/src/server/tls.ts +0 -283
- package/src/storage/instance-store.ts +0 -64
- package/src/ui/__tests__/remote-ui.test.ts +0 -58
- package/src/ui/remote-ui.ts +0 -571
- package/src/workspaces/git-worktrees.ts +0 -241
- package/src/workspaces/instance-events.ts +0 -226
- package/src/workspaces/manager.ts +0 -493
- package/src/workspaces/opencode-auth.ts +0 -22
- package/src/workspaces/runtime.ts +0 -428
- package/src/workspaces/worktree-map.ts +0 -129
- 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
|
-
}
|