9 Commits

Author SHA1 Message Date
09d85c8f22 lalalalal 2026-02-02 19:06:47 -05:00
a0ce5183b2 dkkdkdkdkd 2026-02-02 19:02:40 -05:00
c83202b681 fafafafa 2026-02-02 18:59:54 -05:00
2c1d297be1 fda 2026-02-02 18:55:31 -05:00
2d697c1e61 Move package.json files around 2026-02-02 18:47:54 -05:00
410bb671f1 fads 2026-02-02 18:41:26 -05:00
0ae197f939 fdas 2026-02-02 18:39:49 -05:00
370bea5d98 asdf 2026-02-02 18:37:09 -05:00
9d34768051 Add file list 2026-02-02 18:35:37 -05:00
41 changed files with 215 additions and 1500 deletions

View File

@@ -1,44 +0,0 @@
# Agent Instructions
Read and follow the instructions in `diachron/AGENTS.md`. That file
contains framework conventions, commands, and structure that apply to
all coding agents working on diachron-based projects.
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds

View File

@@ -3,10 +3,6 @@
This file provides guidance to Claude Code (claude.ai/code) when working with This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository. code in this repository.
Read and follow the instructions in `diachron/AGENTS.md`. That file
contains framework conventions, commands, and structure that apply to
all coding agents working on diachron-based projects.
## Project Overview ## Project Overview
Diachron is an opinionated TypeScript/Node.js web framework with a Go-based Diachron is an opinionated TypeScript/Node.js web framework with a Go-based

View File

@@ -1,184 +0,0 @@
# What is diachron?
diachron is a web framework for TypeScript and Node.js. It uses a Go-based
master process that handles file watching, building, process management, and
request proxying. The application code is TypeScript running on Express.js.
If you're joining a project that uses diachron, this document will orient you.
## Why diachron exists
diachron was built around a few frustrations with mainstream web frameworks:
- **No dev/prod split.** Most frameworks behave differently in development and
production. diachron doesn't. The master process watches files, rebuilds,
and manages workers the same way everywhere. There is no `NODE_ENV`.
- **Managed tooling.** Node.js, pnpm, and other tools are downloaded and
pinned to exact versions inside the project. You don't install them
system-wide. Everyone on the team runs the same binaries.
- **PostgreSQL, directly.** No ORM, no database abstraction layer. You write
SQL (via Kysely for type safety) and talk to PostgreSQL. If you need
MySQL or SQLite support, this is not the framework for you.
- **Debuggability over magic.** Everything is explicit and inspectable.
Logging and observability are first-class concerns, not afterthoughts.
diachron is inspired by the
[Taking PHP Seriously](https://slack.engineering/taking-php-seriously/) essay
from Slack Engineering. It's designed for small to medium systems (what we
call "Ring 0 and Ring 1") -- not heavy-compliance or banking-scale
applications.
## How it works
When you run `./master`, the following happens:
1. The Go master process starts and watches your TypeScript source files.
2. It builds the backend using `@vercel/ncc`, producing a single bundled JS
file.
3. It starts one or more Node.js worker processes running your Express app.
4. It proxies HTTP requests from port 8080 to the workers.
5. When you edit a source file, it rebuilds and restarts the workers
automatically.
6. If a worker crashes, it restarts automatically.
There is no separate "dev server" or "hot module replacement." The master
process is the only way to run the application.
## Project structure
A diachron project looks like this:
```
.
├── DIACHRON.md # This file (framework overview for newcomers)
├── master/ # Go master process (framework-owned)
├── logger/ # Go logging service (framework-owned)
├── diachron/ # Managed binaries, shims, framework library
│ ├── AGENTS.md # Guide for AI coding agents
│ ├── binaries/ # Downloaded Node.js, pnpm (gitignored)
│ ├── cmd.d/ # Commands available via ./cmd
│ ├── shims/ # Wrappers that use managed binaries
│ └── ...
├── backend/ # Your application code
│ ├── app.ts # Entry point
│ ├── routes.ts # Route definitions
│ ├── handlers.ts # Route handlers
│ ├── services.ts # Service layer
│ ├── types.ts # Application types
│ ├── config.ts # Application configuration
│ └── diachron/ # Framework library code (framework-owned)
├── cmd # Run managed commands (./cmd pnpm install, etc.)
├── develop # Development-only commands (./develop reset-db, etc.)
├── mgmt # Management commands safe for production
├── sync.sh # Install/update all dependencies
├── master # The compiled master binary (after sync)
└── docker-compose.yml
```
### File ownership
There are two owners of files in a diachron project:
- **You own** everything in `backend/` (except `backend/diachron/`), plus
`docker-compose.yml`, `package.json`, and anything else you create.
- **The framework owns** `master/`, `logger/`, `diachron/`,
`backend/diachron/`, and the top-level scripts (`cmd`, `develop`, `mgmt`,
`sync.sh`, `check.sh`).
Don't modify framework-owned files unless you need to. This separation
keeps framework upgrades clean. If you do need to change framework files
(especially early on, there are rough edges), you can extract your changes
as a patch:
```bash
./diff-upstream.sh # full diff against upstream
./diff-upstream.sh --stat # just list changed files
```
This diffs every file in `file-list` against the upstream ref recorded in
`.diachron-version`.
When you do change framework files, make each change in its own commit with
a clear message explaining what the change is and why it's needed. Mixing
framework fixes with application work in a single commit makes it much
harder to upstream later. A clean history of discrete, well-explained
framework commits is the easiest thing to turn into contributions.
## Getting started
```bash
# Install dependencies and build the master process
./sync.sh
# Start the application
./master
```
The app will be available at `http://localhost:8080`.
You need Linux or macOS on x86_64. For the full stack (database, Redis,
etc.), you also need `docker compose`.
## The command system
diachron has three types of commands, separated by intent and safety:
- **`./cmd <command>`** -- Run managed tools (node, pnpm, tsx, etc.). These
use the project's pinned versions, not whatever is installed on your system.
```bash
./cmd pnpm install
./cmd pnpm test
```
- **`./mgmt <command>`** -- Management commands that are safe to run in
production. Migrations, user management, that sort of thing.
```bash
./mgmt migrate
./mgmt add-user
```
- **`./develop <command>`** -- Development commands that may be destructive.
Database resets, fixture loading, etc. These are gated in production.
```bash
./develop reset-db
./develop db # Open a database shell
```
The rule of thumb: if you'd run it at 3am while tired and worried, it's a
`mgmt` command. If it destroys data on purpose, it's a `develop` command.
## Key concepts
### Call and Result
diachron wraps Express's `Request` and `Response` in its own types called
`Call` and `Result`. This avoids shadowing and keeps the framework's
interface distinct from Express internals. Your handlers receive a `Call`
and return a `Result`.
### Routes
Routes are defined as data (arrays of `Route` objects in `routes.ts`), not
through decorators or method chaining. The framework processes them into
Express handlers.
### No environment variables for behavior
There is no `NODE_ENV`, no `DEBUG`, no mode switching. Configuration that
must vary between deployments (database URLs, secrets) lives in
configuration files, but the application's behavior doesn't branch on
environment.
## Further reading
- `README.md` -- Project introduction and requirements
- `diachron/AGENTS.md` -- Guide for AI coding agents
- `docs/` -- Design documents and philosophy
- `docs/commands.md` -- Detailed explanation of the command system
- `docs/concentric-circles.md` -- What diachron is (and isn't) designed for

View File

@@ -49,16 +49,6 @@ enough libc to run golang binaries.
To run a more complete system, you also need to have docker compose installed. To run a more complete system, you also need to have docker compose installed.
### Database
To connect to the database, you need psql (PostgreSQL client, for
`./diachron/common.d/db`)
- macOS: `brew install libpq` (and follow the caveat to add it to your PATH),
or `brew install postgresql`
- Debian/Ubuntu: `apt install postgresql-client`
- Fedora/RHEL: `dnf install postgresql`
### Development requirements ### Development requirements
To hack on diachron itself, you need the following: To hack on diachron itself, you need the following:

View File

@@ -56,13 +56,6 @@ CREATE TABLE app.customer_metadata (...);
leaves around `master-bin`, `logger-bin`, and `diachron:nnnn` processes. leaves around `master-bin`, `logger-bin`, and `diachron:nnnn` processes.
Huge problem. Huge problem.
- [ ] Fix format used by master (and logger?)'s output: it should be logfmt
- A lot of other stuff should probably be logfmt too but maybe we can get to
that later
- [ ] master rebuilds (or tries to) too many times; need some sort of debounce
or whatever it's called
## medium importance ## medium importance
- [ ] Add a log viewer - [ ] Add a log viewer

View File

@@ -1 +0,0 @@
shamefully-hoist=true

View File

@@ -1,20 +1,172 @@
// This is a sample file provided by diachron. You are encouraged to modify it. import express, {
type Request as ExpressRequest,
type Response as ExpressResponse,
} from "express";
import { match } from "path-to-regexp";
import { Session } from "./diachron/auth";
import { cli } from "./diachron/cli";
import { contentTypes } from "./diachron/content-types";
import { runWithContext } from "./diachron/context";
import { core } from "./diachron/core"; import { core } from "./diachron/core";
import { httpCodes } from "./diachron/http-codes";
import { request } from "./diachron/request";
// import { URLPattern } from 'node:url';
import {
AuthenticationRequired,
AuthorizationDenied,
type Call,
type InternalHandler,
isRedirect,
type Method,
massageMethod,
methodParser,
type ProcessedRoute,
type Result,
type Route,
} from "./diachron/types";
import { routes } from "./routes"; import { routes } from "./routes";
import {makeApp}from'./diachron/app'
const app = express();
const app = makeApp({routes}); // Parse request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
core.logging.log({ source: "logging", text: ["1"] }); core.logging.log({ source: "logging", text: ["1"] });
const processedRoutes: { [K in Method]: ProcessedRoute[] } = {
GET: [],
POST: [],
PUT: [],
PATCH: [],
DELETE: [],
};
function _isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return typeof (value as any)?.then === "function";
}
routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
// const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path });
const matcher = match<Record<string, string>>(route.path);
const methodList = route.methods;
const handler: InternalHandler = async (
expressRequest: ExpressRequest,
): Promise<Result> => {
const method = massageMethod(expressRequest.method);
app.start() console.log("method", method);
if (!methodList.includes(method)) {
// XXX: Worth asserting this?
}
console.log("request.originalUrl", expressRequest.originalUrl);
// Authenticate the request
const auth = await request.auth.validateRequest(expressRequest);
const req: Call = {
pattern: route.path,
path: expressRequest.originalUrl,
method,
parameters: { one: 1, two: 2 },
request: expressRequest,
user: auth.user,
session: new Session(auth.session, auth.user),
};
try {
const retval = await runWithContext({ user: auth.user }, () =>
route.handler(req),
);
return retval;
} catch (error) {
// Handle authentication errors
if (error instanceof AuthenticationRequired) {
return {
code: httpCodes.clientErrors.Unauthorized,
contentType: contentTypes.application.json,
result: JSON.stringify({
error: "Authentication required",
}),
};
}
if (error instanceof AuthorizationDenied) {
return {
code: httpCodes.clientErrors.Forbidden,
contentType: contentTypes.application.json,
result: JSON.stringify({ error: "Access denied" }),
};
}
throw error;
}
};
for (const [_idx, method] of methodList.entries()) {
const pr: ProcessedRoute = { matcher, method, handler };
processedRoutes[method].push(pr);
}
});
async function handler(
req: ExpressRequest,
_res: ExpressResponse,
): Promise<Result> {
const method = await methodParser.parseAsync(req.method);
const byMethod = processedRoutes[method];
console.log(
"DEBUG: req.path =",
JSON.stringify(req.path),
"method =",
method,
);
for (const [_idx, pr] of byMethod.entries()) {
const match = pr.matcher(req.path);
console.log("DEBUG: trying pattern, match result =", match);
if (match) {
console.log("match", match);
const resp = await pr.handler(req);
return resp;
}
}
const retval: Result = {
code: httpCodes.clientErrors.NotFound,
contentType: contentTypes.text.plain,
result: "not found!",
};
return retval;
}
app.use(async (req: ExpressRequest, res: ExpressResponse) => {
const result0 = await handler(req, res);
const code = result0.code.code;
const result = result0.result;
console.log(result);
// Set any cookies from the result
if (result0.cookies) {
for (const cookie of result0.cookies) {
res.cookie(cookie.name, cookie.value, cookie.options ?? {});
}
}
if (isRedirect(result0)) {
res.redirect(code, result0.redirect);
} else {
res.status(code).send(result);
}
});
process.title = `diachron:${cli.listen.port}`;
app.listen(cli.listen.port, cli.listen.host, () => {
console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`);
});

View File

@@ -6,4 +6,4 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR" cd "$DIR"
../cmd pnpm ncc build ./app.ts -o dist --source-map ../cmd pnpm ncc build ./app.ts -o dist

View File

@@ -1,66 +0,0 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface PackageJson {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}
function readPackageJson(path: string): PackageJson {
const content = readFileSync(path, "utf-8");
return JSON.parse(content);
}
function getAllDependencyNames(pkg: PackageJson): Set<string> {
const names = new Set<string>();
for (const name of Object.keys(pkg.dependencies ?? {})) {
names.add(name);
}
for (const name of Object.keys(pkg.devDependencies ?? {})) {
names.add(name);
}
return names;
}
const diachronPkgPath = join(__dirname, "diachron", "package.json");
const backendPkgPath = join(__dirname, "package.json");
const diachronPkg = readPackageJson(diachronPkgPath);
const backendPkg = readPackageJson(backendPkgPath);
const diachronDeps = getAllDependencyNames(diachronPkg);
const backendDeps = getAllDependencyNames(backendPkg);
const duplicates: string[] = [];
for (const dep of diachronDeps) {
if (backendDeps.has(dep)) {
duplicates.push(dep);
}
}
if (duplicates.length > 0) {
console.error("Error: Duplicate dependencies found.");
console.error("");
console.error(
"The following dependencies exist in both backend/package.json and backend/diachron/package.json:",
);
console.error("");
for (const dep of duplicates.sort()) {
console.error(` - ${dep}`);
}
console.error("");
console.error(
"Dependencies in backend/diachron/package.json are provided by the framework",
);
console.error(
"and must not be duplicated in backend/package.json. Remove them from",
);
console.error("backend/package.json to fix this error.");
process.exit(1);
}
console.log("No duplicate dependencies found.");

View File

@@ -11,5 +11,4 @@ out_dir="$check_dir/out"
source "$check_dir"/../diachron/shims/common source "$check_dir"/../diachron/shims/common
source "$check_dir"/../diachron/shims/node.common source "$check_dir"/../diachron/shims/node.common
$ROOT/cmd tsx "$check_dir/check-deps.ts"
$ROOT/cmd pnpm tsc --outDir "$out_dir" $ROOT/cmd pnpm tsc --outDir "$out_dir"

View File

@@ -1,137 +0,0 @@
// FIXME: rename this to make-app.ts and adjust imports accordingly
import{contentTypes} from './content-types'
import{httpCodes}from'./http-codes'
import express, {
type Express,
type NextFunction,
type Request as ExpressRequest,
type Response as ExpressResponse,
} from "express";
import { formatError, formatErrorHtml } from "./errors";
import {isRedirect, InternalHandler, AuthenticationRequired,
AuthorizationDenied, Call,type Method, type ProcessedRoute,methodParser, type Result, type Route,massageMethod } from "./types";
import { cli } from "./cli";
import{processRoutes}from'./routing'
process.on('uncaughtException', (err) => {
console.error(formatError(err));
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(formatError(reason));
});
type MakeAppArgs={routes:Route[],
processTitle?: string,
}
export interface DiachronApp extends Express {
start: () => void
}
const makeApp = ({routes, processTitle}: MakeAppArgs) => {
if (process.title) {
process.title = `diachron:${cli.listen.port}`;
}
const processedRoutes = processRoutes(routes)
async function handler(
req: ExpressRequest,
_res: ExpressResponse,
): Promise<Result> {
const method = await methodParser.parseAsync(req.method);
const byMethod = processedRoutes[method];
console.log(
"DEBUG: req.path =",
JSON.stringify(req.path),
"method =",
method,
);
for (const [_idx, pr] of byMethod.entries()) {
const match = pr.matcher(req.path);
console.log("DEBUG: trying pattern, match result =", match);
if (match) {
console.log("match", match);
const resp = await pr.handler(req, match.params);
return resp;
}
}
const retval: Result = {
code: httpCodes.clientErrors.NotFound,
contentType: contentTypes.text.plain,
result: "not found!",
};
return retval;
}
// I don't like going around tsc but this is simple enough that it's probably OK.
const app = express() as DiachronApp
app.start = function() {
this.listen(cli.listen.port, cli.listen.host, () => {
console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`);
});
};
app.use(express.json())
app.use(express.urlencoded({ extended: true }));
app.use(async (req: ExpressRequest, res: ExpressResponse) => {
const result0 = await handler(req, res);
const code = result0.code.code;
const result = result0.result;
console.log(result);
// Set any cookies from the result
if (result0.cookies) {
for (const cookie of result0.cookies) {
res.cookie(cookie.name, cookie.value, cookie.options ?? {});
}
}
if (isRedirect(result0)) {
res.redirect(code, result0.redirect);
} else {
res.status(code).send(result);
}
});
app.use(
(
err: Error,
_req: ExpressRequest,
res: ExpressResponse,
_next: NextFunction,
) => {
console.error(formatError(err));
res.status(500).type("html").send(formatErrorHtml(err));
},
);
return app;
}
export{makeApp};
function _isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return typeof (value as any)?.then === "function";
}

View File

@@ -10,16 +10,9 @@ const routes: Record<string, Route> = {
hello: { hello: {
path: "/hello", path: "/hello",
methods: ["GET"], methods: ["GET"],
handler: async (call: Call): Promise<Result> => { handler: async (_call: Call): Promise<Result> => {
const now = DateTime.now(); const now = DateTime.now();
const args: {now: DateTime,greeting?: string} = {now}; const c = await render("basic/hello", { now });
if (call.path !== '/hello') {
const greeting = call. path.replaceAll('/','').replaceAll('-', ' ')
args.greeting = greeting;
}
const c = await render("basic/hello", args);
return html(c); return html(c);
}, },

View File

@@ -1,6 +1,5 @@
import nunjucks from "nunjucks"; import nunjucks from "nunjucks";
import { db, migrate, migrationStatus } from "../database"; import { db, migrate, migrationStatus } from "../database";
import { formatError, formatErrorHtml } from "../errors";
import { getLogs, log } from "../logging"; import { getLogs, log } from "../logging";
// FIXME: This doesn't belong here; move it somewhere else. // FIXME: This doesn't belong here; move it somewhere else.
@@ -41,7 +40,6 @@ const misc = {
const core = { const core = {
conf, conf,
database, database,
errors: { formatError, formatErrorHtml },
logging, logging,
misc, misc,
random, random,

View File

@@ -1,229 +0,0 @@
// ANSI escape codes
const bold = "\x1b[1m";
const red = "\x1b[31m";
const cyan = "\x1b[36m";
const dim = "\x1b[2m";
const reset = "\x1b[0m";
interface ParsedFrame {
raw: string;
fn: string;
file: string;
line: string;
col: string;
isApp: boolean;
}
const frameRe = /^\s*at\s+(?:(.+?)\s+)?\(?((?:\/|[a-zA-Z]:\\).+?):(\d+):(\d+)\)?$/;
function parseFrame(line: string): ParsedFrame | null {
const m = line.match(frameRe);
if (!m) return null;
const fn = m[1] ?? "<anonymous>";
const file = m[2];
const lineNum = m[3];
const col = m[4];
const isApp =
!file.includes("node_modules") && !file.startsWith("node:");
return { raw: line, fn, file, line: lineNum, col, isApp };
}
function relativePath(absPath: string): string {
const marker = "backend/";
const idx = absPath.lastIndexOf(marker);
if (idx !== -1) return absPath.slice(idx);
return absPath;
}
function libraryName(file: string): string {
const nmIdx = file.indexOf("node_modules/");
if (nmIdx === -1) return "node";
const after = file.slice(nmIdx + "node_modules/".length);
// Handle scoped packages like @scope/pkg
if (after.startsWith("@")) {
const parts = after.split("/");
return `${parts[0]}/${parts[1]}`;
}
return after.split("/")[0];
}
interface ParsedError {
message: string;
frames: ParsedFrame[];
}
function parseError(error: unknown): ParsedError {
if (!(error instanceof Error)) {
return { message: String(error), frames: [] };
}
const message = error.message ?? String(error);
const stack = error.stack ?? "";
const lines = stack.split("\n");
const frameLines: string[] = [];
for (const line of lines) {
if (line.trimStart().startsWith("at ")) {
frameLines.push(line);
}
}
const frames = frameLines
.map(parseFrame)
.filter((f): f is ParsedFrame => f !== null);
return { message, frames };
}
// Group consecutive library frames into collapsed runs
type FrameGroup =
| { kind: "app"; frame: ParsedFrame }
| { kind: "lib"; count: number; names: string[] };
function groupFrames(frames: ParsedFrame[]): FrameGroup[] {
const groups: FrameGroup[] = [];
let i = 0;
while (i < frames.length) {
if (frames[i].isApp) {
groups.push({ kind: "app", frame: frames[i] });
i++;
} else {
const libNames = new Set<string>();
let count = 0;
while (i < frames.length && !frames[i].isApp) {
libNames.add(libraryName(frames[i].file));
count++;
i++;
}
groups.push({ kind: "lib", count, names: [...libNames] });
}
}
return groups;
}
function libSummary(count: number, names: string[]): string {
const s = count === 1 ? "" : "s";
return `... ${count} internal frame${s} (${names.join(", ")})`;
}
// --- Console formatting (ANSI) ---
function formatError(error: unknown): string {
const { message, frames } = parseError(error);
if (frames.length === 0) {
return `${bold}${red}ERROR${reset} ${message}`;
}
const parts: string[] = [];
parts.push(`${bold}${red}ERROR${reset} ${message}`);
parts.push("");
for (const group of groupFrames(frames)) {
if (group.kind === "app") {
const rel = relativePath(group.frame.file);
const loc = `${rel}:${group.frame.line}`;
parts.push(
` ${bold}${cyan}${loc.padEnd(24)}${reset}at ${group.frame.fn}`,
);
} else {
parts.push(
` ${dim}${libSummary(group.count, group.names)}${reset}`,
);
}
}
return parts.join("\n");
}
// --- HTML formatting (browser) ---
function esc(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatErrorHtml(error: unknown): string {
const { message, frames } = parseError(error);
const groups = groupFrames(frames);
let frameRows = "";
for (const group of groups) {
if (group.kind === "app") {
const rel = relativePath(group.frame.file);
const loc = `${rel}:${group.frame.line}`;
frameRows += `<tr class="app">
<td class="loc">${esc(loc)}</td>
<td class="fn">at ${esc(group.frame.fn)}</td>
</tr>\n`;
} else {
frameRows += `<tr class="lib">
<td colspan="2">${esc(libSummary(group.count, group.names))}</td>
</tr>\n`;
}
}
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Error</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 14px;
background: #1a1a2e;
color: #e0e0e0;
padding: 40px;
}
.error-label {
display: inline-block;
background: #e74c3c;
color: #fff;
font-weight: 700;
font-size: 12px;
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.5px;
}
.message {
margin-top: 12px;
font-size: 18px;
font-weight: 600;
color: #f8f8f2;
line-height: 1.4;
}
table {
margin-top: 24px;
border-collapse: collapse;
}
tr.app td { padding: 4px 0; }
tr.app .loc {
color: #56d4dd;
font-weight: 600;
padding-right: 24px;
white-space: nowrap;
}
tr.app .fn { color: #ccc; }
tr.lib td {
color: #666;
padding: 4px 0;
font-style: italic;
}
</style>
</head>
<body>
<span class="error-label">ERROR</span>
<div class="message">${esc(message)}</div>
<table>${frameRows}</table>
</body>
</html>`;
}
export { formatError, formatErrorHtml };

View File

@@ -4,12 +4,12 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "node:test"; import { describe, it } from "node:test";
import type { Request as ExpressRequest } from "express"; import type { Request as ExpressRequest } from "express";
import { Session } from "./diachron/auth/types"; import { Session } from "./auth/types";
import { contentTypes } from "./diachron/content-types"; import { contentTypes } from "./content-types";
import { multiHandler } from "./handlers"; import { multiHandler } from "./handlers";
import { httpCodes } from "./diachron/http-codes"; import { httpCodes } from "./http-codes";
import type { Call } from "./diachron/types"; import type { Call } from "./types";
import { anonymousUser } from "./diachron/user"; import { anonymousUser } from "./user";
// Helper to create a minimal mock Call // Helper to create a minimal mock Call
function createMockCall(overrides: Partial<Call> = {}): Call { function createMockCall(overrides: Partial<Call> = {}): Call {

View File

@@ -1,9 +1,7 @@
// This is a sample file provided by diachron. You are encouraged to modify it. import { contentTypes } from "./content-types";
import { core } from "./core";
import { contentTypes } from "./diachron/content-types"; import { httpCodes } from "./http-codes";
import { core } from "./diachron/core"; import type { Call, Handler, Result } from "./types";
import { httpCodes } from "./diachron/http-codes";
import type { Call, Handler, Result } from "./diachron/types";
const multiHandler: Handler = async (call: Call): Promise<Result> => { const multiHandler: Handler = async (call: Call): Promise<Result> => {
const code = httpCodes.success.OK; const code = httpCodes.success.OK;

View File

@@ -1,5 +1,5 @@
{ {
"name": "diachron", "name": "express",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",

View File

@@ -1,94 +0,0 @@
import { contentTypes } from "./content-types";
import { httpCodes } from "./http-codes";
import express, {
type NextFunction,
type Request as ExpressRequest,
type Response as ExpressResponse,
} from "express";
import {isRedirect, InternalHandler, AuthenticationRequired,
AuthorizationDenied, Call,type Method, type ProcessedRoute,methodParser, type Result, type Route,massageMethod } from "./types";
import { runWithContext } from "./context";
import { Session } from "./auth";import { request } from "./request";
import { match } from "path-to-regexp";
type ProcessedRoutes= {[K in Method]: ProcessedRoute[] }
const processRoutes=(routes:Route[]) :ProcessedRoutes => {
const retval:ProcessedRoutes= {
GET: [],
POST: [],
PUT: [],
PATCH: [],
DELETE: [],
};
routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
// const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path });
const matcher = match<Record<string, string>>(route.path);
const methodList = route.methods;
const handler: InternalHandler = async (
expressRequest: ExpressRequest,
params: Record<string, string>,
): Promise<Result> => {
const method = massageMethod(expressRequest.method);
console.log("method", method);
if (!methodList.includes(method)) {
// XXX: Worth asserting this?
}
console.log("request.originalUrl", expressRequest.originalUrl);
// Authenticate the request
const auth = await request.auth.validateRequest(expressRequest);
const req: Call = {
pattern: route.path,
path: expressRequest.originalUrl,
method,
parameters: params,
request: expressRequest,
user: auth.user,
session: new Session(auth.session, auth.user),
};
try {
const retval = await runWithContext({ user: auth.user }, () =>
route.handler(req),
);
return retval;
} catch (error) {
// Handle authentication errors
if (error instanceof AuthenticationRequired) {
return {
code: httpCodes.clientErrors.Unauthorized,
contentType: contentTypes.application.json,
result: JSON.stringify({
error: "Authentication required",
}),
};
}
if (error instanceof AuthorizationDenied) {
return {
code: httpCodes.clientErrors.Forbidden,
contentType: contentTypes.application.json,
result: JSON.stringify({ error: "Access denied" }),
};
}
throw error;
}
};
for (const [_idx, method] of methodList.entries()) {
const pr: ProcessedRoute = { matcher, method, handler };
retval[method].push(pr);
}
});
return retval;
}
export{processRoutes}

View File

@@ -29,13 +29,13 @@ export type Call = {
pattern: string; pattern: string;
path: string; path: string;
method: Method; method: Method;
parameters: Record<string, string>; parameters: object;
request: ExpressRequest; request: ExpressRequest;
user: User; user: User;
session: Session; session: Session;
}; };
export type InternalHandler = (req: ExpressRequest, params: Record<string, string>) => Promise<Result>; export type InternalHandler = (req: ExpressRequest) => Promise<Result>;
export type Handler = (call: Call) => Promise<Result>; export type Handler = (call: Call) => Promise<Result>;
export type ProcessedRoute = { export type ProcessedRoute = {

View File

@@ -14,7 +14,6 @@
"license": "ISC", "license": "ISC",
"packageManager": "pnpm@10.12.4", "packageManager": "pnpm@10.12.4",
"dependencies": { "dependencies": {
"diachron": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
} }

View File

@@ -1,5 +1,3 @@
// This is a sample file provided by diachron. You are encouraged to modify it.
/// <reference lib="dom" /> /// <reference lib="dom" />
import nunjucks from "nunjucks"; import nunjucks from "nunjucks";
@@ -8,7 +6,7 @@ import { authRoutes } from "./diachron/auth/routes";
import { routes as basicRoutes } from "./diachron/basic/routes"; import { routes as basicRoutes } from "./diachron/basic/routes";
import { contentTypes } from "./diachron/content-types"; import { contentTypes } from "./diachron/content-types";
import { core } from "./diachron/core"; import { core } from "./diachron/core";
import { multiHandler } from "./handlers"; import { multiHandler } from "./diachron/handlers";
import { httpCodes } from "./diachron/http-codes"; import { httpCodes } from "./diachron/http-codes";
import type { Call, Result, Route } from "./diachron/types"; import type { Call, Result, Route } from "./diachron/types";
@@ -29,9 +27,6 @@ const routes: Route[] = [
...authRoutes, ...authRoutes,
basicRoutes.home, basicRoutes.home,
basicRoutes.hello, basicRoutes.hello,
{...basicRoutes.hello,
path: "/yo-whats-up"
},
basicRoutes.login, basicRoutes.login,
basicRoutes.logout, basicRoutes.logout,
{ {
@@ -40,7 +35,7 @@ const routes: Route[] = [
handler: async (_call: Call): Promise<Result> => { handler: async (_call: Call): Promise<Result> => {
console.log("starting slow request"); console.log("starting slow request");
await core.misc.sleep(5000); await core.misc.sleep(2);
console.log("finishing slow request"); console.log("finishing slow request");
const retval = okText("that was slow"); const retval = okText("that was slow");
@@ -77,6 +72,7 @@ const routes: Route[] = [
`; `;
const result = nunjucks.renderString(template, { rrr }); const result = nunjucks.renderString(template, { rrr });
const _listing = lr(routes).join(", ");
return { return {
code, code,
result, result,

View File

@@ -6,4 +6,4 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR" cd "$DIR"
exec ../cmd node --enable-source-maps dist/index.js "$@" exec ../cmd node dist/index.js "$@"

View File

@@ -1,15 +0,0 @@
// This is a sample file provided by diachron. You are encouraged to modify it.
// Application services go here. A service encapsulates a capability that
// handlers depend on: database queries, external API calls, business logic
// that doesn't belong in a handler.
//
// The framework provides core services via `core` (from ./diachron/core):
// core.database, core.logging, core.misc, etc. This file is for your
// application's own services.
import { core } from "./diachron/core";
const db = core.database.db;
export { db };

View File

@@ -10,5 +10,5 @@
"types": ["node"], "types": ["node"],
"outDir": "out" "outDir": "out"
}, },
"exclude": ["**/*.spec.ts", "**/*.test.ts", "check-deps.ts"] "exclude": ["**/*.spec.ts", "**/*.test.ts"]
} }

View File

@@ -1,8 +0,0 @@
// This is a sample file provided by diachron. You are encouraged to modify it.
// Application-specific types go here. Framework types (Call, Result, Route,
// Handler, etc.) are defined in ./diachron/types and should be imported from
// there.
//
// This file is for your domain types: the nouns and shapes that are specific
// to your application.

View File

@@ -1,49 +0,0 @@
#!/bin/bash
# shellcheck disable=SC2002
set -eu
set -o pipefail
IFS=$'\n\t'
# print useful message on failure
trap 's=$?; echo >&2 "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
# shellcheck disable=SC2034
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# cd "$DIR"
here="$PWD"
"$DIR/update-cached-repository.sh"
# repository="${2:-https://gitea.philologue.net/philologue/diachron}"
repository="${2:-$HOME/.cache/diachron/v1/repositories/diachron.git}"
ref="${1:-hydrators-kysely}"
echo will bootstrap ref "$ref" of repo "$repository"
into=$(mktemp -d)
cd "$into"
echo I am in $(pwd)
echo I will clone repository "$repository", ref "$ref"
git clone "$repository"
r=$(ls -1)
cd "$r"
echo I am in $(pwd)
git checkout "$ref"
ls
echo working dir: $PWD
# ls backend
# exit 0
tar cvf - $(cat "$PWD/file-list" | grep -v '^#' | sed 's/^?//') | (cd "$here" && tar xf -)
echo "$ref" > "$here/.diachron-version"
echo "Now, run the command ./sync.sh"

View File

@@ -1,214 +0,0 @@
# Working with diachron (Agent Guide)
This document helps AI coding agents work effectively with projects built on
the diachron framework. It covers the conventions, commands, and structures
you need to know.
## Quick orientation
diachron is a TypeScript/Express web framework with a Go master process.
Your application code lives in `backend/`. The framework owns `master/`,
`logger/`, `diachron/`, and `backend/diachron/`.
**Do not modify framework-owned files** unless explicitly asked to work on
the framework itself.
## Running the application
```bash
./sync.sh # Install dependencies (run once, or after pulling changes)
./master # Start the application (watches files, rebuilds, proxies)
```
By default, the app listens on port 8080 (proxy) and workers run on
ports starting at 3000.
## Commands you'll need
All tools (node, pnpm, tsx, etc.) are managed by the framework. Do not
invoke system-installed versions.
```bash
./cmd pnpm install # Install npm packages
./cmd pnpm test # Run tests
./cmd pnpm biome check . # Lint (run from backend/)
./develop db # Open database shell
./develop reset-db # Drop and recreate database
./develop migrate # Run migrations (development)
./mgmt migrate # Run migrations (production-safe)
```
### Formatting and linting
```bash
cd backend && ../cmd pnpm biome check --write .
```
### Building Go code
```bash
cd master && go build
cd logger && go build
```
### Quality checks
```bash
./check.sh # shellcheck + golangci-lint
```
## Application structure
### Where to put code
| What | Where |
|--------------------------|-----------------------------|
| Application entry point | `backend/app.ts` |
| Route definitions | `backend/routes.ts` |
| Route handlers | `backend/handlers.ts` |
| Service layer | `backend/services.ts` |
| Application types | `backend/types.ts` |
| Application config | `backend/config.ts` |
| Database migrations | `backend/migrations/` |
| Framework library code | `backend/diachron/` |
### Types and naming
- HTTP request wrapper: `Call` (not Request)
- HTTP response wrapper: `Result` (not Response)
- Route definitions: arrays of `Route` objects
- Handlers: functions that take a `Call` and return a `Result`
These names are intentional. Use them consistently.
Import framework types from `./diachron/types`:
```typescript
import type { Call, Result, Route, Handler } from "./diachron/types";
```
Application-specific domain types go in `backend/types.ts`.
### Services
Application services go in `backend/services.ts`. Framework services are
accessed through the `core` object:
```typescript
import { core } from "./diachron/core";
core.database.db // Kysely database instance
core.logging.log // Logging
core.misc.sleep // Utilities
```
### Exports
When a TypeScript file exports symbols, they should be listed in
alphabetical order.
## Database
diachron uses PostgreSQL exclusively, accessed through Kysely (type-safe
query builder). There is no ORM.
- Write SQL via Kysely, not raw query strings (unless Kysely can't express
the query)
- Migrations live in `backend/migrations/`
- Run `./develop codegen` after schema changes to regenerate Kysely types
## Key conventions
### No dev/prod distinction
There is no `NODE_ENV`. The application behaves identically everywhere.
Do not introduce environment-based branching.
### Managed tooling
Never reference globally installed `node`, `npm`, `npx`, or `pnpm`.
Always use `./cmd node`, `./cmd pnpm`, etc.
### File ownership boundary
```
You may edit: backend/* (except backend/diachron/)
Do not edit: master/*, logger/*, diachron/*, backend/diachron/*
```
If a task requires framework changes, confirm with the user first.
When framework files are modified, the changes can be extracted as a
diff against upstream with `./diff-upstream.sh` (or `--stat` to list
changed files only).
When committing framework changes, keep them in separate commits from
application code. Each framework commit should have a clear message
explaining what was changed and why. This makes it much easier to
upstream the changes later.
### Command safety tiers
- `./cmd` -- Tool wrappers, always safe
- `./mgmt` -- Production-safe operations (migrations, user management)
- `./develop` -- Destructive operations, development only
Never use `./develop` commands against production data.
## Common tasks
### Add a new route
1. Define the route in `backend/routes.ts` as a `Route` object
2. Implement the handler in `backend/handlers.ts`
3. Add any needed types to `backend/types.ts`
### Add a database migration
1. Create a migration file in `backend/migrations/`
2. Run `./develop migrate` to apply it
3. Run `./develop codegen` to regenerate Kysely types
### Install a package
```bash
cd backend && ../cmd pnpm add <package>
```
### Run a one-off TypeScript script
```bash
./develop tsx backend/path/to/script.ts
```
## file-list
The root `file-list` file is a manifest of all files that ship with the
framework. When you create or delete a file that is part of the project
(not a scratch file or generated output), you must update `file-list` to
match. Keep it sorted alphabetically.
Entries can have a `?` prefix (e.g. `?backend/app.ts`). These are
**sample files** -- starter code that `bootstrap.sh` copies into a new
project but that `upgrade.sh` will not overwrite. Once the user has the
file, it belongs to them. On upgrade, new sample files that don't exist
yet in the project are copied in; existing ones are left untouched.
Unprefixed entries are **framework-owned** and are always replaced on
upgrade. When adding a new file to `file-list`, decide which category
it belongs to:
- Framework-owned (no prefix): infrastructure scripts, framework
library code, build tooling, config that must stay in sync.
- Sample (`?` prefix): application starter code the user is expected
to edit (routes, handlers, services, types, package.json, etc.).
## Things to avoid
- Do not introduce `.env` files or `dotenv` without checking with the
team first. The configuration story is still being decided.
- Do not introduce webpack, vite, or other bundlers. The master process
handles building via `@vercel/ncc`.
- Do not add express middleware directly. Use the framework's route
processing in `backend/diachron/routing.ts`.
- Do not use `npm` or globally installed `pnpm`. Use `./cmd pnpm`.
- Do not add `NODE_ENV` checks or development/production branches.

View File

@@ -1,11 +0,0 @@
#!/bin/bash
set -eu
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$DIR/../.."
cd "$ROOT/backend/diachron"
"$ROOT/cmd" tsx check-deps.ts "$@"

View File

@@ -5,5 +5,5 @@ set -eu
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$DIR/../.." ROOT="$DIR/../.."
cd "$ROOT/backend/diachron" cd "$ROOT/backend"
"$DIR"/tsx migrate.ts "$@" "$DIR"/tsx migrate.ts "$@"

View File

@@ -1 +0,0 @@
../common.d/check-deps

View File

@@ -1,5 +0,0 @@
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$DIR"/../shims/pnpm tsx "$@"

View File

@@ -5,5 +5,5 @@ set -eu
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$DIR/../.." ROOT="$DIR/../.."
cd "$ROOT/backend/diachron" cd "$ROOT/backend"
"$DIR"/../cmd.d/tsx mgmt/add-user.ts "$@" "$DIR"/../cmd.d/tsx mgmt/add-user.ts "$@"

View File

@@ -1 +0,0 @@
../common.d/check-deps

View File

@@ -1,5 +0,0 @@
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$DIR"/../shims/pnpm tsx "$@"

View File

@@ -1,116 +0,0 @@
#!/bin/bash
# Generate a diff of framework files against the upstream version this
# project is based on. Useful for contributing changes back to diachron.
#
# Usage:
# ./diff-upstream.sh # diff against .diachron-version
# ./diff-upstream.sh <ref> # diff against a specific ref
# ./diff-upstream.sh --stat # show changed files only
set -eu
set -o pipefail
IFS=$'\n\t'
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
stat_only=false
ref=""
for arg in "$@"; do
case "$arg" in
--stat) stat_only=true ;;
*) ref="$arg" ;;
esac
done
if [ -z "$ref" ]; then
if [ ! -f "$DIR/.diachron-version" ]; then
echo "Error: .diachron-version not found and no ref specified." >&2
echo "Usage: $0 [--stat] [<ref>]" >&2
exit 1
fi
ref=$(cat "$DIR/.diachron-version")
fi
cached_repo="$HOME/.cache/diachron/v1/repositories/diachron.git"
if [ ! -d "$cached_repo" ]; then
echo "Error: cached repository not found at $cached_repo" >&2
echo "Run ./update-cached-repository.sh first." >&2
exit 1
fi
# Update cached repo
"$DIR/update-cached-repository.sh"
# Verify ref exists
if ! git -C "$cached_repo" rev-parse --verify "$ref^{commit}" >/dev/null 2>&1; then
echo "Error: ref '$ref' not found in cached repository." >&2
exit 1
fi
# Read file-list (strip ? prefix from sample entries)
files=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
files+=("${line#\?}")
done < "$DIR/file-list"
# Check out upstream into a temp directory
tmpdir=$(mktemp -d)
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
git clone --quiet "$cached_repo" "$tmpdir/upstream"
git -C "$tmpdir/upstream" checkout --quiet "$ref"
# Generate diff
if $stat_only; then
diff -rq "$tmpdir/upstream" "$DIR" \
--no-dereference \
2>/dev/null \
| grep -v '^\.' \
|| true
# Simpler: just list files that differ
for f in "${files[@]}"; do
# Skip directories
[ -d "$DIR/$f" ] && continue
upstream="$tmpdir/upstream/$f"
local="$DIR/$f"
if [ ! -f "$upstream" ] && [ -f "$local" ]; then
echo "added: $f"
elif [ -f "$upstream" ] && [ ! -f "$local" ]; then
echo "removed: $f"
elif [ -f "$upstream" ] && [ -f "$local" ]; then
if ! diff -q "$upstream" "$local" >/dev/null 2>&1; then
echo "modified: $f"
fi
fi
done
else
for f in "${files[@]}"; do
[ -d "$DIR/$f" ] && continue
upstream="$tmpdir/upstream/$f"
local="$DIR/$f"
if [ ! -f "$upstream" ] && [ -f "$local" ]; then
diff -u /dev/null "$local" \
--label "a/$f" --label "b/$f" \
|| true
elif [ -f "$upstream" ] && [ ! -f "$local" ]; then
diff -u "$upstream" /dev/null \
--label "a/$f" --label "b/$f" \
|| true
elif [ -f "$upstream" ] && [ -f "$local" ]; then
diff -u "$upstream" "$local" \
--label "a/$f" --label "b/$f" \
|| true
fi
done
fi

View File

@@ -1,4 +1,4 @@
diachron uses `Call` and `Result` for its own types that wrap `Request` and We use `Call` and `Result` for our own types that wrap `Request` and
`Response`. `Response`.
This hopefully will make things less confusing and avoid problems with shadowing. This hopefully will make things less confusing and avoid problems with shadowing.

View File

@@ -1,46 +1,14 @@
# please keep this file sorted alphabetically # please keep this file sorted alphabetically
#
# Files prefixed with ? are sample/starter files. bootstrap.sh copies them
# into a new project, but upgrade.sh will not overwrite them if the user has
# already modified or replaced them. Unprefixed files are framework-owned
# and are always replaced on upgrade.
.gitignore
.go-version
DIACHRON.md
backend/.gitignore
backend/.npmrc
?backend/app.ts
backend/build.sh
backend/check-deps.ts
backend/check.sh
backend/diachron backend/diachron
backend/generated backend/package.json
?backend/group.ts
?backend/handlers.spec.ts
?backend/handlers.ts
?backend/package.json
backend/pnpm-workspace.yaml backend/pnpm-workspace.yaml
?backend/routes.ts # express/framework
backend/run.sh
?backend/services.ts
backend/show-config.sh
backend/tsconfig.json
?backend/types.ts
backend/watch.sh
bootstrap.sh
cmd cmd
develop develop
diff-upstream.sh
diachron diachron
diachron/AGENTS.md
file-list
logger logger
master master
mgmt mgmt
sync.sh sync.sh
templates templates
update-cached-repository.sh
upgrade.sh

64
sync.sh
View File

@@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
# Note: This is kind of AI slop and needs to be more carefully reviewed.
set -eu set -eu
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -23,66 +25,50 @@ pnpm_checksum_var="pnpm_checksum_${platform}"
pnpm_binary_url="${!pnpm_binary_var}" pnpm_binary_url="${!pnpm_binary_var}"
pnpm_checksum="${!pnpm_checksum_var}" pnpm_checksum="${!pnpm_checksum_var}"
cache_dir="$HOME/.cache/diachron/v1/binaries" # Set up paths for shims to use
local_dir="$DIR/diachron/binaries" nodejs_dist_dir="diachron/binaries/$nodejs_dirname"
mkdir -p "$cache_dir" "$local_dir" nodejs_bin_dir="$nodejs_dist_dir/bin"
# read_checksum_file <path> # Ensure correct node version is installed
# Prints the contents of a checksum file, or empty string node_installed_checksum_file="$DIR/diachron/binaries/.node.checksum"
# if the file does not exist. node_installed_checksum=""
read_checksum_file() { if [ -f "$node_installed_checksum_file" ]; then
if [ -f "$1" ]; then node_installed_checksum=$(cat "$node_installed_checksum_file")
cat "$1"
fi fi
}
# Ensure Node.js is in the cache if [ "$node_installed_checksum" != "$nodejs_checksum" ]; then
cached_node_checksum=$(read_checksum_file "$cache_dir/.node.checksum")
if [ "$cached_node_checksum" != "$nodejs_checksum" ]; then
echo "Downloading Node.js for $platform..." echo "Downloading Node.js for $platform..."
node_archive="$cache_dir/node.tar.xz" node_archive="$DIR/diachron/downloads/node.tar.xz"
curl -fsSL "$nodejs_binary" -o "$node_archive" curl -fsSL "$nodejs_binary" -o "$node_archive"
echo "Verifying checksum..." echo "Verifying checksum..."
echo "$nodejs_checksum $node_archive" | sha256_check echo "$nodejs_checksum $node_archive" | sha256_check
echo "Extracting Node.js..." echo "Extracting Node.js..."
rm -rf "${cache_dir:?}/$nodejs_dirname" tar -xf "$node_archive" -C "$DIR/diachron/binaries"
tar -xf "$node_archive" -C "$cache_dir"
rm "$node_archive" rm "$node_archive"
echo "$nodejs_checksum" >"$cache_dir/.node.checksum" echo "$nodejs_checksum" >"$node_installed_checksum_file"
fi fi
# Copy Node.js into the working directory if needed # Ensure correct pnpm version is installed
local_node_checksum=$(read_checksum_file "$local_dir/.node.checksum") pnpm_binary="$DIR/diachron/binaries/pnpm"
if [ "$local_node_checksum" != "$nodejs_checksum" ]; then pnpm_installed_checksum_file="$DIR/diachron/binaries/.pnpm.checksum"
echo "Installing Node.js into project..." pnpm_installed_checksum=""
rm -rf "${local_dir:?}/$nodejs_dirname" if [ -f "$pnpm_installed_checksum_file" ]; then
cp -R "$cache_dir/$nodejs_dirname" "$local_dir/$nodejs_dirname" pnpm_installed_checksum=$(cat "$pnpm_installed_checksum_file")
echo "$nodejs_checksum" >"$local_dir/.node.checksum"
fi fi
# Ensure pnpm is in the cache if [ "$pnpm_installed_checksum" != "$pnpm_checksum" ]; then
cached_pnpm_checksum=$(read_checksum_file "$cache_dir/.pnpm.checksum")
if [ "$cached_pnpm_checksum" != "$pnpm_checksum" ]; then
echo "Downloading pnpm for $platform..." echo "Downloading pnpm for $platform..."
curl -fsSL "$pnpm_binary_url" -o "$cache_dir/pnpm" curl -fsSL "$pnpm_binary_url" -o "$pnpm_binary"
echo "Verifying checksum..." echo "Verifying checksum..."
echo "$pnpm_checksum $cache_dir/pnpm" | sha256_check echo "$pnpm_checksum $pnpm_binary" | sha256_check
chmod +x "$cache_dir/pnpm" chmod +x "$pnpm_binary"
echo "$pnpm_checksum" >"$cache_dir/.pnpm.checksum" echo "$pnpm_checksum" >"$pnpm_installed_checksum_file"
fi
# Copy pnpm into the working directory if needed
local_pnpm_checksum=$(read_checksum_file "$local_dir/.pnpm.checksum")
if [ "$local_pnpm_checksum" != "$pnpm_checksum" ]; then
echo "Installing pnpm into project..."
cp "$cache_dir/pnpm" "$local_dir/pnpm"
echo "$pnpm_checksum" >"$local_dir/.pnpm.checksum"
fi fi
# Get golang binaries in place # Get golang binaries in place

View File

@@ -2,7 +2,7 @@
<head></head> <head></head>
<body> <body>
<p> <p>
{{ greeting | default("hello")}} Hello.
</p> </p>
<p> <p>
The current time is {{ now }}. The current time is {{ now }}.

View File

@@ -1,22 +0,0 @@
#!/bin/bash
set -eu
set -o pipefail
IFS=$'\n\t'
trap 's=$?; echo >&2 "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
upstream=https://gitea.philologue.net/philologue/diachron
cache_dir="$HOME/.cache/diachron/v1/repositories"
cached_repo="$cache_dir/diachron.git"
mkdir -p "$cache_dir"
if [ -d "$cached_repo" ]; then
echo "Updating cached repository..."
git -C "$cached_repo" fetch --prune origin
else
echo "Creating cached repository..."
git clone --mirror "$upstream" "$cached_repo"
fi

View File

@@ -1,151 +0,0 @@
#!/bin/bash
set -eu
set -o pipefail
IFS=$'\n\t'
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
new_ref="${1:?Usage: upgrade.sh <new-ref>}"
cached_repo="$HOME/.cache/diachron/v1/repositories/diachron.git"
tmpdir=""
cleanup() {
if [ -n "$tmpdir" ]; then
rm -rf "$tmpdir"
fi
}
trap cleanup EXIT
echo "=== Diachron Framework Upgrade ==="
echo ""
echo "This will replace all framework files in your project."
echo "Make sure you have committed or backed up any local changes."
echo ""
read -r -p "Continue? [y/N] " answer
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
# Update cached repository
"$DIR/update-cached-repository.sh"
# Read current version
if [ ! -f "$DIR/.diachron-version" ]; then
echo "Error: .diachron-version not found." >&2
echo "Is this a diachron project?" >&2
exit 1
fi
old_ref=$(cat "$DIR/.diachron-version")
# Verify both refs exist in cached repo
if ! git -C "$cached_repo" rev-parse --verify "$old_ref^{commit}" >/dev/null 2>&1; then
echo "Error: current version '$old_ref' not found in cached repository." >&2
exit 1
fi
if ! git -C "$cached_repo" rev-parse --verify "$new_ref^{commit}" >/dev/null 2>&1; then
echo "Error: target version '$new_ref' not found in cached repository." >&2
exit 1
fi
# Require a clean working tree
if [ -n "$(git -C "$DIR" status --porcelain)" ]; then
echo "Error: working tree is not clean." >&2
echo "Commit or stash all changes (including untracked files) before upgrading." >&2
exit 1
fi
echo ""
echo "Upgrading: $old_ref -> $new_ref"
echo ""
# Read current file-list (files to remove)
# Entries prefixed with ? are sample files -- we don't remove those on upgrade.
old_files=()
old_samples=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
if [[ "$line" == \?* ]]; then
old_samples+=("${line#\?}")
else
old_files+=("$line")
fi
done < "$DIR/file-list"
# Clone and checkout new version into a temp directory
tmpdir=$(mktemp -d)
git clone --quiet "$cached_repo" "$tmpdir/diachron"
git -C "$tmpdir/diachron" checkout --quiet "$new_ref"
# Read new file-list (files to add)
new_files=()
new_samples=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
if [[ "$line" == \?* ]]; then
new_samples+=("${line#\?}")
else
new_files+=("$line")
fi
done < "$tmpdir/diachron/file-list"
# Remove old framework files (not samples -- those belong to the user)
for f in "${old_files[@]}"; do
git -C "$DIR" rm -rf --quiet --ignore-unmatch "$f"
done
# Copy in new framework files
(cd "$tmpdir/diachron" && tar cf - "${new_files[@]}") | (cd "$DIR" && tar xf -)
# Stage them
for f in "${new_files[@]}"; do
git -C "$DIR" add "$f"
done
# Handle sample files: copy only if the user doesn't already have them
samples_added=()
samples_skipped=()
for f in "${new_samples[@]}"; do
if [ -e "$DIR/$f" ]; then
samples_skipped+=("$f")
else
# New sample that doesn't exist yet -- copy it in
(cd "$tmpdir/diachron" && tar cf - "$f") | (cd "$DIR" && tar xf -)
git -C "$DIR" add "$f"
samples_added+=("$f")
fi
done
# Update version marker
echo "$new_ref" > "$DIR/.diachron-version"
git -C "$DIR" add "$DIR/.diachron-version"
echo "=== Upgrade staged: $old_ref -> $new_ref ==="
echo ""
echo "Framework files have been removed, replaced, and staged."
if [ ${#samples_added[@]} -gt 0 ]; then
echo ""
echo "New sample files added:"
for f in "${samples_added[@]}"; do
echo " + $f"
done
fi
if [ ${#samples_skipped[@]} -gt 0 ]; then
echo ""
echo "Sample files skipped (you already have these):"
for f in "${samples_skipped[@]}"; do
echo " ~ $f"
done
fi
echo ""
echo "Next steps:"
echo " 1. Review: git diff --cached"
echo " 2. Commit: git commit -m 'Upgrade diachron to $new_ref'"
echo " 3. Install: ./sync.sh"