No description
  • Zig 98.4%
  • Objective-C 0.7%
  • Shell 0.5%
  • Python 0.2%
  • Makefile 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sean OMeara a4d1c3173d
feat: export the library surface for dependents
`zelnet` becomes importable as a Zig package. build.zig declares the
library with addModule, so a dependent can write
`b.dependency("zelnet", .{}).module("zelnet")`.

Two modules join root.zig for library callers:

- `source` is the strict source-capture engine behind `src export`,
  lifted out of host.zig. It is generic over the transport and the
  output sink, so a test can drive it with an in-memory host and a
  library can write members wherever it wants. The CLI keeps a
  directory sink and the same stderr progress lines. The capture.json
  bytes do not change; a golden test pins the shape.
- `hostsession` signs on once and opens the file, remote-command and
  database services on first use. It returns errors and never ends the
  process, unlike the CLI verb helpers.

`database.runSelectDiag` and `runExecImmediateDiag` hand the SQLCA
status, SQLCODE and message to the caller through `SqlDiag`; the
existing functions keep printing as before. `readSqlca` has happy,
absent, short and oversized-length tests.

The first consumer is sean/sourcerer, the IBM i source-control CLI.
`make ci` passes: format, Debug and ReleaseSafe tests, and the Binjovi
package contract.

Claude-Session: https://claude.ai/code/session_01HPdKaFBKQVs8hzfgXMrnwV
2026-09-14 06:19:21 +02:00
ci fix: reject closed stdout before opening host connections 2026-09-13 23:23:41 +02:00
docker build: switch container base Debian -> AlmaLinux 10 (match cluster) 2026-06-17 18:00:09 +02:00
docs fix: reject closed stdout before opening host connections 2026-09-13 23:23:41 +02:00
macos fix(5250): receive host updates and acknowledge Cancel Invite 2026-09-10 11:32:20 +02:00
packaging build(packaging): add nfpm spec for .deb/.rpm 2026-06-19 11:42:20 +02:00
scripts fix(release-prep): portable CHANGELOG surgery (BSD awk rejects multi-line -v values) 2026-06-25 13:35:18 +02:00
src feat: export the library surface for dependents 2026-09-14 06:19:21 +02:00
windows docs: rewrite Zig doc comments to ASD-STE100 2026-08-07 16:12:58 +02:00
.gitattributes fix(cicd): auto-resolve CHANGELOG land conflicts via .gitattributes merge=union 2026-07-05 12:14:45 +00:00
.gitignore adding .DS_Store to gitignore 2026-06-23 10:13:51 +02:00
AGENTS.md docs: rewrite all documentation to ASD-STE100 2026-08-07 16:12:40 +02:00
build.zig feat: export the library surface for dependents 2026-09-14 06:19:21 +02:00
build.zig.zon feat: zelnet foundation — TN5250 + TN3270 protocol core (red/green TDD) 2026-06-17 17:48:35 +02:00
CHANGELOG.md feat: export the library surface for dependents 2026-09-14 06:19:21 +02:00
CLAUDE.md feat: zelnet foundation — TN5250 + TN3270 protocol core (red/green TDD) 2026-06-17 17:48:35 +02:00
Makefile feat(ci): add Binjovi desktop package entry point 2026-09-05 01:13:02 +02:00
README.md docs: record Save Restore release and Telnet audit limits 2026-09-13 22:39:52 +02:00
STYLE.md docs: rewrite all documentation to ASD-STE100 2026-08-07 16:12:40 +02:00

zelnet(1)

zelnet is a terminal client for IBM systems, written in idiomatic Zig. It is a clean-room replacement for tn5250. It also drives tn3270 mainframe screens. zelnet includes the IBM i Host Server data layer in the same binary. This layer covers IFS, SQL, CL, data queues, and source export.

NAME

zelnet — a terminal client for TN5250, TN3270, and plain telnet, and an IBM i Host Server client

SYNOPSIS

zelnet [options] <host> [port]
zelnet --tn3270 [--model N] [options] <host> [port]
zelnet --nvt [options] <host> [port]
zelnet <verb> [args ...]
zelnet --demo | --probe <host> [port] | --version

where <verb> is one of auth ifs sql xfer cmd dq pgm src.

DESCRIPTION

zelnet is one statically-linked Zig 0.16 binary. It has two forms:

  • an interactive full-screen terminal for both IBM screen protocols
  • a set of scriptable IBM i Host Server verbs

The verbs are a clean-room build of the ACS data layer. Both forms share the same EBCDIC codec and TLS transport.

zelnet detects how you launch it. If you start it from a shell with a controlling TTY, it runs as the terminal CLI. If you double-click it from Finder, or open it with open(1), and it has no TTY, it opens a native GUI window. The GUI needs a build with -Dgui. It supports macOS AppKit and Windows Win32. Use --gui or --cli to override this detection.

TN5250 supports IBM i and other midrange systems in the AS/400 lineage. It follows RFC 1205, RFC 2877, and RFC 4777. It uses GDS 12A0 records. It uses plain (row, col) buffer addressing. It supports field attributes, colour, and DBCS (double-byte) text through Shift-Out and Shift-In codes. It supports concealed password fields. zelnet negotiates IBM-3179-2 for a 24×80 colour display. It negotiates IBM-3477-FC for a wide 27×132 display.

TN3270 supports IBM z/OS mainframes. It follows RFC 1576 and RFC 2355. It uses 12-bit or 14-bit buffer addressing. It uses the extended 3270 data stream. This data stream supports colour and extended highlighting: blink, reverse, underscore, and intensify. It supports Query Reply. It supports alternate screen sizes for models 2 through 5: 24×80, 32×80, 43×80, and 27×132. zelnet negotiates terminal types from IBM-3278-2-E through IBM-3278-5-E. TN3270E is not yet negotiated. TN3270E is the RFC 2355 option-40 5-byte header. It is staged but not wired in. See CAVEATS for details.

NVT means plain telnet under RFC 854. Every telnet session starts here. --nvt gives a byte-level pass-through, like telnet(1). The enclosing terminal renders the screen. zelnet answers the telnet negotiation: remote ECHO, SGA, the local TERM value as TERMINAL-TYPE, and NAWS window reports. Press Ctrl-] to disconnect.

The default 5250 path can select NVT from the host's option requests. Examples are WILL ECHO and DO NAWS. This is a detection rule, not a guarantee for every host. Use --nvt to select plain Telnet explicitly. See the protocol audit for supported options and gaps.

TLS is implicit. This is sometimes called "telnets". TLS is on by default for terminal port 992 and for every Host Server port. By default, zelnet verifies the certificate against the OS trust store. Use --self-signed or --no-verify to relax this check for hosts whose certificate is not publicly chained.

OPTIONS

Terminal-client options (the zelnet <host> [port] form):

Option Description
<host> [port] Host to connect to. port defaults to 23.
--tls / --no-tls Force the transport on or off. By default, zelnet picks automatically. It uses TLS only when port == 992.
--no-verify Encrypt the connection. Skip certificate verification.
--self-signed Accept a valid self-signed certificate. This is the default cert on IBM i.
--tn3270 Use TN3270 for mainframes. Default is TN5250.
--nvt Use plain telnet as a pass-through. Common targets are Cisco devices, BBSes, and Unix telnetd.
--model N TN3270 screen model: 2, 3, 4, or 5. Each larger model gives an alternate screen size and the -E type. Default is 2.
--demo Show a recorded pub400 sign-on transcript. It needs no network connection.
--probe Run a non-interactive connectivity check. It needs no credentials. Use it for diagnostics.
--login Sign on interactively using ZELNET_USER and ZELNET_PW.
--play <script> Run a scripted TN5250 session. It signs on, then drives and checks panels. With --nvt, it runs a headless expect and send gate: expect <t>; send <t>; type <t>; enter; pause <ms>; dump.
--drive <script> Run a scripted TN3270 session. This implies --tn3270.
--record <file> Write an asciicast recording of a scripted session, from --play or --drive. zelnet uses this for the release reel.
--ccsid <n> Set the terminal display code page by CCSID. The default is 37. An unsupported CCSID is an error.
--gui / --cli Force the GUI or terminal launch mode.
--help, -h Print the option list. Then exit.
--version, -v Print the build banner and version. Then exit.

In an interactive session, use Tab or Shift-Tab to move between fields. Press Enter to transmit. F1 through F24 are the function keys. PgUp and PgDn perform Roll. Press F3, or a lone Esc, to exit or go back. Press Ctrl-C to quit. Password fields do not display typed characters.

COMMANDS

These are the IBM i Host Server verbs. Credentials come from the environment only. Do not pass credentials on the command line. See ENVIRONMENT for details. Each verb signs on, runs, and prints a result. Each verb exits non-zero on failure. This lets you use them as CI gates, for example zelnet cmd "…" || exit 1.

Command Description
auth [host] Sign on. Print the system VRM, server level, and password level.
ifs ls <path> List an IFS directory on the file server.
ifs get <remote> <local> Download a file from the IFS.
ifs put <local> <remote> Upload a file to the IFS.
ifs mkdir <path> / rmdir <path> Create or remove a directory.
ifs rm <path> Delete a file.
ifs mv <src> <dst> Rename or move a path.
ifs stat <path> [--json] Show type, size, attributes, and timestamps. With --json, write a zelnet.ifs-stat.v1 result to stdout. Absence and failure exit nonzero; only status: absent with error_kind: absent proves absence.
sql "<SELECT ...>" Run a query. Print the rows. If the statement is not SELECT, zelnet runs it as EXECUTE IMMEDIATE. It then reports the rows affected.
xfer <table|"SELECT ..."> <file.csv> Export a table or query result to CSV. This is the Data Transfer function.
cmd "<CL COMMAND>" Run a CL command on the remote-command server. Print its messages.
dq write <lib/queue> <data> Write to a data queue.
dq writek <lib/q> <key> <data> Write a keyed entry.
dq read|peek <lib/q> [wait] Read the next entry and remove it, or peek at the next entry without removing it. wait sets how many seconds to block for an entry.
dq readk <lib/q> <EQ|NE|LT|LE|GT|GE> <key> Read by key comparator.
dq clear <lib/q> / attr <lib/q> Clear a queue, or show its attributes.
pgm rtvsysval <SYSVAL> Call the QWCRSVAL program for a system value. Examples: QMODEL, QCCSID, QDATE.
src export <lib> [--out <dir>] [--srcfiles QDDSSRC,QRPGLESRC,QCLSRC] [--filter PART*] [--strict] Pull a library's source members into a local Git layout: <out>/<srcfile>/<member>.<type>. This is a "reverse deploy".

ENVIRONMENT

Variable Meaning
ZELNET_USER / ZELNET_PW Credentials for the Host Server verbs, --login, and --play. You must not pass these on the command line.
ZELNET_HOST Default host for the verbs. Default value is pub400.com.
ZELNET_ALLOW_PLAINTEXT=1 You must set this to use --no-tls on a Host Server verb. Without it, zelnet refuses to send your password substitute and data in the clear.
ZELNET_DUMP=1 Hex-dump every inbound 3270 record. Use this for wire debugging.

EXAMPLES

Terminal sessions:

zelnet pub400.com 992          # IBM i over TLS (auto-on for 992), then sign on
zelnet pub400.com 23           # plain telnet
zelnet --self-signed host      # IBM i with its default self-signed cert
zelnet --tn3270 --model 4 host 23   # a 43×80 mainframe screen
zelnet --demo                  # offline render of a captured sign-on
zelnet --probe pub400.com 992  # connectivity check, no credentials

IBM i Host Server (credentials in the environment):

export ZELNET_USER=you ZELNET_PW=secret ZELNET_HOST=pub400.com
zelnet auth                                    # sign on, print V7R5M0 etc.
zelnet ifs ls /home/YOU                         # list a directory
zelnet sql "SELECT SCHEMA_NAME FROM QSYS2.SYSSCHEMAS FETCH FIRST 10 ROWS ONLY"
zelnet cmd "DSPSYSVAL SYSVAL(QDATE)"            # run a CL command
zelnet xfer "QSYS2.SYSSCHEMAS" /tmp/schemas.csv # export to CSV
zelnet pgm rtvsysval QMODEL                      # program-call a system value
zelnet src export MYLIB --out ./MYLIB --filter PART*   # pull source into Git

Scripted and headless (CI):

zelnet --play  signon.zts pub400.com 992   # drive + assert a 5250 panel flow
zelnet --drive logon.z3270 mainframe 23     # drive a 3270 panel flow

EXIT STATUS

0 means success. Any sign-on, connection, or operation failure prints a diagnostic message. zelnet then exits non-zero. This lets a verb gate a script or CI step.

TLS AND SECURITY

  • By default, TLS verifies the server certificate against the OS trust store. pub400.com's publicly-trusted certificate validates cleanly under this check. --self-signed accepts a valid self-signed certificate. --no-verify encrypts the connection without authenticating the peer.
  • Implicit TLS runs on terminal port 992. It also runs on the Host Server ports: sign-on 9476, file 9473, database 9471, command 9475, and data-queue 9472. The matching 84xx ports are plaintext.
  • The Host Server verbs refuse --no-tls unless you set ZELNET_ALLOW_PLAINTEXT=1. This stops a stray flag from silently leaking your password substitute and data.

BUILDING

zelnet needs Zig 0.16. The make targets are thin wrappers around zig build:

make build      # native CLI binary -> zig-out/bin/zelnet
make run ARGS="pub400.com 992"
make gui        # native GUI (macOS AppKit / Windows Win32, -Dgui)
make app        # assemble zig-out/zelnet.app (macOS bundle)
make ci         # what CI runs: fmt-check + tests (Debug + ReleaseSafe)
make test       # unit tests
make smoke      # live pub400.com sign-on smoke (needs ZELNET_USER / ZELNET_PW)
make dist       # cross-compile every platform -> dist/

make ci includes the Binjovi package-entry contract.

You can also run zig build directly. Examples: zig build -Doptimize=ReleaseSafe, zig build run -- <host> [port], zig build -Dgui, and zig build check. zig build check is the CI-parity preflight. It runs zig fmt --check and all unit tests. Cross-compile targets are: x86_64-windows, x86_64-linux-musl, aarch64-linux-musl, aarch64-macos, x86_64-macos.

DISTRIBUTION AND RELEASING

Binjovi is the authoritative CI and release provider for this repository. Each Build freezes the exact catalog revision and project definition that accepted it. The package Dockerfile and package install test are owned by sean/binjovi-plans. They can change without a Binjovi image release. A Zelnet Release reads the recipe paths from its frozen catalog definition.

src/VERSION is the single source of truth for the version number. It carries a -dev suffix during development. Binjovi uses these phases:

  • Build checks the exact pull-request revision with zig build check on the native Binjovi package-build agent. It does not cross-compile or publish desktop packages. The agent uses the persistent package BuildKit cache and does not submit an Argo Workflow.
  • Release cross-compiles the exact integrated trunk revision for Linux, macOS, and Windows. It uses the native Binjovi task graph. After one staging task, the DEB and RPM package tests and the functional release reels run in parallel. The functional tasks check Gibson, pub400.com, and Telehack.
  • Release promotes the package set only after all tests pass. It writes immutable artifacts to pkg.sean.farm, creates the Forgejo release, and tags the exact source revision.

CAVEATS

  • This software is unstable. It is written in an unstable language, Zig. The people who wrote it are also unstable. Opus 4.8, an AI model, generated most of the code in a few days. Few human eyes have reviewed the code so far. It seems to work, for now.
  • Do not use this software for anything important. We do not know what problems it might cause on your system.
  • TN3270E is staged but not wired in. zelnet currently refuses TN3270E. TN3270E is telnet option 40. It uses the RFC 2355 5-byte data-stream header. It also uses LU binding and responses. zelnet instead speaks basic TN3270 (LU2) with the extended -E data stream. Full-screen TSO/E logon needs TN3270E. This logon is not yet available.

SEE ALSO

tn5250(1), x3270(1). RFC 1205, RFC 2877, RFC 4777 (TN5250). RFC 1576, RFC 2355 (TN3270, TN3270E). RFC 854 (Telnet). docs/HOSTSERVER.md describes the Host Server wire protocol. It includes live pub400.com verification notes.

Strict source capture

Use strict capture before source enters a recovery or release candidate:

zelnet src export SOMEARA1 --srcfiles QDDSSRC,QRPGLESRC --filter LEM* --out capture-001 --strict

The output directory must be new and its parent must exist. Every named source file must be accessible. Strict capture reads the complete selected member set twice and compares names, byte counts, and SHA-256 hashes. A missing file, failed read, unknown source kind, or changed member fails the command. Member lists can exceed 1,024 entries. Individual exported members have a 64 MiB size limit.

capture.json uses schema zelnet.source-capture.v1. It contains the library, relative member paths, byte counts, hashes, and an explicit complete flag. It becomes complete only after both passes succeed. A failed capture can leave partial files and an incomplete manifest. Consumers must check the process status, manifest, and file hashes before using the output. Authentication or local filesystem failures can prevent even an incomplete manifest from being written. A missing manifest is a failed capture.

Two matching reads detect changes; they do not create an atomic snapshot across an application. Stop edits to the selected sources while making a release snapshot. The manifest covers only the selected source files and filter. It does not prove that every application dependency has source or that the captured source produced the running programs.

Source text is converted to UTF-8 with LF line endings. File extensions are inferred from source-file names and DDS content; type_origin records this. QRPGSRC becomes .rpg, QRPGLESRC becomes .rpgle, QSQLSRC becomes .sql, and QCLSRC becomes .clle. Validate the stored member type and compile recipe separately, especially for mixed CL/CLLE and RPGLE/SQLRPGLE files. Unrecognized source files use .src and fail strict capture. Sequence numbers, line dates, and the original source-file attributes need a separate metadata capture.

For a custom source file with a known type, use --source-type SQL (or RPG, RPGLE, SQLRPGLE, CLP, CLLE, DSPF, PF, LF, or BND). This applies to every member in the selected scope. The manifest records type_origin: caller_declared. It does not claim that the type came from member metadata. Unknown types fail before a connection is opened.

SQL row output preserves character values beyond 512 bytes. Direct LOB retrieval still has separate limits; recovery tools must check lengths and completeness.

Native pipeline timing

Native tasks run package checks, functional verification, and release publication. The pipeline ends at the verified package Release. Installation on a user device is not a Binjovi Deploy target.

Open the project dashboard and expand an execution attempt for the task timeline and available CPU, off-CPU, and syscall profiles. Cached BuildKit steps still appear as cached work; a missing sample does not prove that a task used no CPU.

Select an exact successful Build with binjovictl release zelnet --build BUILD_ID --wait. Then use binjovictl status zelnet to check its release and any configured deployment. Telemetry explains time spent; immutable artifact and revision evidence establish completion.

Replace a 5250 field in a test

In a --play script, field 0; fill BUY replaces the complete visible field. For 5250, type and typepw decode UTF-8 into characters before EBCDIC conversion. They reject malformed UTF-8, unsupported characters, and text that exceeds the remaining field space before changing the field. The terminal defaults to CCSID 37. Use --ccsid 273 for a German host display, or another supported numeric CCSID. Match the host display code page; this flag does not change the IBM i job or database encoding. fill with no text clears it. This removes old characters when the new value is shorter. It accepts printable ASCII, refuses hidden fields, and fails if the text is longer than the field. Use typepw for a password field.

Unknown script verbs now fail before a connection opens. Use pf 3 for F3; pf3 is an error. This check applies to 5250, 3270, and NVT scripts.

Host-driven 5250 updates

The POSIX terminal and macOS window receive 5250 updates while the user is idle. The session acknowledges Cancel Invite (RFC 1205 section 4.2) and blocks another AID until the host sends a new Read. Network reads only supply bytes. The UI thread owns screen changes and writes.

The 5250 play runner accepts pause <milliseconds> and await <milliseconds> <screen text>. Both continue to process host output. Waits are limited to 60 seconds. Invalid waits fail before sign-on. A timed script records elapsed time; older scripts retain their fixed frame delay. Windows interactive console support still uses its existing request/reply loop. It needs native asynchronous input tests before this path is enabled.

5250 Save Screen and Restore Screen retain native screen bytes, fields, format controls, and cursor state. The sysreq script command sends System Request. A pub400 check confirms Save, Restore, Cancel Invite, and execution of text entered before the exchange. It does not claim ACS parity. Save rejects states that use unimplemented EA, WEA, WDSF, or unknown commands. See the RPG audit for limits and native evidence.