import { AgentOs } from "@rivet-dev/agentos-core";
import { allowAll } from "@rivet-dev/agentos-core/internal/runtime-compat";
import assert from "node:assert/strict";
import { copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import pkg from "@xterm/headless";
const { Terminal } = pkg;
const SNAP_DIR = "/home/nathan/progress/agent-os/2026-06-28-just-shell-fix/vim-snapshots";
const VIM_COMMAND_DIR = "/home/nathan/.herdr/workspaces/agent-os/workspace-brave-forest-1e98/.local-cmds";
const VIM_ARGS = JSON.parse(process.env.VIM_ARGS || '["-N","-u","NONE","-i","NONE","-n","--cmd","set noesckeys"]');
const VIM_ENV = JSON.parse(process.env.VIM_ENV || '{"TERM":"xterm"}');
const ESC = Uint8Array.of(0x1b);
// Materialize the raw `.local-cmds/vim` wasm into a self-contained agentOS
// package directory (`bin/vim` + package.json + agentos-package.json), matching
// the package model consumed by `AgentOs.create({ software: [
] })`.
function materializeVimPackage() {
const packageDir = mkdtempSync(join(tmpdir(), "agentos-vim-pkg-"));
mkdirSync(join(packageDir, "bin"));
copyFileSync(resolve(VIM_COMMAND_DIR, "vim"), join(packageDir, "bin", "vim"));
writeFileSync(join(packageDir, "package.json"), JSON.stringify({ name: "vim", version: "0.0.0" }));
writeFileSync(join(packageDir, "agentos-package.json"), JSON.stringify({ name: "vim" }));
return packageDir;
}
const term = new Terminal({ cols: 80, rows: 24, allowProposedApi: true });
let writes = Promise.resolve();
const screen = () => {
const b = term.buffer.active;
const lines = [];
for (let y = 0; y < 24; y++) {
const ln = b.getLine(y);
lines.push((ln ? ln.translateToString(true) : "").replace(/\s+$/, ""));
}
return lines.join("\n").replace(/\n+$/, "\n");
};
let n = 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const settle = async (ms = 700) => {
await sleep(ms);
await writes;
await sleep(20);
await writes;
};
const waitForScreen = async (predicate, label, timeoutMs = 20_000) => {
const deadline = Date.now() + timeoutMs;
let latest = screen();
while (Date.now() < deadline) {
await settle(250);
latest = screen();
if (predicate(latest)) return latest;
}
throw new Error(`timed out waiting for ${label}\n\n${latest}`);
};
const snap = async (label, ms = 700) => {
await settle(ms);
const nn = String(n).padStart(2, "0");
const current = screen();
writeFileSync(`${SNAP_DIR}/${nn}.txt`, `## ${nn} — ${label}\n## (vim args: ${JSON.stringify(VIM_ARGS)})\n----- screen 80x24 -----\n${current}\n`);
console.log(`snap ${nn}: ${label}`);
n++;
return current;
};
mkdirSync(SNAP_DIR, { recursive: true });
const vm = await AgentOs.create({
permissions: allowAll,
software: [materializeVimPackage()],
});
await vm.mkdir("/work", { recursive: true });
const { shellId } = vm.openShell({ command: "vim", args: VIM_ARGS, cols: 80, rows: 24, cwd: "/work", env: VIM_ENV });
vm.onShellData(shellId, (d) => {
const bytes = Buffer.from(d);
writes = writes.then(() => new Promise((resolve) => term.write(bytes, resolve)));
});
await waitForScreen((s) => s.includes("VIM - Vi IMproved") && !s.includes("Press ENTER"), "vim startup");
const startup = await snap("startup (vim launched, no file)", 300);
assert.match(startup, /VIM - Vi IMproved/);
assert.doesNotMatch(startup, /Press ENTER/);
const SEQ = [
[":", "type : (enter command-line)"], ["e", "e"], [" ", "space"],
["h", "h"], ["e", "e"], ["l", "l"], ["l", "l"], ["o", "o"], [".", "."], ["t", "t"], ["x", "x"], ["t", "t"],
["\r", "Enter -> run :e hello.txt (open new file)"],
["i", "i (enter INSERT mode)"],
["h", "h"], ["e", "e"], ["l", "l"], ["l", "l"], ["o", "o"], [" ", "space"], ["w", "w"], ["o", "o"], ["r", "r"], ["l", "l"], ["d", "d"],
[ESC, "ESC (back to NORMAL)", 900],
[":", "type : (command-line)"], ["w", "w"], ["\r", "Enter -> run :w (write file)"],
];
const snapshots = [];
for (const [key, label, delayMs] of SEQ) {
await vm.writeShell(shellId, key);
snapshots.push(await snap(label, delayMs ?? 650));
}
const opened = snapshots[12] ?? "";
assert.match(opened, /"hello\.txt" \[New\]/);
assert.doesNotMatch(opened, /No Name/);
const insert = snapshots[13] ?? "";
assert.match(insert, /-- INSERT --/);
const typed = snapshots[24] ?? "";
assert.match(typed, /hello world/);
const normal = snapshots[25] ?? "";
assert.doesNotMatch(normal, /-- INSERT --/);
assert.match(normal, /hello world/);
const written = snapshots.at(-1) ?? "";
assert.match(written, /"hello\.txt" \[New\] 1L, 12B written/);
assert.doesNotMatch(written, /E32/);
assert.doesNotMatch(written, /Press ENTER/);
assert.doesNotMatch(written, /\^\[/);
await vm.writeShell(shellId, ":q\r");
await settle(1500);
// read the file back from the VM
let fileContent = "";
try { fileContent = Buffer.from(await vm.readFile("/work/hello.txt")).toString("utf8"); } catch (e) { fileContent = "ERR: " + (e?.message || e); }
writeFileSync(`${SNAP_DIR}/FILE.txt`, `# /work/hello.txt after :w\n${JSON.stringify(fileContent)}\n\n---raw---\n${fileContent}`);
assert.equal(fileContent, "hello world\n");
console.log("FILE /work/hello.txt =", JSON.stringify(fileContent));
await vm.dispose().catch(() => {});
process.exit(0);