diff --git a/AGENTS.md b/AGENTS.md index 790c97e..d48bfeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,9 +9,17 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 |---|---| | `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | -| `tools/README.md` | QEMU/FreeDOS 자동화 도구 사용법 | +| `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | | `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | +VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범**이다. 문서에 복제하면 +반드시 드리프트하므로 아래로 확인한다. + +```powershell +uv run ferro-vm --help +uv run ferro-vm --help +``` + ## 검증 규칙 - **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py index f268a64..411b144 100644 --- a/src/ferrolang_vm/cli.py +++ b/src/ferrolang_vm/cli.py @@ -64,23 +64,74 @@ def follow_logs() -> int: ]).returncode +EPILOG = r"""examples: + uv run ferro-vm start boot the VM and start the daemon + uv run ferro-vm wait-ready block until TCPAGENT answers PING + uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output + uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' + uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK + uv run ferro-vm logs follow the structured daemon log + +The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat +view is for exchange only). See AGENTS.md for verification rules and build traps. +""" + +SIMPLE_COMMANDS = { + "start": "Start the daemon and boot QEMU. Safe to run when already up.", + "stop": "Quit QEMU cleanly and stop the daemon.", + "status": "Print daemon, QEMU, and TCPAGENT connection state as JSON.", + "ping": "Send PING to TCPAGENT. Expects 'OK 504F4E47' (PONG).", + "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", + "ocr": "Capture the console and print recognized text (RapidOCR).", + "logs": "Follow the append-only daemon log. Uses lnav when available.", +} + + def main() -> int: - parser = argparse.ArgumentParser(description="Windows-only QEMU/FreeDOS automation") - commands = parser.add_subparsers(dest="op", required=True) - for name in ("start", "stop", "status", "ping", "screenshot", "ocr", "logs"): - commands.add_parser(name) - wait = commands.add_parser("wait-ready") - wait.add_argument("--timeout", type=int, default=45) - reset = commands.add_parser("reset") - reset.add_argument("--timeout", type=int, default=45) - execute = commands.add_parser("exec") - execute.add_argument("command") - put = commands.add_parser("put") - put.add_argument("source", type=Path) - put.add_argument("destination") - get = commands.add_parser("get") - get.add_argument("source") - get.add_argument("destination", type=Path) + parser = argparse.ArgumentParser( + prog="ferro-vm", + description="Windows-only QEMU/FreeDOS automation for the Ferro compiler.", + epilog=EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + commands = parser.add_subparsers(dest="op", required=True, metavar="COMMAND") + for name, blurb in SIMPLE_COMMANDS.items(): + commands.add_parser(name, help=blurb, description=blurb) + + wait_help = "Block until TCPAGENT is connected and answers PING." + wait = commands.add_parser("wait-ready", help=wait_help, description=wait_help) + wait.add_argument("--timeout", type=int, default=45, metavar="SECONDS", + help="give up after this many seconds (default: %(default)s)") + + reset_help = "Quit QEMU cleanly, reboot it, and wait for TCPAGENT." + reset = commands.add_parser("reset", help=reset_help, description=reset_help) + reset.add_argument("--timeout", type=int, default=45, metavar="SECONDS", + help="give up after this many seconds (default: %(default)s)") + + exec_help = "Run a DOS command inside the VM and print its exit code and output." + execute = commands.add_parser( + "exec", help=exec_help, + description=exec_help + " Quote the command so the host shell does not eat" + r" backslashes: exec 'wcl386 -q HELLO.C'.") + execute.add_argument("command", metavar="DOS_COMMAND", + help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") + + put_help = "Copy a host file into the VM." + put = commands.add_parser("put", help=put_help, description=put_help) + put.add_argument("source", type=Path, metavar="HOST_PATH", + help="file on this machine") + put.add_argument("destination", metavar="DOS_PATH", + help=r"target inside the VM, e.g. 'C:\FEC\SRC\CHECK.C'." + " DOS uses 8.3 names, so long fixtures must be shortened" + " explicitly (BAD-ARI.FE, TRY-FPR.FE)") + + get_help = "Copy a file out of the VM onto the host." + get = commands.add_parser("get", help=get_help, description=get_help) + get.add_argument("source", metavar="DOS_PATH", + help=r"file inside the VM, e.g. 'C:\FEC\TEST.OK'") + get.add_argument("destination", type=Path, metavar="HOST_PATH", + help="target on this machine") + args = parser.parse_args() try: diff --git a/tools/README.md b/tools/README.md index dd535d7..3d909a5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -6,46 +6,41 @@ Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses portable APIs where possible, but other hosts are not supported yet. -## QEMU and FreeDOS automation - -Start the Python daemon and QEMU: +## Getting started ```powershell uv run ferro-vm start uv run ferro-vm status ``` -`TCPAGENT.EXE` connects only to `127.0.0.1:5558`. Local commands use the -Windows named pipe `\\.\pipe\ferrolang-vm`; there is no controller or observer -TCP port. Monitor the append-only structured log in another terminal: +The command list lives in the CLI itself, not in this file: ```powershell -uv run ferro-vm logs +uv run ferro-vm --help +uv run ferro-vm --help ``` -Commands: +Working rules, verification gates, and DOS build traps are in `AGENTS.md`. -```powershell -uv run ferro-vm reset # clean QEMU quit and restart -uv run ferro-vm wait-ready --timeout 45 -uv run ferro-vm ping -uv run ferro-vm exec 'dir C:\FEC' -uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' -uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK -uv run ferro-vm screenshot -uv run ferro-vm ocr -uv run ferro-vm stop -``` +## How it fits together -`reset` cleanly quits and restarts QEMU, waits for FreeDOS to boot, submits -the default boot-menu Enter, and requires TCPAGENT `PING`/`PONG`. QEMU -`system_reset` is intentionally unsupported because repeated soft resets leave -the FreeDOS NE2000 packet driver stuck during initialization. `logs` starts -`lnav` when installed and otherwise falls back to PowerShell `Get-Content --Wait`. The daemon logs command metadata, DOS output, exit status, transfers, -and agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary +`TCPAGENT.EXE` runs inside FreeDOS and dials out to `127.0.0.1:5558`; its wire +protocol is documented in `tcpagent/README.md`. The `ferro-vm` daemon owns that +connection and the QEMU monitor. Local commands reach the daemon over the +Windows named pipe `\\.\pipe\ferrolang-vm` — there is no controller or observer +TCP port. + +The daemon writes an append-only structured log (`uv run ferro-vm logs`, which +uses `lnav` when installed and otherwise falls back to PowerShell `Get-Content +-Wait`). It records command metadata, DOS output, exit status, transfers, and +agent lifecycle events as UTF-8 lines, and deliberately never logs raw binary payloads or protocol hex. +`reset` quits QEMU cleanly, restarts it, waits for FreeDOS to boot, submits the +default boot-menu Enter, and requires a TCPAGENT `PING`/`PONG` before returning. +QEMU `system_reset` is intentionally unsupported: repeated soft resets leave the +FreeDOS NE2000 packet driver stuck during initialization. + ## Standalone OCR `tools/qemu_ocr.py` remains available for OCRing an existing image: