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
26 changes: 24 additions & 2 deletions apps/server/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog
import rootPackageJson from "../../../package.json" with { type: "json" };
import serverPackageJson from "../package.json" with { type: "json" };

interface PackageJson {
name: string;
repository: {
type: string;
url: string;
directory: string;
};
bin: Record<string, string>;
type: string;
version: string;
engines: Record<string, string>;
files: string[];
dependencies: Record<string, string>;
overrides: Record<string, string>;
}

class CliError extends Data.TaggedError("CliError")<{
readonly message: string;
readonly cause?: unknown;
Expand Down Expand Up @@ -192,22 +208,28 @@ const publishCmd = Command.make(
// Resolve catalog dependencies before any file mutations. If this throws,
// acquire fails and no release hook runs, so filesystem must still be untouched.
const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
const pkg = {
const pkg: PackageJson = {
name: serverPackageJson.name,
repository: serverPackageJson.repository,
bin: serverPackageJson.bin,
type: serverPackageJson.type,
version,
engines: serverPackageJson.engines,
files: serverPackageJson.files,
dependencies: serverPackageJson.dependencies as Record<string, unknown>,
dependencies: serverPackageJson.dependencies,
overrides: rootPackageJson.overrides,
};

pkg.dependencies = resolveCatalogDependencies(
pkg.dependencies,
rootPackageJson.workspaces.catalog,
"apps/server dependencies",
);
pkg.overrides = resolveCatalogDependencies(
pkg.overrides,
rootPackageJson.workspaces.catalog,
"root overrides",
);

const original = yield* fs.readFileString(packageJsonPath);
yield* fs.writeFileString(backupPath, original);
Expand Down
15 changes: 6 additions & 9 deletions apps/server/src/auth/Layers/ServerSecretStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Crypto from "node:crypto";

import { Effect, FileSystem, Layer, Path } from "effect";
import { Effect, FileSystem, Layer, Path, Predicate } from "effect";
import * as PlatformError from "effect/PlatformError";

import { ServerConfig } from "../../config.ts";
Expand Down Expand Up @@ -28,17 +28,14 @@ export const makeServerSecretStore = Effect.gen(function* () {

const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`);

const isMissingSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound";

const isAlreadyExistsSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
cause instanceof PlatformError.PlatformError && cause.reason._tag === "AlreadyExists";
const isPlatformError = (u: unknown): u is PlatformError.PlatformError =>
Predicate.isTagged(u, "PlatformError");

const get: ServerSecretStoreShape["get"] = (name) =>
fileSystem.readFile(resolveSecretPath(name)).pipe(
Effect.map((bytes) => Uint8Array.from(bytes)),
Effect.catch((cause) =>
isMissingSecretFileError(cause)
cause.reason._tag === "NotFound"
? Effect.succeed(null)
: Effect.fail(
new SecretStoreError({
Expand Down Expand Up @@ -108,7 +105,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
return create(name, generated).pipe(
Effect.as(Uint8Array.from(generated)),
Effect.catchTag("SecretStoreError", (error) =>
isAlreadyExistsSecretFileError(error.cause)
isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists"
? get(name).pipe(
Effect.flatMap((created) =>
created !== null
Expand All @@ -129,7 +126,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
const remove: ServerSecretStoreShape["remove"] = (name) =>
fileSystem.remove(resolveSecretPath(name)).pipe(
Effect.catch((cause) =>
isMissingSecretFileError(cause)
cause.reason._tag === "NotFound"
? Effect.void
: Effect.fail(
new SecretStoreError({
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2002,7 +2002,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
"GitCore.removeWorktree",
input.cwd,
args,
`${commandLabel(args)} failed (cwd: ${input.cwd}): ${error instanceof Error ? error.message : String(error)}`,
`${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
error,
),
),
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/git/Layers/GitHubCli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Effect, Layer, Schema } from "effect";
import { Effect, Layer, Schema, SchemaIssue } from "effect";
import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";

import { runProcess } from "../../processRunner";
Expand Down Expand Up @@ -154,7 +154,7 @@ function decodeGitHubJson<S extends Schema.Top>(
(error) =>
new GitHubCliError({
operation,
detail: error instanceof Error ? `${invalidDetail}: ${error.message}` : invalidDetail,
detail: `${invalidDetail}: ${SchemaIssue.makeFormatterDefault()(error.issue)}`,
cause: error,
}),
),
Expand Down
7 changes: 0 additions & 7 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
.pipe(Effect.result);

assert.equal(result._tag, "Failure");
if (result._tag !== "Failure") {
return;
}

assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {
return;
}
assert.equal(result.failure.provider, "codex");
assert.equal(result.failure.threadId, "sess-missing");
assert.equal(result.failure.cause instanceof Error, true);
Expand Down
15 changes: 4 additions & 11 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,11 @@ export interface CodexAdapterLiveOptions {
readonly nativeEventLogger?: EventNdjsonLogger;
}

function toMessage(cause: unknown, fallback: string): string {
if (cause instanceof Error && cause.message.length > 0) {
return cause.message;
}
return fallback;
}

function toSessionError(
threadId: ThreadId,
cause: unknown,
): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
const normalized = toMessage(cause, "").toLowerCase();
const normalized = cause instanceof Error ? cause.message.toLowerCase() : "";
if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {
return new ProviderAdapterSessionNotFoundError({
provider: PROVIDER,
Expand All @@ -88,7 +81,7 @@ function toRequestError(threadId: ThreadId, method: string, cause: unknown): Pro
return new ProviderAdapterRequestError({
provider: PROVIDER,
method,
detail: toMessage(cause, `${method} failed`),
detail: cause instanceof Error ? `${method} failed: ${cause.message}` : `${method} failed`,
cause,
});
}
Expand Down Expand Up @@ -1427,7 +1420,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: input.threadId,
detail: toMessage(cause, "Failed to start Codex adapter session."),
detail: `Failed to start Codex adapter session: ${cause instanceof Error ? cause.message : String(cause)}.`,
cause,
}),
});
Expand Down Expand Up @@ -1455,7 +1448,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
new ProviderAdapterRequestError({
provider: PROVIDER,
method: "turn/start",
detail: toMessage(cause, "Failed to read attachment file."),
detail: `Failed to read attachment file: ${cause.message}.`,
cause,
}),
),
Expand Down
7 changes: 2 additions & 5 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
auth: { status: "unknown" },
message: isCommandMissingCause(error)
? "Codex CLI (`codex`) is not installed or not on PATH."
: `Failed to execute Codex CLI health check: ${error instanceof Error ? error.message : String(error)}.`,
: `Failed to execute Codex CLI health check: ${error.message}.`,
},
});
}
Expand Down Expand Up @@ -489,10 +489,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
version: parsedVersion,
status: "warning",
auth: { status: "unknown" },
message:
error instanceof Error
? `Could not verify Codex authentication status: ${error.message}.`
: "Could not verify Codex authentication status.",
message: `Could not verify Codex authentication status: ${error.message}.`,
},
});
}
Expand Down
3 changes: 1 addition & 2 deletions apps/server/src/provider/providerSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {
return trimmed.length > 0 ? trimmed : undefined;
}

export function isCommandMissingCause(error: unknown): boolean {
if (!(error instanceof Error)) return false;
export function isCommandMissingCause(error: Error): boolean {
const lower = error.message.toLowerCase();
return lower.includes("enoent") || lower.includes("notfound");
}
Expand Down
57 changes: 31 additions & 26 deletions apps/server/src/terminal/Layers/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from "@t3tools/contracts";
import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
import {
Data,
Effect,
Encoding,
Equal,
Expand All @@ -17,6 +16,7 @@ import {
FileSystem,
Layer,
Option,
Schema,
Scope,
Semaphore,
SynchronizedRef,
Expand Down Expand Up @@ -54,22 +54,28 @@ const DEFAULT_OPEN_COLS = 120;
const DEFAULT_OPEN_ROWS = 30;
const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);

type TerminalSubprocessChecker = (
terminalPid: number,
) => Effect.Effect<boolean, TerminalSubprocessCheckError>;

class TerminalSubprocessCheckError extends Data.TaggedError("TerminalSubprocessCheckError")<{
readonly message: string;
readonly cause?: unknown;
readonly terminalPid: number;
readonly command: "powershell" | "pgrep" | "ps";
}> {}
class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()(
"TerminalSubprocessCheckError",
{
message: Schema.String,
cause: Schema.optional(Schema.Defect),
terminalPid: Schema.Number,
command: Schema.Literals(["powershell", "pgrep", "ps"]),
},
) {}

class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()(
"TerminalProcessSignalError",
{
message: Schema.String,
cause: Schema.optional(Schema.Defect),
signal: Schema.Literals(["SIGTERM", "SIGKILL"]),
},
) {}

class TerminalProcessSignalError extends Data.TaggedError("TerminalProcessSignalError")<{
readonly message: string;
readonly cause?: unknown;
readonly signal: "SIGTERM" | "SIGKILL";
}> {}
interface TerminalSubprocessChecker {
(terminalPid: number): Effect.Effect<boolean, TerminalSubprocessCheckError>;
}

interface ShellCandidate {
shell: string;
Expand Down Expand Up @@ -271,9 +277,8 @@ function isRetryableShellSpawnError(error: PtySpawnError): boolean {

if (current instanceof Error) {
messages.push(current.message);
const cause = (current as { cause?: unknown }).cause;
if (cause) {
queue.push(cause);
if (current.cause) {
queue.push(current.cause);
}
continue;
}
Expand Down Expand Up @@ -876,7 +881,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
Effect.logWarning("failed to persist terminal history", {
threadId,
terminalId,
error: error instanceof Error ? error.message : String(error),
error,
}),
),
);
Expand Down Expand Up @@ -959,7 +964,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
Effect.catch((cleanupError) =>
Effect.logWarning("failed to remove legacy terminal history", {
threadId,
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
error: cleanupError,
}),
),
);
Expand All @@ -975,7 +980,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
Effect.logWarning("failed to delete terminal history", {
threadId,
terminalId,
error: error instanceof Error ? error.message : String(error),
error,
}),
),
);
Expand All @@ -985,7 +990,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
Effect.logWarning("failed to delete terminal history", {
threadId,
terminalId,
error: error instanceof Error ? error.message : String(error),
error,
}),
),
);
Expand All @@ -1011,7 +1016,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
Effect.catch((error) =>
Effect.logWarning("failed to delete terminal histories for thread", {
threadId,
error: error instanceof Error ? error.message : String(error),
error,
}),
),
),
Expand Down Expand Up @@ -1463,12 +1468,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
const terminalPid = session.pid;
const hasRunningSubprocess = yield* subprocessChecker(terminalPid).pipe(
Effect.map(Option.some),
Effect.catch((error) =>
Effect.catch((reason) =>
Effect.logWarning("failed to check terminal subprocess activity", {
threadId: session.threadId,
terminalId: session.terminalId,
terminalPid,
error: error instanceof Error ? error.message : String(error),
reason,
}).pipe(Effect.as(Option.none<boolean>())),
),
);
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/workspace/Layers/WorkspaceEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,6 @@ function directoryAncestorsOf(relativePath: string): string[] {
return directories;
}

const processErrorDetail = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);

export const makeWorkspaceEntries = Effect.gen(function* () {
const path = yield* Path.Path;
const gitOption = yield* Effect.serviceOption(GitCore);
Expand Down Expand Up @@ -319,7 +316,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
new WorkspaceEntriesError({
cwd,
operation: "workspaceEntries.readDirectoryEntries",
detail: processErrorDetail(cause),
detail: cause instanceof Error ? cause.message : String(cause),
cause,
}),
}).pipe(
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
worktreePath: input.worktreePath,
scriptId: input.scriptId,
terminalId: input.terminalId,
detail:
error instanceof Error
? error.message
: "Unknown setup activity dispatch failure.",
detail: error.message,
},
),
),
Expand Down
Loading
Loading