cloverleaf-larry/lib/nc-insert-protocol.sh
Bryan Johnson e08f030df5 v0.3.0: initial release of Larry-Anywhere
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>
2026-05-26 09:46:20 -07:00

243 lines
9.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# nc-insert-protocol.sh — write side of Example 1 (and general NetConfig writes).
#
# Two operations:
#
# 1. insert — append a NEW protocol block to a NetConfig file.
# Modes: end (default) | after-protocol NAME | before-protocol NAME
#
# 2. add-route — splice a NEW route entry into an existing protocol's
# DATAXLATE block. The route entry is the inner `{ … }`
# object (with CACHEMSG, ROUTE_DETAILS, TRXID, etc.) that
# nc-make-jump.sh emits as the route_add snippet.
#
# Both operations go through journal.sh: snapshot the original, write a diff
# entry, then atomically replace the target. Roll back later via:
# larry-rollback.sh --target <netconfig> # newest-first
# larry-rollback.sh --session <session-id> # whole session
# larry-rollback.sh --entry <entry-id> # one specific write
#
# Usage:
# nc-insert-protocol.sh insert <netconfig> <block_file> [--mode end|after|before --anchor NAME]
# nc-insert-protocol.sh add-route <netconfig> <protocol_name> <route_file>
#
# <block_file> path to a file containing the TCL `protocol NAME { … }` block to insert
# <route_file> path to a file containing the route entry (just the inner `{ … }`)
#
# Exit codes: 0 OK, 2 usage, 3 target not found, 4 already exists / wouldn't change
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
NCP="$LIB_DIR/nc-parse.sh"
JOURNAL="$LIB_DIR/journal.sh"
die() { printf 'nc-insert-protocol: %s\n' "$*" >&2; exit 1; }
# Source journal so we can call journal_write directly
# shellcheck disable=SC1090
. "$JOURNAL"
cmd_insert() {
local nc="$1" block_file="$2"
shift 2
local mode="end" anchor=""
while [ $# -gt 0 ]; do
case "$1" in
--mode) shift; mode="$1" ;;
--anchor) shift; anchor="$1" ;;
*) die "unknown flag for insert: $1" ;;
esac
shift
done
[ -f "$nc" ] || { die "no such NetConfig: $nc"; }
[ -f "$block_file" ] || { die "no such block file: $block_file"; }
# Extract block name to detect collisions
local block_name
block_name=$(awk '/^protocol [A-Za-z0-9_]+/ {print $2; exit}' "$block_file")
[ -n "$block_name" ] || die "block file does not start with 'protocol NAME {' — bad input"
if "$NCP" list-protocols "$nc" 2>/dev/null | grep -qx "$block_name"; then
die "protocol '$block_name' already exists in $nc — refusing to insert. Use a different name or update the existing block via a different tool."
fi
local tmp; tmp=$(mktemp)
case "$mode" in
end)
cat "$nc" > "$tmp"
# Ensure trailing newline before appending
[ -n "$(tail -c1 "$tmp")" ] && printf '\n' >> "$tmp"
printf '\n' >> "$tmp"
cat "$block_file" >> "$tmp"
printf '\n' >> "$tmp"
;;
after)
[ -n "$anchor" ] || die "--mode after needs --anchor NAME"
local end_line
end_line=$("$NCP" list-protocols "$nc" >/dev/null 2>&1
# Compute end line via the same logic as protocol-block
awk -v target="$anchor" '
BEGIN { depth=0; in_block=0; name="" }
/^protocol [A-Za-z0-9_]+ \{$/ && !in_block {
name=$2; depth=1; in_block=1
if (name==target) start=NR
next
}
in_block {
n_open=gsub(/\{/,"{",$0); n_close=gsub(/\}/,"}",$0)
depth += n_open - n_close
if (depth==0) {
if (name==target) { print NR; exit }
in_block=0; name=""
}
}' "$nc")
[ -n "$end_line" ] || die "anchor protocol not found: $anchor"
head -n "$end_line" "$nc" > "$tmp"
printf '\n' >> "$tmp"
cat "$block_file" >> "$tmp"
printf '\n' >> "$tmp"
tail -n +$((end_line + 1)) "$nc" >> "$tmp"
;;
before)
[ -n "$anchor" ] || die "--mode before needs --anchor NAME"
local start_line
start_line=$("$NCP" protocol-line "$nc" "$anchor" 2>/dev/null)
[ -n "$start_line" ] || die "anchor protocol not found: $anchor"
head -n $((start_line - 1)) "$nc" > "$tmp"
cat "$block_file" >> "$tmp"
printf '\n' >> "$tmp"
tail -n +"$start_line" "$nc" >> "$tmp"
;;
*) die "bad --mode: $mode (use end|after|before)" ;;
esac
# Hand off to journal for atomic backup+write
local entry_id
entry_id=$(journal_write "$nc" "$tmp")
rm -f "$tmp"
printf 'inserted protocol %s into %s (mode=%s)\n' "$block_name" "$nc" "$mode"
printf 'journal entry: %s\n' "$entry_id"
printf 'rollback: larry-rollback.sh --entry %s OR larry-rollback.sh --target %s\n' "$entry_id" "$nc"
}
cmd_add_route() {
local nc="$1" prot="$2" route_file="$3"
[ -f "$nc" ] || die "no such NetConfig: $nc"
[ -f "$route_file" ] || die "no such route file: $route_file"
# Find the protocol's line range
local start end
start=$("$NCP" protocol-line "$nc" "$prot" 2>/dev/null)
[ -n "$start" ] || die "no such protocol: $prot"
# Compute end-line of the protocol block
end=$(awk -v s="$start" '
NR == s { depth = 1; in_block = 1; next }
in_block {
n_open = gsub(/\{/, "{", $0)
n_close = gsub(/\}/, "}", $0)
depth += n_open - n_close
if (depth == 0) { print NR; exit }
}
' "$nc")
[ -n "$end" ] || die "could not determine end of protocol block: $prot"
# Find the DATAXLATE inner block boundaries (just inside the protocol).
# Lines look like:
# { DATAXLATE {
# {existing route 1}
# {existing route 2}
# } }
local dx_start dx_end
dx_start=$(awk -v s="$start" -v e="$end" '
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{$/ { print NR; exit }
' "$nc")
if [ -z "$dx_start" ]; then
# Handle the empty case: { DATAXLATE { } } or { DATAXLATE {<blank> } }
# If empty, replace with a populated block on a single line plus the new route.
local dx_empty_line
dx_empty_line=$(awk -v s="$start" -v e="$end" '
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{[[:space:]]*$/ { print NR; exit }
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{[[:space:]]*\}[[:space:]]*\}/ { print NR; exit }
' "$nc")
[ -n "$dx_empty_line" ] || die "could not locate DATAXLATE block in protocol $prot"
dx_start="$dx_empty_line"
fi
# Find the matching close of the DATAXLATE block:
# search forward from dx_start, counting braces to find the line where
# depth-since-dx_start returns to 0.
dx_end=$(awk -v s="$dx_start" -v e="$end" '
NR>=s && NR<=e {
n_open = gsub(/\{/, "{", $0)
n_close = gsub(/\}/, "}", $0)
if (NR == s) {
# The DATAXLATE-open line; the outer "{ DATAXLATE {" has 2 opens
depth = 0
}
depth += n_open - n_close
if (NR > s && depth <= -2) { # closing "} }" brings us down 2 levels
print NR; exit
}
}
' "$nc")
# If our naive count failed, try a simpler approach: find next line that is
# exactly the closing pattern of DATAXLATE ( } } at the appropriate indent).
if [ -z "$dx_end" ]; then
dx_end=$(awk -v s="$dx_start" -v e="$end" '
NR>s && NR<=e && /^[[:space:]]+\}[[:space:]]\}[[:space:]]*$/ { print NR; exit }
' "$nc")
fi
[ -n "$dx_end" ] || die "could not locate end of DATAXLATE block in protocol $prot"
# Indent the route content to match the DATAXLATE inner indentation (8 spaces typical).
local indent=" "
local indented_route; indented_route=$(awk -v IND="$indent" '{print IND $0}' "$route_file")
# Build the new file:
# lines 1..dx_start
# route content (indented)
# lines dx_start+1..end of file
# Skip the simple "empty" case: if dx_start was a "{ DATAXLATE {} }" single line,
# we need to split it. Detect by reading that line.
local dx_start_line; dx_start_line=$(sed -n "${dx_start}p" "$nc")
local tmp; tmp=$(mktemp)
if [[ "$dx_start_line" =~ \{[[:space:]]DATAXLATE[[:space:]]\{[[:space:]]*\}[[:space:]]*\}[[:space:]]*$ ]]; then
# Single-line "{ DATAXLATE { } }" — replace with multi-line form
head -n $((dx_start - 1)) "$nc" > "$tmp"
printf ' { DATAXLATE {\n' >> "$tmp"
printf '%s\n' "$indented_route" >> "$tmp"
printf ' } }\n' >> "$tmp"
tail -n +$((dx_start + 1)) "$nc" >> "$tmp"
else
head -n "$dx_start" "$nc" > "$tmp"
printf '%s\n' "$indented_route" >> "$tmp"
tail -n +$((dx_start + 1)) "$nc" >> "$tmp"
fi
local entry_id
entry_id=$(journal_write "$nc" "$tmp")
rm -f "$tmp"
printf 'added route to protocol %s in %s (DATAXLATE block at line %s)\n' "$prot" "$nc" "$dx_start"
printf 'journal entry: %s\n' "$entry_id"
printf 'rollback: larry-rollback.sh --entry %s OR larry-rollback.sh --target %s\n' "$entry_id" "$nc"
}
# ─────────────────────────────────────────────────────────────────────────────
SUB="${1:-help}"
case "$SUB" in
insert)
[ $# -ge 3 ] || { echo "usage: $0 insert <netconfig> <block_file> [--mode end|after|before --anchor NAME]" >&2; exit 2; }
cmd_insert "$2" "$3" "${@:4}" ;;
add-route)
[ $# -ge 4 ] || { echo "usage: $0 add-route <netconfig> <protocol_name> <route_file>" >&2; exit 2; }
cmd_add_route "$2" "$3" "$4" ;;
help|-h|--help) sed -n '2,30p' "$NC_SELF" ;;
*) echo "unknown subcommand: $SUB" >&2; exit 2 ;;
esac