Compare commits
6 Commits
hydrators-
...
64b1b12379
| Author | SHA1 | Date | |
|---|---|---|---|
| 64b1b12379 | |||
| 07bd447fff | |||
| 9aad5fcb5f | |||
| c2e703d642 | |||
| 0b52b92819 | |||
| ad1b2de806 |
44
AGENTS.md
44
AGENTS.md
@@ -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
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
184
DIACHRON.md
184
DIACHRON.md
@@ -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
|
|
||||||
3
TODO.md
3
TODO.md
@@ -60,9 +60,6 @@ CREATE TABLE app.customer_metadata (...);
|
|||||||
- A lot of other stuff should probably be logfmt too but maybe we can get to
|
- A lot of other stuff should probably be logfmt too but maybe we can get to
|
||||||
that later
|
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
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const makeApp = ({routes, processTitle}: MakeAppArgs) => {
|
|||||||
console.log("DEBUG: trying pattern, match result =", match);
|
console.log("DEBUG: trying pattern, match result =", match);
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log("match", match);
|
console.log("match", match);
|
||||||
const resp = await pr.handler(req, match.params);
|
const resp = await pr.handler(req);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const processRoutes=(routes:Route[]) :ProcessedRoutes => {
|
|||||||
|
|
||||||
const handler: InternalHandler = async (
|
const handler: InternalHandler = async (
|
||||||
expressRequest: ExpressRequest,
|
expressRequest: ExpressRequest,
|
||||||
params: Record<string, string>,
|
|
||||||
): Promise<Result> => {
|
): Promise<Result> => {
|
||||||
const method = massageMethod(expressRequest.method);
|
const method = massageMethod(expressRequest.method);
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ const processRoutes=(routes:Route[]) :ProcessedRoutes => {
|
|||||||
pattern: route.path,
|
pattern: route.path,
|
||||||
path: expressRequest.originalUrl,
|
path: expressRequest.originalUrl,
|
||||||
method,
|
method,
|
||||||
parameters: params,
|
parameters: { one: 1, two: 2 },
|
||||||
request: expressRequest,
|
request: expressRequest,
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
session: new Session(auth.session, auth.user),
|
session: new Session(auth.session, auth.user),
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -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.
|
|
||||||
@@ -42,8 +42,8 @@ echo working dir: $PWD
|
|||||||
|
|
||||||
# exit 0
|
# exit 0
|
||||||
|
|
||||||
tar cvf - $(cat "$PWD/file-list" | grep -v '^#' | sed 's/^?//') | (cd "$here" && tar xf -)
|
tar cvf - $(cat "$PWD/file-list" | grep -v '^#') | (cd "$here" && tar xf -)
|
||||||
|
|
||||||
echo "$ref" > "$here/.diachron-version"
|
echo "$ref" > .diachron-version
|
||||||
|
|
||||||
echo "Now, run the command ./sync.sh"
|
echo "Now, run the command ./sync.sh"
|
||||||
|
|||||||
@@ -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.
|
|
||||||
116
diff-upstream.sh
116
diff-upstream.sh
@@ -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
|
|
||||||
22
file-list
22
file-list
@@ -1,39 +1,29 @@
|
|||||||
# 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
|
.gitignore
|
||||||
.go-version
|
.go-version
|
||||||
DIACHRON.md
|
|
||||||
backend/.gitignore
|
backend/.gitignore
|
||||||
backend/.npmrc
|
backend/.npmrc
|
||||||
?backend/app.ts
|
backend/app.ts
|
||||||
backend/build.sh
|
backend/build.sh
|
||||||
backend/check-deps.ts
|
backend/check-deps.ts
|
||||||
backend/check.sh
|
backend/check.sh
|
||||||
backend/diachron
|
backend/diachron
|
||||||
backend/generated
|
backend/generated
|
||||||
?backend/group.ts
|
backend/group.ts
|
||||||
?backend/handlers.spec.ts
|
backend/handlers.spec.ts
|
||||||
?backend/handlers.ts
|
backend/handlers.ts
|
||||||
?backend/package.json
|
backend/package.json
|
||||||
backend/pnpm-workspace.yaml
|
backend/pnpm-workspace.yaml
|
||||||
?backend/routes.ts
|
backend/routes.ts
|
||||||
backend/run.sh
|
backend/run.sh
|
||||||
?backend/services.ts
|
|
||||||
backend/show-config.sh
|
backend/show-config.sh
|
||||||
backend/tsconfig.json
|
backend/tsconfig.json
|
||||||
?backend/types.ts
|
|
||||||
backend/watch.sh
|
backend/watch.sh
|
||||||
bootstrap.sh
|
bootstrap.sh
|
||||||
cmd
|
cmd
|
||||||
develop
|
develop
|
||||||
diff-upstream.sh
|
|
||||||
diachron
|
diachron
|
||||||
diachron/AGENTS.md
|
|
||||||
file-list
|
file-list
|
||||||
logger
|
logger
|
||||||
master
|
master
|
||||||
|
|||||||
50
upgrade.sh
50
upgrade.sh
@@ -62,17 +62,11 @@ echo "Upgrading: $old_ref -> $new_ref"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Read current file-list (files to remove)
|
# Read current file-list (files to remove)
|
||||||
# Entries prefixed with ? are sample files -- we don't remove those on upgrade.
|
|
||||||
old_files=()
|
old_files=()
|
||||||
old_samples=()
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
[[ -z "$line" ]] && continue
|
[[ -z "$line" ]] && continue
|
||||||
if [[ "$line" == \?* ]]; then
|
old_files+=("$line")
|
||||||
old_samples+=("${line#\?}")
|
|
||||||
else
|
|
||||||
old_files+=("$line")
|
|
||||||
fi
|
|
||||||
done < "$DIR/file-list"
|
done < "$DIR/file-list"
|
||||||
|
|
||||||
# Clone and checkout new version into a temp directory
|
# Clone and checkout new version into a temp directory
|
||||||
@@ -82,44 +76,25 @@ git -C "$tmpdir/diachron" checkout --quiet "$new_ref"
|
|||||||
|
|
||||||
# Read new file-list (files to add)
|
# Read new file-list (files to add)
|
||||||
new_files=()
|
new_files=()
|
||||||
new_samples=()
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
[[ -z "$line" ]] && continue
|
[[ -z "$line" ]] && continue
|
||||||
if [[ "$line" == \?* ]]; then
|
new_files+=("$line")
|
||||||
new_samples+=("${line#\?}")
|
|
||||||
else
|
|
||||||
new_files+=("$line")
|
|
||||||
fi
|
|
||||||
done < "$tmpdir/diachron/file-list"
|
done < "$tmpdir/diachron/file-list"
|
||||||
|
|
||||||
# Remove old framework files (not samples -- those belong to the user)
|
# Remove old framework files
|
||||||
for f in "${old_files[@]}"; do
|
for f in "${old_files[@]}"; do
|
||||||
git -C "$DIR" rm -rf --quiet --ignore-unmatch "$f"
|
git -C "$DIR" rm -rf --quiet --ignore-unmatch "$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Copy in new framework files
|
# Copy in new framework files
|
||||||
(cd "$tmpdir/diachron" && tar cf - "${new_files[@]}") | (cd "$DIR" && tar xf -)
|
(cd "$tmpdir/diachron" && tar cvf - "${new_files[@]}") | (cd "$DIR" && tar xf -)
|
||||||
|
|
||||||
# Stage them
|
# Stage them
|
||||||
for f in "${new_files[@]}"; do
|
for f in "${new_files[@]}"; do
|
||||||
git -C "$DIR" add "$f"
|
git -C "$DIR" add "$f"
|
||||||
done
|
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
|
# Update version marker
|
||||||
echo "$new_ref" > "$DIR/.diachron-version"
|
echo "$new_ref" > "$DIR/.diachron-version"
|
||||||
git -C "$DIR" add "$DIR/.diachron-version"
|
git -C "$DIR" add "$DIR/.diachron-version"
|
||||||
@@ -127,23 +102,6 @@ git -C "$DIR" add "$DIR/.diachron-version"
|
|||||||
echo "=== Upgrade staged: $old_ref -> $new_ref ==="
|
echo "=== Upgrade staged: $old_ref -> $new_ref ==="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Framework files have been removed, replaced, and staged."
|
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 ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Review: git diff --cached"
|
echo " 1. Review: git diff --cached"
|
||||||
|
|||||||
Reference in New Issue
Block a user