Portable AI agent for Cloverleaf integration work. Pure bash + curl + jq. Zero dependency on v1 wrapper scripts or v2 cloverleaf-tools.pyz. 27 native Anthropic tools: NetConfig parsing (read) nc_list_protocols, nc_list_processes, nc_protocol_block, nc_protocol_field, nc_protocol_nested, nc_protocol_summary, nc_destinations, nc_sources, nc_xlate_refs, nc_tclproc_refs NetConfig modification (journal-backed writes with rollback) nc_insert_protocol, nc_add_route, larry_rollback_list Workflows nc_find_inbound, nc_make_jump (3-thread jump pattern), nc_find (tbn/tbp/tbh/tbpr/where replacements), nc_document, nc_diff_interface, nc_regression Messages hl7_field, nc_msgs (smat is SQLite!), hl7_diff (with --ignore MSH.7) File system read_file, list_dir, grep_files, glob_files, write_file, bash_exec Validated against a 22-site real Cloverleaf test install. Five worked examples end-to-end: jump-thread generation, smat MRN search, system documentation, interface+connected diff, HL7-aware regression diff. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
425 lines
16 KiB
Bash
Executable File
425 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-make-jump.sh — generate the 3-thread jump pattern for cross-environment
|
|
# data replay during a Cloverleaf migration. Matches Bryan's house pattern
|
|
# (linux_<tag>_out / windows_<tag>_in / windows_<tag>_out).
|
|
#
|
|
# Topology:
|
|
#
|
|
# OLD env (e.g. windows) NEW env (linux)
|
|
# ── adt site (existing) ── ── server_jump site (new) ── ── adt site (cloned, unchanged) ──
|
|
# ┌─────────────────────┐ ┌────────────────────────────┐ ┌─────────────────────────┐
|
|
# │ <existing inbound> │ │ windows_<tag>_in (NEW) │ │ <existing inbound> │
|
|
# │ ├─→ existing dests │ │ tcpip-server, ISSERVER=1 │ │ listens on ORIG_PORT │
|
|
# │ └─→ linux_<tag>_ │ ──TCP──→ │ PORT = jump_port │ │ (UNCHANGED — no route │
|
|
# │ out (NEW) │ │ │ internal route │ │ changes here) │
|
|
# │ tcpip-client │ │ ▼ │ │ │
|
|
# │ → jump_port │ │ windows_<tag>_out (NEW) │ │ ▲ │
|
|
# └─────────────────────┘ │ tcpip-client │ │ │ │
|
|
# │ HOST=127.0.0.1 │ │ │ TCP localhost │
|
|
# │ PORT=ORIG_PORT ─────────┼───────┘ │ ──────────────────── │
|
|
# └────────────────────────────┘ │
|
|
# │
|
|
# (so OLD's existing inbound gets ONE new route; NEW's existing inbound is UNTOUCHED)
|
|
#
|
|
# Three new protocol blocks:
|
|
# 1. `linux_<tag>_out` → add to OLD env NetConfig (same process as original inbound).
|
|
# 2. `windows_<tag>_in` → add to NEW env server_jump/NetConfig.
|
|
# 3. `windows_<tag>_out` → add to NEW env server_jump/NetConfig.
|
|
#
|
|
# Plus one route-add snippet for OLD's existing inbound's DATAXLATE block.
|
|
#
|
|
# Tag = inbound thread name (auto-derived per Bryan's preference).
|
|
#
|
|
# Usage:
|
|
# nc-make-jump.sh <netconfig> --inbound NAME --new-host HOST --jump-port PORT
|
|
# [--inbound-host HOST] # default 127.0.0.1
|
|
# [--process-jump PROC] # process for NEW-side threads, default server_jump
|
|
# [--encoding ENC] # default = ENCODING from existing inbound
|
|
# [--out-prefix PREFIX] # write files instead of stdout
|
|
set -u
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
NCP="$LIB_DIR/nc-parse.sh"
|
|
|
|
die() { printf 'nc-make-jump: %s\n' "$*" >&2; exit 1; }
|
|
|
|
NC=""
|
|
INBOUND=""
|
|
NEW_HOST=""
|
|
JUMP_PORT=""
|
|
INBOUND_HOST="127.0.0.1"
|
|
PROC_JUMP="server_jump"
|
|
ENC_OVERRIDE=""
|
|
OUT_PREFIX=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--inbound) shift; INBOUND="$1" ;;
|
|
--new-host) shift; NEW_HOST="$1" ;;
|
|
--jump-port) shift; JUMP_PORT="$1" ;;
|
|
--inbound-host) shift; INBOUND_HOST="$1" ;;
|
|
--process-jump) shift; PROC_JUMP="$1" ;;
|
|
--encoding) shift; ENC_OVERRIDE="$1" ;;
|
|
--out-prefix) shift; OUT_PREFIX="$1" ;;
|
|
-h|--help) sed -n '2,50p' "$NC_SELF"; exit 0 ;;
|
|
-*) die "unknown flag: $1" ;;
|
|
*) [ -z "$NC" ] && NC="$1" || die "extra arg: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$NC" ] || die "usage: see --help"
|
|
[ -n "$INBOUND" ] || die "missing --inbound"
|
|
[ -n "$NEW_HOST" ] || die "missing --new-host"
|
|
[ -n "$JUMP_PORT" ] || die "missing --jump-port"
|
|
[ -f "$NC" ] || die "not a file: $NC"
|
|
|
|
# Read fields from the existing inbound
|
|
T_PROCESS=$("$NCP" protocol-field "$NC" "$INBOUND" PROCESSNAME 2>/dev/null | head -1)
|
|
[ -n "$T_PROCESS" ] || die "no such protocol in $NC: $INBOUND"
|
|
|
|
T_ENC=$("$NCP" protocol-field "$NC" "$INBOUND" ENCODING 2>/dev/null | head -1)
|
|
[ -z "$T_ENC" ] && T_ENC="ASCII"
|
|
ENC="${ENC_OVERRIDE:-$T_ENC}"
|
|
|
|
ORIG_PORT=$("$NCP" protocol-nested "$NC" "$INBOUND" PROTOCOL.PORT 2>/dev/null | head -1)
|
|
[ -n "$ORIG_PORT" ] || die "could not read PROTOCOL.PORT of inbound $INBOUND (is it a TCP listener? if it's a file/ICL inbound, this pattern may not apply directly)"
|
|
|
|
# tag = the inbound name itself (Bryan's "auto-derived" preference)
|
|
TAG="$INBOUND"
|
|
|
|
OLD_OUT_NAME="linux_${TAG}_out"
|
|
NEW_IN_NAME="windows_${TAG}_in"
|
|
NEW_OUT_NAME="windows_${TAG}_out"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Common helpers — emit the standard set of fields that every protocol block
|
|
# carries. Differences between the three threads are isolated below.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
emit_dataformat_passthrough() {
|
|
cat <<'EOF'
|
|
{ DATAFORMAT {
|
|
{ FRLTYPE offlen }
|
|
{ OFFLEN { { LEN 0 } { OFF 0 } } }
|
|
{ TYPE frl }
|
|
} }
|
|
EOF
|
|
}
|
|
|
|
emit_edibatch_empty() {
|
|
cat <<'EOF'
|
|
{ EDIBATCH {
|
|
{ IN_DATA { { TYPE {} } { VERSION {} } } }
|
|
{ OUT_DATA { { HEADER {} } { TRIGGER { { COUNT {} } { SCHEDULER {} } { TIMER {} } } } { TYPE {} } { VERSION {} } } }
|
|
} }
|
|
EOF
|
|
}
|
|
|
|
emit_errdbtps_default() {
|
|
cat <<'EOF'
|
|
{ ERRDBTPS {
|
|
{ ERRTPSPROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ RETRIES -1 }
|
|
} }
|
|
EOF
|
|
}
|
|
|
|
emit_proc_blocks_empty() {
|
|
cat <<'EOF'
|
|
{ RECVCONTROL { { ACKCONTROL { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } } { EOMSG {} } { HOLDMSGS 0 } { MSGPRIO 5120 } } }
|
|
{ SAVECONTROL { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ TPS_INBOUND { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ TPS_OUTBOUND { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ TRACING 0 }
|
|
EOF
|
|
}
|
|
|
|
emit_inner_protocol_tcp_client() {
|
|
local host="$1" port="$2"
|
|
cat <<EOF
|
|
{ PROTOCOL {
|
|
{ CA_FILE {} }
|
|
{ CA_PATH {} }
|
|
{ CERT_FILE {} }
|
|
{ CIPHERSUITES {} }
|
|
{ CLOSE 0 }
|
|
{ CONTROLMSGS 0 }
|
|
{ COPYCLIENTIPP 0 }
|
|
{ DELAYCONNECT 0 }
|
|
{ ENCODE_FILL {} }
|
|
{ ENCODE_INCLUSIVE 1 }
|
|
{ ENCODE_ISNATIVE 0 }
|
|
{ ENCODE_JUST r }
|
|
{ ENCODE_LEN 4 }
|
|
{ ENCODE_TYPE encapsulated }
|
|
{ HOST ${host} }
|
|
{ IPV4_V6_DUAL 0 }
|
|
{ IS_SSL 0 }
|
|
{ ISMULTI 0 }
|
|
{ ISSERVER 0 }
|
|
{ LOCAL_IP {} }
|
|
{ MAXCLIENT 0 }
|
|
{ MAXOBQD 0 }
|
|
{ MAXPREXLTQD 0 }
|
|
{ MLP_ERROR RESET }
|
|
{ MLP_MODE MLP }
|
|
{ MLP_TIMEOUT 30 }
|
|
{ MODE {} }
|
|
{ PASSWORD {} }
|
|
{ PORT ${port} }
|
|
{ PRIVATE_KEY {} }
|
|
{ RECONNECT 1 }
|
|
{ REOPEN 5 }
|
|
{ SSL_PROTOCOL All }
|
|
{ TCP_CONNECTION_TIMEOUT {} }
|
|
{ TYPE tcpip }
|
|
{ WRITEZERO 0 }
|
|
} }
|
|
EOF
|
|
}
|
|
|
|
emit_inner_protocol_tcp_server() {
|
|
local port="$1"
|
|
cat <<EOF
|
|
{ PROTOCOL {
|
|
{ CA_FILE {} }
|
|
{ CA_PATH {} }
|
|
{ CERT_FILE {} }
|
|
{ CIPHERSUITES {} }
|
|
{ CLOSE 0 }
|
|
{ CONTROLMSGS 0 }
|
|
{ COPYCLIENTIPP 0 }
|
|
{ DELAYCONNECT 0 }
|
|
{ ENCODE_FILL {} }
|
|
{ ENCODE_INCLUSIVE 1 }
|
|
{ ENCODE_ISNATIVE 0 }
|
|
{ ENCODE_JUST r }
|
|
{ ENCODE_LEN 4 }
|
|
{ ENCODE_TYPE encapsulated }
|
|
{ HOST {} }
|
|
{ IPV4_V6_DUAL 0 }
|
|
{ IS_SSL 0 }
|
|
{ ISMULTI 0 }
|
|
{ ISSERVER 1 }
|
|
{ LOCAL_IP {} }
|
|
{ MAXCLIENT 0 }
|
|
{ MAXOBQD 0 }
|
|
{ MAXPREXLTQD 0 }
|
|
{ MLP_ERROR RESET }
|
|
{ MLP_MODE MLP }
|
|
{ MLP_TIMEOUT 30 }
|
|
{ MODE {} }
|
|
{ PASSWORD {} }
|
|
{ PORT ${port} }
|
|
{ PRIVATE_KEY {} }
|
|
{ RECONNECT 1 }
|
|
{ REOPEN 5 }
|
|
{ SSL_PROTOCOL All }
|
|
{ TCP_CONNECTION_TIMEOUT {} }
|
|
{ TYPE tcpip }
|
|
{ WRITEZERO 0 }
|
|
} }
|
|
EOF
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Thread 1 — OLD env: linux_<tag>_out
|
|
# Outbound TCP client, same process as the existing inbound.
|
|
# No DATAXLATE (pass-through). Receives data via the route-add on the original inbound.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
emit_old_out() {
|
|
printf 'protocol %s {\n' "$OLD_OUT_NAME"
|
|
cat <<EOF
|
|
{ AUTOSTART 1 }
|
|
{ BITMAP {} }
|
|
{ COORDS {0 0} }
|
|
EOF
|
|
emit_dataformat_passthrough
|
|
printf ' { DATAXLATE {\n\n } }\n'
|
|
emit_edibatch_empty
|
|
cat <<EOF
|
|
{ ENCODING ${ENC} }
|
|
{ ENCODING_BOM_IB 0 }
|
|
{ ENCODING_BOM_OB 0 }
|
|
{ ENCODING_HL7 0 }
|
|
{ ENCODING_XML 0 }
|
|
{ EOCONFIG {} }
|
|
EOF
|
|
emit_errdbtps_default
|
|
cat <<EOF
|
|
{ GROUPS {server_jump OB_jump} }
|
|
{ HOSTDOWN 0 }
|
|
{ ICLSERVERPORT {} }
|
|
{ KEEPMSGONDISK 0 }
|
|
{ META {} }
|
|
{ OBWORKASIB 0 }
|
|
{ OUTBOUNDONLY 1 }
|
|
{ PROCESSNAME ${T_PROCESS} }
|
|
EOF
|
|
emit_inner_protocol_tcp_client "$NEW_HOST" "$JUMP_PORT"
|
|
emit_proc_blocks_empty
|
|
printf '}\n'
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Thread 2 — NEW env, server_jump site: windows_<tag>_in
|
|
# Inbound TCP server. Routes internally to windows_<tag>_out (same site).
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
emit_new_in() {
|
|
printf 'protocol %s {\n' "$NEW_IN_NAME"
|
|
cat <<EOF
|
|
{ AUTOSTART 1 }
|
|
{ BITMAP {} }
|
|
{ COORDS {0 0} }
|
|
EOF
|
|
emit_dataformat_passthrough
|
|
cat <<EOF
|
|
{ DATAXLATE {
|
|
{
|
|
{ CACHEMSG 0 }
|
|
{ DEL_ON_ERR_ROUTE 0 }
|
|
{ ROUTE_DETAILS {
|
|
{
|
|
{ DEST ${NEW_OUT_NAME} }
|
|
{ PROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ TYPE raw }
|
|
}
|
|
} }
|
|
{ ROUTE_ENABLED 1 }
|
|
{ TRXID .* }
|
|
{ WILDCARD ON }
|
|
}
|
|
} }
|
|
EOF
|
|
emit_edibatch_empty
|
|
cat <<EOF
|
|
{ ENCODING ${ENC} }
|
|
{ ENCODING_BOM_IB 0 }
|
|
{ ENCODING_BOM_OB 0 }
|
|
{ ENCODING_HL7 0 }
|
|
{ ENCODING_XML 0 }
|
|
{ EOCONFIG {} }
|
|
EOF
|
|
emit_errdbtps_default
|
|
cat <<EOF
|
|
{ GROUPS {server_jump IB_jump} }
|
|
{ HOSTDOWN 0 }
|
|
{ ICLSERVERPORT {} }
|
|
{ KEEPMSGONDISK 0 }
|
|
{ META {} }
|
|
{ OBWORKASIB 0 }
|
|
{ OUTBOUNDONLY 0 }
|
|
{ PROCESSNAME ${PROC_JUMP} }
|
|
EOF
|
|
emit_inner_protocol_tcp_server "$JUMP_PORT"
|
|
emit_proc_blocks_empty
|
|
printf '}\n'
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Thread 3 — NEW env, server_jump site: windows_<tag>_out
|
|
# Outbound TCP client, connects to localhost on the ORIGINAL inbound port.
|
|
# Receives via internal route from windows_<tag>_in.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
emit_new_out() {
|
|
printf 'protocol %s {\n' "$NEW_OUT_NAME"
|
|
cat <<EOF
|
|
{ AUTOSTART 1 }
|
|
{ BITMAP {} }
|
|
{ COORDS {0 0} }
|
|
EOF
|
|
emit_dataformat_passthrough
|
|
printf ' { DATAXLATE {\n\n } }\n'
|
|
emit_edibatch_empty
|
|
cat <<EOF
|
|
{ ENCODING ${ENC} }
|
|
{ ENCODING_BOM_IB 0 }
|
|
{ ENCODING_BOM_OB 0 }
|
|
{ ENCODING_HL7 0 }
|
|
{ ENCODING_XML 0 }
|
|
{ EOCONFIG {} }
|
|
EOF
|
|
emit_errdbtps_default
|
|
cat <<EOF
|
|
{ GROUPS {server_jump OB_jump} }
|
|
{ HOSTDOWN 0 }
|
|
{ ICLSERVERPORT {} }
|
|
{ KEEPMSGONDISK 0 }
|
|
{ META {} }
|
|
{ OBWORKASIB 0 }
|
|
{ OUTBOUNDONLY 1 }
|
|
{ PROCESSNAME ${PROC_JUMP} }
|
|
EOF
|
|
emit_inner_protocol_tcp_client "$INBOUND_HOST" "$ORIG_PORT"
|
|
emit_proc_blocks_empty
|
|
printf '}\n'
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Route-add snippet — to splice into the OLD env existing inbound's DATAXLATE block.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
emit_route_add() {
|
|
cat <<EOF
|
|
{
|
|
{ CACHEMSG 0 }
|
|
{ DEL_ON_ERR_ROUTE 0 }
|
|
{ ROUTE_DETAILS {
|
|
{
|
|
{ DEST ${OLD_OUT_NAME} }
|
|
{ PROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
|
|
{ TYPE raw }
|
|
}
|
|
} }
|
|
{ ROUTE_ENABLED 1 }
|
|
{ TRXID .* }
|
|
{ WILDCARD ON }
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Output
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
if [ -n "$OUT_PREFIX" ]; then
|
|
emit_old_out > "${OUT_PREFIX}.old_out.tcl"
|
|
emit_new_in > "${OUT_PREFIX}.new_in.tcl"
|
|
emit_new_out > "${OUT_PREFIX}.new_out.tcl"
|
|
emit_route_add > "${OUT_PREFIX}.route_add.tcl"
|
|
printf '%s\n%s\n%s\n%s\n' \
|
|
"${OUT_PREFIX}.old_out.tcl" \
|
|
"${OUT_PREFIX}.new_in.tcl" \
|
|
"${OUT_PREFIX}.new_out.tcl" \
|
|
"${OUT_PREFIX}.route_add.tcl"
|
|
else
|
|
cat <<EOF
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
# Jump-thread set for inbound '${INBOUND}'
|
|
# tag (auto) = ${TAG}
|
|
# OLD process = ${T_PROCESS}
|
|
# NEW process = ${PROC_JUMP}
|
|
# ENCODING = ${ENC}
|
|
# ORIG inbound port = ${ORIG_PORT} (used by windows_${TAG}_out → 127.0.0.1)
|
|
# JUMP TCP port = ${JUMP_PORT} (OLD → NEW, used by linux_${TAG}_out and windows_${TAG}_in)
|
|
# NEW linux host = ${NEW_HOST}
|
|
# NEW-side dest = ${INBOUND_HOST}:${ORIG_PORT}
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
# ── ① OLD env — add to NetConfig (process ${T_PROCESS}) ──
|
|
EOF
|
|
emit_old_out
|
|
cat <<EOF
|
|
|
|
# ── ② NEW env — add to server_jump/NetConfig (process ${PROC_JUMP}) ──
|
|
EOF
|
|
emit_new_in
|
|
echo ""
|
|
emit_new_out
|
|
cat <<EOF
|
|
|
|
# ── ③ ROUTE_ADD snippet — splice into OLD inbound '${INBOUND}' DATAXLATE block ──
|
|
EOF
|
|
emit_route_add
|
|
fi
|