Skip to content
Closed
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
67 changes: 67 additions & 0 deletions apps/server/src/git/Errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Schema } from "effect";

/**
* GitCommandError - Git command execution failed.
*/
export class GitCommandError extends Schema.TaggedErrorClass<GitCommandError>()("GitCommandError", {
operation: Schema.String,
command: Schema.String,
cwd: Schema.String,
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {
override get message(): string {
return `Git command failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`;
}
}

/**
* GitHubCliError - GitHub CLI execution or authentication failed.
*/
export class GitHubCliError extends Schema.TaggedErrorClass<GitHubCliError>()("GitHubCliError", {
operation: Schema.String,
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {
override get message(): string {
return `GitHub CLI failed in ${this.operation}: ${this.detail}`;
}
}

/**
* TextGenerationError - Commit or PR text generation failed.
*/
export class TextGenerationError extends Schema.TaggedErrorClass<TextGenerationError>()(
"TextGenerationError",
{
operation: Schema.String,
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
},
) {
override get message(): string {
return `Text generation failed in ${this.operation}: ${this.detail}`;
}
}

/**
* GitManagerError - Stacked Git workflow orchestration failed.
*/
export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()("GitManagerError", {
operation: Schema.String,
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {
override get message(): string {
return `Git manager failed in ${this.operation}: ${this.detail}`;
}
}

/**
* GitManagerServiceError - Errors emitted by stacked Git workflow orchestration.
*/
export type GitManagerServiceError =
| GitManagerError
| GitCommandError
| GitHubCliError
| TextGenerationError;
6 changes: 6 additions & 0 deletions apps/server/src/git/isRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { existsSync } from "node:fs";
import { join } from "node:path";

export function isGitRepository(cwd: string): boolean {
return existsSync(join(cwd, ".git"));
Comment on lines +4 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Harden repo detection to avoid false positives from arbitrary .git artifacts.

The current predicate returns true for any existing .git path, even malformed files. Since this result gates git workflows, a malformed .git can incorrectly enable downstream git operations.

🔧 Proposed hardening
-import { existsSync } from "node:fs";
+import { existsSync, lstatSync, readFileSync } from "node:fs";
 import { join } from "node:path";
 
 export function isGitRepository(cwd: string): boolean {
-  return existsSync(join(cwd, ".git"));
+  const gitPath = join(cwd, ".git");
+  if (!existsSync(gitPath)) return false;
+
+  try {
+    const stat = lstatSync(gitPath);
+    if (stat.isDirectory()) return true;
+    if (stat.isFile()) {
+      return readFileSync(gitPath, "utf8").startsWith("gitdir:");
+    }
+    return false;
+  } catch {
+    return false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/git/isRepo.ts` around lines 4 - 5, The isGitRepository
function should not just check for the existence of a .git path; update
isGitRepository to validate that .git is a real git repository by: (1) using
fs.stat to confirm join(cwd, ".git") is a directory (stat.isDirectory()), and if
not a directory, (2) read the .git file to detect a "gitdir: <path>" pointer and
resolve that path and confirm it exists and contains git metadata (e.g., HEAD
and refs) before returning true; otherwise return false. Target the
isGitRepository function for these changes and ensure you handle errors (ENOENT,
permission) cleanly and synchronously/asynchronously consistent with the
surrounding code.

}
96 changes: 96 additions & 0 deletions apps/server/src/orchestration/projectMetadataProjection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { OrchestrationEvent } from "@t3tools/contracts";
import { Effect, Option } from "effect";

import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
import type { ProjectionProjectRepositoryShape } from "../persistence/Services/ProjectionProjects.ts";
import type { ProjectionStateRepositoryShape } from "../persistence/Services/ProjectionState.ts";

export type ProjectMetadataOrchestrationEvent = Extract<
OrchestrationEvent,
{ type: "project.created" | "project.meta-updated" | "project.deleted" }
>;

export const PROJECT_METADATA_SNAPSHOT_PROJECTORS = [
"projection.projects",
"projection.threads",
"projection.thread-messages",
"projection.thread-proposed-plans",
"projection.thread-activities",
"projection.thread-sessions",
"projection.checkpoints",
] as const;

export const applyProjectMetadataProjection = (input: {
readonly event: ProjectMetadataOrchestrationEvent;
readonly projectionProjectRepository: ProjectionProjectRepositoryShape;
}): Effect.Effect<void, ProjectionRepositoryError> =>
Effect.gen(function* () {
switch (input.event.type) {
case "project.created":
yield* input.projectionProjectRepository.upsert({
projectId: input.event.payload.projectId,
title: input.event.payload.title,
workspaceRoot: input.event.payload.workspaceRoot,
defaultModelSelection: input.event.payload.defaultModelSelection,
scripts: input.event.payload.scripts,
createdAt: input.event.payload.createdAt,
updatedAt: input.event.payload.updatedAt,
deletedAt: null,
});
break;

case "project.meta-updated": {
const existingRow = yield* input.projectionProjectRepository.getById({
projectId: input.event.payload.projectId,
});
if (Option.isSome(existingRow)) {
yield* input.projectionProjectRepository.upsert({
...existingRow.value,
...(input.event.payload.title !== undefined
? { title: input.event.payload.title }
: {}),
...(input.event.payload.workspaceRoot !== undefined
? { workspaceRoot: input.event.payload.workspaceRoot }
: {}),
...(input.event.payload.defaultModelSelection !== undefined
? { defaultModelSelection: input.event.payload.defaultModelSelection }
: {}),
...(input.event.payload.scripts !== undefined
? { scripts: input.event.payload.scripts }
: {}),
updatedAt: input.event.payload.updatedAt,
});
}
break;
}

case "project.deleted": {
const existingRow = yield* input.projectionProjectRepository.getById({
projectId: input.event.payload.projectId,
});
if (Option.isSome(existingRow)) {
yield* input.projectionProjectRepository.upsert({
...existingRow.value,
deletedAt: input.event.payload.deletedAt,
updatedAt: input.event.payload.deletedAt,
});
}
break;
Comment on lines +42 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast when update/delete arrives before the row exists.

If getById(...) returns None on Line 43 or Line 68, these branches silently drop the event. In the same module, advanceProjectMetadataSnapshotState(...) can still record the sequence as applied, which makes the projection drift permanent after replay gaps or partial backfills. Return an explicit projection error here, or otherwise prevent snapshot advancement until the base row exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/projectMetadataProjection.ts` around lines 42 -
78, The projection currently silently ignores "project.meta-updated" and
"project.deleted" events when projectionProjectRepository.getById(...) returns
None, which allows advanceProjectMetadataSnapshotState(...) to mark the sequence
applied and permanently drift; fix by failing fast: in both the
"project.meta-updated" and "project.deleted" cases, detect
Option.isNone(existingRow) and return/throw an explicit projection error (or
call the module's projection-failure helper) rather than silently continuing, so
the snapshot advancement is prevented until the base row exists. Ensure you
reference the existingRow variable and the
projectionProjectRepository.getById(...) call and produce a clear error (e.g.,
throw new Error or a ProjectionError) so the caller/runner can stop and
retry/backfill before advancing state.

}
}
});

export const advanceProjectMetadataSnapshotState = (input: {
readonly event: ProjectMetadataOrchestrationEvent;
readonly projectionStateRepository: ProjectionStateRepositoryShape;
}): Effect.Effect<void, ProjectionRepositoryError> =>
Effect.forEach(
PROJECT_METADATA_SNAPSHOT_PROJECTORS,
(projector) =>
input.projectionStateRepository.upsert({
projector,
lastAppliedSequence: input.event.sequence,
updatedAt: input.event.occurredAt,
}),
{ concurrency: 1 },
).pipe(Effect.asVoid);
182 changes: 182 additions & 0 deletions apps/server/src/projectFaviconRoute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";

import { afterEach, describe, expect, it } from "vitest";
import { tryHandleProjectFaviconRequest } from "./projectFaviconRoute";

interface HttpResponse {
statusCode: number;
contentType: string | null;
body: string;
}

const tempDirs: string[] = [];

function makeTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}

async function withRouteServer(run: (baseUrl: string) => Promise<void>): Promise<void> {
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://127.0.0.1");
if (tryHandleProjectFaviconRequest(url, res)) {
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
});

await new Promise<void>((resolve, reject) => {
server.listen(0, "127.0.0.1", (error?: Error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});

const address = server.address();
if (typeof address !== "object" || address === null) {
throw new Error("Expected server address to be an object");
}
const baseUrl = `http://127.0.0.1:${address.port}`;

try {
await run(baseUrl);
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error?: Error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
}

async function request(baseUrl: string, pathname: string): Promise<HttpResponse> {
const response = await fetch(`${baseUrl}${pathname}`);
return {
statusCode: response.status,
contentType: response.headers.get("content-type"),
body: await response.text(),
};
}

describe("tryHandleProjectFaviconRequest", () => {
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("returns 400 when cwd is missing", async () => {
await withRouteServer(async (baseUrl) => {
const response = await request(baseUrl, "/api/project-favicon");
expect(response.statusCode).toBe(400);
expect(response.body).toBe("Missing cwd parameter");
});
});

it("serves a well-known favicon file from the project root", async () => {
const projectDir = makeTempDir("t3code-favicon-route-root-");
fs.writeFileSync(path.join(projectDir, "favicon.svg"), "<svg>favicon</svg>", "utf8");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe("<svg>favicon</svg>");
});
});

it("resolves icon href from source files when no well-known favicon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-source-");
const iconPath = path.join(projectDir, "public", "brand", "logo.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "index.html"),
'<link rel="icon" href="/brand/logo.svg">',
);
fs.writeFileSync(iconPath, "<svg>brand</svg>", "utf8");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe("<svg>brand</svg>");
});
});

it("resolves icon link when href appears before rel in HTML", async () => {
const projectDir = makeTempDir("t3code-favicon-route-html-order-");
const iconPath = path.join(projectDir, "public", "brand", "logo.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "index.html"),
'<link href="/brand/logo.svg" rel="icon">',
);
fs.writeFileSync(iconPath, "<svg>brand-html-order</svg>", "utf8");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe("<svg>brand-html-order</svg>");
});
});

it("resolves object-style icon metadata when href appears before rel", async () => {
const projectDir = makeTempDir("t3code-favicon-route-obj-order-");
const iconPath = path.join(projectDir, "public", "brand", "obj.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "src", "root.tsx"),
'const links = [{ href: "/brand/obj.svg", rel: "icon" }];',
"utf8",
);
fs.writeFileSync(iconPath, "<svg>brand-obj-order</svg>", "utf8");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe("<svg>brand-obj-order</svg>");
});
});

it("serves a fallback favicon when no icon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-fallback-");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toContain('data-fallback="project-favicon"');
});
});

it("returns 204 when fallback=none and no icon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-no-fallback-");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}&fallback=none`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(204);
expect(response.body).toBe("");
});
});
});
Loading
Loading