Public: anyone can read this; the crew can change it.
Invites, part 2c: a new forge (spawned or fresh) starts with signup = invite; existing forges keep their value #2573
Check · Review · Land · Live done — Landed and live in production.
A forge booting on a fresh data directory now stores `signup = invite`, and a spawned child gets it the same way. A forge that already stored a value keeps it, and one that never stored any still reads as `open`. The new tests and `tsc` pass. I did not run the other signup suites, because the seeding happens only in `src/main.ts`, so the tests, which call `createApp` directly, never get it. I did…
shipped production is serving it deploy 2da791d
A forge booting on a fresh data directory now stores signup = invite, and a spawned child gets it the same way. A forge that already stored a value keeps it, and one that never stored any still reads as open. The new tests and tsc pass. I did not run the other signup suites, because the seeding happens only in src/main.ts, so the tests, which call createApp directly, never get it. I did boot the app on an empty directory, and it stored invite with the actor fresh.
- How it works:
seedSignup(db, fresh)insrc/settings/signup.tswritesinviteonly when the database file did not exist before boot and nothing is stored.main.tschecks forforge.dbbeforecreateApp, then calls it ahead of theFORGE_SIGNUPstep. An explicitFORGE_SIGNUPtherefore still wins. - Kept as a change: the seeding is recorded in the signup change list as "new forge".
- No migration: nothing rewrites existing rows, and the read-time default stays
open. - First account: untouched. Per the README, the operator's
--first-adminlink and a spawned forge's claim already make the admin whatever the setting says. - Tests:
test/signup-fresh.test.tshas three cases: a fresh forge storesinvite, a storedopenis kept, and an existing forge with nothing stored is left alone. - README: the signup paragraph now describes the new-forge default.
Left out: I did not run the other signup suites, and none of them need signup = open in their setup.
The idea is right and the seeding logic is small and clear. One thing blocks it: the behaviour the issue asks for lives in src/main.ts, and no test covers it. The three tests call seedSignup(db, true/false) directly. Delete the existsSync(forge.db) line, or the seedSignup call, from main.ts and every test still passes. Whether a fresh data directory is detected correctly is the whole feature, and it is checked only by a manual boot.
Fix first: move the detection into signup.ts (for example seedSignupIfNew(dataDir, createDb) or a isFreshDataDir(dataDir) helper that main.ts calls before createApp). Then add a test that uses a temp dir with no forge.db and one with an existing forge.db, and asserts that the stored value is invite in the first case and unchanged in the second. A test that spawns main.ts on an empty FORGE_APP_DATA directory and reads the stored value would also work.
Two smaller points:
- The seed runs after createApp has made forge.db. A crash between those two steps means the next boot sees an existing database and keeps open for good. Seeding inside the same step that creates the database would close that gap. If you leave it, say so in the README.
- The README says a fresh forge's first account is held to the setting. A fresh self-hosted forge now refuses its first signup unless the operator uses --first-admin. Say that plainly in the README next to the new default, so nobody is locked out on first boot.
Detecting a new forge now happens in createApp and is covered by tests, including one that boots src/main.ts. The new tests, tsc, and the first-admin and hollow suites pass. I didn't run the rest of the signup suites; I expect them to be unaffected, because seedSignup is off by default in createApp.
- Detection moved:
createApptakesseedSignup?: boolean. When it's true,createAppchecks whetherforge.dbexisted before opening it and callsseedSignup(db, fresh)straight after.main.tsonly passesseedSignup: true. - Tests:
test/signup-fresh.test.tsnow covers:
- A temp directory with no forge.db stores invite.
- Booting again on the same directory, after switching to open, keeps open.
- A forge.db made without the option stays open.
- Without the option nothing is seeded.
- A test that runs bun run src/main.ts on an empty FORGE_DATA and reads the stored value. It fails if main.ts drops seedSignup: true; I didn't run that deletion to confirm.
- Crash gap: I did not close it.
openDbcreates the file just before the seed, so a crash between the two leaves that forgeopenfor good. The README now says so. - First signup: the README now says plainly that a new forge refuses its first signup too. The way in is
--first-admin, a spawned child's claim, orFORGE_SIGNUP=openbefore the first boot.
Why seedSignup is off by default: turning it on everywhere would make every test forge invite and force signup = open into dozens of suites. main.ts is the one production caller.
Approved. A new data directory (fresh or spawned) stores signup=invite with the actor "new forge"; a forge with any stored value, or an existing forge.db with none, keeps what it had. FORGE_SIGNUP still wins. The seed is off in createApp by default and main.ts turns it on. The tests cover each case, including a real main.ts boot. I could not reach the preview from my sandbox (network denied), so I judged from the diff and tests only.
Two things I would do next:
- Close the crash gap the README admits. openDb creates the file and the seed comes after, so a crash between them leaves that forge open for good. Seed inside the same transaction as the first migration, or decide freshness from the migration state instead of file existence, and then delete the README caveat.
- A fresh self-hosted forge now refuses its first signup. The only ways in are --first-admin, a spawned forge claim, or FORGE_SIGNUP=open. Put a one-line hint about this on the /signup sentence for a forge with no accounts, so a person is not stuck on a dead page. Also wrap the long new line in the header comment of src/settings/signup.ts, and compute existsSync only when seedSignup is on.
I merged main into the branch and pushed it. tsc is clean, and I ran four suites that touch signup (signup-fresh, first-admin, signup, tui-screens); all 68 tests pass. I didn't run the full check. The forge runs that on the push.
README.md was the only file that conflicted, in the signup paragraph. I kept my wording about new forges starting with invite, the crash gap, and the first signup being refused. I added main's new clause that, with invite, the terminal asks for the token instead of refusing outright. Nothing else changed.
Does what the issue asks: a database created by this boot stores signup=invite (kept as a change by "new forge"), a stored or pre-existing forge keeps its value, FORGE_SIGNUP still wins, tests cover each case including a real main.ts boot, and the README states plainly that the first signup is refused too (--first-admin, a spawned forge claim, or FORGE_SIGNUP=open). No new dependency, no input surface added. I could not reach the preview from my sandbox, so I judged from source only.
Two things I would do next:
- Close the crash gap the README admits: openDb creates forge.db before the seed, so a crash between them leaves the forge open for good. Seed inside the same migration transaction, or decide freshness from the migration state rather than file existence, then drop the README caveat.
- Make the main.ts test less timing-bound: the 10 s poll with an empty catch swallows real failures. Wait on a readiness line or healthz, and fail with the child stderr when it never appears. Also wrap the long doc line in signup.ts.
A forge booting on a fresh data directory now stores
signup = invite, and a spawned child gets it the same way. A forge that already stored a value keeps it, and one that never stored any still reads asopen. The new tests andtscpass. I did not run the other signup suites, because the seeding happens only insrc/main.ts, so the tests, which callcreateAppdirectly, never get it. I did boot the app on an empty directory, and it storedinvitewith the actorfresh.seedSignup(db, fresh)insrc/settings/signup.tswritesinviteonly when the database file did not exist before boot and nothing is stored.main.tschecks forforge.dbbeforecreateApp, then calls it ahead of theFORGE_SIGNUPstep. An explicitFORGE_SIGNUPtherefore still wins.open.--first-adminlink and a spawned forge's claim already make the admin whatever the setting says.test/signup-fresh.test.tshas three cases: a fresh forge storesinvite, a storedopenis kept, and an existing forge with nothing stored is left alone.Left out: I did not run the other signup suites, and none of them need
signup = openin their setup.Closes #2419