feat(core): the command line with its local commands after Java, measured against Java

The terminal splits a line as Java's window does and runs ls, cd, pwd,
mkdir, touch, cp, mv, rm, cat, test, help, ipconfig and route on the
node's files, which are the project's own. Its texts are generated from
the bundle Java loads. TerminalCases feeds Java's Terminal without a
network (16 cases, de and en); eight cases name Java bugs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:32:35 +00:00
co-authored by Claude Opus 5.5
parent 058675a148
commit 016f9461df
17 changed files with 14404 additions and 7 deletions
+20
View File
@@ -118,6 +118,7 @@ import { EchoServer, SimpleClient } from './apps/echo.js';
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
import { EmailClient, type EmailClientFolders } from './apps/emailClient.js';
import { EmailServer } from './apps/emailServer.js';
import { Terminal } from './apps/terminal.js';
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
@@ -915,6 +916,7 @@ export class NetworkRuntimeEngine {
private readonly webBrowsersByNodeId = new Map<Id, WebBrowser>();
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
private readonly emailClientsByNodeId = new Map<Id, EmailClient>();
private readonly terminalsByNodeId = new Map<Id, Terminal>();
/** The time of day a program writes into a mail (Java's new Date()). */
private readonly wallClock: () => number;
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
@@ -1046,6 +1048,7 @@ export class NetworkRuntimeEngine {
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.clear();
this.terminalsByNodeId.clear();
this.arpLookups = [];
this.protocolThreads.clear();
this.dnsQueriesBySocketId.clear();
@@ -1136,6 +1139,11 @@ export class NetworkRuntimeEngine {
return this.emailServersByNodeId.get(nodeId);
}
/** The node's command line while the simulation runs, if it has one installed. */
terminal(nodeId: Id): Terminal | undefined {
return this.terminalsByNodeId.get(nodeId);
}
/** The node's e-mail program while the simulation runs, if it has one installed. */
emailClient(nodeId: Id): EmailClient | undefined {
return this.emailClientsByNodeId.get(nodeId);
@@ -1171,6 +1179,7 @@ export class NetworkRuntimeEngine {
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.clear();
this.terminalsByNodeId.clear();
for (const node of snapshot.nodes) {
const echoServer = snapshot.echoServersByNodeId.get(node.id);
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
@@ -1196,6 +1205,17 @@ export class NetworkRuntimeEngine {
),
);
}
if (snapshot.terminalNodeIds.has(node.id)) {
let files = snapshot.fileSystemsByNodeId.get(node.id);
if (files === undefined) {
// A node without files gets an empty file system, kept in the snapshot.
files = new VirtualFileSystem();
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, {
fileSystemsByNodeId: new Map([...this.stateValue.topologySnapshot.fileSystemsByNodeId, [node.id, files]]),
});
}
this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, files));
}
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
if (emailClient !== undefined) {
const nodeId = node.id;