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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Cargo.lock
/codeview-ui/.svelte-kit
/codeview-ui/build
/codeview-ui/.vite
/codeview-ui/.vite-plus
/codeview-ui/.alchemy
/codeview-ui/coverage
/codeview-ui/.env

# Local/IDE
Expand All @@ -29,7 +32,6 @@ opencode.json
/design-input/
/design-input-v2/
/.codemogger/
AGENTS.md

# Playwright
/codeview-ui/test-results/
Expand Down
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Agents

SvelteKit 3, Effect v4, Cloudflare via Alchemy, Better Auth, Drizzle, Tailwind v4, shadcn-svelte, Vite+, pnpm. Celld is an optional self-hosted host instead of Cloudflare.

App imports use `#lib`. Kit config lives in `codeview-ui/vite.config.ts`. The host lives in `codeview-ui/alchemy.run.ts`. Auth, sqlite, and bindings share one Effect runtime through Alchemy. Pages are shadcn plus Tailwind, progressive enhancement, remote functions with Effect Schema.

Local `codeview ui` still builds a sidecar with `@jesterkit/exe-sveltekit`. Alchemy injects the Cloudflare adapter for hosted builds.

Follow the effect skill. Follow create-webapp conventions when adding packages or rewriting config.

Anti-slop oxlint rules stay at error. Existing TypeScript still has hundreds of findings (object parameters, type assertions, known-value widening). New code should pass. Do not turn the rules off.
20 changes: 9 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,21 @@ cargo run -p codeview-cli -- analyze --manifest-path {path/Cargo.toml} --out {ou

## Package Manager

Use **bun** for all package management operations:
- Install: `bun add <package>` or `bun add -D <package>` for dev dependencies
- Run scripts: `bun run <script>`
- Execute: `bunx <command>`
Use **pnpm** for all package management operations:
- Install: `pnpm add --save-exact <package>` or `pnpm add -D --save-exact <package>`
- Run scripts: `pnpm <script>` or `vp <script>`
- Execute: `pnpm dlx <command>` or `vp dlx <command>`

Do not use npm, yarn, or pnpm.
Do not use bun, npm, or yarn.

## Cloudflare Dev Server

Run the hosted/Cloudflare mode dev server with:
```
cd /e/Projects/codeview/codeview-ui && bun run cf:dev
cd codeview-ui && pnpm infra:dev
```

This runs `cf:build` first (builds the SvelteKit app), then starts wrangler dev. Use `/e/...` Unix-style paths in Bash (not `E:\...` Windows paths — Git Bash doesn't handle them).

Run this as a **background task**. When you need to restart (e.g. after changing server code or config), stop the existing task first, then re-run. After restarting, a hard refresh in the browser is sufficient — no need to stop/start manually between builds for client-only changes.
Alchemy owns Cloudflare resources (`alchemy.run.ts`). `alchemy dev` runs Kit's Vite server with live bindings. After changing server or infra code, stop the existing task and re-run. Client-only changes only need a hard refresh.

After starting the server, wait for it to be ready before navigating:
```bash
Expand All @@ -69,9 +67,9 @@ for i in $(seq 1 30); do
done
```

To clear all wrangler persisted state (Durable Objects, R2, KV) and start fresh:
To clear Alchemy/Wrangler persisted state and start fresh:
```
cd /e/Projects/codeview/codeview-ui && bun run cf:dev:clear
cd codeview-ui && pnpm cf:dev:clear
```

## Build Lock Issues
Expand Down
10 changes: 5 additions & 5 deletions codeview-cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ fn main() {
);
println!(
"cargo:rerun-if-changed={}",
ui_dir.join("svelte.config.js").display()
ui_dir.join("vite.config.ts").display()
);
println!(
"cargo:rerun-if-changed={}",
ui_dir.join("vite.config.ts").display()
ui_dir.join("alchemy.run.ts").display()
);

// Rebuild when the codeview-rustdoc Rust source changes (triggers wasm:build)
Expand Down Expand Up @@ -87,15 +87,15 @@ fn main() {

// Build SvelteKit app with @jesterkit/exe-sveltekit adapter
// This adapter builds directly to dist/codeview-server(.exe)
let status = Command::new("bun")
let status = Command::new("pnpm")
.args(["run", "build"])
.env("EXE_TARGET", exe_target)
.current_dir(&ui_dir)
.status()
.expect("failed to run `bun run build` - is bun installed?");
.expect("failed to run `pnpm run build` - is pnpm installed?");

if !status.success() {
panic!("`bun run build` failed in {}", ui_dir.display());
panic!("`pnpm run build` failed in {}", ui_dir.display());
}

// Copy the built binary to sidecar dir
Expand Down
21 changes: 11 additions & 10 deletions codeview-cli/src/publisher/r2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ pub struct LocalMiniflareBackend {
/// `CODEVIEW_WRANGLER_CWD` when the cron CLI is invoked from a
/// directory that doesn't have `node_modules/wrangler` resolvable.
/// `None` means inherit the parent's cwd (matches the typical
/// `bun run cron:mimic` invocation from `codeview-ui/`).
/// `pnpm cron:mimic` invocation from `codeview-ui/`).
wrangler_cwd: Option<PathBuf>,
/// R2 binding name from `wrangler.toml` (`[[r2_buckets]] binding =
/// "CRATE_GRAPHS"`). Needed so the bulk-writer's miniflare config
Expand All @@ -367,7 +367,7 @@ pub struct LocalMiniflareBackend {
}

/// Long-running miniflare R2 writer. Speaks JSON-Lines over stdio to
/// `codeview-ui/scripts/bulk-put-local.ts` (Bun + miniflare 4).
/// `codeview-ui/scripts/bulk-put-local.ts` (Node + miniflare).
///
/// **Why a sidecar process at all:** every `wrangler r2 object put`
/// invocation pays ~3s of Node startup before the actual ~10ms SQLite
Expand Down Expand Up @@ -466,8 +466,9 @@ impl LocalMiniflareBackend {
.context("persist_to path is not valid UTF-8")?
.to_string();

let mut cmd = tokio::process::Command::new("bun");
cmd.arg(&script_path)
let mut cmd = tokio::process::Command::new("node");
cmd.args(["--experimental-strip-types", "--no-warnings=ExperimentalWarning"])
.arg(&script_path)
.args([
"--binding",
&self.binding,
Expand All @@ -483,7 +484,7 @@ impl LocalMiniflareBackend {

let mut child = cmd
.spawn()
.with_context(|| format!("spawn bun {}", script_path.display()))?;
.with_context(|| format!("spawn node {}", script_path.display()))?;
let stdin = child
.stdin
.take()
Expand Down Expand Up @@ -573,12 +574,12 @@ impl LocalMiniflareBackend {
}
}

/// `bunx wrangler` invocation with the right cwd. We use `bunx`
/// instead of bare `wrangler` because the project's wrangler comes
/// from `codeview-ui/node_modules/.bin`, not a global install.
/// `pnpm exec wrangler` invocation with the right cwd. We use
/// `pnpm exec` because the project's wrangler comes from
/// `codeview-ui/node_modules/.bin`, not a global install.
fn wrangler_cmd(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("bunx");
cmd.arg("wrangler");
let mut cmd = tokio::process::Command::new("pnpm");
cmd.args(["exec", "wrangler"]);
if let Some(cwd) = &self.wrangler_cwd {
cmd.current_dir(cwd);
}
Expand Down
1 change: 1 addition & 0 deletions codeview-ui/.node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.19.0
2 changes: 2 additions & 0 deletions codeview-ui/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
engine-strict=true
save-exact=true
7 changes: 0 additions & 7 deletions codeview-ui/.prettierignore

This file was deleted.

16 changes: 0 additions & 16 deletions codeview-ui/.prettierrc

This file was deleted.

127 changes: 127 additions & 0 deletions codeview-ui/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import { Stage } from "alchemy/Stage";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";

const compatibilityDate = "2026-08-14";
const parserScriptName = "codeview-parser";

export const AuthDb = Cloudflare.D1.Database("AuthDb", {
name: "codeview-auth",
migrationsDir: "./src/lib/server/db/auth-migrations",
});

export const CrateGraphs = Cloudflare.R2.Bucket("CrateGraphs", {
name: "crate-graphs",
});

export const ParseRequests = Cloudflare.Queues.Queue("ParseRequests", {
name: "codeview-parse-requests",
});

export const ParseDeadLetters = Cloudflare.Queues.Queue("ParseDeadLetters", {
name: "codeview-parse-dead",
});

export const Parser = Cloudflare.Worker("Parser", {
name: parserScriptName,
main: "./src/parse-worker.ts",
compatibility: { date: compatibilityDate },
env: {
CRATE_GRAPHS: CrateGraphs,
PARSE_REQUESTS: ParseRequests,
PARSE_STATUS: Cloudflare.DurableObject("PARSE_STATUS", {
className: "ParseStatusDurableObject",
}),
PARSE_WORKFLOW: Cloudflare.Workflow("PARSE_WORKFLOW", {
className: "ParseCrateWorkflow",
}),
GITHUB_REPO: "themixednuts/codeview",
GITHUB_REF: "main",
GITHUB_WORKFLOW_FILE: "parse.yml",
PARSE_CALLBACK_BASE_URL: "https://codeview-parser.jonfonts.workers.dev",
PLAN_DRAIN_ACTIVE_TARGET: "4",
PLAN_DRAIN_BATCH_SIZE: "0",
PARSE_DISPATCH_BURST: "4",
PARSE_DISPATCH_REFILL_SECONDS: "45",
DOCSRS_PARSE_BURST: "4",
DOCSRS_PARSE_REFILL_SECONDS: "20",
SYSROOT_PARSE_BURST: "1",
SYSROOT_PARSE_REFILL_SECONDS: "600",
GITHUB_TOKEN: Config.redacted("GITHUB_TOKEN"),
PARSE_CALLBACK_SECRET: Config.redacted("PARSE_CALLBACK_SECRET"),
},
});

export const Website = Cloudflare.Website.SvelteKit(
"Website",
Effect.gen(function* () {
const stage = yield* Stage;
return {
name: `codeview-website-${stage}`,
domain: "codeview.codes",
compatibility: { date: compatibilityDate },
env: {
AUTH_DB: AuthDb,
CRATE_GRAPHS: CrateGraphs,
PARSE_REQUESTS: ParseRequests,
PARSE_STATUS: Cloudflare.DurableObject("PARSE_STATUS", {
className: "ParseStatusDurableObject",
scriptName: parserScriptName,
}),
RATE_LIMIT_API: Cloudflare.RateLimit("RATE_LIMIT_API", {
namespaceId: 1001,
simple: { limit: 60, period: 60 },
}),
RATE_LIMIT_PARSE_ANON: Cloudflare.RateLimit("RATE_LIMIT_PARSE_ANON", {
namespaceId: 1002,
simple: { limit: 6, period: 60 },
}),
RATE_LIMIT_PARSE_AUTH: Cloudflare.RateLimit("RATE_LIMIT_PARSE_AUTH", {
namespaceId: 1003,
simple: { limit: 60, period: 60 },
}),
BETTER_AUTH_URL: "https://codeview.codes",
GITHUB_OAUTH_CLIENT_ID: "Ov23liaAIxx0Vmp8F0zE",
GITHUB_ADMIN_LOGINS: "themixednuts",
GITHUB_REPO: "themixednuts/codeview",
GITHUB_REF: "main",
GITHUB_WORKFLOW_FILE: "parse.yml",
PLAN_DRAIN_ACTIVE_TARGET: "4",
PLAN_DRAIN_BATCH_SIZE: "0",
GITHUB_ACTIONS_REPO_USAGE_TARGET_PERCENT: "35",
BETTER_AUTH_SECRET: Config.redacted("BETTER_AUTH_SECRET"),
GITHUB_OAUTH_CLIENT_SECRET: Config.redacted("GITHUB_OAUTH_CLIENT_SECRET"),
},
};
}),
);

export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
export type ParserEnv = Cloudflare.InferEnv<typeof Parser>;

export default Alchemy.Stack(
"Codeview",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const website = yield* Website;
const parser = yield* Parser;
const parseRequests = yield* ParseRequests;
yield* ParseDeadLetters;
yield* Cloudflare.Queues.Consumer("ParseConsumer", {
queueId: parseRequests.queueId,
scriptName: parser.workerName,
deadLetterQueue: "codeview-parse-dead",
settings: {
batchSize: 10,
maxRetries: 3,
maxWaitTimeMs: 5000,
},
});
return { url: website.url, parser: parser.url };
}),
);
Loading