chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds the knotless git helper. set it up
// The world the screenshots show: a reference PDS holding the same
// repositories, pulls and issues every time, so each page renders to the
// same pixels. Commits carry fixed dates, so their hashes are fixed too, and
// every record's createdAt is rewritten to a fixed time, in the order the
// records were made. The index a knotless-serve built while it was seeded
// is kept in `dir`, with its mirrors, for the next server to start from.
//
// node harness/shots/world.mjs seed one and keep it up, for
// `just shots` runs to reuse, until
// ctrl-c; written to target/shots/world.json
import { spawn, execFileSync } from 'node:child_process'
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { createInterface } from 'node:readline'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
export const root = resolve(here, '../..')
const fixture = join(here, 'fixture')
const bin = join(root, 'target/debug')
// Where the world answers, and the pages behind it on the next two ports:
// SHOTS_PORT moves them, for a second checkout's shots beside a first's.
export const port = Number(process.env.SHOTS_PORT ?? 2593)
export const base = `http://127.0.0.1:${port}`
export const saved = join(root, 'target/shots/world.json')
const people = {
alice: { name: 'Alice Moreau', email: 'alice@example.com' },
bob: { name: 'Bob Tanaka', email: 'bob@example.com' },
}
export const until = async (what, check) => {
for (let i = 0; i < 300; i++) {
try {
if (await check()) return
} catch {}
await new Promise((r) => setTimeout(r, 100))
}
throw new Error(`timed out waiting for ${what}`)
}
export const page = async (path) => {
const res = await fetch(base + path)
return res.ok ? res.text() : ''
}
/// The environment `who` runs the CLI, git or the server in.
const environment = (net, dir, who, when) => {
const clean = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('KNOTLESS_')))
const person = people[who] ?? { name: who, email: `${who}@example.com` }
return {
...clean,
PATH: `${bin}:${process.env.PATH}`,
KNOTLESS_HANDLE_RESOLVER: net.pds,
KNOTLESS_PLC_URL: net.plc,
KNOTLESS_COMPACT: 'off',
KNOTLESS_APPVIEW: base,
KNOTLESS_CONFIG_DIR: join(dir, who, 'config'),
KNOTLESS_CACHE_DIR: join(dir, who, 'cache'),
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_CONFIG_NOSYSTEM: '1',
GIT_AUTHOR_NAME: person.name,
GIT_AUTHOR_EMAIL: person.email,
GIT_COMMITTER_NAME: person.name,
GIT_COMMITTER_EMAIL: person.email,
GIT_AUTHOR_DATE: when ?? '2026-10-01T09:00:00Z',
GIT_COMMITTER_DATE: when ?? '2026-10-01T09:00:00Z',
}
}
/// The pages, from web/build, answering on `at` and reading from `data`.
export const pages = (at, data) =>
spawn('node', [join(root, 'web/build')], {
env: {
...process.env,
HOST: '127.0.0.1',
PORT: String(at),
KNOTLESS_SERVE: data,
HOST_HEADER: 'x-forwarded-host',
PROTOCOL_HEADER: 'x-forwarded-proto',
},
stdio: 'inherit',
})
/// knotless-serve from this build, over the world's index and mirrors, on
/// `at`: in front of the pages, unless `alone`.
export const serve = async (world, { at = port, alone = false } = {}) => {
const drawn = alone ? null : pages(port + 1, base)
const server = spawn(
join(bin, 'knotless-serve'),
[
'--listen', `127.0.0.1:${at}`,
'--index', join(world.dir, 'index.db'),
'--firehose', world.net.pds.replace('http', 'ws'),
...(alone ? [] : ['--web', `http://127.0.0.1:${port + 1}`]),
],
{ env: environment(world.net, world.dir, 'server'), stdio: 'inherit' },
)
const kill = () => {
server.kill()
drawn?.kill()
}
const up = alone ? `http://127.0.0.1:${at}/healthz` : base
try {
await until('knotless-serve and the pages', async () => (await fetch(up)).ok)
} catch (e) {
kill()
throw e
}
return { kill }
}
/// A new world: its PDS, a server, and the seeded data. Returns them with
/// the pages to shoot.
export async function create() {
const dir = mkdtempSync(join(tmpdir(), 'knotless-shots-'))
const children = []
const stop = () => {
for (const child of children.reverse()) child.kill()
rmSync(dir, { recursive: true, force: true })
}
try {
const pds = spawn('node', ['harness/pds.mjs', '--accounts', 'alice,bob'], {
cwd: root,
stdio: ['pipe', 'pipe', 'inherit'],
})
children.push(pds)
const [line] = await createInterface({ input: pds.stdout })[Symbol.asyncIterator]().next().then((r) => [r.value])
const net = JSON.parse(line)
const account = Object.fromEntries(net.accounts.map((a) => [a.handle.split('.')[0], a]))
const work = dir
const env = (who, when) => environment(net, dir, who, when)
const run = (who, cmd, args, opts = {}) =>
execFileSync(cmd, args, { env: env(who, opts.when), cwd: opts.cwd ?? work, input: opts.input ?? '' })
.toString()
.trim()
const knotless = (who, ...args) => run(who, join(bin, 'knotless'), args)
const git = (who, dir, when, ...args) => run(who, 'git', args, { cwd: dir, when })
/// Copies files from a fixture step into `dir` and commits them.
const commit = (who, dir, when, message, files) => {
for (const [from, to] of Object.entries(files)) {
mkdirSync(dirname(join(dir, to)), { recursive: true })
cpSync(join(fixture, from), join(dir, to))
}
git(who, dir, when, 'add', '-A')
git(who, dir, when, 'commit', '-q', '-m', message)
}
children.push(await serve({ net, dir }))
for (const who of ['alice', 'bob']) {
run(who, join(bin, 'knotless'), ['login', account[who].handle, '--password-stdin'], { input: account[who].password })
}
const tally = 'knotless://alice.test/tally'
knotless('alice', 'create', tally, '--description', 'Counts lines, words and bytes, with output that lines up')
const a = join(work, 'alice/tally')
mkdirSync(a, { recursive: true })
git('alice', a, null, 'init', '-q', '-b', 'main')
commit('alice', a, '2026-10-01T09:12:00Z', 'Start tally', {
'start/Cargo.toml': 'Cargo.toml',
'start/.gitignore': '.gitignore',
'start/README.md': 'README.md',
'start/main.rs': 'src/main.rs',
})
commit('alice', a, '2026-10-02T14:40:00Z', 'Count words, lines and bytes', {
'count/lib.rs': 'src/lib.rs',
'count/main.rs': 'src/main.rs',
})
commit(
'alice',
a,
'2026-10-03T08:05:00Z',
'Read from stdin when no file is given\n\nSo tally works at the end of a pipe, as wc does. The name shown\nfor stdin is "-".',
{ 'stdin/main.rs': 'src/main.rs' },
)
commit('alice', a, '2026-10-05T17:21:00Z', 'Add a logo', { 'logo/logo.svg': 'assets/logo.svg' })
commit('alice', a, '2026-10-06T10:02:00Z', 'Document usage and output', {
'docs/README.md': 'README.md',
'docs/design.md': 'docs/design.md',
})
git('alice', a, null, 'mv', 'docs/design.md', 'docs/DESIGN.md')
commit('alice', a, '2026-10-08T11:47:00Z', 'Split parsing into its own module', {
'parse/parse.rs': 'src/parse.rs',
'parse/lib.rs': 'src/lib.rs',
})
const parse = git('alice', a, null, 'rev-parse', 'HEAD')
git('alice', a, null, 'push', '-q', tally, 'main')
const notes = 'knotless://alice.test/notes'
knotless('alice', 'create', notes, '--description', 'Scratch notes 📝')
const n = join(work, 'alice/notes')
mkdirSync(n, { recursive: true })
git('alice', n, null, 'init', '-q', '-b', 'main')
commit('alice', n, '2026-09-20T19:30:00Z', 'Begin', { 'notes/README.md': 'README.md' })
git('alice', n, null, 'push', '-q', notes, 'main')
knotless('alice', 'create', 'knotless://alice.test/scratch')
const fork = knotless('bob', 'fork', tally)
const b = join(work, 'bob/tally')
git('bob', work, null, 'clone', '-q', fork, b)
git('bob', b, null, 'checkout', '-q', '-b', 'empty-input')
commit('bob', b, '2026-10-09T09:31:00Z', 'Count no lines in empty input', { 'fix/parse.rs': 'src/parse.rs' })
git('bob', b, null, 'push', '-q', 'origin', 'empty-input')
git('bob', b, null, 'checkout', '-q', '-b', 'json', 'main')
commit('bob', b, '2026-10-10T16:12:00Z', 'Print counts as JSON with --json', { 'json/main.rs': 'src/main.rs' })
git('bob', b, null, 'push', '-q', 'origin', 'json')
const pull = knotless(
'bob', 'pull', tally, 'main', fork, 'empty-input', 'Count no lines in empty input',
'-m', '`tally` printed one line for an empty file. `lines` now yields nothing when the text is empty, and a test covers it.',
)
knotless('alice', 'comment', tally, pull, 'Could `lines` check for emptiness once, rather than in the filter?', '--changes')
knotless('bob', 'comment', tally, pull, 'It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.')
knotless('alice', 'comment', tally, pull, 'Fair enough. Thanks!', '--approve')
// Merging and closing read the item from the index, which follows the
// PDS a moment behind.
await until('the pull', async () => (await page('/alice.test/tally/pulls')).includes('Count no lines in empty input'))
knotless('alice', 'merge', tally, pull)
const json = knotless(
'bob', 'pull', tally, 'main', fork, 'json', 'Print counts as JSON with --json',
'-m', 'For scripts. The object has `lines`, `words`, `bytes` and `name`.',
)
const utf8 = knotless(
'bob', 'issue', tally, 'Fails on files that are not UTF-8',
'-m', 'Running `tally image.png` panics:\n\n```\nthread \'main\' panicked at src/main.rs:8:56:\nstream did not contain valid UTF-8\n```\n\nwc counts bytes for any file.',
)
knotless('alice', 'comment', tally, utf8, 'Confirmed. Reading bytes and counting words lossily would fix it.')
const typo = knotless('bob', 'issue', tally, 'Typo in the README ✏️')
await until('the issue', async () => (await page('/alice.test/tally/issues')).includes('Typo in the README'))
knotless('alice', 'close', tally, typo)
// Every record a fixed createdAt, keeping their order.
const records = []
for (const who of ['alice', 'bob']) {
const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ identifier: account[who].handle, password: account[who].password }),
}).then((r) => r.json())
const did = account[who].did
if (who === 'alice') {
const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}` },
body: JSON.stringify({
repo: did,
collection: 'app.bsky.actor.profile',
rkey: 'self',
record: {
$type: 'app.bsky.actor.profile',
displayName: people.alice.name,
description: 'Small tools for text. Writes tally.',
},
}),
})
if (!res.ok) throw new Error(`profile: ${await res.text()}`)
}
const { collections } = await fetch(`${net.pds}/xrpc/com.atproto.repo.describeRepo?repo=${did}`).then((r) => r.json())
for (const collection of collections) {
let cursor = ''
do {
const listed = await fetch(
`${net.pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=100${cursor}`,
).then((r) => r.json())
for (const r of listed.records) {
if (typeof r.value.createdAt === 'string') records.push({ ...r, did, collection, jwt: session.accessJwt })
}
cursor = listed.cursor ? `&cursor=${listed.cursor}` : ''
} while (cursor)
}
}
records.sort((x, y) => (x.value.createdAt < y.value.createdAt ? -1 : x.value.createdAt > y.value.createdAt ? 1 : 0))
const start = Date.parse('2026-10-09T10:00:00Z')
const cids = new Map()
// Strong references to a record whose CID changed are pointed at the new one.
const repoint = (v) => {
if (Array.isArray(v)) return v.map(repoint)
if (v && typeof v === 'object') {
const out = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, repoint(x)]))
if (typeof out.uri === 'string' && typeof out.cid === 'string' && cids.has(out.uri)) out.cid = cids.get(out.uri)
return out
}
return v
}
for (const [i, r] of records.entries()) {
const value = repoint({ ...r.value, createdAt: new Date(start + i * 37 * 60_000).toISOString() })
const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${r.jwt}` },
body: JSON.stringify({ repo: r.did, collection: r.collection, rkey: r.uri.split('/').pop(), record: value }),
})
if (!res.ok) throw new Error(`putRecord ${r.uri}: ${await res.text()}`)
cids.set(r.uri, (await res.json()).cid)
}
const item = (kind, uri) => {
const [, , did, , rkey] = uri.split('/')
return `/alice.test/tally/${kind}/${did}/${rkey}`
}
const views = {
home: '/',
search: '/?q=t',
start: '/start',
profile: '/alice.test',
'profile-fork': '/bob.test',
repository: '/alice.test/tally',
directory: '/alice.test/tally/tree/main/src',
file: '/alice.test/tally/blob/main/src/lib.rs',
markdown: '/alice.test/tally/blob/main/README.md',
image: '/alice.test/tally/blob/main/assets/logo.svg',
commits: '/alice.test/tally/commits/main',
commit: `/alice.test/tally/commit/${parse}`,
pulls: '/alice.test/tally/pulls',
'pulls-closed': '/alice.test/tally/pulls?state=closed',
pull: item('pull', pull),
'pull-open': item('pull', json),
issues: '/alice.test/tally/issues',
'issues-closed': '/alice.test/tally/issues?state=closed',
issue: item('issue', utf8),
'issue-closed': item('issue', typo),
fork: '/bob.test/tally',
empty: '/alice.test/scratch',
missing: '/alice.test/tally/blob/main/nowhere',
}
// The pages whose social cards are shot, by the names of the shots.
const cards = {
home: '/',
repository: '/alice.test/tally',
notes: '/alice.test/notes',
pull: views.pull,
'pull-open': views['pull-open'],
issue: views.issue,
'issue-closed': item('issue', typo),
}
// The index has caught up when no page shows today's date, every
// record now being dated before it, and the merge shows.
const today = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' })
await until('the index', async () => {
const pages = await Promise.all(Object.values(views).map(page))
const all = pages.join('')
return !all.includes(today) && all.includes('Merged as') && all.includes('Forked from') && all.includes('1 Closed')
})
return { net, dir, views, cards, pds, server: children[1], stop }
} catch (e) {
stop()
throw e
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const world = await create()
world.server.kill()
mkdirSync(dirname(saved), { recursive: true })
writeFileSync(saved, JSON.stringify({ net: world.net, dir: world.dir, views: world.views, cards: world.cards }, null, 2))
console.log(`seeded; kept up for \`just shots\` until ctrl-c (${saved})`)
const end = () => {
rmSync(saved, { force: true })
world.stop()
process.exit(0)
}
process.on('SIGINT', end)
process.on('SIGTERM', end)
process.stdin.on('end', end)
process.stdin.resume()
}