diff --git a/public/demos/commands/contribute.mp4 b/public/demos/commands/contribute.mp4 index d226ae0..905b6bf 100644 Binary files a/public/demos/commands/contribute.mp4 and b/public/demos/commands/contribute.mp4 differ diff --git a/public/demos/commands/contribute.png b/public/demos/commands/contribute.png index 4ed1c7c..9f95164 100644 Binary files a/public/demos/commands/contribute.png and b/public/demos/commands/contribute.png differ diff --git a/public/demos/commands/contribute.txt b/public/demos/commands/contribute.txt index 5a3ea81..7cbff56 100644 --- a/public/demos/commands/contribute.txt +++ b/public/demos/commands/contribute.txt @@ -1,4 +1,17 @@ $ omm contribute --no-color -omm contribute requires benchmark uploads to be enabled. Run `omm setting upload --enable` or -`--ask` first. +Disk preflight passed: 50.2 GiB free on the tightest model volume. Each candidate is checked +again before download. +omm contribute - before you start: + - Downloads, benchmarks, and deletes GGUF models repeatedly until you press Esc + - Uses real bandwidth, disk space, and compute; runs unattended (no per-model confirmation) + - Uploads every benchmark result per your current upload policy (ask) + - Reserves space per candidate (central GGUF + worst-case engine copy + headroom); skips +anything that won't fit + - Uses a fixed 1024-token context and 128-token batch for comparable results + - Gates committed runtime memory before download; monitors paging and measurement stability +while running + - Defers transient memory shortages up to three times instead of losing the candidate + - Each benchmark has a 10-minute cutoff, with a status line every 30s +? Start contributing compute now? No +Cancelled. diff --git a/public/demos/commands/install.mp4 b/public/demos/commands/install.mp4 index 90045d6..c22cfda 100644 Binary files a/public/demos/commands/install.mp4 and b/public/demos/commands/install.mp4 differ diff --git a/public/demos/commands/install.png b/public/demos/commands/install.png index 427dadf..e8dad47 100644 Binary files a/public/demos/commands/install.png and b/public/demos/commands/install.png differ diff --git a/public/demos/commands/install.txt b/public/demos/commands/install.txt index cf01ca2..30c8b36 100644 --- a/public/demos/commands/install.txt +++ b/public/demos/commands/install.txt @@ -1,5 +1,5 @@ -$ omm install zzzz-totally-fake-model-name-xyz --no-color +$ omm install hf:example/oversized-405b-gguf:oversized-405b-q4_k_m.gguf --no-color -Unknown model 'zzzz-totally-fake-model-name-xyz'. Use a curated name (tinyllama-1.1b-q4, -llama3.1-8b-instruct-q4, mistral-7b-instruct-q4), an 'org/repo:file.gguf' ref (optionally -prefixed 'hf:' or 'ms:'), or a direct URL. +Warning: this hardware is predicted not to run oversized-405b-q4_k_m.gguf. +? Install anyway? No +Cancelled. diff --git a/public/demos/commands/manifest.json b/public/demos/commands/manifest.json index a406fcd..1080668 100644 --- a/public/demos/commands/manifest.json +++ b/public/demos/commands/manifest.json @@ -1,20 +1,20 @@ { "schemaVersion": 1, - "generatedAt": "2026-08-24T12:31:16.389037Z", + "generatedAt": "2026-08-24T13:15:20.904067Z", "provenance": { "repository": "https://github.com/omm-hippo/omm.git", "commit": "e66db6f82e8344c33473eb5f722ab41359a85231", "version": "0.2.167", "sourceWasClean": true, "captureScript": "scripts/capture-command-demos.py", - "processEvidence": "Each transcript is stdout+stderr captured from the real omm console script installed editable from this commit. Videos reveal only those captured transcript lines; no command result text is synthesized.", + "processEvidence": "Each transcript is stdout+stderr captured from the real omm console script installed editable from this commit. Interactive videos use VT100 screen checkpoints reconstructed from those real process bytes and record the exact input events; no command result text is synthesized.", "sourcePathPolicy": "The source clone, virtualenv, HOME, OMM_HOME, XDG cache, uv cache, and TMPDIR were disposable paths removed after capture. Absolute temporary paths are intentionally omitted from public artifacts." }, "safety": { "liveOmmRunRequests": 0, "liveWorkersDevRequests": 0, - "cliNetworkPolicy": "HTTP(S) and ALL_PROXY forced to unused 127.0.0.1:9", - "telemetryPolicy": "never", + "cliNetworkPolicy": "External HTTP(S) and ALL_PROXY forced to unused 127.0.0.1:9. The contribute demo alone allows a temporary GET-only loopback fixture on 127.0.0.1:11434 and fails on any POST.", + "telemetryPolicy": "never by default; contribute uses ask only until the captured 'n' response cancels before compute or upload", "userHomeTouched": false, "modelDownloadsStarted": false, "modelExecutionsStarted": false, @@ -52,35 +52,44 @@ ], "exitCode": 0, "expectedExitCode": 0, - "safetyPath": "Read-only search against the built-in catalog and catalog copied from the exact source commit. All CLI outbound HTTP(S) is routed to an unused loopback proxy, so live Hugging Face, ModelScope, omm.run, and Workers endpoints are not reached." + "safetyPath": "Read-only search against the built-in catalog and catalog copied from the exact source commit. All CLI outbound HTTP(S) is routed to an unused loopback proxy, so live Hugging Face, ModelScope, omm.run, and Workers endpoints are not reached.", + "outcome": "success" }, { "slug": "install", "src": "/demos/commands/install.mp4", "poster": "/demos/commands/install.png", "transcript": "/demos/commands/install.txt", - "duration": 5.75, + "duration": 5.375, "codec": "h264", "width": 1280, "height": 720, "pixelFormat": "yuv420p", "audioStreams": 0, - "bytes": 32395, - "sha256": "19f881cb78dac33bb088fa1a3f93c6ffe727c468e3d8dad8609774a83718a672", - "posterBytes": 58820, - "posterSha256": "07b5d11cefaaa548762752a0c7e58ce612dccf609f13f72d42a45787a70e21ac", - "transcriptBytes": 281, - "transcriptSha256": "e9aacacb2db736774f8fb4465375f91629d476a39316850c55a15c71c2e31777", - "command": "omm install zzzz-totally-fake-model-name-xyz --no-color", + "bytes": 28699, + "sha256": "7b83ae73c5ce6a3039c2e3951f489128a76602abb9b02a2da0bedfbc31a0ee9c", + "posterBytes": 53385, + "posterSha256": "f81b7fda094d31e0cad3948d8590fdeb9ea6b0949a913586c71744e0bd2fd040", + "transcriptBytes": 191, + "transcriptSha256": "b489718b8aec3ed9e1171fb6fe44f40d9cb9b418ba96cda14d687bd457ae2b05", + "command": "omm install hf:example/oversized-405b-gguf:oversized-405b-q4_k_m.gguf --no-color", "argv": [ "omm", "install", - "zzzz-totally-fake-model-name-xyz", + "hf:example/oversized-405b-gguf:oversized-405b-q4_k_m.gguf", "--no-color" ], - "exitCode": 1, - "expectedExitCode": 1, - "safetyPath": "The local model-reference parser rejects the deliberately invalid name before the downloader, checksum, linking, or runtime code can run. Any best-effort suggestion lookup is contained by the unused loopback proxy." + "exitCode": 0, + "expectedExitCode": 0, + "outcome": "cancelled", + "captureMode": "pseudo-tty", + "inputEvents": [ + "n - cancel at hardware-fit warning" + ], + "fixture": { + "description": "Syntactically valid example/oversized-405b reference; no remote model is claimed to exist." + }, + "safetyPath": "A syntactically valid but deliberately non-published 405B fixture reference reaches OMM's real hardware-fit warning. The captured 'n' response cancels before provider metadata, downloader, checksum, linking, or runtime code." }, { "slug": "run", @@ -93,22 +102,35 @@ "height": 720, "pixelFormat": "yuv420p", "audioStreams": 0, - "bytes": 17551, - "sha256": "b7d32e21f22b35dbae4411e3d19d4af9564fec00f8b7cbb9aea98b0899b5644b", - "posterBytes": 31483, - "posterSha256": "e13592feed09e93db7aa9f190e6498e5057cd8fe55043dda63bea7947d8b0fb7", - "transcriptBytes": 96, - "transcriptSha256": "ff85d7635259b498bd24a7aa06a706922f33b0360da96ebd0289fdc46b1a4ac7", - "command": "omm run demo-model.gguf --no-color", + "bytes": 21285, + "sha256": "df478786b4014d8837385e8eefce5d4e3caedd8af659c2812ab44ac7ad697914", + "posterBytes": 40275, + "posterSha256": "34d1b99e897f9d87f69db45f02ae43eaf4bbfd2f05b863f4bd9b13fce2ec6560", + "transcriptBytes": 102, + "transcriptSha256": "457cf89ec4a646bb1d5fa0f68b0584ce45639046ca7611bb3d73870fa0b15e7f", + "command": "omm run --no-color", "argv": [ "omm", "run", - "demo-model.gguf", "--no-color" ], "exitCode": 1, "expectedExitCode": 1, - "safetyPath": "The isolated registry is empty, so the command exits at the local not-installed guard before selecting or launching any runner." + "outcome": "cancelled", + "captureMode": "pseudo-tty", + "inputEvents": [ + "Escape - cancel model picker" + ], + "fixture": { + "description": "Two isolated registry names only; no model files, engine links, or runnable weights exist.", + "kind": "metadata-only isolated registry", + "entries": [ + "fixture-alpha.gguf", + "fixture-beta.gguf" + ], + "modelFilesCreated": false + }, + "safetyPath": "Two metadata-only model names are placed in the isolated registry to exercise the real model picker. Escape cancels before engine selection or launcher code." }, { "slug": "recommend", @@ -136,52 +158,72 @@ ], "exitCode": 0, "expectedExitCode": 0, - "safetyPath": "JSON mode is read-only and never enters the interactive installer. The recommendation artifact is copied from the exact source commit and remote model/rules URLs are disabled in the isolated config." + "safetyPath": "JSON mode is read-only and never enters the interactive installer. The recommendation artifact is copied from the exact source commit and remote model/rules URLs are disabled in the isolated config.", + "outcome": "success" }, { "slug": "contribute", "src": "/demos/commands/contribute.mp4", "poster": "/demos/commands/contribute.png", "transcript": "/demos/commands/contribute.txt", - "duration": 5.375, + "duration": 6.208, "codec": "h264", "width": 1280, "height": 720, "pixelFormat": "yuv420p", "audioStreams": 0, - "bytes": 20538, - "sha256": "d4ea47430208d9b228472cbdddf03b929b5a0c6c6a16c90aac5e03c14218949a", - "posterBytes": 36459, - "posterSha256": "314530781d9aae97250c94d77c842048d7d69c11a196eccb0b03fd6b88c6e77e", - "transcriptBytes": 138, - "transcriptSha256": "c39cdeabb44609f35a1cb9eb085da70522df005845085f235e9f7969d9f385c4", + "bytes": 74648, + "sha256": "1128cc8977aed8f7b8982e96524fbaf883a148660c65737d9b1cb67bb3a39372", + "posterBytes": 142872, + "posterSha256": "3d9819dbf8941028291f7128582bd98d59b41dd0178f031a409deeea0ea6f8f1", + "transcriptBytes": 938, + "transcriptSha256": "8e7886e3b7fb7ba52d7f1b18aaa3c43e6052992b2ba175e70721e72fe4c84f9a", "command": "omm contribute --no-color", "argv": [ "omm", "contribute", "--no-color" ], - "exitCode": 1, - "expectedExitCode": 1, - "safetyPath": "The isolated config sets telemetry_send_policy=never. The command exits at that first guard before engine detection, downloads, benchmarking, model execution, deletion, or telemetry/error-report upload." + "exitCode": 0, + "expectedExitCode": 0, + "outcome": "cancelled", + "captureMode": "pseudo-tty", + "inputEvents": [ + "n - cancel before compute" + ], + "fixture": { + "description": "Temporary GET-only loopback API on 127.0.0.1:11434; capture aborts if the port is occupied or any POST occurs.", + "kind": "empty GET-only loopback Ollama API", + "address": "127.0.0.1:11434", + "allowedRequests": [ + "GET /api/tags", + "GET /api/ps", + "GET /api/version" + ], + "requestsObserved": [ + "GET /api/tags" + ], + "postRequestsObserved": 0 + }, + "safetyPath": "A temporary empty loopback Ollama API fixture permits only GET /api/tags so the real safety notice can render. The captured 'n' response cancels at 'Start contributing compute now?' before downloads, benchmarks, model execution, deletion, POST requests, or uploads." }, { "slug": "setup", "src": "/demos/commands/setup.mp4", "poster": "/demos/commands/setup.png", "transcript": "/demos/commands/setup.txt", - "duration": 7.042, + "duration": 7.875, "codec": "h264", "width": 1280, "height": 720, "pixelFormat": "yuv420p", "audioStreams": 0, - "bytes": 42089, - "sha256": "b3e42e08b50a72a443d7b3edbfcc7ee3d384ffb7b64e09ebcfed92fcb974abf0", - "posterBytes": 97283, - "posterSha256": "cbba4eaaee79f037cae869190581f69a82ede6387be77ba710fa128badb493f7", - "transcriptBytes": 988, - "transcriptSha256": "35c45946c286272f74b587fc54e67d94aac70e358d9e8fc63d20e5eeb2925a93", + "bytes": 137449, + "sha256": "c932f0a175b86362ac50d85d5631231128dfb251641df2236442f9359d939cc6", + "posterBytes": 103585, + "posterSha256": "6a0146dd5d8baf542ad4b6778154ddbf7d0373d779a8ea0a23d72adf912bc63b", + "transcriptBytes": 515, + "transcriptSha256": "c659b887334c2cc3c7ecac5676acda7b933fb3aee2e48dd0b48a509a3dcd954c", "command": "omm setup --no-color", "argv": [ "omm", @@ -190,7 +232,14 @@ ], "exitCode": 1, "expectedExitCode": 1, - "safetyPath": "stdin is explicitly closed. The real wizard prints its banner and isolated hardware summary, then exits at the non-interactive engine-selection guard before selection or any engine installer can run." + "outcome": "cancelled", + "captureMode": "pseudo-tty", + "inputEvents": [ + "Enter - accept recommended theme", + "Escape - cancel engine checklist" + ], + "fixture": null, + "safetyPath": "A pseudo-TTY accepts the recommended theme, renders the real hardware summary and engine checklist, then sends Escape. No engine is selected and the installer is never entered." } ] } diff --git a/public/demos/commands/run.mp4 b/public/demos/commands/run.mp4 index 55a427c..9234059 100644 Binary files a/public/demos/commands/run.mp4 and b/public/demos/commands/run.mp4 differ diff --git a/public/demos/commands/run.png b/public/demos/commands/run.png index ee7e4bd..e55288a 100644 Binary files a/public/demos/commands/run.png and b/public/demos/commands/run.png differ diff --git a/public/demos/commands/run.txt b/public/demos/commands/run.txt index d0c78a0..aef22d9 100644 --- a/public/demos/commands/run.txt +++ b/public/demos/commands/run.txt @@ -1,3 +1,7 @@ -$ omm run demo-model.gguf --no-color +$ omm run --no-color -demo-model.gguf is not installed via omm. See `omm list`. +? Which model do you want to run? (Use arrow keys) + +Cancelled by user + +Aborted! diff --git a/public/demos/commands/setup.mp4 b/public/demos/commands/setup.mp4 index 0c82ed2..390b0d9 100644 Binary files a/public/demos/commands/setup.mp4 and b/public/demos/commands/setup.mp4 differ diff --git a/public/demos/commands/setup.png b/public/demos/commands/setup.png index fba0d4f..8a9371a 100644 Binary files a/public/demos/commands/setup.png and b/public/demos/commands/setup.png differ diff --git a/public/demos/commands/setup.txt b/public/demos/commands/setup.txt index 4128887..86dd28b 100644 --- a/public/demos/commands/setup.txt +++ b/public/demos/commands/setup.txt @@ -1,12 +1,7 @@ $ omm setup --no-color - ██████╗ ███╗ ███╗███╗ ███╗ -██╔═══██╗████╗ ████║████╗ ████║ -██║ ██║██╔████╔██║██╔████╔██║ -██║ ██║██║╚██╔╝██║██║╚██╔╝██║ -╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║ - ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ -Let's get you set up. + high-contrast + no-color Your machine Field Value @@ -14,9 +9,13 @@ Let's get you set up. CPU Apple M5 RAM (total) 24.0 GB Model budget 19.2 GB - Free right now 5.6 GB (close other apps before running big models) + Free right now 5.7 GB (close other apps before running big models) GPU Apple M5 omm home $CAPTURE_ROOT/h… - (51.0 GB free) + (50.2 GB free) -Engine selection requires an interactive terminal. Re-run this command from a real terminal. +? Install any local AI runners you'd like to use? (space to select, enter to confirm) + +Cancelled by user + +Aborted! diff --git a/scripts/capture-command-demos.py b/scripts/capture-command-demos.py index 8f16ec6..c506564 100644 --- a/scripts/capture-command-demos.py +++ b/scripts/capture-command-demos.py @@ -10,19 +10,30 @@ from __future__ import annotations import argparse +import codecs +import fcntl import hashlib import json import math import os +import pty import re +import select import shlex import shutil +import struct import subprocess +import sys import tempfile +import termios +import threading import time +from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +from typing import Iterator ROOT = Path(__file__).resolve().parents[1] @@ -40,12 +51,23 @@ CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") +@dataclass(frozen=True) +class InputStep: + pattern: str + response: bytes + label: str + + @dataclass(frozen=True) class Demo: slug: str argv: tuple[str, ...] expected_exit_code: int + outcome: str safety_path: str + input_steps: tuple[InputStep, ...] = () + context_label: str = "real CLI capture" + fixture: str | None = None @property def command(self) -> str: @@ -57,29 +79,43 @@ def command(self) -> str: "search", ("search", "llama", "--skip-ms", "--limit", "4", "--no-color"), 0, + "success", "Read-only search against the built-in catalog and catalog copied from the exact source commit. " "All CLI outbound HTTP(S) is routed to an unused loopback proxy, so live " "Hugging Face, ModelScope, omm.run, and Workers endpoints are not reached.", ), Demo( "install", - ("install", "zzzz-totally-fake-model-name-xyz", "--no-color"), - 1, - "The local model-reference parser rejects the deliberately invalid name " - "before the downloader, checksum, linking, or runtime code can run. Any " - "best-effort suggestion lookup is contained by the unused loopback proxy.", + ( + "install", + "hf:example/oversized-405b-gguf:oversized-405b-q4_k_m.gguf", + "--no-color", + ), + 0, + "cancelled", + "A syntactically valid but deliberately non-published 405B fixture reference " + "reaches OMM's real hardware-fit warning. The captured 'n' response cancels " + "before provider metadata, downloader, checksum, linking, or runtime code.", + (InputStep("Install anyway?", b"n\r", "n - cancel at hardware-fit warning"),), + "fixture ref · cancelled before download", + "Syntactically valid example/oversized-405b reference; no remote model is claimed to exist.", ), Demo( "run", - ("run", "demo-model.gguf", "--no-color"), + ("run", "--no-color"), 1, - "The isolated registry is empty, so the command exits at the local " - "not-installed guard before selecting or launching any runner.", + "cancelled", + "Two metadata-only model names are placed in the isolated registry to exercise " + "the real model picker. Escape cancels before engine selection or launcher code.", + (InputStep("Which model do you want to run?", b"\x1b", "Escape - cancel model picker"),), + "metadata fixture · cancelled before launch", + "Two isolated registry names only; no model files, engine links, or runnable weights exist.", ), Demo( "recommend", ("recommend", "--json", "--no-color"), 0, + "success", "JSON mode is read-only and never enters the interactive installer. The " "recommendation artifact is copied from the exact source commit and remote " "model/rules URLs are disabled in the isolated config.", @@ -87,18 +123,29 @@ def command(self) -> str: Demo( "contribute", ("contribute", "--no-color"), - 1, - "The isolated config sets telemetry_send_policy=never. The command exits at " - "that first guard before engine detection, downloads, benchmarking, model " - "execution, deletion, or telemetry/error-report upload.", + 0, + "cancelled", + "A temporary empty loopback Ollama API fixture permits only GET /api/tags so " + "the real safety notice can render. The captured 'n' response cancels at " + "'Start contributing compute now?' before downloads, benchmarks, model " + "execution, deletion, POST requests, or uploads.", + (InputStep("Start contributing compute now?", b"n\r", "n - cancel before compute"),), + "empty local API fixture · cancelled before compute", + "Temporary GET-only loopback API on 127.0.0.1:11434; capture aborts if the port is occupied or any POST occurs.", ), Demo( "setup", ("setup", "--no-color"), 1, - "stdin is explicitly closed. The real wizard prints its banner and isolated " - "hardware summary, then exits at the non-interactive engine-selection guard " - "before selection or any engine installer can run.", + "cancelled", + "A pseudo-TTY accepts the recommended theme, renders the real hardware summary " + "and engine checklist, then sends Escape. No engine is selected and the installer " + "is never entered.", + ( + InputStep("Pick a color theme for omm's output:", b"\r", "Enter - accept recommended theme"), + InputStep("Install any local AI runners you'd like to use?", b"\x1b", "Escape - cancel engine checklist"), + ), + "interactive wizard · cancelled before install", ), ) @@ -175,15 +222,18 @@ def project_version(source: Path) -> str: def prepare_runtime(workspace: Path, source: Path, commit: str) -> tuple[Path, Path, dict[str, str]]: venv = workspace / "venv" + capture_libraries = workspace / "capture-libraries" uv_env = {**os.environ, "UV_CACHE_DIR": str(workspace / "uv-cache")} python_request = os.environ.get("OMM_DEMO_PYTHON", ">=3.10") for argv in ( ["uv", "venv", "--quiet", str(venv), "--python", python_request], ["uv", "pip", "install", "--quiet", "--python", str(venv / "bin" / "python"), "-e", str(source)], + ["uv", "pip", "install", "--quiet", "--target", str(capture_libraries), "pyte==0.8.2"], ): result = run(argv, env=uv_env) if result.returncode != 0: raise SystemExit(f"Isolated runtime setup failed: {shlex.join(argv)}") + sys.path.insert(0, str(capture_libraries)) demo_home = workspace / "home" omm_home = demo_home / ".omm-demo" @@ -273,8 +323,7 @@ def normalize_transcript(raw: str, replacements: dict[str, str]) -> str: return "\n".join(lines) + "\n" -def capture_demo(demo: Demo, env: dict[str, str], workspace: Path) -> tuple[int, str]: - result = run(["omm", *demo.argv], cwd=workspace, env=env, capture=True) +def replacement_map(env: dict[str, str], workspace: Path) -> dict[str, str]: replacements: dict[str, str] = {} for key in ("OMM_HOME", "HOME", "TMPDIR", "XDG_CACHE_HOME"): value = env[key] @@ -282,20 +331,263 @@ def capture_demo(demo: Demo, env: dict[str, str], workspace: Path) -> tuple[int, replacements[str(Path(value).resolve())] = f"${key}" replacements[str(workspace)] = "$CAPTURE_ROOT" replacements[str(workspace.resolve())] = "$CAPTURE_ROOT" - transcript = normalize_transcript(result.stdout or "", replacements) - if result.returncode != demo.expected_exit_code: + return replacements + + +def replace_paths(text: str, replacements: dict[str, str]) -> str: + for original, placeholder in sorted(replacements.items(), key=lambda item: len(item[0]), reverse=True): + text = text.replace(original, placeholder) + return text + + +class _FixtureHandler(BaseHTTPRequestHandler): + requests_seen: list[tuple[str, str]] = [] + + def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler contract + self.requests_seen.append(("GET", self.path)) + if self.path == "/api/tags": + payload = {"models": []} + elif self.path == "/api/ps": + payload = {"models": []} + elif self.path == "/api/version": + payload = {"version": "0.0.0-empty-capture-fixture"} + else: + self.send_response(404) + self.end_headers() + return + encoded = json.dumps(payload).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + self.wfile.write(encoded) + + def do_POST(self) -> None: # noqa: N802 - reject every state-changing/runtime path + self.requests_seen.append(("POST", self.path)) + self.send_response(409) + self.end_headers() + + def log_message(self, _format: str, *args: object) -> None: + return + + +@contextmanager +def demo_fixture( + demo: Demo, env: dict[str, str], omm_home: Path +) -> Iterator[tuple[dict[str, str], dict[str, object] | None]]: + demo_env = dict(env) + config_path = omm_home / "config.json" + registry_path = omm_home / "models.json" + original_config = config_path.read_bytes() + original_registry = registry_path.read_bytes() if registry_path.exists() else None + server: ThreadingHTTPServer | None = None + server_thread: threading.Thread | None = None + fixture_record: dict[str, object] | None = ( + {"description": demo.fixture} if demo.fixture is not None else None + ) + try: + if demo.slug == "run": + registry_path.write_text( + json.dumps( + { + "fixture-alpha.gguf": {"linked": {}}, + "fixture-beta.gguf": {"linked": {}}, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + fixture_record = { + "description": demo.fixture, + "kind": "metadata-only isolated registry", + "entries": ["fixture-alpha.gguf", "fixture-beta.gguf"], + "modelFilesCreated": False, + } + elif demo.slug == "contribute": + config = json.loads(original_config) + config.update( + telemetry_send_policy="ask", + telemetry_endpoint=None, + telemetry_backend="local", + error_report_send_policy="never", + ) + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") + registry_path.write_text("{}\n", encoding="utf-8") + _FixtureHandler.requests_seen = [] + try: + server = ThreadingHTTPServer(("127.0.0.1", 11434), _FixtureHandler) + except OSError as error: + raise SystemExit( + "contribute fixture requires unused 127.0.0.1:11434; refusing to " + f"contact or replace an existing runtime ({error})" + ) from error + server.daemon_threads = True + server_thread = threading.Thread(target=server.serve_forever, daemon=True) + server_thread.start() + demo_env["NO_PROXY"] = "localhost,127.0.0.1" + demo_env["no_proxy"] = "localhost,127.0.0.1" + fixture_record = { + "description": demo.fixture, + "kind": "empty GET-only loopback Ollama API", + "address": "127.0.0.1:11434", + "allowedRequests": ["GET /api/tags", "GET /api/ps", "GET /api/version"], + } + yield demo_env, fixture_record + finally: + if server is not None: + server.shutdown() + server.server_close() + if server_thread is not None: + server_thread.join(timeout=2) + unexpected = [ + request + for request in _FixtureHandler.requests_seen + if request[0] != "GET" + or request[1] not in {"/api/tags", "/api/ps", "/api/version"} + ] + if fixture_record is not None: + fixture_record["requestsObserved"] = [ + f"{method} {path}" for method, path in _FixtureHandler.requests_seen + ] + fixture_record["postRequestsObserved"] = sum( + method == "POST" for method, _path in _FixtureHandler.requests_seen + ) + if unexpected: + raise SystemExit( + f"contribute fixture observed forbidden runtime requests: {unexpected}" + ) + config_path.write_bytes(original_config) + if original_registry is None: + registry_path.unlink(missing_ok=True) + else: + registry_path.write_bytes(original_registry) + + +def terminal_screen_lines(screen: object, replacements: dict[str, str]) -> list[str]: + lines = [replace_paths(line.rstrip(), replacements) for line in screen.display] + while lines and not lines[0]: + lines.pop(0) + while lines and not lines[-1]: + lines.pop() + return lines + + +def capture_interactive( + demo: Demo, + env: dict[str, str], + workspace: Path, + replacements: dict[str, str], +) -> tuple[int, str, list[list[str]], list[str]]: + import pyte + + columns, rows = 96, 22 + interactive_env = {**env, "TERM": "xterm-256color", "COLUMNS": str(columns), "LINES": str(rows)} + master, slave = pty.openpty() + fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", rows, columns, 0, 0)) + process = subprocess.Popen( + ["omm", *demo.argv], + cwd=workspace, + env=interactive_env, + stdin=slave, + stdout=slave, + stderr=slave, + close_fds=True, + ) + os.close(slave) + screen = pyte.Screen(columns, rows) + stream = pyte.Stream(screen) + decoder = codecs.getincrementaldecoder("utf-8")("replace") + raw = bytearray() + snapshots: list[list[str]] = [] + sent_labels: list[str] = [] + next_step = 0 + cpr_answered = False + deadline = time.monotonic() + 30 + try: + while time.monotonic() < deadline: + readable, _, _ = select.select([master], [], [], 0.1) + if readable: + try: + chunk = os.read(master, 65_536) + except OSError: + break + if not chunk: + break + raw.extend(chunk) + stream.feed(decoder.decode(chunk)) + if not cpr_answered and b"\x1b[6n" in chunk: + # prompt_toolkit asks a real terminal for the cursor position + # before accepting keys. Answer as a minimal VT100 terminal so + # the scripted input is not consumed as the CPR response. + os.write(master, b"\x1b[1;1R") + cpr_answered = True + continue + current = terminal_screen_lines(screen, replacements) + if current and (not snapshots or current != snapshots[-1]): + snapshots.append(current) + if next_step < len(demo.input_steps): + step = demo.input_steps[next_step] + if step.pattern.encode("utf-8") in raw: + os.write(master, step.response) + sent_labels.append(step.label) + next_step += 1 + if process.poll() is not None: + break + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=2) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=2) + diagnostic = normalize_transcript(raw.decode("utf-8", "replace"), replacements) + raise SystemExit( + f"{demo.slug}: interactive capture timed out after inputs {sent_labels}; " + f"captured output:\n{diagnostic[-4000:]}" + ) + finally: + os.close(master) + if next_step != len(demo.input_steps): + missing = [step.pattern for step in demo.input_steps[next_step:]] + raise SystemExit(f"{demo.slug}: interactive prompt(s) not observed: {missing}") + if process.returncode != demo.expected_exit_code: raise SystemExit( - f"{demo.slug}: expected exit {demo.expected_exit_code}, got {result.returncode}\n{transcript}" + f"{demo.slug}: expected exit {demo.expected_exit_code}, got {process.returncode}" ) - return result.returncode, transcript + final_lines = snapshots[-1] if snapshots else [] + transcript = "\n".join(final_lines) + "\n" + if len(snapshots) > 10: + indices = sorted({round(index * (len(snapshots) - 1) / 9) for index in range(10)}) + snapshots = [snapshots[index] for index in indices] + return process.returncode, transcript, snapshots, sent_labels + + +def capture_demo( + demo: Demo, env: dict[str, str], workspace: Path, omm_home: Path +) -> tuple[int, str, list[list[str]] | None, list[str], dict[str, object] | None]: + replacements = replacement_map(env, workspace) + with demo_fixture(demo, env, omm_home) as (demo_env, fixture_record): + if demo.input_steps: + exit_code, transcript, snapshots, input_events = capture_interactive( + demo, demo_env, workspace, replacements + ) + return exit_code, transcript, snapshots, input_events, fixture_record + result = run(["omm", *demo.argv], cwd=workspace, env=demo_env, capture=True) + transcript = normalize_transcript(result.stdout or "", replacements) + if result.returncode != demo.expected_exit_code: + raise SystemExit( + f"{demo.slug}: expected exit {demo.expected_exit_code}, got {result.returncode}\n{transcript}" + ) + return result.returncode, transcript, None, [], fixture_record def visible_lines(command: str, transcript: str) -> list[str]: return [f"$ {command}", "", *transcript.rstrip("\n").splitlines()] -def render_png(slug: str, lines: list[str], destination: Path) -> None: - title = f"omm {slug} · real CLI capture" +def render_png(demo: Demo, lines: list[str], destination: Path) -> None: + title = f"omm {demo.slug} · {demo.context_label}" argv = [ "magick", "-size", @@ -361,7 +653,7 @@ def render_png(slug: str, lines: list[str], destination: Path) -> None: argv.append(str(destination)) converted = run(argv, capture=True) if converted.returncode != 0: - raise SystemExit(f"ImageMagick failed for {slug}:\n{converted.stdout}") + raise SystemExit(f"ImageMagick failed for {demo.slug}:\n{converted.stdout}") def reveal_counts(total: int) -> list[int]: @@ -375,23 +667,35 @@ def reveal_counts(total: int) -> list[int]: return list(dict.fromkeys(min(total, count) for count in counts)) -def render_asset(demo: Demo, transcript: str, frame_root: Path) -> tuple[Path, Path, Path]: +def render_asset( + demo: Demo, + transcript: str, + frame_root: Path, + terminal_snapshots: list[list[str]] | None = None, +) -> tuple[Path, Path, Path]: all_lines = visible_lines(demo.command, transcript) demo_frames = frame_root / demo.slug demo_frames.mkdir(parents=True) frame_paths: list[Path] = [] - counts = reveal_counts(len(all_lines)) - for index, count in enumerate(counts): - shown = all_lines[:count] - window = shown[-MAX_VISIBLE_LINES:] + if terminal_snapshots: + rendered_windows = [ + ([f"$ {demo.command}", "", *snapshot])[-MAX_VISIBLE_LINES:] + for snapshot in terminal_snapshots + ] + else: + rendered_windows = [] + for count in reveal_counts(len(all_lines)): + shown = all_lines[:count] + rendered_windows.append(shown[-MAX_VISIBLE_LINES:]) + for index, window in enumerate(rendered_windows): png = demo_frames / f"frame-{index:02d}.png" - render_png(demo.slug, window, png) + render_png(demo, window, png) frame_paths.append(png) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) poster = OUTPUT_DIR / f"{demo.slug}.png" poster_lines = all_lines[:MAX_VISIBLE_LINES] - render_png(demo.slug, poster_lines, poster) + render_png(demo, poster_lines, poster) concat = demo_frames / "frames.ffconcat" concat_lines = ["ffconcat version 1.0"] @@ -516,9 +820,24 @@ def verify_manifest(*, probe_when_available: bool = True) -> None: raise SystemExit( f"Manifest slugs differ: expected {sorted(expected_slugs)}, got {sorted(map(str, actual_slugs))}" ) + expected_outcomes = {demo.slug: demo.outcome for demo in DEMOS} + expected_demos = {demo.slug: demo for demo in DEMOS} for asset in assets: slug = asset["slug"] + demo = expected_demos[slug] + if asset.get("command") != demo.command: + raise SystemExit( + f"{slug}: command mismatch ({asset.get('command')!r} != {demo.command!r})" + ) + if asset.get("expectedExitCode") != demo.expected_exit_code: + raise SystemExit(f"{slug}: expectedExitCode differs from the capture contract") + if asset.get("outcome") != expected_outcomes[slug]: + raise SystemExit( + f"{slug}: outcome mismatch ({asset.get('outcome')!r} != {expected_outcomes[slug]!r})" + ) + if asset.get("outcome") == "success" and asset.get("exitCode") != 0: + raise SystemExit(f"{slug}: success outcome requires exitCode 0") records = ( ("src", "bytes", "sha256"), ("poster", "posterBytes", "posterSha256"), @@ -583,8 +902,9 @@ def write_manifest( "captureScript": "scripts/capture-command-demos.py", "processEvidence": ( "Each transcript is stdout+stderr captured from the real omm console " - "script installed editable from this commit. Videos reveal only those " - "captured transcript lines; no command result text is synthesized." + "script installed editable from this commit. Interactive videos use " + "VT100 screen checkpoints reconstructed from those real process bytes " + "and record the exact input events; no command result text is synthesized." ), "sourcePathPolicy": ( "The source clone, virtualenv, HOME, OMM_HOME, XDG cache, uv cache, and " @@ -595,8 +915,15 @@ def write_manifest( "safety": { "liveOmmRunRequests": 0, "liveWorkersDevRequests": 0, - "cliNetworkPolicy": "HTTP(S) and ALL_PROXY forced to unused 127.0.0.1:9", - "telemetryPolicy": "never", + "cliNetworkPolicy": ( + "External HTTP(S) and ALL_PROXY forced to unused 127.0.0.1:9. The " + "contribute demo alone allows a temporary GET-only loopback fixture " + "on 127.0.0.1:11434 and fails on any POST." + ), + "telemetryPolicy": ( + "never by default; contribute uses ask only until the captured 'n' " + "response cancels before compute or upload" + ), "userHomeTouched": False, "modelDownloadsStarted": False, "modelExecutionsStarted": False, @@ -623,6 +950,12 @@ def main() -> None: action="store_true", help="Skip optional ffprobe metadata checks during --verify-only.", ) + parser.add_argument( + "--only", + nargs="+", + choices=[demo.slug for demo in DEMOS], + help="Recapture only these slugs and preserve the other manifest rows/assets.", + ) args = parser.parse_args() if args.verify_only: verify_manifest(probe_when_available=not args.no_ffprobe) @@ -634,13 +967,34 @@ def main() -> None: workspace = Path(temporary) source, commit, remote = source_checkout(workspace) version = project_version(source) - _, _, env = prepare_runtime(workspace, source, commit) + _, omm_home, env = prepare_runtime(workspace, source, commit) frame_root = workspace / "frames" + selected = set(args.only or [demo.slug for demo in DEMOS]) + existing_rows: dict[str, dict[str, object]] = {} + if args.only: + manifest_path = OUTPUT_DIR / "manifest.json" + if not manifest_path.is_file(): + raise SystemExit("--only requires an existing manifest.json") + existing_manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + existing_rows = { + row["slug"]: row for row in existing_manifest.get("assets", []) + } rows: list[dict[str, object]] = [] for demo in DEMOS: - exit_code, transcript = capture_demo(demo, env, workspace) - video, poster, transcript_path = render_asset(demo, transcript, frame_root) + if demo.slug not in selected: + if demo.slug not in existing_rows: + raise SystemExit(f"Existing manifest is missing preserved slug {demo.slug}") + preserved = dict(existing_rows[demo.slug]) + preserved["outcome"] = demo.outcome + rows.append(preserved) + continue + exit_code, transcript, snapshots, input_events, fixture_record = capture_demo( + demo, env, workspace, omm_home + ) + video, poster, transcript_path = render_asset( + demo, transcript, frame_root, snapshots + ) probe = probe_video(video) video_meta = file_record(video) poster_meta = file_record(poster) @@ -663,6 +1017,10 @@ def main() -> None: "argv": ["omm", *demo.argv], "exitCode": exit_code, "expectedExitCode": demo.expected_exit_code, + "outcome": demo.outcome, + "captureMode": "pseudo-tty" if demo.input_steps else "closed-stdin", + "inputEvents": input_events, + "fixture": fixture_record, "safetyPath": demo.safety_path, } ) diff --git a/src/components/commands/CommandCapture.tsx b/src/components/commands/CommandCapture.tsx index 5331da6..5a83298 100644 --- a/src/components/commands/CommandCapture.tsx +++ b/src/components/commands/CommandCapture.tsx @@ -3,9 +3,10 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react"; /** - * A real captured command and its real captured output (see - * `src/i18n/commands/base.ts`), typed into view the way `Terminal.tsx` types - * the homepage demo — except triggered once this block scrolls into view + * A documented terminal example (see `src/i18n/commands/base.ts`), typed into + * view the way `Terminal.tsx` types the homepage demo. Some source entries are + * literal captures and others are explicitly labeled format-accurate + * reproductions in their footnote. The animation is triggered once this block scrolls into view * (`Reveal.tsx`'s IntersectionObserver, not on-mount) and never replays once * played, since it sits mid-page rather than above the fold. */ @@ -46,7 +47,7 @@ type Props = { readonly command: string; readonly output: string; readonly footnote: string; - /** Screen-reader label for the whole block — the typed/output text itself + /** Screen-reader label for the documented example — the typed/output text itself * is `aria-hidden`, same pattern as `Terminal.tsx`. */ readonly label: string; }; diff --git a/src/components/commands/CommandDemo.tsx b/src/components/commands/CommandDemo.tsx index 4e84d74..422b4c0 100644 --- a/src/components/commands/CommandDemo.tsx +++ b/src/components/commands/CommandDemo.tsx @@ -5,6 +5,7 @@ type Props = { readonly slug: Slug; readonly recordedCommand: string; readonly recordedExitCode: number; + readonly recordedOutcome: "success" | "cancelled" | "guard"; readonly documentedCommand: string; readonly documentedOutput: string; readonly documentedFootnote: string; @@ -13,7 +14,12 @@ type Props = { readonly transcriptLabel: string; readonly recordedCommandLabel: string; readonly exitCodeLabel: string; + readonly outcomeLabel: string; + readonly successLabel: string; + readonly cancelledLabel: string; + readonly guardLabel: string; readonly safeSuccessNote: string; + readonly safeCancelledNote: string; readonly safeGuardNote: string; readonly documentedCaptureLabel: string; }; @@ -30,6 +36,7 @@ export default function CommandDemo({ slug, recordedCommand, recordedExitCode, + recordedOutcome, documentedCommand, documentedOutput, documentedFootnote, @@ -38,11 +45,28 @@ export default function CommandDemo({ transcriptLabel, recordedCommandLabel, exitCodeLabel, + outcomeLabel, + successLabel, + cancelledLabel, + guardLabel, safeSuccessNote, + safeCancelledNote, safeGuardNote, documentedCaptureLabel, }: Props) { const base = `/demos/commands/${slug}`; + const outcomeText = + recordedOutcome === "success" + ? successLabel + : recordedOutcome === "cancelled" + ? cancelledLabel + : guardLabel; + const outcomeNote = + recordedOutcome === "success" + ? safeSuccessNote + : recordedOutcome === "cancelled" + ? safeCancelledNote + : safeGuardNote; return (