doxa-maps/internal/dev-server.mjs
Doxa Maps 1ca78d6d5d A laptop that follows the table says when it needs credentials, and the last main defaults say development
The joining page, DEPLOY and the laptop sync default moved to development in the
commit before this one. This finishes the move and closes what a fresh clone's
friend test found.

- landing-preconditions diffs against origin/development when no landing branch
  is exported, instead of main.
- CLOUD-DEVELOPMENT-ENVIRONMENT, the plugin-designer skill (SKILL.md, workflow,
  glossary) and the shared dev server's notes land on and follow development;
  the skill says never to push to main.
- The sync fetches with the laptop's own credentials. git runs with prompts off,
  so on a clone with no credential helper it failed every interval with a bare
  "fetch-failed / remote: Unauthorized", which read as a broken server. That case
  is now its own state, needs-credentials: the log (once, not every tick) and
  /api/dev-sync say "dev-sync needs git credentials for <remote>". The joining
  page and .env.example say so, and that a fresh clone starts on main and must
  switch to development before the sync can fast-forward it.

Measured in scratch repos: the private remote with no helper gives
needs-credentials and one warning over two ticks; a local remote with no
DX_SYNC_BRANCH follows development and fast-forwards; a missing remote stays
fetch-failed.
2026-09-29 00:59:14 +00:00

64 lines
3.2 KiB
JavaScript

/**
* dev-server.mjs — the SHARED DEV SERVER every agent can check against (headless: curl-style fetches).
*
* One `bun run dev` on this machine follows origin/development (fast-forward, ~30 s) from a checkout that is
* nobody's worktree, so it shows what a developer running the dev server on development sees. It answers
* /__dev-sha with the commit it is serving.
*
* USE IT FOR: "does development work under the dev server", dev-parity checks, and waiting for a landing
* to appear. DO NOT use it for an UNLANDED change (it does not have it): start a short-lived dev
* server in your own worktree, or BUNDLE=<name> bun x vite build. Release/embed checks (SRI, manifest,
* iframe/script cells) stay on the BUILT tree.
*
* import { url, devSha, waitForSha } from './dev-server.mjs'
* await waitForSha('ce42977c') // resolves once the server serves that commit (or a descendant)
* const r = await fetch(url('/assets/boundaries/world-adm0.geojson'))
*
* DOXA_DEV_URL overrides the address (default http://127.0.0.1:5190).
* CLI: node internal/dev-server.mjs [--wait <sha>] [--timeout <s>] prints the served sha.
*/
import { spawnSync } from 'node:child_process'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
export const DEV_URL = (process.env.DOXA_DEV_URL || 'http://127.0.0.1:5190').replace(/\/+$/, '')
export const url = (path = '/') => DEV_URL + (path.startsWith('/') ? path : '/' + path)
/** The commit the dev server is serving ("<sha>" or "<sha>+dirty"), or '' when it does not answer. */
export async function devSha() {
try {
const r = await fetch(url('/__dev-sha'), { signal: AbortSignal.timeout(5000) })
return r.ok ? (await r.text()).trim() : ''
} catch { return '' }
}
/** Is `sha` the served commit or one of its ancestors? (Needs `sha` in this clone's object store.) */
function contains(served, sha) {
const head = served.replace(/\+dirty$/, '')
if (!head) return false
if (head.startsWith(sha) || sha.startsWith(head)) return true
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
return spawnSync('git', ['merge-base', '--is-ancestor', sha, head], { cwd: ROOT }).status === 0
}
/** Resolve with the served sha once it contains `sha`; reject after `timeoutMs`. */
export async function waitForSha(sha, { timeoutMs = 180000, everyMs = 5000 } = {}) {
const until = Date.now() + timeoutMs
let last = ''
while (Date.now() < until) {
last = await devSha()
if (last && contains(last, sha)) return last
await new Promise((r) => setTimeout(r, everyMs))
}
throw new Error(`the shared dev server (${DEV_URL}) did not serve ${sha} within ${Math.round(timeoutMs / 1000)} s (serving ${last || 'nothing'})`)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const argv = process.argv.slice(2)
const i = argv.indexOf('--wait'), t = argv.indexOf('--timeout')
try {
const got = i >= 0 ? await waitForSha(argv[i + 1], { timeoutMs: (t >= 0 ? Number(argv[t + 1]) : 180) * 1000 }) : await devSha()
if (!got) { console.log(`DEV-SERVER: ${DEV_URL} does not answer /__dev-sha`); process.exit(75) }
console.log(`DEV-SERVER: ${DEV_URL} serves ${got}`)
} catch (e) { console.log(`DEV-SERVER: ${e.message}`); process.exit(1) }
}