Two CLI knobs that together let PRG #pragma BEGINDUMP blocks import
private Go modules and their internal/* packages:
--go-replace pkg=path
Adds `require pkg v0.0.0` + `replace pkg => path` to the temp
go.mod, so `go mod tidy` doesn't try (and fail) to fetch a
module that lives only on local disk or behind a private
forge (Gitea, etc.).
--module <name>
Overrides the temp-build module name (default fnode_build).
Use when the BEGINDUMP block needs to reach into another
module's internal/ packages — Go's internal-visibility rule
requires the importer to share a parent path with the
importee, so the build module needs to live somewhere under
that parent.
Worked example (solmade integration PoC):
fnode build /tmp/poc_dartapi.prg \\
--go-replace gitea.fivego.org/kwon_ai/solmade=/Users/charleskwon/solmade \\
--module gitea.fivego.org/kwon_ai/solmade/_fnode_build \\
-o poc_dart
A 4-line BEGINDUMP block now imports
"gitea.fivego.org/kwon_ai/solmade/internal/dartapi", links its
AllAliases() function, and returns the real count (3) at run time.
Single 23 MB Go binary, no Node, no FFI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When fnode is invoked from a cwd outside fivenode_go (e.g. a sibling
project like /Users/.../solmade), the legacy fallback
"../../fivedev/five" was resolved against the caller's cwd, pointing
fnode at /Users/fivedev/five which doesn't exist. go mod tidy then
failed with "open /Users/fivedev/five/go.mod: no such file".
New resolution order in fiveRoot():
1. walkUpForModule("five") — same as before, wins inside the
Five source tree.
2. fiveFromFnodeGoMod() — parse the `replace five => <path>`
line in fivenode_go's own go.mod (found via fnodeRoot()),
resolving relative paths against that root.
3. Hardcoded relative fallback (legacy).
Lets sibling apps run `fnode build app/foo.prg ...` from their own
directory without needing to cd into fivenode_go first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops labdb's index.html / login.html / css / js into the binary via
embed.FS so the single 24 MB fivenode_go binary now ships both the
HTML/JS frontend AND the JSON API. No web server, no Apache,
no asset bundler.
hbrtl_ext/labdb_static/
public/ mirror of fivenode/labdb/public/
assets.go //go:embed public + two PRG-callable
HB_FUNCs: LABDB_STATIC_FILE(cPath) returns
the bytes, LABDB_STATIC_MIME(cPath)
returns the Content-Type derived from
the resolved (not raw) extension so "/"
-> text/html, not application/octet-stream.
app/bridge_server.prg
BridgeDispatch now falls through to the static FS for any path
that isn't /api/*. Missing assets get a 404 instead of being
routed to the path-to-symbol dispatcher.
Verified:
GET / -> 200 text/html, full index.html body
GET /login.html -> 200 text/html, 1850 bytes
GET /css/app.css -> 200 text/css
GET /api/admin-stats -> 200 JSON {devices:2,...} (still live PG)
GET /nonexistent.html -> 404 text/plain
Phase 1a complete: HTTP serves both the labdb frontend and the
real-data labdb API from one binary, end to end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end PRG → Go RTL → pgxpool → labdb queries → JSON response,
from a single 24 MB binary with no Node.js, FFI, or Apache anywhere
in the request path.
app/bridge_server.prg:
Main now opens a pgxpool handle from LABDB_DSN (or the local
Homebrew default) before HTTP_SERVER_START, then publishes it
via LABDB_SET_PG so request goroutines can pick it up.
BridgeDispatch calls FetchRouteData before the per-path PRG
handler runs. FetchRouteData maps a few /api/* routes to the
same SQL the upstream server.js endpoint runs, encodes results
via hb_jsonEncode, and stuffs them into ctx fields under the
same keys the existing labdb api/*.prg handlers already read
(rows, total, devices, etc.). PRG handlers stay unmodified.
hbrtl_ext/labdb_state/:
Tiny Go RTL that holds the PG handle in a sync/atomic.Int64.
Picked this over a PRG STATIC/PUBLIC variable to sidestep
Five's stricter parser (now that parser.go errors on missing
terminators, anything more elaborate in the entry .prg adds
friction without buying anything).
Verified against a Homebrew postgres@16 cluster seeded with 2 devices
+ 2 sessions:
/api/admin-stats.prg
→ {"devices":2,"sessions":2,"records":0,"active_sessions":1}
/api/sessions-list.prg
→ {"sessions":[...two rows with full fields...],"total":2}
/api/admin-devices.prg
→ {"devices":[...two rows with api_key/created_at...]}
/api/hello.prg (no DB)
→ unchanged from 1a.4-3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets app/api/foo.prg keep its idiomatic `FUNCTION Main()` shape while
multiple such files compile into one binary. fnode auto-renames each
library file's Main into a unique symbol derived from the basename:
app/api/hello.prg -> HELLO__MAIN
app/api/admin-stats.prg -> ADMIN_STATS__MAIN (hyphen -> underscore)
Three moving parts:
cmd/fnode/main.go
parseOne for every PRG, then rename Main on every file except
the first (the entry). crossFile map updated so the analyzer
treats the renamed symbol as declared.
hbrtl_ext/dispatch/dispatch.go
New HB_FUNC FNODE_CALL(cFuncName) that does VM.FindSymbol +
PushSymbol/Function dance and discards the return value. Same
pattern pgserver's callPRG helper uses internally.
app/bridge_server.prg
BridgeDispatch now derives the symbol name from hReq["path"]
( /api/foo[.prg] -> FOO__MAIN ), invokes FNODE_CALL, and
maps "not found" errors to HTTP 404 (other errors -> 500).
Hardcoded /api/hello and /api/echo handlers replaced by the
path-driven model.
Verified end-to-end with app/api/hello.prg and app/api/admin-stats.prg:
GET /api/hello.prg -> 200 + JSON from HELLO__MAIN
GET /api/hello -> 200 (extension optional)
GET /api/admin-stats.prg?from=2026 -> 200 from ADMIN_STATS__MAIN
with query string echoed
GET /api/nope -> 404 "function not found"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PG_OPEN(cDsn) -> integer handle, -1 on failure
PG_CLOSE(nH) -> NIL
PG_QUERY(nH, cSQL [, aArgs]) -> array of { col => val } hashes
PG_EXEC (nH, cSQL [, aArgs]) -> rows affected, -1 on error
PG_LAST_ERROR(nH) -> last error string
Backed by github.com/jackc/pgx/v5/pgxpool, which is already in Five's
indirect dep tree (pgserver uses pgproto3 from the same repo). Pool
limits: MaxConns 8, MinConns 1, 5-min idle. Query timeout is capped at
30s so a runaway query can't pin a goroutine forever.
aArgs uses standard Postgres $1/$2/... placeholders — pgx parameter
binding prevents SQL injection. Never concatenate user input into cSQL.
Smoke-tested with app/pg_test.prg: bad DSN returns -1 cleanly (no
panic), the error path prints the expected fallback message, and the
real round-trip path is wired so setting LABDB_DSN to a live database
exercises SELECT + parameter binding + multi-row return without any
further code change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven HB_FUNCs that fivenode's bridge_*.prg layer relies on:
_CTX_SET_JSON / _CTX_GET_JSON — per-request context payload
_OUT_APPEND / _OUT_GET / _OUT_CLEAR — response body buffer
_BRIDGE_SET_RESULT / _BRIDGE_GET_RESULT — fast-path response
Crucially per-thread, not process-global like the original C
implementation. fivenode runs single-threaded under N-API so a static
buffer per process was fine; fivenode_go runs one *hbrt.Thread per
HTTP request goroutine, so the state is keyed by *hbrt.Thread in a
sync.Map. The HTTP dispatcher will call CleanupThread once per
request to keep the map bounded (sub-phase 1a.3-3).
Also exposes Go-side helpers (OutputBytes, Result, SetContextJSON,
CleanupThread) so the dispatcher can seed the context and harvest
the response without bouncing back through PRG.
Verified with app/capi_test.prg: all seven functions behave as
expected; combined with the Five hb_jsonDecode byref fix, ctx_get()
now correctly returns hash values rather than the fallback default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds hbrtl_ext/httpserver — a Five RTL extension that exposes a
single-process HTTP server controlled entirely from PRG.
Wire contract:
HTTP_SERVER_START(cAddr, cHandlerFunc) → blocking; returns NIL or cErr
HTTP_SERVER_STOP() → graceful shutdown
PRG handler signature:
FUNCTION OnRequest( hReq ) -> hResp
hReq: method, path, query, headers (hash), body, remote_addr
hResp: status (default 200), headers (hash), body
Each request runs on its own hbrt.Thread via vm.NewThread(), the same
pattern pgserver uses for connection isolation. Handler panics are
caught and turned into a 500.
The package is wired into fnode's defaultRTL list so any build that
doesn't override --rtl picks it up automatically.
Verified end-to-end with app/echo_server.prg: GET/POST against :8089
return JSON envelopes with the correct method, path, query, body
length, remote_addr, and roundtripped user-agent header.
The mod_harbour-compatible AP_* surface (AP_METHOD, AP_RPUTS,
AP_JSONRESPONSE, etc.) will sit on top of this dispatcher in
sub-phase 1a.3 as PRG, not Go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* cmd/fnode — build/run CLI that drives Five's compiler packages
(pp, parser, analyzer, gengo) and stitches generated prg_*.go
together with fivenode_go's own hbrtl_ext/* packages in a temp
module. Result is one self-contained Go binary; no FFI, no Node.
* hbrtl_ext/hello — bootstrap RTL extension proving the
blank-import-init() registration path works end-to-end. Exposes
FNODE_HELLO() to PRG.
* app/hello.prg — minimum end-to-end test: calls Date() (Five RTL)
and FNODE_HELLO() (fivenode_go RTL) from the same binary.
Verified: ./fnode build app/hello.prg -o hello_app → 17 MB single
binary that prints both lines. The same pattern will host the
HTTP server, bridge capi helpers, and PostgreSQL client coming
in 1a.2b–1a.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>