Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base18aee8fa1). Dewey approved round 3 (comment 28832) and Filbert approved it with no findings (28835). Mos gave the decision 123 go on public diff 8360122b (mos-0636): no vendored code, no innerHTML or other HTML sinks. The Docs page #/docs lists each project's spec/north-star.md and the Markdown files directly under docs/. Save writes the typed text as one local commit authored by business.human, through a new human-only bus verb project.file.save that carries the opened commit as base and refuses stale-base if the file moved. The preview is a textarea plus a renderer built with createElement and textContent only; raw HTML shows as source text and images never load. CSP is unchanged. Round 3 replaced the backreference HR regex, which overflowed the regexp stack, with an isRule scan. Round 4 added a textContent fallback when paintDoc throws, and the submit handler resets saving in a finally. Mos's rule (ceiling 1 s at 1 MiB, growth x3 per doubling over 1 to 4 MiB): 130/130 and 138/138 cells pass, worst 513 ms at 1 MiB, worst ratio x2.50, control x2.01; the 756-cell grid has 0 timeouts. Mutants 43/44, v21 equivalent. Follow-ups: Vikunja P19 tasks 318 (Save stays off after a non-render paint throw), 319 (dead NUL check in bus.js) and 320 (the 1 MiB + 64 KiB buffer before the human proof, Gitea #1650). Sage's landing gate ran ona4779d2a5plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z, network sage-r148g_default removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
80 lines
5.0 KiB
JavaScript
80 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import { startServer, previewOptions, DEFAULT_BOARD } from './serve.mjs';
|
|
import { busCaller } from './bus.mjs';
|
|
import { views } from '../../bus/src/views.mjs';
|
|
import { loadSystem, socketPath, REPO, rolesDir } from '../../cli/src/config.mjs';
|
|
import { configDir } from '../../business/src/business.mjs';
|
|
import { consoleReads, redactor } from './reads.mjs';
|
|
import { statSync } from 'node:fs';
|
|
import { isAbsolute } from 'node:path';
|
|
import { ID_PATTERN } from '../../business/src/vocabulary.mjs';
|
|
const args = process.argv.slice(2);
|
|
const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]';
|
|
try {
|
|
if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(usage); process.exit(0); }
|
|
if (args.shift() !== 'serve') throw new Error(usage);
|
|
const options = { board: DEFAULT_BOARD, port: 7330 }, preview = {};
|
|
let sendOn = false;
|
|
while (args.length) {
|
|
const flag = args.shift();
|
|
// Row 76 (#1570): the one flag without a value.
|
|
if (flag === '--send') { sendOn = true; continue; }
|
|
const value = args.shift();
|
|
if (!value) throw new Error(`missing value for ${flag}`);
|
|
if (flag === '--port') {
|
|
if (!/^\d+$/.test(value) || Number(value) > 65535) throw new Error('port must be 0..65535');
|
|
options.port = Number(value);
|
|
} else if (flag === '--board') options.board = value;
|
|
// Row 77 (#1571): the canonical checkout the queue read runs from, when
|
|
// serve runs from a pinned copy; everything else loads from the copy.
|
|
else if (flag === '--checkout') {
|
|
let dir = false;
|
|
try { dir = isAbsolute(value) && statSync(value).isDirectory(); } catch { /* refused below */ }
|
|
if (!dir) throw new Error('--checkout must be the absolute path of a directory');
|
|
options.checkout = value;
|
|
}
|
|
else if (flag === '--business') {
|
|
if (!ID_PATTERN.test(value)) throw new Error('business must be a business id');
|
|
options.business = value;
|
|
}
|
|
// Row 74 (#1568): the preview mode, all three or none, checked before any config is read.
|
|
else if (flag === '--public-host') preview.publicHost = value;
|
|
else if (flag === '--auth-header') preview.authHeader = value;
|
|
else if (flag === '--allow-user') preview.allowUser = value;
|
|
else throw new Error(`unknown option: ${flag}`);
|
|
}
|
|
options.preview = previewOptions(preview);
|
|
if (sendOn && !options.preview) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user)');
|
|
// The bus views (slice 1 S5) need the system config. Without it the
|
|
// board still serves and the bus routes answer not-configured.
|
|
// The Queue, Business and Settings reads (row 54) need no bus; without
|
|
// the system config, Business can't resolve authority and Settings has
|
|
// no notifier. A refusal printed here names no path (redactor).
|
|
// --send without a bus refuses to start rather than serve a composer that can't send.
|
|
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, busLine, system = null;
|
|
try {
|
|
system = loadSystem();
|
|
const { dataRoot } = system;
|
|
const call = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
|
bus = views({ call });
|
|
// Row 148: the spec and docs editor reads like the other reads and saves only under --send.
|
|
projectFiles = args => call('project.files', args);
|
|
projectFile = args => call('project.file', args);
|
|
if (sendOn) {send = args => call('message.send', args);settingsWrite=args=>call('limits.set',args);projectSave=args=>call('project.file.save',args);}
|
|
busLine = `Bus: ${options.business ?? 'the running bus host\'s business'}`;
|
|
} catch (err) {
|
|
const why = redactor({ configDir: configDir() })(err.message);
|
|
if (sendOn) throw new Error(`--send needs the bus configured (${why})`);
|
|
busLine = `Bus: not configured (${why})`;
|
|
}
|
|
const reads = consoleReads({ root: REPO, queueRoot: options.checkout ?? REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null });
|
|
const server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, send, settingsWrite, projectFiles, projectFile, projectSave });
|
|
const previewLine = options.preview ? `\nPreview: https://${options.preview.host}/ through the proxy, signed-in user from ${options.preview.header}, ${send ? 'reads and human messages to roles (--send); one line per send below' : 'read-only'}` : '';
|
|
const queueLine = options.checkout ? '\nQueue: read from the --checkout directory' : '';
|
|
console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}\nCtrl-C stops this server.`);
|
|
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => server.close());
|
|
} catch (err) {
|
|
console.error(`refused: ${err.message}`);
|
|
process.exitCode = 2;
|
|
}
|