@annetutil/petrusshka
v0.12.1
Published
Petrusshka network CLI emulator for browsers and workers
Readme
@annetutil/petrusshka
Network CLI devices running in Go WebAssembly. Works in a browser or module worker.
import { loadPetrusshka } from "@annetutil/petrusshka";
const runtime = await loadPetrusshka();
const lab = runtime.createLab([
{ id: "edge", vendor: "huawei", profile: "huawei-vrp",
initial: { hostname: "edge", interfaces: [{ name: "GigabitEthernet0/0/1" }] } },
]);
const session = lab.open("edge");
const completion = lab.assist(session.sessionId, "dis", "tab");
const result = lab.execute(session.sessionId, "display current-configuration");
const snapshot = lab.snapshot();
lab.close(session.sessionId);
lab.dispose();lab.assist(sessionId, line, key) completes or describes an unfinished command
without executing it, adding it to history, or changing device state. key is
"tab", "help", or "f1"; the returned value is
{handled, line, output}. Vendor policy decides which help key is handled:
Huawei, Cisco, Arista, and Junos use "help", RouterOS uses "f1", and PC
does not handle a help key. Tab behavior also follows the vendor: cycling for
Huawei, unique completion for Cisco and Arista, a completion list for Junos,
and common-prefix completion for RouterOS and PC.
Assistance is limited to one line of at most 64 KiB without NUL, CR, or LF. Suggestions come from the implemented command tree, schema, and current model; they do not promise byte-exact vendor help. Incomplete quotes are not completed. PC path completion reads only the virtual filesystem and does not perform shell expansion.
Pass snapshot as the second argument to createLab to restore device state.
Sessions are not restored. A command returning question accepts its answer through
another execute call on the same session. failed does not roll back previous
commands. exitCode contains the exact status when the session supplies one.
Unknown API operations and invalid sessions throw errors.
xterm terminal adapter
The browser-only @annetutil/petrusshka/terminal entrypoint connects an already
opened xterm terminal to execute and assist. It accepts synchronous or
asynchronous callbacks and uses the standard CommandResult and Assistance
shapes from the main SDK entrypoint.
Available since @annetutil/petrusshka 0.11.0; requires xterm.js 6.
import {Terminal} from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import type {Assistance, CommandResult, OpenResult} from "@annetutil/petrusshka";
import {attachTerminal} from "@annetutil/petrusshka/terminal";
interface Transport {
open(deviceId: string): Promise<OpenResult>;
execute(sessionId: string, line: string): Promise<CommandResult>;
assist(
sessionId: string,
line: string,
key: "tab" | "help" | "f1",
): Promise<Assistance>;
close(sessionId: string): Promise<void>;
}
declare const transport: Transport; // RPC client for a module worker
const session = await transport.open("edge");
const terminal = new Terminal();
terminal.open(document.querySelector<HTMLElement>("#terminal")!);
const binding = attachTerminal(terminal, {
prompt: session.prompt,
history: [], // Optional saved command history (since 0.11.1)
execute: (line) => transport.execute(session.sessionId, line),
assist: (line, key) => transport.assist(session.sessionId, line, key),
});The binding owns terminal input and output until binding.dispose(). The
application still owns the xterm instance, transport, and CLI session. Before
a reset or reconnect, stop input, wait for the current request, and detach the
binding:
binding.setEnabled(false);
await binding.whenIdle();
binding.dispose();
await transport.close(session.sessionId);
terminal.dispose();setEnabled(false) lets in-flight work finish, preserves queued actions, and
ignores new input. setEnabled(true) resumes only this manual pause. If
execute or assist throws or rejects, binding.faulted becomes true, the
adapter displays the error, preserves later queued actions, and ignores new
input until binding.recover(). Recovery never retries the failed request or
changes the enabled state. After failed execute, it keeps the command in the
transcript but clears the editable draft before continuing the queue; after
failed assist, it preserves the draft.
clear() works while disabled, faulted, or closed. It clears the screen and
scrollback while keeping the prompt, current line, and history; a closed session
does not print another prompt. whenIdle() resolves when the current drain
stops, including when disabled or faulted. Supported editing keys are Enter,
Backspace, Up/Down, Tab, ?, F1, Ctrl+C, Ctrl+V followed by one printable
character, and Cmd+K. Other cursor editing keys are not implemented.
onResult, onError, and onBusyChange may be async. The adapter does not
wait for them. Their errors are reported through globalThis.reportError or
console.error and do not stop the queue; an onError failure does not invoke
onError again.
Do not call terminal.write() while attached: redraws assume the binding is the
only writer. External writes are safe after binding.dispose().
The main @annetutil/petrusshka entrypoint does not import DOM, xterm, or the
terminal adapter and continues to work in a module worker. The terminal
entrypoint imports the official @xterm/addon-serialize runtime dependency to
preserve colored output when an edited prompt has moved into scrollback.
@xterm/xterm remains an optional peer used by the adapter's types; install it
only when using the adapter.
NETCONF
Profiles can expose NETCONF through the same device state as their CLI.
lab.netconfCapabilities(deviceId) returns the profile capabilities, or an
empty array when NETCONF is unavailable. lab.netconf(deviceId, xml) accepts a
complete unframed rpc element and returns the complete rpc-reply.
Each netconf call creates a short-lived session inside WebAssembly; it does
not open SSH. Invalid XML and API usage throw an error. A valid request rejected
by NETCONF returns rpc-error in the XML response. Long-lived sessions,
lock, and the NETCONF candidate datastore are not supported.
Build with make wasm:build; package with make wasm:pack from the repository root.
Go 1.26.0 or newer is required. The Go compiler and bundled wasm_exec.js must
come from the same toolchain.
The build derives the exported Vendor and Profile unions from the Go
registry and writes src/vendor-types.ts. Do not edit the
generated file. Consumers continue to import both types from
@annetutil/petrusshka.
Devices with a filesystem
const lab = runtime.createLab([
{ id: "pc", vendor: "pc", initial: { hostname: "pc-lab" } },
]);
lab.writeFile("pc", "/etc/demo.conf", "enabled=yes\n");
const session = lab.open("pc");
const result = lab.execute(session.sessionId, `
for file in /etc/*.conf; do
cat "$file"
done
`);
console.log(result.output);
console.log(result.exitCode); // 0
console.log(lab.readFile("pc", "/etc/demo.conf"));The file API reads and replaces UTF-8 text at absolute virtual paths. Parent
folders must exist. Models that implement the filesystem capability share the
same data between shell sessions, file API calls and snapshots. The shell in
this example accepts multiple lines; an incomplete construct returns
question: "> " and continues through the next execute call. Devices without
the filesystem capability reject file API calls. Shell commands never run host
programs.
Релиз
Версия @annetutil/petrusshka совпадает с версией проекта: тег v1.2.3
публикует npm-пакет 1.2.3. Источник версии — git-тег; 0.0.0 в package.json
и lockfile служит заглушкой для локальной сборки.
Из чистой ветки main выполни make release:tag в корне репозитория.
По умолчанию повышается patch; для других уровней — make release:tag BUMP=minor
или make release:tag BUMP=major. Команда пушит main и новый тег в origin.
Без предыдущих тегов первый patch-релиз — v0.0.1.
Workflow npm-publish.yml запускает проверки, собирает WASM, подставляет версию
из тега, публикует npm-пакет и затем создаёт GitHub Release.
Для публикации используй secret репозитория NPM_TOKEN с правом публикации
@annetutil/petrusshka и включённым Bypass 2FA. Пакет публикуется без provenance,
поскольку исходный репозиторий приватный.
PR и ручной запуск без параметра tag выполняют только проверки и сборку.
Если публикация не состоялась и нужно использовать исправленный workflow,
запусти его из main с существующим тегом:
gh workflow run npm-publish.yml --ref main -f tag=v1.2.3Workflow соберёт исходники указанного тега и опубликует ту же версию. Если npm-пакет уже опубликован, а создание GitHub Release завершилось ошибкой, восстанови только GitHub Release, без повторной публикации npm.
Для выпуска через агента используй локальный skill $release [patch|minor|major].
