Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181#!/usr/bin/env node/** * git-host: a git forge served from a PDS reached over the wire. * * git-host --pds https://pds.example.com --account alice.example.com * * The PDS stays the data store. This process serves git smart HTTP, the * file browser, the dev.pdsjs.git.* methods and the browser app, indexes * collaboration records off Jetstream, and catches up from the archive on * boot when replay is on. */
import { mkdirSync } from 'node:fs';import { dirname, join } from 'node:path';import { parseArgs } from 'node:util';import { zstdDecompressSync } from 'node:zlib';import { backfill, watch } from '@pdsjs/appview';import { createAppViewStore } from '@pdsjs/appview/store';import { GIT_ISSUE_COLLECTION, GIT_PULL_COLLECTION, GIT_REPO_COLLECTION, GIT_REVIEW_COLLECTION,} from '@pdsjs/git';import { createForgeAuth } from './auth.js';import { requestContext } from './context.js';import { createRemoteAccount } from './remote.js';import { createGitHostServer } from './server.js';
/** The collections the forge's reads are about. */const WATCHED_COLLECTIONS = [ GIT_REPO_COLLECTION, GIT_PULL_COLLECTION, GIT_REVIEW_COLLECTION, GIT_ISSUE_COLLECTION, 'dev.pdsjs.git.offer',];
/** * The SQLite driver, whichever is installed: better-sqlite3 first, node's * own module otherwise (node 22.5 and up). * @param {string} path * @returns {Promise<import('@pdsjs/storage-sqlite/driver').SQLiteDatabase>} */async function openDatabase(path) { try { const { default: Database } = await import('better-sqlite3'); return new Database(path); } catch { const { DatabaseSync } = await import('node:sqlite'); return new DatabaseSync(path); }}
const { values } = parseArgs({ options: { pds: { type: 'string' }, account: { type: 'string' }, db: { type: 'string', default: 'git-appview.sqlite' }, port: { type: 'string', default: '8046' }, origin: { type: 'string' }, ui: { type: 'string' }, 'plc-url': { type: 'string', default: 'https://plc.directory' }, jetstream: { type: 'string', default: 'wss://jetstream.us-east.bsky.network', }, 'jetstream-api-key': { type: 'string', default: process.env.JETSTREAM_API_KEY, }, replay: { type: 'boolean', default: false }, 'list-service': { type: 'string', default: 'https://bsky.network' }, 'no-watch': { type: 'boolean', default: false }, 'no-backfill': { type: 'boolean', default: false }, help: { type: 'boolean', default: false }, },});
if (values.help || !values.pds || !values.account) { console.log(`usage: git-host --pds <url> --account <handle-or-did> [options]
--db <path> the index database (default ./git-appview.sqlite) --port <n> the port to serve on (default 8046) --origin <url> the origin requests are read as carrying --ui <dir> the browser app's build, served at / --plc-url <url> the directory DIDs resolve against --jetstream <url> the jetstream to index from (default a v2 instance) --jetstream-api-key the archive's bearer token, for replay on a metered instance; also read from JETSTREAM_API_KEY --replay catch up from the jetstream archive on boot instead of walking the relay; implies --no-backfill --list-service <url> the relay the backfill lists repos from --no-watch skip the jetstream tail --no-backfill skip the first-boot historical walk`); process.exit(values.help ? 0 : 1);}
const notice = (/** @type {string} */ message) => console.error(`[git-host] ${message}`);
const dbPath = join(values.db);mkdirSync(dirname(dbPath), { recursive: true });const db = await openDatabase(dbPath);const store = createAppViewStore(db);
const account = createRemoteAccount({ service: values.pds, account: values.account, // Writes carry the requesting account's own token, which the dispatch // placed in this request's context. getToken: async () => requestContext.getStore()?.token ?? null,});
// The login round trip needs a stable origin: the client_id names it. An// origin-less deployment serves reads only.const auth = values.origin ? createForgeAuth({ origin: values.origin, accountDid: await account.getDid(), service: values.pds, plcUrl: values['plc-url'], onNotice: notice, }) : undefined;
const host = createGitHostServer({ account, store, auth, origin: values.origin, uiDir: values.ui, plcUrl: values['plc-url'], onNotice: notice,});
await host.listen(Number(values.port));notice(`serving ${values.account} from ${values.pds} on :${values.port}`);
const controller = new AbortController();for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => { controller.abort(); void host.close().then(() => process.exit(0)); });}
// Replay replaces the relay walk: the archive is the same history, asked// of the jetstream itself instead of every account's PDS.const replay = values.replay || Boolean(values['jetstream-api-key']);
if (!values['no-backfill'] && !replay) { void backfill({ store, collections: WATCHED_COLLECTIONS, listService: values['list-service'], plcUrl: values['plc-url'], signal: controller.signal, onNotice: notice, });}
if (!values['no-watch']) { void watch({ store, service: values.jetstream, collections: WATCHED_COLLECTIONS, replay: replay ? { service: values.jetstream, apiKey: values['jetstream-api-key'], decompress: async (bytes) => new Uint8Array(zstdDecompressSync(Buffer.from(bytes))), } : undefined, signal: controller.signal, onNotice: notice, });}