Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { randomUUID } from "node:crypto";

import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment";

import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@okcode/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@okcode/shared/git";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { getRuntimeEnv } from "../../runtimeEnvironment.ts";
import { TextGenerationError } from "../Errors.ts";
import {
type BranchNameGenerationInput,
Expand Down Expand Up @@ -227,7 +229,10 @@ const makeCodexTextGeneration = Effect.gen(function* () {
{
cwd,
shell: process.platform === "win32",
env: process.env,
env: mergeNodeProcessEnv(
compactNodeProcessEnv(process.env),
compactNodeProcessEnv(yield* getRuntimeEnv()),
),
stdin: {
stream: Stream.make(new TextEncoder().encode(prompt)),
},
Expand Down
18 changes: 11 additions & 7 deletions apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment";

import { getRuntimeEnv } from "../../runtimeEnvironment.ts";

import { GitCommandError } from "../Errors.ts";
import {
GitCore,
Expand Down Expand Up @@ -592,17 +594,19 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute"
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.mapError(toGitCommandError(commandInput, "failed to create trace2 monitor.")),
);
const runtimeEnv = input.env ? compactNodeProcessEnv(input.env) : undefined;
const contextEnv = yield* getRuntimeEnv();
const explicitEnv = input.env ? compactNodeProcessEnv(input.env) : undefined;
const baseEnv = compactNodeProcessEnv(process.env);
const combinedEnv = {
...baseEnv,
...contextEnv,
...explicitEnv,
};
const child = yield* commandSpawner
.spawn(
ChildProcess.make("git", commandInput.args, {
cwd: commandInput.cwd,
env: mergeNodeProcessEnv(
compactNodeProcessEnv(
mergeNodeProcessEnv(compactNodeProcessEnv(process.env), runtimeEnv),
),
compactNodeProcessEnv(trace2Monitor.env),
),
env: mergeNodeProcessEnv(combinedEnv, compactNodeProcessEnv(trace2Monitor.env)),
}),
)
.pipe(Effect.mapError(toGitCommandError(commandInput, "failed to spawn.")));
Expand Down
25 changes: 17 additions & 8 deletions apps/server/src/git/Layers/GitHubCli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect, Layer, Schema } from "effect";
import { PositiveInt, TrimmedNonEmptyString } from "@okcode/contracts";
import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment";

import { runProcess } from "../../processRunner";
import { getRuntimeEnv } from "../../runtimeEnvironment.ts";
import { GitHubCliError } from "../Errors.ts";
import {
GitHubCli,
Expand Down Expand Up @@ -167,14 +169,21 @@ function decodeGitHubJson<S extends Schema.Top>(

const makeGitHubCli = Effect.sync(() => {
const execute: GitHubCliShape["execute"] = (input) =>
Effect.tryPromise({
try: () =>
runProcess("gh", input.args, {
cwd: input.cwd,
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
env: process.env,
}),
catch: (error) => normalizeGitHubCliError("execute", error),
Effect.gen(function* () {
const contextEnv = yield* getRuntimeEnv();
const mergedEnv = mergeNodeProcessEnv(
compactNodeProcessEnv(process.env),
compactNodeProcessEnv(contextEnv),
);
return yield* Effect.tryPromise({
try: () =>
runProcess("gh", input.args, {
cwd: input.cwd,
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
env: mergedEnv,
}),
catch: (error) => normalizeGitHubCliError("execute", error),
});
});

const service = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ const make = Effect.gen(function* () {
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
env: yield* resolveRuntimeEnvironment({
projectId: thread.projectId,
cwd: effectiveCwd ?? null,
readModel,
}),
runtimeMode: desiredRuntimeMode,
});
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/runtimeEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { OrchestrationReadModel, ProjectId } from "@okcode/contracts";
import { Effect } from "effect";
import { Effect, Option, ServiceMap } from "effect";

import {
mergeEnvironmentRecords,
Expand All @@ -9,6 +9,28 @@ import {

import { EnvironmentVariables } from "./persistence/Services/EnvironmentVariables";

/**
* Optional service carrying resolved runtime environment variables.
*
* Provided via `Effect.provideService` at the handler boundary so that
* downstream services (GitCore, GitHubCli, …) can read the project-/global-
* scoped env without requiring explicit parameter threading through every
* layer.
*
* Uses `Effect.serviceOption` on the consumer side so that the service
* requirement does NOT propagate into every downstream type signature.
*/
export class RuntimeEnv extends ServiceMap.Service<RuntimeEnv, EnvironmentRecord>()(
"okcode/RuntimeEnv",
) {}

/**
* Read the current runtime environment from the fiber context.
* Returns an empty record when the service has not been provided.
*/
export const getRuntimeEnv = (): Effect.Effect<EnvironmentRecord> =>
Effect.serviceOption(RuntimeEnv).pipe(Effect.map((opt) => (Option.isSome(opt) ? opt.value : {})));

export interface RuntimeEnvironmentInput {
readonly projectId?: ProjectId | null;
readonly cwd?: string | null;
Expand Down
87 changes: 67 additions & 20 deletions apps/server/src/wsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import { GitActionExecutionError } from "./git/Errors.ts";
import { EnvironmentVariables } from "./persistence/Services/EnvironmentVariables.ts";
import { SkillService } from "./skills/SkillService.ts";
import { TokenManager } from "./tokenManager.ts";
import { resolveRuntimeEnvironment } from "./runtimeEnvironment.ts";
import { resolveRuntimeEnvironment, RuntimeEnv } from "./runtimeEnvironment.ts";
import { version as serverVersion } from "../package.json" with { type: "json" };

/**
Expand Down Expand Up @@ -1070,38 +1070,60 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<

case WS_METHODS.gitStatus: {
const body = stripRequestTag(request.body);
return yield* gitManager.status(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* gitManager.status(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitPull: {
const body = stripRequestTag(request.body);
return yield* git.pullCurrentBranch(body.cwd);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* git
.pullCurrentBranch(body.cwd)
.pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitRunStackedAction: {
const body = stripRequestTag(request.body);
return yield* gitManager.runStackedAction(body, {
actionId: body.actionId,
progressReporter: {
publish: (event) =>
pushBus.publishClient(ws, WS_CHANNELS.gitActionProgress, event).pipe(Effect.asVoid),
},
});
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* gitManager
.runStackedAction(body, {
actionId: body.actionId,
progressReporter: {
publish: (event) =>
pushBus.publishClient(ws, WS_CHANNELS.gitActionProgress, event).pipe(Effect.asVoid),
},
})
.pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitResolvePullRequest: {
const body = stripRequestTag(request.body);
return yield* gitManager.resolvePullRequest(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* gitManager
.resolvePullRequest(body)
.pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitPreparePullRequestThread: {
const body = stripRequestTag(request.body);
return yield* gitManager.preparePullRequestThread(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* gitManager
.preparePullRequestThread(body)
.pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitListPullRequests: {
const body = stripRequestTag(request.body);
return yield* gitManager.listPullRequests(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* gitManager
.listPullRequests(body)
.pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.prReviewGetConfig: {
Expand Down Expand Up @@ -1226,37 +1248,58 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<

case WS_METHODS.gitListBranches: {
const body = stripRequestTag(request.body);
return yield* git.listBranches(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* git.listBranches(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitCreateWorktree: {
const body = stripRequestTag(request.body);
return yield* git.createWorktree(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* git.createWorktree(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitRemoveWorktree: {
const body = stripRequestTag(request.body);
return yield* git.removeWorktree(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* git.removeWorktree(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitCreateBranch: {
const body = stripRequestTag(request.body);
return yield* git.createBranch(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* Effect.scoped(git.createBranch(body)).pipe(
Effect.provideService(RuntimeEnv, gitEnv),
);
}

case WS_METHODS.gitCheckout: {
const body = stripRequestTag(request.body);
return yield* Effect.scoped(git.checkoutBranch(body));
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* Effect.scoped(git.checkoutBranch(body)).pipe(
Effect.provideService(RuntimeEnv, gitEnv),
);
}

case WS_METHODS.gitInit: {
const body = stripRequestTag(request.body);
return yield* git.initRepo(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot });
return yield* git.initRepo(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.gitCloneRepository: {
const body = stripRequestTag(request.body);
return yield* git.cloneRepository(body);
const snapshot = yield* projectionReadModelQuery.getSnapshot();
const gitEnv = yield* resolveRuntimeEnvironment({
cwd: body.targetDir,
readModel: snapshot,
});
return yield* git.cloneRepository(body).pipe(Effect.provideService(RuntimeEnv, gitEnv));
}

case WS_METHODS.terminalOpen: {
Expand All @@ -1267,6 +1310,8 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
snapshot.threads.find(
(thread) => thread.id === body.threadId && thread.deletedAt === null,
)?.projectId ?? null,
cwd: body.cwd,
readModel: snapshot,
...(body.env !== undefined ? { extraEnv: body.env } : {}),
});
return yield* terminalManager.open({
Expand Down Expand Up @@ -1298,6 +1343,8 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
snapshot.threads.find(
(thread) => thread.id === body.threadId && thread.deletedAt === null,
)?.projectId ?? null,
cwd: body.cwd,
readModel: snapshot,
...(body.env !== undefined ? { extraEnv: body.env } : {}),
});
return yield* terminalManager.restart({
Expand Down
Loading