Repo map: console process split and OpenTUI console
Snapshot of the repository at 1990d875c3 (branch hfi/.../elm), gathered for the plan to run the interactive console in its own process and rebuild it on OpenTUI, with the pnpm run dev supervisor notices shown inside the TUI. Paths are repo-relative; line numbers are from this snapshot.
1. Everything that touches the console, stdin/stdout or the TTY
packages/prismarine/src/Console.ts (185 lines)
| Lines | What |
|---|---|
| 1 | import type { Server, Service } from './' (barrel import; Service is the abstract class in src/utils/Service.ts) |
| 6-8 | node:process, node:readline (CompleterResult) |
| 11-20 | declare module 'node:readline' augmentation: setRawMode?, output.write, input: any, _refreshLine? |
| 23-28 | export interface PromptWriter (the readline subset writeAbovePrompt needs) |
| 42-58 | export const writeAbovePrompt(cli, line): \x1b[2K\r${line}\n, then _refreshLine() or prompt(true) + echo of cli.line |
| 66-70 | export const completeCommand(names, line): CompleterResult |
| 75 | export default class Console implements CommandExecutor, Service |
| 78 | constructor(private readonly server: Server) |
| 84-143 | enable(): returns early when process.stdin.setRawMode is not a function (line 88-91, the headless/test guard); setRawMode(true), setNoDelay, setKeepAlive, resume() (93-97); readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, prompt: '> ', tabSize: 4, removeHistoryDuplicates: true, completer }) (99-107); subscribes server.on('chat', ...) and prints consoleTextOf(evt.getChat()) through sendMessage (109-112); server.getLogger().setConsole(this) (113); keypress handler: Ctrl+C calls server.shutdown() (115-127); line handler writes \x1b[2D then commandManager.dispatchCommand(this, this, input) and logs failures (129-142) |
| 149-152 | disable(): cli.close(), cli.removeAllListeners() |
| 154-159 | complete(): names from server.getCommandManager().getCommands() |
| 161-168 | write(line): process.stdout.write when no readline, else writeAbovePrompt |
| 170-184 | CommandExecutor members: getName() returns 'CONSOLE', getFormattedUsername() returns '[CONSOLE]', sendMessage() routes to logger.info, getServer() |
Exported from the package barrel: packages/prismarine/src/index.ts:15,23 (export { ..., Console, ... }).
packages/prismarine/src/Console.test.ts (87 lines)
Pure unit tests, no TTY. fakeCli() builds a PromptWriter recording writes (8-17).
writeAbovePrompt(20-69): erases the row before writing (21-29); calls_refreshLineonce and neverprompt(31-41); falls back toprompt(true)plus echo of the buffer without_refreshLine(43-50); echoes nothing when nothing typed (52-56); 50-line burst produces 50 writes, each starting with\x1b[2K\r, 50 refreshes (58-68).completeCommand(71-86): prefix matching, all names when nothing matches, dedupe keeping last occurrence.
packages/logger (@jsprismarine/logger 0.15.2)
src/transport.ts:5-7export type ConsoleLike = { write: (input: string) => void }.src/transport.ts:13-21LogRecord { level, message, namespace, timestamp, formatted };:24-27LogTransport { log(record); close?() }.src/transport.ts:34-55PrismarineTransport: publicconsole: ConsoleLike | undefined; buffers records until a console is set, replays in order on the nextlog, swallows write errors.src/transport.ts:66-92FileTransport(strips ANSI and§codes, appends to a file).src/logger.ts:33private readonly consoleTransport = new PrismarineTransport();:46it is always the first transport.src/logger.ts:81-84setConsole(console?: ConsoleLike): no-op whenundefined, otherwise assignsconsoleTransport.console.src/logger.ts:73-75onLine(listener: (line: string) => void)receives the raw (unformatted, no level) message. Nothing in the repo calls it today.src/logger.ts:90-106emit()buildsformattedas[HH:mm:ss LEVEL namespace]: messagewith ANSI colors throughcolorParser.src/logger.ts:61-66disable()marks closed and callstransport.close?.().src/index.tsre-exports both modules.- README (
packages/logger/README.md:6) still says "Custom transport forwinstonto deal withreadlinecli I/O"; stale.packages/logger/CHANGELOG.md:1is titled# @jsprismarine/color-parser(pre-existing mistake).
Consequence: the logger's console path is already a one-method interface (write(formatted)). An out-of-process console only needs a ConsoleLike that forwards formatted lines over IPC; nothing in Logger knows about readline.
packages/prismarine/src/Server.ts (899 lines)
| Lines | What |
|---|---|
| 3 | import Console from './Console' |
| 87 | private readonly console: Console | undefined |
| 161 | private readonly headless: boolean |
| 186 | constructor({ logger, config, headless = false }) |
| 202 | if (!this.headless) this.console = new Console(this); |
| 215-230 | enable() order: config, console?.enable() (217), logger.enable(), managers..., then this.logger.setConsole(this.console) (229) |
| 237-249 | disable() order: managers in reverse, config.disable(), logger.disable(); the console is not disabled here |
| 512-521 | once per second, when config.getEnableProcessTitle() and not headless: process.title = 'TPS: x | Tick: n | <original title>' |
| 602-636 | shutdown({ crash?, stayAlive? }): guard stopping, log Stopping server, await this.console?.disable() (607), clearInterval(tickerTimer), kick players with DisconnectReason.SHUTDOWN, disable(), raknet?.kill(), removeAllListeners(), console.debug('Server stopped, Goodbye!\n') (630, the global console, which the dev smoke test waits for), then process.exit(crash ? 1 : 0) unless stayAlive (632); on error console.error + process.exit(1) (634-635) |
| 856-858 | getConsole() returns Console | undefined (no annotation) |
| 888-897 | getTick(): number, getTPS(): number |
Service (src/utils/Service.ts:1-13) is an abstract class with enable(): Promise<void> and disable(): Promise<void>; Console uses it structurally via implements.
Callers that assume a console exists
All four build a Chat whose sender: CommandExecutor is server.getConsole()!; in headless mode this is undefined behind a non-null assertion:
packages/prismarine/src/command/CommandManager.ts:309(command feedback to*.ops)packages/prismarine/src/Player.ts:324and:844packages/prismarine/src/network/handler/ResourcePackClientResponseHandler.ts:267
Chat keeps sender: CommandExecutor (src/chat/Chat.ts:38,56). If Console leaves the server process, these need a server-side CommandExecutor (a "system"/console sender) that is always present, independent of any attached UI.
CommandExecutor and what dispatchCommand needs
packages/prismarine/src/command/CommandExecutor.ts:13-40:sendMessage(message): Promise<void> | void,getName(),getFormattedUsername(),getServer().:47-48isCommandExecutor()checks'sendMessage' in target.packages/prismarine/src/command/CommandManager.ts:249-346dispatchCommand(sender, target, input): strips a leading/; parses with brigadier againsttarget; permission check only whensender instanceof Player(271-286), so a non-player executor is trusted unconditionally (comment 268-270); alias re-dispatch (290-292); executes; reads thesendCommandFeedbackgame rule fromsender.getWorld()for players, elseworldManager.getDefaultWorld()(299-301); broadcasts feedback as aChatfromgetConsole()!to channel*.ops(306-317); errors are reported throughsender.sendMessage(320-345).getCommands()on the manager backs tab completion (Console.ts:155).
Chat delivery to the console
Console.ts:109-112listens to the'chat'server event, not toChatManager.packages/prismarine/src/chat/ConsoleText.ts:80consoleTextOf(chat: Chat): stringand:58translateForConsole(key, parameters).packages/prismarine/src/chat/ChatManager.ts:12async send(chat: Chat).
Other stdout/TTY writers inside the server process
packages/prismarine/src/utils/Progress.ts:45-46: defaults toprocess.stdoutandBoolean(process.stdout.isTTY); interactive mode redraws one line with\x1b[2K\r(:80); non-interactive mode logs at 10 percent milestones through the injected logger. Only call site:packages/prismarine/src/world/World.ts:420(start-region preload). In a split, the server process must treat stdout as non-interactive or the bar's control codes will land in the pipe.Server.ts:520process.title(see above).Server.ts:630,634globalconsole.debug/console.errorafter the logger is closed.
packages/server/src/Server.ts (97 lines, the prismarine bin entry)
| Lines | What |
|---|---|
| 10-15 | parseServerArgs(process.argv.slice(2)); on failure writes to process.stderr, sets process.exitCode = 2 |
| 17 | returns early for --help, --version and the completion positional |
| 19 | process.title = 'JSPrismarine' |
| 23-29 | process.loadEnvFile for .env.local, .env, .env.development.local, .env.development |
| 31-44 | log file name: jsprismarine-development.log under NODE_ENV=development, else timestamped |
| 46-54 | new Config({ serverPort: args.port }); new Logger(level, [new FileTransport({...})]), file at path.resolve(cwd, JSP_DIR, 'logs', logFile) |
| 55 | new Server({ config, logger }) (not headless) |
| 57-72 | SIGSEGV, SIGHUP, SIGINT, SIGTERM, uncaughtException handlers: second signal does process.exit(1); first calls server.shutdown({ crash: error === 'uncaughtException' }) |
| 76-92 | bootstrap() in a function (SEA runs the entry as CommonJS, so no top-level await); after server.bootstrap() a setInterval(() => {}, 1 << 30) keeps a headless process alive (84-89) |
packages/server/src/index.ts:1 is export * from './Server'; the bin (package.json:30-32) is "prismarine": "./dist/index.js".
packages/server/src/cli/Cli.ts (31 lines)
parseServerArgs(argv) with yargs 18: scriptName('prismarine'), usage('$0 [options]'), one option --port/-p (number), .help('help'), .version('version', ..., pkg.version), .completion('completion'), .strict(), .check() that throws on any positional (_.length > 0, line 19) and validates the port range, .exitProcess(false), .fail() rethrows, .parseSync().
Fitting new modes:
- A boolean flag such as
--no-tui/--headlessis one more.option(...);Server.ts:55would passheadlessthrough and skip the console entirely. - A
consolesubcommand (attach a TUI to a running server) needs.command('console', ...)and the positional check at line 19 relaxed to allow it. Thecompletionpositional is already special-cased atServer.ts:17, so there is precedent for a positional that bypasses the server start. packages/client/src/cli/Cli.ts:69-77is the richer yargs example in the repo: an injectableCliIo { out, err }andrun(argv): Promise<number>returning an exit code, withpackages/client/src/cli.tsas a thin bin entry settingprocess.exitCode. There is aCli.test.tsbeside it.packages/serverhas no CLI test.
packages/server/scripts/dev.mts (187 lines, the watch supervisor)
- 1-19: rationale:
node --watchkills stdin after a restart (nodejs/node#35536),tsx watcheats stdin; so the supervisor never touches stdin and spawns the child withstdio: 'inherit'. - 33-35:
DEBOUNCE_MS = 150,FORCE_KILL_MS = 10_000. - 45-47:
shouldIgnore(tests, bedrock-data resources/generated). - 49:
log()prints\x1b[33m[dev] ...\x1b[0mwithconsole.log(colors31red,32green,33yellow,36cyan). These are the notices the TUI should display:change detected: <path>,restarting...,server exited with code N, waiting for changes...,graceful shutdown timed out, killing...,generating bedrock-data...,bedrock-data generation failed,watching <dir> for changes. - 55-56: server args pass through after an optional
--. - 58-82:
start()spawnsnode --import tsx ./src/Server.ts ...argsinserverDir; on exit restarts if queued, exits on clean exit or shutdown, otherwise waits. - 88-90:
restoreTerminal()doesprocess.stdin.setRawMode(false)whenisTTYafter every child exit. - 92-100:
stopChild()sendsSIGTERM,SIGKILLafter 10 s. - 117-137: bedrock-data regeneration (
spawnSync('pnpm', ['run', 'generate'])) holds restarts while running. - 141:
ensureSubmodules()fromscripts/ensure-submodules.mts. - 145-165:
fs.watchrecursive onpackages/*/src, plus the resources submodule andbedrock-data/utils. - 173-179:
SIGINT/SIGTERM/SIGHUPhandlers: mark shutting down, forward to the child, exit when no child. - 184-187: generate once, log,
start().
Compiled under tsconfig.scripts.json (erasableSyntaxOnly, verbatimModuleSyntax, NodeNext); runs via Node type stripping, so no enums, namespaces, or parameter properties here.
packages/server/scripts/dev-smoke.exp (expect script) boots node ./scripts/dev.mts, waits for listening on port, types version and expects issued server command, touches ../prismarine/src/Console.ts and ./src/Server.ts to force restarts, types help, then stop and waits for Goodbye. Manual only; not run in CI. Its path references (Console.ts, the Goodbye string from Server.ts:630) will need updating.
Repo-wide stdin/stdout/readline/raw-mode usage (non-test)
packages/prismarine/src/Console.ts(all of it, above).packages/prismarine/src/utils/Progress.ts:45-46(stdout,isTTY).packages/prismarine/src/Server.ts:520(process.title),:632,:635(process.exit).packages/server/src/Server.ts:12-13(stderr, exit code),:19(title),:58-72(signals,process.exit(1)).packages/server/scripts/dev.mts:77,89,94,173-177.packages/client/src/cli.ts:30(process.exitCode),packages/client/src/cli/Cli.ts:75-76(stdout/stderr),packages/client/src/cli/commands.ts:46-52(SIGINT/SIGTERMonce-handlers).- Root scripts (
scripts/*.mts) callprocess.exitfor their own failures only. vite.config.ts:50,56listsreadlineandttyas externals for library builds.packages/prismarine/package.jsonstill has@types/terminal-kitindevDependencies(unused leftover).
Bun: Console.ts:94-96 guards setNoDelay/setKeepAlive because Bun lacks them; see section 7.
2. How packages/server is built and shipped
packages/server/package.json(0.15.2):"type": "module",bin.prismarine = ./dist/index.js,files: [dist, CHANGELOG.md, README.md], devexportspoint atsrc/*.ts;publishConfig.exportspoints atdist/*.js+dist/*.d.ts(ESM only, norequireentry, unlike the library packages). Scripts:build=vite build && node ../../scripts/emit-dts.mts;start=cross-env NODE_ENV=production node --unhandled-rejections=strict --import tsx ./src/Server.ts;dev=cross-env NODE_ENV=development node ./scripts/dev.mts. Dependencies:@jsprismarine/{color-parser,logger,minecraft,prismarine,raknet},yargs 18.2.0; dev:@types/yargs,tsx 4.23.15,cross-env,glob,rimraf,vite 8.3.5,vitest 5.0.5,typescript 7.0.2.packages/server/vite.config.ts: everysrc/**/*.{ts*,json}except tests becomes an entry (11-19);formats: ['es']only (31); externals arepkg.dependencies(36) on top of the root externals;codecovVitePlugin(40-44). Rootvite.config.ts:target: 'esnext',minify: false,sourcemap: 'inline',preserveModules: true,exports: 'named', externals include/^@jsprismarine/,/^node:.*/, and bare Node built-ins (readline,tty, ...). Note the glob is*.ts*, so.tsxfiles would also be picked up as entries.scripts/emit-dts.mts: runstsc-ts -p tsconfig.dts.jsonin the package dir (27-28), copies hand-writtensrc/**/*.d.tsintodist/, and writes*.json.d.tsshims for JSON imports. Every package therefore needs atsconfig.dts.json.scripts/package-sea.mts: requires Node >= 25.5 (19-23); rolldown bundlespackages/server/src/Server.tswithplatform: 'node'and theyargsCjsImportMetaPlugin(30-34), writesdist/sea/server.cjswithformat: 'cjs',inlineDynamicImports: true(35-39); thennode --build-sea=dist/sea/sea-config.json(43-56) andcodesignon macOS (58-62). Outputdist/prismarine[.exe]. Implications for a TUI: the whole console must bundle into one CJS file, so native addons orimport.meta-dependent packages need checking; a separate console process would mean either a second bundle or spawning the same executable with a subcommand.scripts/yargs-cjs-import-meta.tspatchesyargs/build/lib/utils/apply-extends.jsto avoidimport.meta.resolvein CJS.- Root
package.jsonscripts:build=turbo run build --env-mode=loose;server:buildfilters@jsprismarine/server;package=node scripts/package-sea.mts;package:scriptc=scriptc build ./packages/server/src/Server.ts --npm-static auto --out ./dist/prismarine-scriptc;typecheck=turbo run typecheck && tsc-ts -p tsconfig.scripts.json;test=vitest run --coverage;lint= concurrentlylint:prettier(prettier --check .),lint:biome(node scripts/check-american-filenames.mts && biome lint .),lint:jsdoc(node scripts/check-jsdoc.mts),lint:encoding(node scripts/check-unicode-escapes.mts).packageManager: [email protected],engines.node >= 22.18.0;.nvmrcis26.x. turbo.json:builddepends on^build, outputs**/dist/**,**/docs/**,**/src/generated/**;build:watchanddevpersistent/no cache;typecheckno cache.globalDependencies:tsconfig.json,tsconfig.build.json,package.json,vite.config.ts,.env.tsconfig.build.json(shared by every package):module: ESNext,moduleResolution: Bundler,target: ESNext,lib: [ESNext, Decorators],strict,noUncheckedIndexedAccess,isolatedModules,experimentalDecorators+emitDecoratorMetadata,useDefineForClassFields,resolveJsonModule,types: [node],typeRootsincludespackages/**/src/@types. Nojsxsetting anywhere (grep '"jsx"' tsconfig*.json packages/*/tsconfig*.jsonis empty). NoverbatimModuleSyntaxorerasableSyntaxOnlyfor packages; both are set only intsconfig.scripts.json(forscripts/**/*.mtsandpackages/*/scripts/**/*.mts,module: NodeNext).tsconfig.test.jsonaddstypes: [vitest/globals]. An OpenTUI package using JSX would needjsx/jsxImportSourcein its owntsconfig.jsonandtsconfig.dts.json;useDefineForClassFields: trueandexperimentalDecorators: trueare inherited.biome.json(2.6.x, formatter off, Prettier formats): recommended preset plus:style/noCommonJs: error,style/useExportType: error,style/useImportType: errorwithstyle: separatedType(soimport type { X } fromon its own line, not inlinetypespecifiers),correctness/noUnusedImports: error,suspicious/noConsole: errorallowing onlyconsole.debug/warn/error(off underscripts/**),noExplicitAny: off,noNonNullAssertion: off,noConstEnum: off,noDuplicateEnumValues: off(so enums are allowed). Files covered:packages/*/src/**,scripts/**,packages/*/scripts/**, root*.ts/*.mts,packages/*/*.ts; JSON excluded. Plugins:.biome/no-barrel-imports.grit(value imports from./../indexare errors;import typeis fine) and.biome/no-british-spellings.grit(identifiers and string literals). No import-ordering rule.- Prettier:
@nordcom/prettier(node_modules/@nordcom/prettier/prettier.json):printWidth 120,tabWidth 4, spaces,singleQuote,trailingComma: none,arrowParens: always,endOfLine: lf..prettierignoreskipsdist, generated data,*.yml/*.yaml. - Hooks:
.husky/pre-commitrunslint-staged;.lintstagedrc.jsonrunsbiome lint --writeandprettier --writeon stagedjs|mjs|mts|ts(not tests, not.d.ts)..husky/commit-msgruns commitlint;commitlint.config.mtsenforcesheader-max-length 72, kebab-case scope, lowercase subject without trailing period, body lines <= 100, and a custom rule requiring file names, member names, function calls and--flagsin backticks. - Vitest: root
vitest.config.ts(globals: true,pool: 'forks',isolate: false,bail: 2,passWithNoTests: true, setup file.test/vitest.setup.tsstubbingJSP_DIR=jsp, reportersverbose+github-actions/juniton CI; coverage provider v8 withall: true, reportersjson,json-summary,text-summary;excludelist drops**/src/index.ts,**/*.js*, config files,dist,docs). No coverage thresholds;codecov.ymlsets project and patch status off. Each package'svitest.config.tsmerges the base withdefineProject({ root, test.typecheck.tsconfig: <pkg>/tsconfig.test.json })(that per-packagetsconfig.test.jsondoes not exist inlogger,color-parser, orserver; vitest tolerates it). scripts/check-jsdoc.mts: scanspackages/*/src/**/*.ts(not.tsx, not tests, not@types, not generated data). Requires a JSDoc block on every exported function, class, interface, type alias, enum andconst, and on every public (non-private, non-protected) method of an exported class. Findings keyed as"<file> <kind> \<name>`"inscripts/jsdoc-baseline.json(2089 entries). Entries for the code being moved: lines 424-427 (ConsoleLike,FileTransport.close/log,PrismarineTransport.log) and 492-496 (Console.getFormattedUsername/getName/getServer/sendMessage/write). Moving a file changes its key, so the moved symbols must gain JSDoc or the baseline must be regenerated withnode scripts/check-jsdoc.mts --update`.scripts/check-american-filenames.mts: rejects tracked paths containing British spellings (words split on case and separators), exit code 1.scripts/check-unicode-escapes.mts: rejects\uXXXXescapes for readable non-ASCII characters in tracked text files; control characters like\u001bstay allowed.
3. What a new workspace package needs
Reference packages: packages/color-parser (no deps) and packages/logger (depends on color-parser). Both are at 0.15.2, as is every package in the repo (auth, bedrock-data, binaryutils, brigadier, client, color-parser, errors, leveldb, logger, math, minecraft, nbt, prismarine, protocol, raknet, server, and tools/docs). Per docs/guide/versioning.md, below 1.0.0 a feature is patch, a breaking change is minor. .changeset/config.json has one fixed group covering all @jsprismarine/* except color-parser, brigadier, bedrock-data; updateInternalDependencies: patch. Pending changesets: 14 prismarine patches, one prismarine minor (tender-items-say.md, UUID-keyed permissions), two brigadier patches, one server patch.
Files in each package (identical scaffolding, copy from packages/logger):
| File | Notes |
|---|---|
package.json | name, version (match 0.15.2), description, sideEffects: false, type: module, files: [dist, CHANGELOG.md, README.md], main (./dist/index.cjs.cjs), module, types, dev exports to ./src/index.ts / ./src/*.ts / ./package.json, publishConfig.access: public + publishConfig.exports (types, import, module, require per entry), scripts clean, build (vite build && node ../../scripts/emit-dts.mts), build:watch, typecheck (tsc-ts --noEmit), repository, keywords, author, license: MIT, homepage, devDependencies pinned to the same versions (@types/node 24.19.2, glob 13.0.6, rimraf 6.1.3, typescript 7.0.2, vite 8.3.5, vitest 5.0.5), workspace deps as workspace:* |
tsconfig.json | extends ../../tsconfig.build.json; outDir ./dist, rootDir ../../, typeRoots incl. ./src/@types; excludes tests |
tsconfig.dts.json | extends ./tsconfig.json; rootDir ./src, declaration, emitDeclarationOnly, paths: { "@jsprismarine/*": ["../*/dist/index.d.ts"] } |
vite.config.ts | copy verbatim; formats: ['es', 'cjs'] for libraries, ['es'] for the server bin |
vitest.config.ts | copy verbatim |
README.md | title, Discord and npm badges, one-line description |
CHANGELOG.md | heading # @jsprismarine/<name>; Changesets appends releases |
.npmignore | present in logger/color-parser (copy) |
src/index.ts | barrel; excluded from coverage |
Root registration:
pnpm-workspace.yaml:packages/**andtools/*globs, nothing to add.onlyBuiltDependencies/allowBuildslistesbuild,scriptc,core-js-pure; a dependency with a postinstall or native build (an OpenTUI native renderer, for instance) must be added here or pnpm 12 skips its build.tsconfig.build.json:48-66: add{ "path": "./packages/<name>/tsconfig.json" }toreferences.tools/docs/typedoc.json:entryPoints: ["../../packages/*"],entryPointStrategy: packages, andpackageOptions.entryPoints: ["src/index.ts"]. VitePress uses TypeDoc's generated sidebar, so a package withsrc/index.tsappears in the API docs without package-specific TypeDoc config. The docs build (tools/docs/package.json, TypeDoc 0.28 on TypeScript 6.0.3) must still type-check each package; TSX and JSX runtime types need to be configured if a package uses them..changeset/config.json: nothing to add; a new package automatically joins thefixedglob unless excluded.turbo.json: nothing to add.turbo/generators/config.ts+turbo/generators/template/*:pnpm turbo gen Packagescaffoldspackage.json,README.md,tsconfig.json,vite.config.ts,vitest.config.ts, andsrc/index.ts. It does not producetsconfig.dts.jsonorCHANGELOG.md, and itsupdateTsConfigaction is a TODO, so the reference must be added by hand.scripts/check-jsdoc.mtsonly reads.ts;biome.jsoncoverspackages/*/src/**regardless of extension.
4. Status-bar data sources in packages/prismarine
| Metric | Accessor |
|---|---|
| TPS | Server.getTPS(): number (Server.ts:896), rolling over the last second; Server.getTick(): number (:888) |
| Tick event | server.on('tick', (e: TickEvent) => ...); TickEvent.getTick() |
| Late-tick diagnostics | logged at verbose under namespace Server/tick (Server.ts:565-575), not exposed as data |
| Player count / list | Server.getSessionManager().getAllPlayers(): Player[] (SessionManager.ts:37), getPlayerList(): Map<string, PlayerListEntry> (:25), findPlayer, getPlayerByExactName, getPlayerById; Player.getName() (Player.ts:620), getAddress() (:616), getNetworkSession() (:611) |
| Player join/leave | typed events playerConnect, playerDisconnect, playerSpawn, playerDespawn (events/EventEmitter.ts:22-42 EventTypes; events/Events.ts exports the classes) |
| Max players | Config.getMaxPlayers() (config/Config.ts:265); RakNetListener.getMaxConnections() (raknet/src/ServerSocket.ts:205) |
| Worlds | Server.getWorldManager().getWorlds(): World[] (WorldManager.ts:216), getDefaultWorld() (:224), getWorldByName() (:209); World.getName() (World.ts:1201), getFormattedName() (:1204), getPlayers() (:1152), getEntities() (:1123) |
| MOTD / server name | Config.getMotd() (Config.ts:301); live value via Server.getRaknet()?.serverName (ServerName from packages/raknet/src/utils/ServerName.ts: getMotd(), getName(), getProtocol(), setOnlinePlayerCount is called by the socket); Server.getMetadata() returns the same object and throws before bootstrap |
| Version | Server.getVersion(): string (Server.ts:678, package version); Server.getIdentifiers().MinecraftVersions.at(-1) and .Protocol |
| Config | Server.getConfig() (Server.ts:835): getServerPort(), getServerIp(), getLevelName(), getGamemode(), getDifficulty(), getViewDistance(), getOnlineMode(), getEnableProcessTitle(), getEnableTicking(), getLogLevel() |
| Memory / uptime | nothing in the server; use process.memoryUsage() and process.uptime() in whichever process reports them. utils/Timer.ts wraps process.hrtime for ad-hoc timings only |
| Chat | server.on('chat', (e: ChatEvent) => consoleTextOf(e.getChat())) as in Console.ts:109-112 |
| Log lines | Logger.setConsole(ConsoleLike) for formatted lines; Logger.onLine() for raw messages |
Event bus: Server extends EventEmitter (events/EventEmitter.ts:149-190): on(id, cb), once, emit(id, event): Promise<void> (awaited, handlers run inside the tick), post([id, event]) (fire and forget).
5. Tests touching the console path and the test fixtures
packages/prismarine/src/Console.test.ts(section 1):writeAbovePromptandcompleteCommandonly; no readline, no TTY.packages/logger/src/transport.test.ts:PrismarineTransportbuffers untilconsoleis assigned, replays in order, and writes once per record afterwards (21-50).packages/logger/src/logger.test.ts: useslogger.setConsole({ write })as the sink (line 10); asserts suppressed levels never reach the transport, enabled levels do, lowering the level emits everything,getNamespaceis not called for suppressed messages, and the stack-depth assumption ofgetNamespaceholds (14-74).packages/prismarine/src/command/CommandManager.test.ts:97documents that the console bypasses permissions by not being aPlayer(noisConsole()).packages/prismarine/src/Server.test.ts: mocks@jsprismarine/raknetRakNetListener(13-23);debugConfig(levelName)subclassesConfigwithenable()as a no-op and overrides for IP, world, preload radius 0, ticking off, process title off (52-111);beforeAllpointsJSP_DIRat a temp dir (28-36). Two cases:new Server({ logger, config })(not headless, 113-122) andnew Server({ ..., headless: true })(124-133); bothbootstrap('0.0.0.0', 12345)thenshutdown({ stayAlive: true }). The non-headless case passes under vitest becauseConsole.enable()bails whenprocess.stdin.setRawModeis absent (Console.ts:88-91); a console that spawns a process or opens a renderer must keep an equivalent guard or default to headless when stdin is not a TTY.- Headless servers elsewhere:
packages/client/src/Login.test.ts:32,packages/client/src/cli/Cli.test.ts:91,packages/client/src/bot/Fleet.test.ts:26, allnew Server({ logger, config: new Config(), headless: true }). packages/prismarine/src/testing/:stub.test.helper.ts(stub<T>(members),stubOf,StubMembers<T>) andblockTarget.test.helper.ts(LoggedLines,sessionNearBlocks). No server fixture lives there;Server.test.tsbuilds its own..test/vitest.setup.tsstubsJSP_DIR=jspbefore each test.packages/server/scripts/dev-smoke.exp: manual PTY smoke test of console input across restarts (section 1).
6. Docs to update
docs/guide/running.md:30-37(pnpm run dev/pnpm run start),:85-96("How the development server works": supervisor, "the interactive console keeps working across restarts"),:107,120-123(env vars withpnpm run dev). New flags or aconsolesubcommand belong here.docs/guide/log-levels.md: level list only; mention if the TUI filters by level.tools/docs/site/index.md:18: feature blurb "pnpm run dev starts the server from source and restarts it when any package changes, with a working interactive console."tools/docs/site/.vitepress/config.mts:14-20: guide sidebar listsrunningandlog-levels;tools/docs/assemble.mts:23-26copies only top-leveldocs/*.mdinto the site, so a new guide page needs a sidebar entry anddocs/agents/**stays out of the site.README.md:46-49("Run the server (usepnpm run devfor development)").packages/logger/README.md:6(stalewinston/readlinewording).packages/server/README.mdandpackages/prismarine/README.mddo not mention the console.packages/server/scripts/dev.mts:1-19anddev-smoke.exp:1-12header comments describe the current stdin-inheritance design.Dockerfile:25-27: runsnode --import tsx ./src/Server.tsas PID 1 with no TTY; the server must keep starting headless there without flags.
7. Bun and CI
Bun is not used. bunfig.toml only sets the npm registry and $BUN_AUTH_TOKEN; the other mentions are prose (packages/prismarine/README.md:18, packages/brigadier/README.md:22, packages/leveldb/README.md:15 noting there is no bun build --compile) and the compatibility comment at Console.ts:94. Node is the runtime everywhere (.nvmrc 26.x, Dockerfile node:26-slim, SEA needs >= 25.5).
CI (.github/workflows/), all using the composite .github/common/install (setup-node from .nvmrc, pnpm from packageManager, pnpm install --no-frozen-lockfile):
| Workflow | Jobs and commands |
|---|---|
lint.yml | pnpm run lint (Prettier check, American filenames + Biome, JSDoc check, unicode escapes) |
test.yml | matrix ubuntu/macos/windows: pnpm run build, pnpm run typecheck, pnpm run test; vitest coverage report action, Codecov upload |
commitlint.yml | commitlint over the PR range |
nightly.yml | matrix ubuntu/macos/windows: pnpm run build, pnpm run package (SEA), upload artifact |
scriptc.yml | pnpm run package:scriptc:coverage, pnpm run package:scriptc |
release.yml | submodule init, pnpm run build, changesets/action (version PR / publish), .github/scripts/create-github-releases.mjs |
docs.yml | pnpm run build:docs (TypeDoc + VitePress), deploy to gh-pages |
codeql.yml | CodeQL analysis |
pull-request.yml | dependency-changesets-action for Renovate PRs |
A new package must therefore pass: Prettier, Biome (plus the two grit plugins), the JSDoc check on every exported .ts symbol, tsc-ts --noEmit, vite build + emit-dts on three OSes, vitest, the TypeDoc build under TypeScript 6, and (if it ends up in the server bundle) rolldown-to-CJS bundling for the SEA and scriptc.