Skip to content

Commit 42c20ab

Browse files
committed
feat(api,app): sync terminal session routing and mobile-ready web terminal flow
1 parent 1887717 commit 42c20ab

51 files changed

Lines changed: 2101 additions & 310 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/api/src/api/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ export type TerminalSession = {
362362
readonly sshCommand: string
363363
readonly status: TerminalSessionStatus
364364
readonly createdAt: string
365+
readonly attachedClients?: number | undefined
365366
readonly startedAt?: string | undefined
366367
readonly closedAt?: string | undefined
367368
readonly exitCode?: number | undefined

packages/api/src/api/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export const TerminalSessionSchema = Schema.Struct({
269269
sshCommand: Schema.String,
270270
status: TerminalSessionStatusSchema,
271271
createdAt: Schema.String,
272+
attachedClients: Schema.optional(Schema.Number),
272273
startedAt: OptionalString,
273274
closedAt: OptionalString,
274275
exitCode: Schema.optional(Schema.Number),

packages/api/src/http.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import {
7474
downAllProjects,
7575
downProject,
7676
getProject,
77+
getProjectItemByKey,
7778
listProjects,
7879
readProjectLogs,
7980
readProjectPs,
@@ -106,7 +107,13 @@ import {
106107
} from "./services/project-port-forwards.js"
107108
import { proxyProjectPortForward } from "./services/project-port-proxy.js"
108109
import { parseProjectPortProxyPath } from "./services/project-port-proxy-core.js"
109-
import { createTerminalSession, deleteTerminalSession } from "./services/terminal-sessions.js"
110+
import {
111+
createTerminalSession,
112+
deleteTerminalSession,
113+
getProjectTerminalSession,
114+
listProjectTerminalSessions,
115+
lookupTerminalSessionById
116+
} from "./services/terminal-sessions.js"
110117
import {
111118
commitStateFromRequest,
112119
initStateFromRequest,
@@ -121,6 +128,10 @@ const ProjectParamsSchema = Schema.Struct({
121128
projectId: Schema.String
122129
})
123130

131+
const ProjectKeyParamsSchema = Schema.Struct({
132+
projectKey: Schema.String
133+
})
134+
124135
const ProjectPortForwardParamsSchema = Schema.Struct({
125136
projectId: Schema.String,
126137
targetPort: Schema.String
@@ -141,6 +152,11 @@ const TerminalSessionParamsSchema = Schema.Struct({
141152
sessionId: Schema.String
142153
})
143154

155+
const TerminalSessionByProjectKeyParamsSchema = Schema.Struct({
156+
projectKey: Schema.String,
157+
sessionId: Schema.String
158+
})
159+
144160
const ContainerTaskParamsSchema = Schema.Struct({
145161
projectId: Schema.String,
146162
pid: Schema.String
@@ -311,10 +327,12 @@ const errorResponse = (error: ApiError | unknown) => {
311327
}
312328

313329
const projectParams = HttpRouter.schemaParams(ProjectParamsSchema)
330+
const projectKeyParams = HttpRouter.schemaParams(ProjectKeyParamsSchema)
314331
const projectPortForwardParams = HttpRouter.schemaParams(ProjectPortForwardParamsSchema)
315332
const projectDatabaseProfileParams = HttpRouter.schemaParams(ProjectDatabaseProfileParamsSchema)
316333
const agentParams = HttpRouter.schemaParams(AgentParamsSchema)
317334
const terminalSessionParams = HttpRouter.schemaParams(TerminalSessionParamsSchema)
335+
const terminalSessionByProjectKeyParams = HttpRouter.schemaParams(TerminalSessionByProjectKeyParamsSchema)
318336
const containerTaskParams = HttpRouter.schemaParams(ContainerTaskParamsSchema)
319337
const authTerminalSessionParams = HttpRouter.schemaParams(AuthTerminalSessionParamsSchema)
320338

@@ -521,6 +539,14 @@ export const makeRouter = () => {
521539
"/auth/terminal-sessions/:sessionId/ws",
522540
terminalWebSocketUpgradeResponse.pipe(Effect.catchAll(errorResponse))
523541
),
542+
HttpRouter.get(
543+
"/terminal-sessions/:sessionId",
544+
Effect.gen(function*(_) {
545+
const params = yield* _(authTerminalSessionParams)
546+
const session = yield* _(lookupTerminalSessionById(params.sessionId))
547+
return yield* _(jsonResponse(session, 200))
548+
}).pipe(Effect.catchAll(errorResponse))
549+
),
524550
HttpRouter.del(
525551
"/auth/terminal-sessions/:sessionId",
526552
Effect.gen(function*(_) {
@@ -954,6 +980,58 @@ export const makeRouter = () => {
954980
Effect.catchAll(errorResponse)
955981
)
956982
),
983+
HttpRouter.post(
984+
"/projects/by-key/:projectKey/terminal-sessions",
985+
projectKeyParams.pipe(
986+
Effect.flatMap(({ projectKey }) =>
987+
getProjectItemByKey(projectKey).pipe(
988+
Effect.flatMap((project) => createTerminalSession(project.projectDir))
989+
)
990+
),
991+
Effect.flatMap(({ project, session }) => jsonResponse({ ok: true, project, session }, 201)),
992+
Effect.catchAll(errorResponse)
993+
)
994+
),
995+
HttpRouter.get(
996+
"/projects/by-key/:projectKey/terminal-sessions",
997+
projectKeyParams.pipe(
998+
Effect.flatMap(({ projectKey }) =>
999+
getProjectItemByKey(projectKey).pipe(
1000+
Effect.map((project) => ({ sessions: listProjectTerminalSessions(project.projectDir) }))
1001+
)
1002+
),
1003+
Effect.flatMap((body) => jsonResponse(body, 200)),
1004+
Effect.catchAll(errorResponse)
1005+
)
1006+
),
1007+
HttpRouter.get(
1008+
"/projects/by-key/:projectKey/terminal-sessions/:sessionId",
1009+
terminalSessionByProjectKeyParams.pipe(
1010+
Effect.flatMap(({ projectKey, sessionId }) =>
1011+
getProjectItemByKey(projectKey).pipe(
1012+
Effect.flatMap((project) => getProjectTerminalSession(project.projectDir, sessionId))
1013+
)
1014+
),
1015+
Effect.flatMap((session) => jsonResponse({ session }, 200)),
1016+
Effect.catchAll(errorResponse)
1017+
)
1018+
),
1019+
HttpRouter.get(
1020+
"/projects/by-key/:projectKey/terminal-sessions/:sessionId/ws",
1021+
terminalWebSocketUpgradeResponse.pipe(Effect.catchAll(errorResponse))
1022+
),
1023+
HttpRouter.del(
1024+
"/projects/by-key/:projectKey/terminal-sessions/:sessionId",
1025+
terminalSessionByProjectKeyParams.pipe(
1026+
Effect.flatMap(({ projectKey, sessionId }) =>
1027+
getProjectItemByKey(projectKey).pipe(
1028+
Effect.flatMap((project) => deleteTerminalSession(project.projectDir, sessionId))
1029+
)
1030+
),
1031+
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
1032+
Effect.catchAll(errorResponse)
1033+
)
1034+
),
9571035
HttpRouter.post(
9581036
"/projects/:projectId/terminal-sessions",
9591037
projectParams.pipe(
@@ -962,6 +1040,22 @@ export const makeRouter = () => {
9621040
Effect.catchAll(errorResponse)
9631041
)
9641042
),
1043+
HttpRouter.get(
1044+
"/projects/:projectId/terminal-sessions",
1045+
projectParams.pipe(
1046+
Effect.flatMap(({ projectId }) => Effect.succeed({ sessions: listProjectTerminalSessions(projectId) })),
1047+
Effect.flatMap((body) => jsonResponse(body, 200)),
1048+
Effect.catchAll(errorResponse)
1049+
)
1050+
),
1051+
HttpRouter.get(
1052+
"/projects/:projectId/terminal-sessions/:sessionId",
1053+
terminalSessionParams.pipe(
1054+
Effect.flatMap(({ projectId, sessionId }) => getProjectTerminalSession(projectId, sessionId)),
1055+
Effect.flatMap((session) => jsonResponse({ session }, 200)),
1056+
Effect.catchAll(errorResponse)
1057+
)
1058+
),
9651059
HttpRouter.get(
9661060
"/projects/:projectId/terminal-sessions/:sessionId/ws",
9671061
terminalWebSocketUpgradeResponse.pipe(Effect.catchAll(errorResponse))

packages/api/src/services/projects.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,24 @@ const findProjectById = (projectId: string) =>
262262

263263
export const getProjectItemById = (projectId: string) => findProjectById(projectId)
264264

265+
const findProjectByKey = (projectKey: string) =>
266+
Effect.gen(function*(_) {
267+
const projects = yield* _(listProjectItems)
268+
const matches = projects.filter((item) => projectShortKey(item.projectDir) === projectKey)
269+
if (matches.length === 0) {
270+
return yield* _(Effect.fail(new ApiNotFoundError({ message: `Project key not found: ${projectKey}` })))
271+
}
272+
if (matches.length > 1) {
273+
return yield* _(Effect.fail(new ApiConflictError({ message: `Project key is ambiguous: ${projectKey}` })))
274+
}
275+
const project = matches[0]
276+
return project === undefined
277+
? yield* _(Effect.fail(new ApiNotFoundError({ message: `Project key not found: ${projectKey}` })))
278+
: project
279+
})
280+
281+
export const getProjectItemByKey = (projectKey: string) => findProjectByKey(projectKey)
282+
265283
const resolveCreatedProject = (
266284
containerName: string,
267285
repoUrl: string,
@@ -772,3 +790,4 @@ export const readProjectLogs = (
772790
}).pipe(Effect.mapError(toProjectApiError))
773791

774792
export const resolveProjectById = findProjectById
793+
export const resolveProjectByKey = findProjectByKey

0 commit comments

Comments
 (0)