Loading repository data…
Loading repository data…
niquola / repository
Clojure-style procedural TypeScript. Functions, data, REPL — no classes, no frameworks. Agent-first architecture.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
Clone this repo, point your coding agent at it, and try building something in this paradigm. We'd love your feedback — open an issue!
CLAUDE.mdis a symlink to this file — AI agents read the same doc as humans.
Clojure-style procedural TypeScript. Functions, data, REPL — no classes, no frameworks.
We wanted the simplest possible environment for an AI coding agent — no abstractions, no magic, predictable results.
Most TypeScript projects end up with layers: classes, DI containers, decorators, middleware chains. Each layer hides state and makes it harder for an agent to understand what's happening, verify changes, and move fast.
The foundation is data and functions. That's it. An agent reads a 30-line function, changes it, reloads via REPL, calls it, sees the result — all in one cycle, no restarts. Add some types for guardrails so the compiler catches typos before runtime. And always tests — so changes are verifiable, not just "looks right."
Inspired by Clojure: functions over methods, data over objects, REPL over restart. But in TypeScript, with Bun, zero dependencies.
Every function lives in its own file. Filename = function name. No classes, no closures, no hidden state.
db_query.ts → db_query(ctx, sql, params)
http_issues.ts → http_issues(ctx, session, request)
escapeHtml.ts → escapeHtml(str)
Why: Each file is a self-contained unit. You can read it in isolation, test it in isolation, replace it in isolation. An AI agent can understand a 30-line file instantly. A 500-line class with 20 methods — not so much.
ctxctx is the single global state object. Database connection, HTTP server, routes, runtime state — everything lives here.
type Ctx = {
fns: CtxFns; // all loaded functions
routes: Record<string, Function>; // registered HTTP routes
db: Database | null; // SQLite connection
server: Server | null; // HTTP server
state: Record<string, any>; // runtime data (counters, caches)
t: any; // REPL scratch space
}
Why: No hidden singletons, no "what's in this?", no dependency injection. You can inspect all state at any moment via eval 'Object.keys(ctx)'. Tests construct their own ctx — no global setup, no mocking.
state is the escape hatch for runtime data. t is scratch space for REPL debugging. Everything else is typed.
ctx.fns instead of importsFunctions don't import each other. They access dependencies through ctx.fns:
// NOT this:
import db_query from "./db_query";
// This:
export default function system_start(ctx: Ctx, opts = {}) {
const { db_start, db_query, server_start } = ctx.fns;
db_start(ctx);
// ...
}
Why: Traditional imports create frozen references. If you change db_query.ts, every file that imported the old version still holds a stale reference. You need to reload the entire dependency tree.
With ctx.fns, functions resolve at call time — like Clojure vars. Reload one file, and every caller immediately sees the new version. No stale references, no reload_all, no import cache busting.
Ctx, Req, SessionTypes are declared globally in ctx.ts via declare global. No imports needed:
// ctx.ts
declare global {
type Ctx = import("./ctx").Ctx;
type Req = import("./ctx").Req;
type Session = import("./ctx").Session;
}
So every function just writes ctx: Ctx — no import type { Ctx } from "./ctx" boilerplate.
type Req = Request & { // Bun's native Request
params: Record<string, string>; // + route params from router
}
type Session = {
user: { id: number; username: string } | null; // resolved from cookie
token: string | null;
}
Why: These three types appear in every handler signature. Making them global removes repetitive imports while keeping full type safety. Typo in request.methood → compile error.
CtxFns — typo = compile errorload_all generates ctx_fns.d.ts with exact function signatures:
// Auto-generated by load_all — do not edit
export default interface CtxFns {
db_query: typeof import("./db_query").default;
db_exec: typeof import("./db_exec").default;
http_issues: typeof import("./http_issues").default;
// ...every function in the project
}
The CtxFns type is strict — no index signature. ctx.fns.db_queryyy → compile error.
Why: Without this, ctx.fns would be Record<string, Function> — any key valid, any typo silent. The generated interface catches mistakes at compile time while keeping the runtime dynamic.
The only place that uses as any to write to ctx.fns is repl-proc-start.ts — the loader itself.
db_query<T> and db_exec — typed databaseTwo functions instead of one, each with a precise return type:
db_query<T>(ctx, sql, params) → T[] // SELECT — typed rows
db_exec(ctx, sql, params) → { changes, lastInsertRowid } // mutations
Usage:
type Issue = { id: number; title: string; status: string };
const issues = db_query<Issue>(ctx, "SELECT * FROM issues");
issues[0].title // string ✓
issues[0].typo // compile error ✓
const r = db_exec(ctx, "INSERT INTO issues (title) VALUES (?)", ["Bug"]);
r.lastInsertRowid // number ✓
Why: A single db_query returning any infects everything downstream. Splitting into two functions with precise types means the compiler catches mistakes after the database boundary.
db_migrateAll schema lives in one function — db_migrate(ctx). Called by system_start, but can also be called independently via REPL:
bun repl_send.ts eval 'db_migrate(ctx)'
Why: Adding a column? Change one file. Tests call system_start({env: "test"}) which calls db_migrate — schema is always in sync. No separate migration files, no migration runner, no drift between test and prod schema.
server_start resolves session from cookie and checks auth before calling the handler:
const session = ctx.fns.session_from_cookie(ctx, req);
if (!session.user && !publicPaths.includes(pattern)) {
return redirect("/ui/login");
}
return await handler(ctx, session, req);
Handlers never check auth themselves — they receive a guaranteed session with user populated.
Why: Auth logic in one place, not scattered across handlers. Every handler gets the same typed Session. Public paths (login, health) are explicitly listed.
bun install
tmux new-session -d -s proc-ts 'bun run repl-proc-start.ts'
bun repl_send.ts load_all
bun repl_send.ts eval 'system_start(ctx)'
# Login: admin / admin
open http://localhost:3002/ui/issues
The REPL server runs on :3001. The process stays alive — you never restart it during normal development. State (DB connections, server, data) persists across reloads.
bun repl_send.ts load_all # first time: load all functions, generate types
bun repl_send.ts reload <name> # after editing: reload one file
load_all is needed once at startup (or when you add new files). After that, reload <name> is enough — since functions resolve through ctx.fns at call time, there are no stale references.
bun repl_send.ts eval 'db_query(ctx, "SELECT * FROM issues")'
bun repl_send.ts eval 'system_stop(ctx)'
bun repl_send.ts eval 'Object.keys(ctx.fns).length'
All loaded functions are available by name. ctx and session are in scope. await works.
ctx.t is a scratch pad for interactive debugging — like a notebook:
bun repl_send.ts eval 'ctx.t = {}'
bun repl_send.ts eval 'ctx.t.issues = db_query(ctx, "SELECT * FROM issues")'
bun repl_send.ts eval 'ctx.t.issues' # inspect
bun repl_send.ts eval 'ctx.t = null' # cleanup
bun repl_send.ts eval 'db_migrate(ctx)' # run migrations without restart
If things are broken (port conflicts, bad state):
lsof -ti:3001 | xargs kill; lsof -ti:3002 | xargs kill
tmux kill-session -t proc-ts 2>/dev/null
tmux new-session -d -s proc-ts 'bun run repl-proc-start.ts'
bun repl_send.ts load_all
bun repl_send.ts eval 'system_start(ctx)'
bunx tsc --noEmit # full type check
What the compiler catches:
ctx.fns.typo — Property 'typo' does not exist on type 'CtxFns'ctx.blabla = 1 — no index signature on Ctxrequest.methood — Property 'methood' does not exist on type 'Req'session.usr — Property 'usr' does not exist on type 'Session'db_exec(...).length — Property 'length' does not exist on type 'ExecResult'What it doesn't catch (by design):
ctx.state.anything — runtime data bag, intentionally anyctx.t — REPL scratch, intentionally anydb_query rows without generic — returns any[] if you skip <T>Tests use system_start(ctx, {env: "test"}) — :memory: DB, migrations run, no server started:
import db_query from "./db_query";
import db_exec from "./db_exec";
// ...wire all deps into ctx.fns
const ctx = { db: null, fns: { db_start, db_stop, db_query, db_exec, db_migrate, server_start } } as any;
system_start(ctx, { env: "test" });
// typed query
type Issue = { id: number; title: string; status: string };
const issues = db_query<Issue>(ctx, "SELECT * FROM issues");
Tests import functions directly (they don't go through the REPL), but wire them into ctx.fns for the function under test. The as any cast on ctx is needed because we construct a partial object — only the fns the test actually needs.
bun test # all tests
bun test login.test.ts # one file
Files auto-register as HTTP routes on reload:
| Prefix | Example file | Route |
|---|---|---|
http_ | http_ui_issues_$id.ts | /ui/issues/:id |
api_ | api_issues_$id.ts | /api/issues/:id |
$ in filename → :param in route. _ → /.
Convention: http_* for pages and JSON APIs, api_* for form actions (POST → redirect).
admin / admin (created by db_migrate)/ui/login (form prefilled for convenience)/ui/login and /health require authsession_from_cookie before handler runs/ui/logout — clears cookie and DB sessionctx.ts / ctx_fns.d.ts — types: Ctx, Req, Session + auto-generated fn signatures
repl-proc-start.ts — REPL server (:3001)
repl_send.ts — CLI client
db_start / db_stop — SQLite connection (WAL mode)
db_query<T> / db_exec — typed SELECT → T[], mutations → {changes, lastInsertRowid}
db_migrate — schema + seed data
server_start / server_stop — HTTP server with routing + auth guard
system_start / system_stop — boot/shutdown orchestration
session_from_cookie — resolve session from request cookie
http_ui_login / http_ui_logout — auth UI
http_ui_issues* — issue tracker UI (Tailwind)
http_issues* — JSON API
api_issues* — form action handlers (POST → redirect)
layout / escapeHtml — shared UI helpers
Ensure REPL is running:
tmux new-session -d -s proc-ts 'bun run repl-proc-start.ts'
Write a function — export default, typed ctx: Ctx, deps from ctx.fns. No imports from other project files.
Load/Reload:
bun repl_send.ts load_all # new files
b