Skip to content

Commit 3c70e84

Browse files
{  "message": "Bad credentials",  "documentation_url": "https://docs.github.com/rest",  "status": "401"}{  "message": "Bad credentials",  "documentation_url": "https://docs.github.com/rest",  "status": "401"}
authored andcommitted
fix: return created project from realtime events
1 parent ae73519 commit 3c70e84

5 files changed

Lines changed: 97 additions & 29 deletions

File tree

packages/api/src/services/projects.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,35 @@ const toProjectDetails = (
175175
codexHome: project.codexHome
176176
})
177177

178+
const projectIdAliases = (
179+
path: Path.Path,
180+
projectId: string
181+
): ReadonlySet<string> => {
182+
const projectsRoot = path.resolve(defaultProjectsRoot(process.cwd()))
183+
const normalized = projectId
184+
.replaceAll("\\", "/")
185+
.replace(/^\.\//u, "")
186+
const rooted = normalized === ".docker-git"
187+
? projectsRoot
188+
: normalized.startsWith(".docker-git/")
189+
? path.join(projectsRoot, normalized.slice(".docker-git/".length))
190+
: projectId
191+
const absolute = path.isAbsolute(rooted) ? path.resolve(rooted) : path.resolve(process.cwd(), rooted)
192+
return new Set([projectId, rooted, absolute])
193+
}
194+
178195
const findProjectById = (projectId: string) =>
179-
listProjectItems.pipe(
180-
Effect.flatMap((projects) => {
181-
const project = projects.find((item) => item.projectDir === projectId)
196+
Effect.gen(function*(_) {
197+
const path = yield* _(Path.Path)
198+
const aliases = projectIdAliases(path, projectId)
199+
const projects = yield* _(listProjectItems)
200+
const project = projects.find((item) => item.projectDir === projectId)
201+
?? projects.find((item) => aliases.has(item.projectDir) || aliases.has(path.resolve(item.projectDir)))
202+
if (project) {
182203
return project
183-
? Effect.succeed(project)
184-
: Effect.fail(new ApiNotFoundError({ message: `Project not found: ${projectId}` }))
185-
})
186-
)
204+
}
205+
return yield* _(Effect.fail(new ApiNotFoundError({ message: `Project not found: ${projectId}` })))
206+
})
187207

188208
export const getProjectItemById = (projectId: string) => findProjectById(projectId)
189209

@@ -401,12 +421,14 @@ const emitCreateStatus = (
401421

402422
const emitProjectCreatedEvents = (
403423
projectId: string,
404-
project: ProjectItem
424+
project: ProjectItem,
425+
details: ProjectDetails
405426
) =>
406427
Effect.sync(() => {
407428
const payload = {
408429
projectId: project.projectDir,
409-
containerName: project.containerName
430+
containerName: project.containerName,
431+
project: details
410432
}
411433
emitProjectEvent(project.projectDir, "project.created", payload)
412434
if (project.projectDir !== projectId) {
@@ -449,10 +471,11 @@ const runPreparedCreateProject = (
449471
)
450472
const runtimeByProject = yield* _(loadProjectRuntimeByProject([project]))
451473
const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project)))
474+
const details = toProjectDetails(project, summary)
452475

453-
yield* _(emitProjectCreatedEvents(projectId, project))
476+
yield* _(emitProjectCreatedEvents(projectId, project, details))
454477

455-
return toProjectDetails(project, summary)
478+
return details
456479
}).pipe(Effect.mapError(toProjectApiError))
457480

458481
const startCreateProjectJob = (

packages/api/tests/projects.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ describe("projects service", () => {
245245
phase: "create",
246246
message: "Project creation started"
247247
})
248+
expect(events.find((event) => event.type === "project.created")?.payload).toMatchObject({
249+
projectId,
250+
project: {
251+
projectDir: projectId
252+
}
253+
})
248254
})
249255
)
250256
)

packages/app/src/docker-git/api-client-events.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Fiber from "effect/Fiber"
33

44
import { request } from "./api-http.js"
55
import { asArray, asObject, asString, type JsonValue } from "./api-json.js"
6+
import { type ApiProjectDetails, decodeProjectDetails } from "./api-project-codec.js"
67
import type { ControllerRuntime } from "./controller.js"
78
import type { ApiAuthRequiredError, ApiRequestError } from "./host-errors.js"
89
import { formatProjectEventLine } from "./project-event-lines.js"
@@ -23,6 +24,11 @@ type ProjectEventPollResponse = {
2324

2425
type ProjectCreationWaitError = ApiAuthRequiredError | ApiRequestError
2526

27+
export type ProjectCreationResult = {
28+
readonly projectId: string
29+
readonly project: ApiProjectDetails | null
30+
}
31+
2632
export type ProjectEventPolling = {
2733
readonly cursorRef: Ref.Ref<number>
2834
readonly fiber: Fiber.RuntimeFiber<number>
@@ -88,13 +94,19 @@ const readProjectEventPayloadField = (
8894
return object === null ? null : asString(object[key])
8995
}
9096

91-
const readCreatedProjectId = (
97+
const readCreatedProject = (
9298
event: ProjectEvent,
9399
fallbackProjectId: string
94-
): string | null =>
95-
event.type === "project.created"
96-
? (readProjectEventPayloadField(event, "projectId") ?? fallbackProjectId)
97-
: null
100+
): ProjectCreationResult | null => {
101+
if (event.type !== "project.created") {
102+
return null
103+
}
104+
105+
const payload = asObject(event.payload)
106+
const projectId = readProjectEventPayloadField(event, "projectId") ?? fallbackProjectId
107+
const project = payload === null ? null : decodeProjectDetails(payload["project"] ?? null)
108+
return { projectId, project }
109+
}
98110

99111
const readFailedMessage = (event: ProjectEvent): string | null =>
100112
event.type === "project.deployment.status" && readProjectEventPayloadField(event, "phase") === "failed"
@@ -137,14 +149,14 @@ const pollProjectEventsOnce = (
137149
return response
138150
})
139151

140-
const findCreatedProjectId = (
152+
const findCreatedProject = (
141153
projectId: string,
142154
events: ReadonlyArray<ProjectEvent>
143-
): string | null => {
155+
): ProjectCreationResult | null => {
144156
for (const event of events) {
145-
const createdProjectId = readCreatedProjectId(event, projectId)
146-
if (createdProjectId !== null) {
147-
return createdProjectId
157+
const created = readCreatedProject(event, projectId)
158+
if (created !== null) {
159+
return created
148160
}
149161
}
150162
return null
@@ -165,17 +177,17 @@ const findFailureMessage = (
165177
const waitForProjectCreationLoop = (
166178
projectId: string,
167179
cursorRef: Ref.Ref<number>
168-
): Effect.Effect<string, ProjectCreationWaitError, ControllerRuntime> =>
180+
): Effect.Effect<ProjectCreationResult, ProjectCreationWaitError, ControllerRuntime> =>
169181
Effect.gen(function*(_) {
170182
const response = yield* _(pollProjectEventsOnce(projectId, cursorRef))
171183
const failureMessage = findFailureMessage(response.events)
172184
if (failureMessage !== null) {
173185
return yield* _(Effect.fail(toProjectCreationError(projectId, failureMessage)))
174186
}
175187

176-
const createdProjectId = findCreatedProjectId(projectId, response.events)
177-
if (createdProjectId !== null) {
178-
return createdProjectId
188+
const created = findCreatedProject(projectId, response.events)
189+
if (created !== null) {
190+
return created
179191
}
180192

181193
yield* _(Effect.sleep(projectEventPollInterval))

packages/app/src/docker-git/api-client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ const createProjectAsync = (
7272
return yield* _(Effect.fail(invalidCreateAcceptedResponse()))
7373
}
7474

75-
const createdProjectId = yield* _(waitForProjectCreation(accepted.projectId, accepted.cursor))
76-
return yield* _(getProject(createdProjectId))
75+
const created = yield* _(waitForProjectCreation(accepted.projectId, accepted.cursor))
76+
if (created.project !== null) {
77+
return created.project
78+
}
79+
return yield* _(getProject(created.projectId))
7780
})
7881

7982
export const listProjects = () =>

packages/app/src/web/actions-project-create.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import * as ParseResult from "@effect/schema/ParseResult"
2+
import { Either } from "effect"
3+
14
import { createProjectDraftFromInputs } from "../docker-git/menu-create-shared.js"
25
import type { CreateInputs } from "../docker-git/menu-types.js"
36
import { appendOutputLine, appendOutputLineHandler, notifyProjectEventRateLimit } from "./actions-output.js"
47
import { type BrowserActionContext, withBusy } from "./actions-shared.js"
8+
import { ProjectDetailsSchema } from "./api-schema.js"
59
import { type ApiEvent, loadProjectDetails, type ProjectDetails, startCreateProject } from "./api.js"
610
import { openProjectEventStream } from "./project-events.js"
711
import { outputScreen, projectPickerScreen } from "./screen.js"
@@ -21,6 +25,21 @@ const readEventPayloadString = (
2125
const readCreatedProjectId = (event: ApiEvent): string | null =>
2226
event.type === "project.created" ? readEventPayloadString(event, "projectId") : null
2327

28+
const readCreatedProject = (event: ApiEvent): ProjectDetails | null => {
29+
if (event.type !== "project.created") {
30+
return null
31+
}
32+
const payload = event.payload
33+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
34+
return null
35+
}
36+
const project = Object.entries(payload).find(([name]) => name === "project")?.[1]
37+
return Either.match(ParseResult.decodeUnknownEither(ProjectDetailsSchema)(project), {
38+
onLeft: () => null,
39+
onRight: (value) => value
40+
})
41+
}
42+
2443
const readCreateFailureMessage = (event: ApiEvent): string | null =>
2544
event.type === "project.deployment.status" && readEventPayloadString(event, "phase") === "failed"
2645
? (readEventPayloadString(event, "message") ?? "Project creation failed.")
@@ -41,9 +60,14 @@ const applyCreatedProject = (
4160

4261
const finishCreateFromEvent = (
4362
context: BrowserActionContext,
44-
projectId: string
63+
projectId: string,
64+
project: ProjectDetails | null
4565
) => {
4666
appendOutputLine(context, "[create] Project created")
67+
if (project !== null) {
68+
applyCreatedProject(context, project)
69+
return
70+
}
4771
withBusy({
4872
context,
4973
effect: loadProjectDetails(projectId),
@@ -89,7 +113,7 @@ export const submitCreateInputs = (
89113
const projectId = readCreatedProjectId(event)
90114
if (projectId !== null) {
91115
stream?.close()
92-
finishCreateFromEvent(context, projectId)
116+
finishCreateFromEvent(context, projectId, readCreatedProject(event))
93117
}
94118
},
95119
onLine: appendOutputLineHandler(context),

0 commit comments

Comments
 (0)