Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
403 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API: notifications over REST and MCP, with notifications scopes1#!/usr/bin/env node
2// Writes the Wrangler configs a self-hosted g1t runs with, derived from the
3// hosted ones, so the two never drift apart.
4//
5// Each hosted service's wrangler.jsonc is read and changed only where
6// Cloudflare-only things live:
7//
8// - account, routes, placement, observability and builds are dropped;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.9// - GITSTORE (git storage, Cloudflare Artifacts) becomes a service binding to workers/gitstore,
API: notifications over REST and MCP, with notifications scopes10// which keeps repositories in the git store (gitstore/server.mjs);
11// - EMAIL (Email Sending) becomes a service binding to workers/mail;
12// - the packages service keeps files in S3-compatible storage (RustFS)
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store13// instead of R2, the repos service its nightly backups (a bucket of
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.14// their own, BACKUP_S3_BUCKET), and the artifacts service the files in pages
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store15// (DOCS_S3_BUCKET);
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.16// - services that are off in this phase (the cloud runner, the g1t.page
17// dispatcher) are bound to workers/off instead, and events stop queueing
18// work for them;
API: notifications over REST and MCP, with notifications scopes19// - URLs that name g1t.sh name PUBLIC_URL instead, and billing is free.
20//
21// Usage: node configs.mjs [outDir]
22// Environment: PUBLIC_URL, GITSTORE_URL, GITSTORE_SECRET, MAIL_URL,
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.23// ACTIONS_KEY, INTEGRATIONS_KEY, WEBHOOKS_KEY, IDENTITY_KEY, USERCONTENT_KEY, REPOS_KEY,
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address24// USERCONTENT_URL,
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store25// PACKAGES_TOKEN_SECRET, S3_ENDPOINT, S3_BUCKET, BACKUP_S3_BUCKET, DOCS_S3_BUCKET, S3_REGION,
API: notifications over REST and MCP, with notifications scopes26// S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_PUBLIC_ENDPOINT, and optionally
27// your own GitHub App: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID,
28// GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET.
29//
30// The output is for `wrangler dev` (see start.sh): every Worker in one
31// workerd, the site first, with D1, KV and Queues kept on disk.
32
33import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
34import { dirname, join, relative, resolve } from "node:path";
35import { fileURLToPath } from "node:url";
36
37const here = dirname(fileURLToPath(import.meta.url));
38const root = resolve(here, "../..");
39const out = resolve(process.argv[2] ?? join(here, ".generated"));
40mkdirSync(out, { recursive: true });
41
42const PUBLIC_URL = (process.env.PUBLIC_URL ?? "http://localhost:8787").replace(/\/$/, "");
43
44/**
45 * Where the API (REST, OAuth) is reached: API_URL, or PUBLIC_URL's host on
46 * API_PORT (8789). The MCP server is a path on it: MCP_URL, or
47 * `<API_URL>/mcp`. The API's OAuth issuer is API_URL.
48 */
49export function apiAddresses(env = process.env, publicUrl = PUBLIC_URL) {
50 let api = (env.API_URL ?? "").trim().replace(/\/$/, "");
51 if (!api) {
52 const url = new URL(publicUrl);
53 url.port = env.API_PORT || "8789";
54 api = url.origin;
55 }
56 const mcp = (env.MCP_URL ?? "").trim().replace(/\/$/, "") || `${api}/mcp`;
57 return { api, mcp };
58}
59const { api: API_URL, mcp: MCP_URL } = apiAddresses();
60
61// What runs, and what is off, is each unit's `self_host` in
62// deploy/stack.jsonc: the list hosted g1t deploys from.
63const STACK = Object.values(parseJsonc(readFileSync(join(root, "deploy/stack.jsonc"), "utf8")).units);
64
65/** Services that run, in the order Wrangler is given them (the site first). */
66export const RUNNING = STACK.filter((unit) => unit.self_host === "run")
67 .map((unit) => ({ name: unit.worker, dir: unit.path, web: unit.kind === "react-router" }))
68 .sort((a, b) => Number(b.web) - Number(a.web));
69
70/** What the off Worker calls each service that is off in phase 1. */
71const OFF_NAMES = {
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.72 "g1t-runner": "Cloud runners",
API: notifications over REST and MCP, with notifications scopes73};
74const OFF = Object.fromEntries(
75 STACK.filter((unit) => unit.self_host === "off").map((unit) => [unit.worker, OFF_NAMES[unit.worker] ?? unit.worker]),
76);
77
78/** Sealing keys, by the service that holds each (hosted: Wrangler secrets). */
79const SECRETS = {
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address80 g1t: "USERCONTENT_KEY",
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.81 "g1t-repos": "REPOS_KEY",
API: notifications over REST and MCP, with notifications scopes82 "g1t-actions": "ACTIONS_KEY",
83 "g1t-integrations": "INTEGRATIONS_KEY",
84 "g1t-webhooks": "WEBHOOKS_KEY",
85};
86
87/**
88 * g1t.sh's GitHub App is its own: an installation registers one of its
89 * own, or has none, and then no GitHub buttons appear. Its public settings
90 * replace the hosted vars; its secrets go only to the service that uses each.
91 */
92const GITHUB_VARS = ["GITHUB_APP_ID", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID"];
93const GITHUB_SECRETS = {
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.94 "g1t-identity": ["GITHUB_APP_CLIENT_SECRET", "IDENTITY_KEY"],
API: notifications over REST and MCP, with notifications scopes95 "g1t-integrations": ["GITHUB_APP_PRIVATE_KEY", "GITHUB_APP_WEBHOOK_SECRET"],
96};
97
98/**
99 * Services whose cron triggers scheduler.mjs runs here: sweeps and
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.100 * reminders that need nothing self-hosting lacks (the artifacts service's is
The docs service answers every artifacts call: docs can be made, listed, shared, moved, trashed, restored, searched, versioned and edited live in their own rooms, agents read, write and recall them only where their person and everyone in the conversation can, and folio events go out on the bus, while Docs' pages keep working as before.101 * emptying artifacts' trash after 30 days). Not run: actions (its
API: notifications over REST and MCP, with notifications scopes102 * minute would start scheduled workflows with no runner to take them),
103 * billing (reconciles against Cloudflare and Stripe), deployments (calls
104 * Cloudflare's API) and the services that are off.
105 */
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.106const SELF_HOST_CRONS = new Set(["g1t-repos", "g1t-events", "g1t-identity", "g1t-security", "g1t-webhooks", "g1t-packages", "g1t-artifacts"]);
API: notifications over REST and MCP, with notifications scopes107
108/** Queues whose consumers are off: events stops sending to them. */
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.109const OFF_QUEUES = new Set(["g1t-events-runner"]);
API: notifications over REST and MCP, with notifications scopes110
111/** Strips comments and trailing commas from JSONC. Strings are respected. */
112function parseJsonc(text) {
113 let result = "";
114 let inString = false;
115 for (let i = 0; i < text.length; i++) {
116 const char = text[i];
117 if (inString) {
118 result += char;
119 if (char === "\\") result += text[++i];
120 else if (char === '"') inString = false;
121 } else if (char === '"') {
122 inString = true;
123 result += char;
124 } else if (char === "/" && text[i + 1] === "/") {
125 while (i < text.length && text[i] !== "\n") i++;
126 result += "\n";
127 } else if (char === "/" && text[i + 1] === "*") {
128 i = text.indexOf("*/", i + 2) + 1;
129 } else {
130 result += char;
131 }
132 }
133 return JSON.parse(result.replace(/,(\s*[}\]])/g, "$1"));
134}
135
136const rel = (path) => relative(out, resolve(root, path)).replaceAll("\\", "/");
137
138function hostedUrl(value) {
139 return typeof value === "string"
140 ? value.replace(/https:\/\/api\.g1t\.sh/g, API_URL).replace(/https:\/\/g1t\.sh/g, PUBLIC_URL)
141 : value;
142}
143
144function selfHosted(service) {
145 const hosted = parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8"));
146 const config = {
147 name: hosted.name,
148 compatibility_date: hosted.compatibility_date,
149 compatibility_flags: hosted.compatibility_flags,
150 rules: hosted.rules,
151 vars: {},
152 };
153
154 if (service.web) {
155 // The site as React Router built it (apps/web/build), not its sources.
156 config.main = rel(`${service.dir}/build/server/index.js`);
157 config.no_bundle = true;
158 config.rules = [{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] }];
159 config.assets = { directory: rel(`${service.dir}/build/client`) };
160 } else {
161 config.main = rel(join(service.dir, hosted.main));
162 }
163
164 for (const [key, value] of Object.entries(hosted.vars ?? {})) config.vars[key] = hostedUrl(value);
165
166 if (hosted.d1_databases) {
167 config.d1_databases = hosted.d1_databases.map((db) => ({
168 binding: db.binding,
169 database_name: db.database_name,
170 database_id: db.database_id,
171 migrations_dir: rel(join(service.dir, db.migrations_dir ?? "migrations")),
172 }));
173 }
174 if (hosted.kv_namespaces) config.kv_namespaces = hosted.kv_namespaces.map(({ binding, id }) => ({ binding, id }));
175 if (hosted.triggers) config.triggers = hosted.triggers;
176
177 if (hosted.queues) {
178 config.queues = {};
179 if (hosted.queues.producers) {
180 config.queues.producers = hosted.queues.producers.filter((producer) => !OFF_QUEUES.has(producer.queue));
181 }
182 if (hosted.queues.consumers) config.queues.consumers = hosted.queues.consumers;
183 }
184
185 config.services = (hosted.services ?? []).map((binding) =>
186 OFF[binding.service] ? { binding: binding.binding, service: offName(binding.service) } : binding,
187 );
188
189 // Cloudflare-only bindings, and what stands in for them.
190 if (hosted.artifacts) {
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.191 for (const store of hosted.artifacts) {
192 config.services.push({ binding: store.binding, service: "g1t-gitstore" });
API: notifications over REST and MCP, with notifications scopes193 }
194 }
195 if (hosted.send_email) {
196 for (const email of hosted.send_email) config.services.push({ binding: email.name, service: "g1t-mail" });
197 }
198
199 // Secrets the hosted services hold, given here from the environment, each
200 // only to the service that uses it.
201 const secret = SECRETS[hosted.name];
202 if (secret && process.env[secret]) config.vars[secret] = process.env[secret];
203 for (const name of GITHUB_VARS) {
204 if (name in config.vars) config.vars[name] = process.env[name] ?? "";
205 }
206 for (const name of GITHUB_SECRETS[hosted.name] ?? []) {
207 if (process.env[name]) config.vars[name] = process.env[name];
208 }
209
210 // Self-hosted g1t charges nothing: billing records usage at cost and never
211 // stops work for it.
212 if (hosted.name === "g1t-billing") config.vars.FREE_WHILE_BUILDING = "true";
Merge branch 'worktree-agent-a6da31375506fc440'213 // How many friends each person may invite to g1t by email (20 unless
214 // set; 0 turns Invite a friend off). Never needed to sign up.
215 if (hosted.name === "g1t-identity" && process.env.INVITES_PER_USER) config.vars.INVITES_PER_USER = process.env.INVITES_PER_USER;
API: notifications over REST and MCP, with notifications scopes216 // Packages' files go to the compose file's RustFS (or any S3-compatible
217 // store) instead of R2, with no request size limit, and package
218 // addresses start with this installation's host.
219 if (hosted.name === "g1t-packages") {
220 Object.assign(config.vars, {
221 BLOB_STORE: "s3",
222 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
223 S3_BUCKET: process.env.S3_BUCKET ?? "g1t-packages",
224 S3_REGION: process.env.S3_REGION ?? "us-east-1",
225 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
226 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
227 S3_PUBLIC_ENDPOINT: process.env.S3_PUBLIC_ENDPOINT ?? "",
228 MAX_REQUEST_BYTES: "0",
229 STORAGE_LIMITS: "off",
230 REGISTRY_HOST: new URL(PUBLIC_URL).host,
231 PACKAGES_TOKEN_SECRET: process.env.PACKAGES_TOKEN_SECRET ?? "",
232 });
233 delete config.vars.R2_ACCOUNT_ID;
234 delete config.vars.R2_BUCKET;
235 }
236 // Where this installation is reached, for the links and addresses each
237 // shows: the site's clone URLs, meta tags and agent setup, the API's
238 // OAuth issuer and MCP server, and identity's mail. No social cards: the
239 // card service (services/og) is not run here.
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address240 // Repository files and avatars: USERCONTENT_URL, a host of its own that
241 // reaches this same site, or, empty, a path on it (PUBLIC_URL/-/usercontent).
242 if (service.web) {
243 Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL, OG_URL: "", USERCONTENT_URL: (process.env.USERCONTENT_URL ?? "").trim() });
244 }
API: notifications over REST and MCP, with notifications scopes245 if (hosted.name === "g1t-api") Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL });
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)246 // Notifications on the g1t phone app go through Expo's push service (and
247 // from there Google's and Apple's), with no setup here, unless
248 // PHONE_PUSH=off: notify then sends phones nothing, and the site's
249 // /.well-known/g1t.json tells the app so.
250 if (service.web || hosted.name === "g1t-notify") config.vars.PHONE_PUSH = process.env.PHONE_PUSH === "off" ? "off" : "on";
API: notifications over REST and MCP, with notifications scopes251 if (hosted.name === "g1t-identity") config.vars.SITE_URL = PUBLIC_URL;
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.252 // Cloudflare Turnstile on sign-up, only with both of your widget's keys.
253 if (service.web) {
254 config.vars.TURNSTILE_SITE_KEY = process.env.TURNSTILE_SITE_KEY ?? "";
255 if (process.env.TURNSTILE_SECRET) config.vars.TURNSTILE_SECRET = process.env.TURNSTILE_SECRET;
256 }
API: notifications over REST and MCP, with notifications scopes257 // Nightly backups' bundles go to a bucket of their own on the same
258 // S3-compatible store, instead of the BACKUPS R2 bucket, and so do the
259 // packs kept for fresh clones (src/pack_cache.rs), instead of GIT_PACKS.
260 if (hosted.name === "g1t-repos") {
261 Object.assign(config.vars, {
262 BACKUP_STORE: "s3",
263 BACKUP_S3_BUCKET: process.env.BACKUP_S3_BUCKET ?? "g1t-backups",
264 PACK_STORE: "s3",
265 PACK_S3_BUCKET: process.env.PACK_S3_BUCKET ?? "g1t-git-packs",
266 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
267 S3_REGION: process.env.S3_REGION ?? "us-east-1",
268 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
269 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
270 });
271 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store272 // Files people put in Docs pages go to a bucket of their own on the same
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.273 // S3-compatible store, instead of the FILES R2 bucket (services/artifacts
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store274 // src/files.ts, `s3FileStore`).
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.275 if (hosted.name === "g1t-artifacts") {
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store276 Object.assign(config.vars, {
277 DOCS_FILES: "s3",
278 DOCS_S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
279 DOCS_S3_BUCKET: process.env.DOCS_S3_BUCKET ?? "g1t-docs-files",
280 DOCS_S3_REGION: process.env.S3_REGION ?? "us-east-1",
281 DOCS_S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
282 DOCS_S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
283 });
284 }
API: notifications over REST and MCP, with notifications scopes285 // Nothing to deploy to: deployments are off (no Cloudflare API token).
286 if (hosted.name === "g1t-deployments") delete config.vars.CUSTOM_HOSTNAMES_ZONE_ID;
287
288 return config;
289}
290
291function offName(service) {
292 return `${service}-off`;
293}
294
295function write(name, config) {
296 const path = join(out, `${name}.json`);
297 writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
298 return path;
299}
300
301const files = [];
302for (const service of RUNNING) files.push(write(service.name, selfHosted(service)));
303
304const compatibility_date = "2026-09-26";
305files.push(
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.306 write("g1t-gitstore", {
307 name: "g1t-gitstore",
308 main: rel("deploy/self-host/workers/gitstore/index.js"),
API: notifications over REST and MCP, with notifications scopes309 compatibility_date,
310 vars: {
311 GITSTORE_URL: process.env.GITSTORE_URL ?? "http://gitstore:8080",
312 GITSTORE_SECRET: process.env.GITSTORE_SECRET ?? "",
313 },
314 }),
315);
316files.push(
317 write("g1t-mail", {
318 name: "g1t-mail",
319 main: rel("deploy/self-host/workers/mail/index.js"),
320 compatibility_date,
321 vars: {
322 PUBLIC_URL,
323 MAIL_URL: process.env.MAIL_URL ?? "",
324 MAIL_FROM: process.env.MAIL_FROM ?? "",
325 },
326 }),
327);
328for (const [service, feature] of Object.entries(OFF)) {
329 files.push(
330 write(offName(service), {
331 name: offName(service),
332 main: rel("deploy/self-host/workers/off/index.js"),
333 compatibility_date,
334 vars: { OFF_NAME: feature },
335 }),
336 );
337}
338
339// The order Wrangler takes them in: the site first, as the one that serves.
340writeFileSync(join(out, "workers.txt"), `${files.map((file) => relative(out, file)).join("\n")}\n`);
341
342// The API (REST, MCP and OAuth) is served on a port of its own (API_PORT),
343// by a second `wrangler dev` (start.sh): not in workers.txt. Its service
344// bindings reach the Workers above through Wrangler's dev registry, as
345// any two `wrangler dev` sessions on one machine do.
346{
347 const api = STACK.find((unit) => unit.worker === "g1t-api");
348 write("g1t-api", selfHosted({ name: api.worker, dir: api.path, web: false }));
349 writeFileSync(join(out, "api.json"), `${JSON.stringify({ api: API_URL, mcp: MCP_URL }, null, 2)}\n`);
350}
351
352// What scheduler.mjs runs: each service's own crons, as hosted g1t's Cron
353// Triggers run them, for the services in SELF_HOST_CRONS.
354const schedules = RUNNING.filter((service) => SELF_HOST_CRONS.has(service.name))
355 .map((service) => ({
356 worker: service.name,
357 crons: parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8")).triggers?.crons ?? [],
358 }))
359 .filter((schedule) => schedule.crons.length > 0);
360writeFileSync(join(out, "schedules.json"), `${JSON.stringify(schedules, null, 2)}\n`);
361
362// The status page runs in a workerd of its own (status.sh, the `status`
363// service in docker-compose.yml), so it stays up when the site does not:
364// not in workers.txt. It checks the site from inside Compose
365// (STATUS_CHECK_URL) and links to it at PUBLIC_URL. Parts this
366// installation does not run (the API, MCP, docs, g1t.page, the model
367// proxy, billing) are left off its page; a public repository of yours in
368// STATUS_PROBE_REPO adds the git check.
369{
370 const hosted = parseJsonc(readFileSync(join(root, "apps/status/wrangler.jsonc"), "utf8"));
371 const db = hosted.d1_databases[0];
372 write("g1t-status", {
373 name: hosted.name,
374 main: rel(join("apps/status", hosted.main)),
375 compatibility_date: hosted.compatibility_date,
376 rules: hosted.rules,
377 triggers: hosted.triggers,
378 d1_databases: [
379 {
380 binding: db.binding,
381 database_name: db.database_name,
382 database_id: db.database_id,
383 migrations_dir: rel(join("apps/status", db.migrations_dir)),
384 },
385 ],
386 vars: {
387 SITE_URL: (process.env.STATUS_CHECK_URL ?? "http://g1t:8787").replace(/\/$/, ""),
388 PUBLIC_SITE_URL: PUBLIC_URL,
389 API_URL: "",
390 MCP_URL: "",
391 DOCS_URL: "",
392 PAGES_URL: "",
393 PROBE_REPO: process.env.STATUS_PROBE_REPO ?? "",
394 SUPPORT_URL: `${PUBLIC_URL}/support`,
395 OG_IMAGE: "",
396 // Its own address, for links made outside a request. No email
397 // binding here: subscribing by email is off, the feeds work.
398 STATUS_URL: `http://localhost:${process.env.STATUS_PORT ?? "8788"}`,
399 STATUS_ALERT_EMAIL: "",
400 },
401 });
402}
403console.log(`Wrote ${files.length} configs to ${out}`);

This file's history is long; its oldest lines are credited to the oldest commit read.