#!/usr/bin/env node /** * seedDemo.mjs — the deployment the screenshots are taken of. PLAN.md §13 phase 9, D45. * * node scripts/seedDemo.mjs → seed (idempotent; safe to re-run) * node scripts/seedDemo.mjs --dry-run → say what it would do, write nothing * node scripts/seedDemo.mjs --game rust → the Rust demo instead (PLAN.md §16 phase 18) * * Environment (all optional; the defaults are this machine's review stack): * * RG_BASE http://localhost:3000 the website the seed drives * RG_ADMIN_USER demoadmin (rust: admin) an existing admin, created by website's own * RG_ADMIN_PASS DemoReview!2026 `npm run seed` — see PLAN.md §13 phase 9 * RG_DEMO_PASS DemoReview!2026 the password every seeded cast member gets * UOLINK_BASE http://127.0.0.1:8080 sidecar REST, written to Admin → Shard * UOLINK_WS ws://127.0.0.1:8080/ws sidecar WebSocket * UOLINK_TOKEN (unset) sidecar auth token; skipped when absent * UOLINK_PROTOCOL (platform.json) wire protocol to pin — see the note below * RG_RUST_SERVERS (unset) rust only: a JSON file outside the repository, * [{ id, name, sidecarBaseUrl, sidecarToken }] * * One deployment runs one game module (the core refuses a second at boot), so the Rust demo * is a second deployment with its own database, and `--game rust` seeds that one. Its cast * is named for its roles — admin, moderator, player — and so is every stand-in on the game * server behind it (D80): a name in a screenshot should say what the account is, not * invent a person. * * --------------------------------------------------------------------------------------- * WHY THE SEED DRIVES THE API AND NEVER THE DATABASE * --------------------------------------------------------------------------------------- * Every row this creates could have been an INSERT, and every INSERT would have been a * second implementation of a rule the website already owns: how a body is sanitized, what * a slug may contain, which excerpt is derived when none is given, how a password is * hashed. A seed that writes SQL directly produces a database the product could not have * produced, and screenshots of that database show a product that does not exist. * * So this speaks HTTP to a running site, as an admin, through the same endpoints the admin * panel calls. The cost is that the site has to be up; the benefit is that the content is * real, and that this script keeps working when a column moves. * * --------------------------------------------------------------------------------------- * WHY IT IS IDEMPOTENT RATHER THAN DESTRUCTIVE * --------------------------------------------------------------------------------------- * Re-running must not double the news list, and must not erase a screenshot rig somebody * has been adjusting by hand. Every step therefore looks before it writes and reports * `= exists` rather than failing. That also makes the script usable as a repair: point it * at a stack that has drifted and it puts back only what is missing. * * What it deliberately does NOT create: anything the shard owns. Teams arrive from the * guild board over the bridge, the marketplace from player vendors, the atlas from real * spawners (PLAN.md §13 phase 9, D42). Seeding those would be inventing game state that * the product is supposed to be showing, which is exactly what D4 forbids. */ import { readFileSync } from 'node:fs'; import platform from '../src/data/platform.json' with { type: 'json' }; const gameAt = process.argv.indexOf('--game'); const GAME = gameAt === -1 ? 'uo' : process.argv[gameAt + 1]; if (!['uo', 'rust'].includes(GAME)) { console.error(`--game is uo or rust, not "${GAME}"`); process.exit(1); } const BASE = (process.env.RG_BASE || 'http://localhost:3000').replace(/\/+$/, ''); const API = `${BASE}/api/v1`; const ADMIN_USER = process.env.RG_ADMIN_USER || (GAME === 'rust' ? 'admin' : 'demoadmin'); const ADMIN_PASS = process.env.RG_ADMIN_PASS || 'DemoReview!2026'; const DEMO_PASS = process.env.RG_DEMO_PASS || 'DemoReview!2026'; const UOLINK_BASE = process.env.UOLINK_BASE || 'http://127.0.0.1:8080'; const UOLINK_WS = process.env.UOLINK_WS || 'ws://127.0.0.1:8080/ws'; const UOLINK_TOKEN = process.env.UOLINK_TOKEN || ''; // The pinned wire protocol, read from `platform.json` rather than written down here. // // It was a literal `4` until the Asset Bridge cutover, with a note explaining that // `module-uo` pinned 3 on a fresh install while the sidecar spoke 4, so a new deployment // read nothing from its shard until somebody edited the number in Admin → Shard. That debt // has since been paid: the module's schema fragment defaults the column to the protocol its // build speaks and carries a one-shot migration per bump, so both a fresh install and an // upgraded one land on the right number by themselves. // // What remains is the rig's own reason to state it: this seed points a demo deployment at a // sidecar, and if it pins the wrong number every REST call comes back `409`. Reading it from // `platform.json` means the number is the one `checkFacts.mjs` verified against `link`'s // `main` — so the rig cannot quietly drift two protocols behind the platform again, which is // exactly what the literal did. const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.games.uo.protocol); const DRY = process.argv.includes('--dry-run'); // ── The demo deployment's identity (D43) ─────────────────────────────────────────────── // // A neutral demo brand rather than UOMysticmoon: the screenshots show the platform, not a // private shard, and §15's demo VM can wear the same identity so the imagery stays true the // day it exists. The name is deliberately "… Demo" rather than an invented community — // nobody should have to wonder whether they are looking at a real server they could join. // The published contact address lives in exactly one file in this repository (D13), and // `checkFacts.mjs` fails the build if a literal address appears anywhere else — including // here. So the demo wears the same address the site publishes, read from the same place. const brandDefault = JSON.parse( readFileSync(new URL('../brand-default/brand.json', import.meta.url), 'utf8'), ); const SETTINGS = { site_title: 'Runic Gateway Demo', site_mode: 'live', status_message: 'Live — the demo shard is up.', homepage_teaser: 'A public demonstration of Runic Gateway: a self-hosted community site wired to a ' + 'live game server. Everything on this site is real data from the shard behind it.', contact_email: brandDefault.contactEmail, }; // ── The cast ─────────────────────────────────────────────────────────────────────────── // // Five accounts, one per role the admin screens distinguish, so a screenshot of the users // table shows the role column doing something. Names are ordinary fantasy given names and // belong to nobody. const USERS = [ { username: 'aldricmoss', role: 'moderator' }, { username: 'brannwen', role: 'editor' }, { username: 'sablequill', role: 'player' }, { username: 'tobinreed', role: 'player' }, { username: 'mirenavox', role: 'player' }, ]; // ── The Rust demo (PLAN.md §16 phase 18) ─────────────────────────────────────────────── // // The same neutral identity (D43) over a Rust server. The cast is two accounts besides the // admin, each named for its role (D80). Teams, the feed, the leaderboard and the map are the // game's, as they are for UO; what the site owns here is the server row, the NPC profiles // and a permission group, so those are seeded. const RUST_SETTINGS = { ...SETTINGS, status_message: 'Live — the demo server is up.', homepage_teaser: 'A public demonstration of Runic Gateway: a self-hosted community site wired to a ' + 'live Rust server. Everything on this site is real data from the server behind it.', }; const RUST_USERS = [ { username: 'moderator', role: 'moderator' }, { username: 'player', role: 'player' }, ]; // Two RunicNPC profiles: a guard who fights, and a passive trader who talks. Their kit is // one the rig's Kits holds; a profile naming a kit Kits does not have is refused by RunicNPC // and shows as refused on the screen, which is not the screen being shown. The guard's name // sorts first, so the editor the NPC screenshot opens (its first Edit) is the fuller one. const NPC_KIT = process.env.RG_NPC_KIT || 'rnpc_campguard'; const NPC_PROFILES = [ { name: 'guard', allServers: true, body: { names: ['Camp Guard', 'Gate Guard'], kits: [NPC_KIT], prefab: 'scientistnpc_roam', role: 'guard', movement: { mode: 'wander', radius: 12 }, health: 300, faction: 'wardens', alertRadius: 30, }, }, { name: 'trader', allServers: true, body: { names: ['Quartermaster'], kits: [NPC_KIT], prefab: 'scientistnpc_roam', role: 'passive', movement: { mode: 'stand', radius: 0 }, hurtByPlayers: false, use: { mode: 'chat', lines: ['Back again, {player}?', 'Supplies are short this wipe.'], title: '', text: '' }, }, }, ]; // One group the site authored, so the permissions screen shows the site doing something // beside what the server already had. Its permissions are ones the rig's plugins register, // and its member is the `player` account, through its linked Steam id. const PERM_GROUP = { name: 'vip', title: 'VIP', rank: 10, permissions: ['backpacks.use', 'autodoors.use'], members: ['player'], }; // Who sees who is online, and the map's player layer. Both default to staff, which is right // for a real community and wrong for a demo whose public pages exist to be looked at: with // them at staff, the public feed leaves out every death and the map shows no one. The only // players on the demo server are the stand-ins phase 18 staged, named by role (D80), so // opening them to the public shows nobody's real name. const RUST_VISIBILITY = { fleet: 'public', map: { fleet: { players: 'public' } } }; // A weekly wipe, so the server list has a next wipe to show: Thursday evening, the usual // Rust community habit. const RUST_WIPE = { wipeRule: 'weekly', wipeDay: 4, wipeTime: '19:00', wipeTz: 'America/Chicago' }; const siteSettings = GAME === 'rust' ? RUST_SETTINGS : SETTINGS; const cast = GAME === 'rust' ? RUST_USERS : USERS; // ── News, five-on-friday, the newsletter ─────────────────────────────────────────────── // // Written as a small community's real output rather than lorem: a patch note, an event, a // maintenance notice and a Friday post. Bodies are short HTML because that is what the // editor stores, and the list screens show the excerpt anyway. const POSTS = [ { category: 'news', title: 'Autumn patch: vendor search, and a fix for house decay', excerpt: 'Player-vendor listings are now searchable from the site, and the decay timer no ' + 'longer resets when a co-owner logs in.', body: '

The autumn patch is live. The headline change is that every player ' + 'vendor on the shard is now searchable from this site — the marketplace ' + 'page reads the same live feed the game does, so a listing appears within a minute ' + 'of being priced.

We also fixed the house decay timer resetting when a ' + 'co-owner logged in. That bug had been quietly keeping condemned houses alive since ' + 'spring.

Full notes are on the wiki.

', published: true, }, { category: 'news', title: 'The Harvest Moon festival opens this weekend', excerpt: 'Three days of gatherings at the crossroads, with a champion spawn on the last ' + 'night. Everyone is welcome, no signup needed.', body: '

The Harvest Moon festival runs from Friday evening to Sunday night at the ' + 'crossroads north of town. There is no signup and no entry fee — turn up.

' + '

Saturday is the market day; bring anything you want to sell and we will set out ' + 'extra vendor stalls. Sunday night closes with a champion spawn, which will be ' + 'announced in game and on the shard status page here.

', published: true, }, { category: 'news', title: 'Scheduled maintenance, Tuesday 03:00 UTC', excerpt: 'About twenty minutes of downtime for a server restart and a world save. The site ' + 'stays up throughout.', body: '

We are restarting the shard on Tuesday at 03:00 UTC for a world save and a ' + 'server update. Expect about twenty minutes of downtime.

This site stays up ' + 'while the shard is down — the status panel will simply show the shard as offline, ' + 'and the marketplace and atlas will show their last known state.

', published: true, }, { category: 'five-on-friday', title: 'Five on Friday: the ones who keep the roads clear', excerpt: 'Five players who spent the week doing unglamorous work, and what they were up to.', body: '

Five people who made the week better for everybody else:

  1. Sable, for ' + 'restocking the free reagent stall three times without being asked.
  2. Tobin, ' + 'for guiding two new players through their first dungeon.
  3. Mirena, for the ' + 'map corrections on the wiki.
  4. Brannwen, for writing up the champion ' + 'rotation.
  5. Aldric, for handling a difficult report quietly and well.
  6. ' + '
', published: true, }, { category: 'newsletter', title: 'Monthly notes — what changed, and what is next', excerpt: 'A month of changes in one place: the vendor search, the new guides, and what we ' + 'are working on next.', body: '

A quiet, productive month. The vendor search shipped, the wiki gained four ' + 'guides, and the guild boards now update on the site within a minute of a change in ' + 'game.

Next month we are looking at the champion boards and at making the ' + 'atlas easier to read on a phone.

', published: true, }, ]; // ── The wiki ─────────────────────────────────────────────────────────────────────────── // // One category and four pages, because the wiki index screenshot needs a category with // enough in it to look like a wiki rather than a placeholder. const WIKI_CATEGORY = { slug: 'guides', title: 'Guides', description: 'How things work here, written by the people who play here.', }; const WIKI_PAGES = [ { slug: 'getting-started', title: 'Getting started', excerpt: 'What to install, how to connect, and the first hour.', body: '

Before you connect

You need a game client and an account. Make the ' + 'account on this site — the shard accepts accounts created here, and it saves you ' + 'typing your password into a chat window.

The first hour

Start in ' + 'town, take the newcomer quest, and do not sell your starting tools. If you get ' + 'stuck, ask in Discord: somebody is usually around.

', }, { slug: 'player-vendors', title: 'Player vendors', excerpt: 'How to hire one, how to price, and how the site search finds you.', body: '

Hiring a vendor

Any house you own or co-own can hold vendors. Hire one ' + 'from an innkeeper and place it inside.

Being findable

Everything a ' + 'vendor holds is published to the marketplace on this site within about a minute, ' + 'including the price and the house it stands in. If a listing looks stale, the ' + 'shard was probably down when you priced it — it will correct itself on the next ' + 'sweep.

', }, { slug: 'housing-and-decay', title: 'Housing and decay', excerpt: 'Placement rules, the decay timer, and what IDOC actually means here.', body: '

Placement

Houses can be placed anywhere the client allows, with the ' + 'usual clearance rules. There is no lottery.

Decay

A house decays if ' + 'nobody with access logs in for long enough. The site lists houses approaching ' + 'collapse on the housing page, which is the same data the game uses — not a ' + 'prediction.

', }, { slug: 'community-rules', title: 'Community rules', excerpt: 'The short version: do not be the reason somebody stops playing.', body: '

The rules

  1. No harassment, in game or on the site.
  2. No ' + 'exploiting bugs — report them instead, and you will usually be thanked in ' + 'public.
  3. One account per person for events with prizes.
' + '

Appeals

Every moderation action can be appealed from your account page. ' + 'An appeal is read by somebody who was not involved in the original action.

', }, ]; // ── HTTP plumbing ────────────────────────────────────────────────────────────────────── // // One cookie jar, because the session is a cookie and `fetch` has no jar of its own. Only // the value of the auth cookie matters, so this keeps exactly that. let cookie = ''; let created = 0; let existed = 0; function keepCookies(res) { const raw = res.headers.getSetCookie?.() ?? []; for (const line of raw) { const [pair] = line.split(';'); if (pair.trim()) cookie = pair.trim(); } } async function call(method, path, body) { const res = await fetch(`${API}${path}`, { method, headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}), }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); keepCookies(res); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; } return { ok: res.ok, status: res.status, data }; } function say(mark, what) { console.log(` ${mark} ${what}`); if (mark === '+') created += 1; if (mark === '=') existed += 1; } function fail(what, res) { console.error(`\n ! ${what} failed — HTTP ${res.status}`); console.error(` ${JSON.stringify(res.data)?.slice(0, 400)}`); process.exitCode = 1; } // ── The steps ────────────────────────────────────────────────────────────────────────── async function login() { const res = await call('POST', '/auth/login', { username: ADMIN_USER, password: ADMIN_PASS }); if (!res.ok) { console.error( `\nCould not log in as "${ADMIN_USER}". Create the admin first, from the website repo:\n` + ` cd website/server && DB_NAME= ADMIN_USERNAME=${ADMIN_USER} ` + `ADMIN_PASSWORD='…' node db/seed.js\n`, ); fail('login', res); process.exit(1); } console.log(`\nsigned in as ${ADMIN_USER} at ${BASE}`); } async function settings() { console.log('\nsite settings (D43 — the neutral demo identity)'); if (DRY) { for (const [k, v] of Object.entries(siteSettings)) say('~', `${k} = ${v}`); return; } const res = await call('PUT', '/admin/settings', siteSettings); if (!res.ok) return fail('settings', res); for (const [k, v] of Object.entries(siteSettings)) say('+', `${k} = ${String(v).slice(0, 60)}`); } async function uoLink() { console.log('\nshard connection (Admin → Shard)'); if (!UOLINK_TOKEN) { say('~', 'UOLINK_TOKEN unset — leaving the sidecar config alone'); return; } const now = await call('GET', '/admin/uo-link/config'); if (now.status === 404) { say('~', 'no /admin/uo-link route — the uo module is not installed'); return; } if ( now.ok && now.data?.config?.baseUrl === UOLINK_BASE && now.data?.config?.protocol === UOLINK_PROTOCOL && now.data?.config?.enabled ) { say('=', `already pointed at ${UOLINK_BASE} (protocol ${UOLINK_PROTOCOL})`); return; } if (DRY) return say('~', `would point the site at ${UOLINK_BASE}`); const res = await call('PUT', '/admin/uo-link/config', { baseUrl: UOLINK_BASE, wsUrl: UOLINK_WS, token: UOLINK_TOKEN, protocol: UOLINK_PROTOCOL, enabled: true, }); if (!res.ok) return fail('uo-link config', res); say('+', `pointed at ${UOLINK_BASE} (protocol ${UOLINK_PROTOCOL})`); } async function users() { console.log('\naccounts'); const list = await call('GET', '/admin/users'); if (!list.ok) return fail('list users', list); const rows = Array.isArray(list.data) ? list.data : (list.data?.users ?? []); const have = new Set(rows.map((u) => u.username)); for (const user of cast) { if (have.has(user.username)) { say('=', `${user.username} (${user.role})`); continue; } if (DRY) { say('~', `${user.username} (${user.role})`); continue; } const res = await call('POST', '/admin/users', { username: user.username, password: DEMO_PASS, role: user.role, }); if (!res.ok) { fail(`create ${user.username}`, res); continue; } say('+', `${user.username} (${user.role})`); } } async function posts() { console.log('\nposts'); const list = await call('GET', '/admin/posts'); if (!list.ok) return fail('list posts', list); const rows = Array.isArray(list.data) ? list.data : (list.data?.posts ?? []); const have = new Set(rows.map((p) => p.title)); for (const post of POSTS) { if (have.has(post.title)) { say('=', `${post.category}: ${post.title}`); continue; } if (DRY) { say('~', `${post.category}: ${post.title}`); continue; } const res = await call('POST', '/admin/posts', post); if (!res.ok) { fail(`create post "${post.title}"`, res); continue; } say('+', `${post.category}: ${post.title}`); } } async function wiki() { console.log('\nwiki'); const cats = await call('GET', '/admin/wiki/categories'); if (!cats.ok) return fail('list wiki categories', cats); const catRows = Array.isArray(cats.data) ? cats.data : (cats.data?.categories ?? []); let category = catRows.find((c) => c.slug === WIKI_CATEGORY.slug); if (category) { say('=', `category ${WIKI_CATEGORY.slug}`); } else if (DRY) { say('~', `category ${WIKI_CATEGORY.slug}`); } else { const res = await call('POST', '/admin/wiki/categories', WIKI_CATEGORY); if (!res.ok) return fail('create wiki category', res); category = res.data?.category ?? res.data; say('+', `category ${WIKI_CATEGORY.slug}`); } const pages = await call('GET', '/admin/wiki'); if (!pages.ok) return fail('list wiki pages', pages); const pageRows = Array.isArray(pages.data) ? pages.data : (pages.data?.pages ?? []); const have = new Set(pageRows.map((p) => p.slug)); for (const page of WIKI_PAGES) { if (have.has(page.slug)) { say('=', `page ${page.slug}`); continue; } if (DRY) { say('~', `page ${page.slug}`); continue; } const res = await call('POST', '/admin/wiki', { ...page, category_id: category?.id ?? null, published: true, }); if (!res.ok) { fail(`create wiki page "${page.slug}"`, res); continue; } say('+', `page ${page.slug}`); } } // ── The Rust steps ───────────────────────────────────────────────────────────────────── /** The servers file, read once. The tokens in it are sent to the site and never printed. */ function rustServerList() { const file = process.env.RG_RUST_SERVERS; if (!file) return []; return JSON.parse(readFileSync(file, 'utf8')); } async function rustServers() { console.log('\nRust servers (Admin → Rust → Servers)'); const wanted = rustServerList(); if (wanted.length === 0) { say('~', 'RG_RUST_SERVERS unset — leaving the server list alone'); return; } const now = await call('GET', '/admin/rust/servers'); if (now.status === 404) { say('~', 'no /admin/rust route — the rust module is not installed'); return; } if (!now.ok) return fail('list servers', now); for (const server of wanted) { const have = (now.data?.servers ?? []).find((row) => row.id === server.id); if ( have && have.sidecarBaseUrl === server.sidecarBaseUrl && have.enabled && have.schedule?.rule === RUST_WIPE.wipeRule ) { say('=', `${server.id} at ${server.sidecarBaseUrl}`); continue; } if (DRY) { say('~', `${server.id} at ${server.sidecarBaseUrl}`); continue; } const res = await call('PUT', `/admin/rust/servers/${encodeURIComponent(server.id)}`, { name: server.name, sidecarBaseUrl: server.sidecarBaseUrl, // Omitted on an existing row, which is how the API says "keep the token" — so a re-run // never needs the token file to hold a token it already sent. sidecarToken: have ? undefined : server.sidecarToken, enabled: true, ...RUST_WIPE, }); if (!res.ok) { fail(`save server ${server.id}`, res); continue; } say('+', `${server.id} "${server.name}" at ${server.sidecarBaseUrl}`); } } async function rustVisibility() { console.log('\nvisibility (Admin → Rust → Visibility)'); const now = await call('GET', '/admin/rust/visibility'); if (!now.ok) return fail('read visibility', now); if (now.data?.presence?.fleet === 'public' && now.data?.map?.fleet?.players === 'public') { say('=', 'who is online and the map’s players are public'); return; } if (DRY) return say('~', 'would make who is online and the map’s players public'); const res = await call('PUT', '/admin/rust/visibility', RUST_VISIBILITY); if (!res.ok) return fail('visibility', res); say('+', 'who is online and the map’s players are public'); } async function npcProfiles() { console.log('\nRunicNPC profiles (Admin → Rust → NPCs)'); const now = await call('GET', '/admin/rust/npcs'); if (!now.ok) return fail('list NPC profiles', now); const live = (now.data?.profiles ?? []).filter((profile) => !profile.replaced); for (const profile of NPC_PROFILES) { const have = live.find((row) => row.name === profile.name); if (DRY) { say('~', `profile ${profile.name}`); continue; } // An existing profile is saved again rather than skipped, so a change to its body here // reaches the demo on the next run. RunicNPC answers the push the same either way. const res = have ? await call('PUT', `/admin/rust/npcs/profiles/${have.id}`, profile) : await call('POST', '/admin/rust/npcs/profiles', profile); if (!res.ok) { fail(`${have ? 'save' : 'create'} profile ${profile.name}`, res); continue; } say(have ? '=' : '+', `profile ${profile.name}${have ? ' (saved again)' : ''}`); } } async function permGroup() { console.log('\npermission group (Admin → Rust → Permissions)'); for (const server of rustServerList()) { const base = `/admin/rust/permissions/servers/${encodeURIComponent(server.id)}`; const now = await call('GET', base); if (!now.ok) { fail(`read permissions on ${server.id}`, now); continue; } if (DRY) { say('~', `${server.id}: group ${PERM_GROUP.name}`); continue; } let id = (now.data?.groups ?? []).find((group) => group.name === PERM_GROUP.name)?.id; if (id) { say('=', `${server.id}: group ${PERM_GROUP.name}`); } else { const made = await call('POST', `${base}/groups`, { name: PERM_GROUP.name, title: PERM_GROUP.title, rank: PERM_GROUP.rank, }); if (!made.ok) { fail(`create group ${PERM_GROUP.name} on ${server.id}`, made); continue; } id = made.data.id; say('+', `${server.id}: group ${PERM_GROUP.name}`); } // The permission set is replaced whole, so saving it again is the update. const perms = await call('PUT', `/admin/rust/permissions/groups/${id}/permissions`, { permissions: PERM_GROUP.permissions, }); if (!perms.ok) fail(`grant ${PERM_GROUP.name}'s permissions`, perms); else say('=', ` permissions ${PERM_GROUP.permissions.join(', ')}`); for (const username of PERM_GROUP.members) { const added = await call('POST', `/admin/rust/permissions/groups/${id}/members`, { username }); if (added.ok) say('=', ` member ${username}`); else if (added.status === 409) say('=', ` member ${username} (already)`); else fail(`add ${username} to ${PERM_GROUP.name}`, added); } } } // ── main ─────────────────────────────────────────────────────────────────────────────── console.log( `${DRY ? '\nseedDemo — DRY RUN, nothing will be written' : '\nseedDemo'} (${GAME})`, ); await login(); await settings(); await users(); // The news and the wiki are written in one game's voice (a patch note about house decay, a // champion spawn), so they belong to the UO demo. The Rust captures show none of them. if (GAME === 'rust') { await rustServers(); await rustVisibility(); await npcProfiles(); await permGroup(); } else { await uoLink(); await posts(); await wiki(); } console.log( `\n${DRY ? 'would create' : 'created'} ${created}, already present ${existed}` + (process.exitCode ? ' — with failures above' : ''), ); console.log( GAME === 'rust' ? '\nWhat this does NOT seed, on purpose: the feed, the leaderboard, who is online, clans\n' + 'and the map. Those arrive from the server over the bridge — start the server, and\n' + 'they populate themselves. PLAN.md §16 phase 18 says how its players were staged.\n' : '\nWhat this does NOT seed, on purpose: teams, the marketplace, houses, points boards\n' + 'and the atlas. Those arrive from the shard over the bridge (D42) — start the sidecar\n' + 'and the shard, and they populate themselves.\n', );