v0.8.31: nc_set_field — change a thread's PORT/HOST/PROCESSNAME/ENCODING (journaled)
New mutating tool, built on the proven journal/rollback foundation. Curated safe field set only (rejects anything else; never creates a missing field). Edits are line-number-anchored to the target thread's protocol block via nc-parse (a shared port/host value in another thread is never touched), brace-balance-checked before an atomic write, journaled for byte-identical rollback. Flags: --dry-run (no write), --confirm yes, --site, --netconfig. Copy-tested: PORT + HOST applied surgically, rollback byte-identical. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5bc3195f98
commit
7a715c802a
35
CHANGELOG.md
35
CHANGELOG.md
@ -4,6 +4,41 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
|
|||||||
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
||||||
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
||||||
|
|
||||||
|
## v0.8.31 — 2026-05-28
|
||||||
|
|
||||||
|
**★ NEW WRITE TOOL: `nc_set_field` — change a settable field (PORT, HOST/IP,
|
||||||
|
PROCESSNAME, ENCODING) on an existing thread, JOURNALED + rollback-reversible.**
|
||||||
|
Bryan's top-requested write feature ("changing port numbers and ip addresses").
|
||||||
|
Built on the exact journal/atomic-write foundation the v0.8.30 mutate pass proved
|
||||||
|
byte-identical-reversible — same idiom as `nc-table.sh` / `nc-insert-protocol.sh`
|
||||||
|
(snapshot → diff → atomic write → journal entry; undo via `larry-rollback.sh`).
|
||||||
|
|
||||||
|
- **Invocation:** `nc-set-field <thread>[.<site>] <field> <value>` (bare thread →
|
||||||
|
`$HCISITE`; `thread.site` cross-site; also `--site`). Flags: `--dry-run` (show
|
||||||
|
before→after, NO write), `--confirm yes` (skip the y/N prompt; still journaled),
|
||||||
|
`--netconfig PATH`, `--hciroot PATH`, `--completion` (emit a bash-completion
|
||||||
|
snippet: thread names + the field enum).
|
||||||
|
- **Curated safe set, explicit reject otherwise** — never blind-edits arbitrary
|
||||||
|
tokens and never CREATES a missing field: PORT → nested `PROTOCOL.PORT`; HOST
|
||||||
|
(alias IP) → nested `PROTOCOL.HOST`; PROCESSNAME → top-level; ENCODING →
|
||||||
|
top-level (must already exist). Field name is case-insensitive.
|
||||||
|
- **Anchored edit, NOT a global sed.** Locates the thread's protocol-block line
|
||||||
|
range via `nc-parse` (`protocol-line` + brace-balanced end walk), then the exact
|
||||||
|
field line within it (nested vs. top-level scoped), and replaces ONLY that value
|
||||||
|
token — preserving indentation + brace shape. A port/host SHARED by another
|
||||||
|
thread is provably untouched. Re-verifies balanced braces before the journal
|
||||||
|
write; a broken structure aborts with nothing written. No-op (value already set)
|
||||||
|
exits 4 cleanly.
|
||||||
|
- **Wired into all 4 larry.sh surfaces** (manual-tools registry, `tool_nc_set_field`
|
||||||
|
wrapper, `execute_tool` case, TOOLS_JSON schema) + the bash-completion snippet.
|
||||||
|
- Verified on a COPY of the real 24-site integrator (`/tmp/clvf_setfield_test`,
|
||||||
|
read fixture sha-confirmed untouched): dry-run shows before→after without writing;
|
||||||
|
a real PORT change (39500→39600) and HOST change (172.31.23.2→10.34.48.11) each
|
||||||
|
apply as a single surgical line edit (braces balanced, the two threads sharing
|
||||||
|
PORT 51205 untouched); both journaled; whole-session rollback restored the file
|
||||||
|
BYTE-IDENTICAL; an unsupported field (MLP_TIMEOUT, TYPE) and an unknown thread
|
||||||
|
are rejected cleanly.
|
||||||
|
|
||||||
## v0.8.30 — 2026-05-28
|
## v0.8.30 — 2026-05-28
|
||||||
|
|
||||||
**★ WRITE/MUTATE TOOL VALIDATION PASS + journal-rollback foundation verified —
|
**★ WRITE/MUTATE TOOL VALIDATION PASS + journal-rollback foundation verified —
|
||||||
|
|||||||
7
MANIFEST
7
MANIFEST
@ -23,16 +23,16 @@
|
|||||||
# scripts/make-manifest.sh and bump VERSION.
|
# scripts/make-manifest.sh and bump VERSION.
|
||||||
|
|
||||||
# Top-level scripts
|
# Top-level scripts
|
||||||
larry.sh 5f7c82ac08e6be85db8acaf314fcbdc394ac6182f5f784aba8540c05cb139172
|
larry.sh 940d8fad8bffc42f6b5b7e7b295f8218ee43c03f327ca250e71bbb7dbce1a002
|
||||||
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
||||||
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
||||||
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
||||||
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
VERSION fe9f7fe00061bf3b44b38df8fcaa8d09d689c6ee8be0fee57e7d3fe528d18e50
|
VERSION bae94dce70052efa657cca9bf24209ef8ae9cb277deb79f38e7fdbdfdc5bd254
|
||||||
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
||||||
CHANGELOG.md 75b131794cc273ba1dba430637008c975b2532bfc2bcc99cc9fd65bd12701537
|
CHANGELOG.md 8696119944e16e8b7ab798d0641fb9f6beda48f871208132b7929401fc611d75
|
||||||
|
|
||||||
# Agent personas (system-prompt overlays)
|
# Agent personas (system-prompt overlays)
|
||||||
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
||||||
@ -96,6 +96,7 @@ lib/nc-table.sh a6d5c11dd460cfb100ea50c74d57c1a46ef49112632037534a32cd28600abe7f
|
|||||||
lib/nc-xlate.sh 8621e6f0ef55524dd6ecba91fee055cf9cdc168791e75ba7c15d9bf501fe09bf
|
lib/nc-xlate.sh 8621e6f0ef55524dd6ecba91fee055cf9cdc168791e75ba7c15d9bf501fe09bf
|
||||||
lib/nc-smat-diff.sh 9c04d9e2f35f22c78d5f3c40a884ed23a3b6aaabc53ee27dfbfb66ab3166a567
|
lib/nc-smat-diff.sh 9c04d9e2f35f22c78d5f3c40a884ed23a3b6aaabc53ee27dfbfb66ab3166a567
|
||||||
lib/nc-create-thread.sh e35b0ee27f2c0327928adc21467f2b9d5a29f7436e5b89773f65420281739df7
|
lib/nc-create-thread.sh e35b0ee27f2c0327928adc21467f2b9d5a29f7436e5b89773f65420281739df7
|
||||||
|
lib/nc-set-field.sh 0977fcab1cd931ecdd78c8aea673db93d898f11f535daf26456a4fb7845542e4
|
||||||
lib/nc-tclgen.sh 5b8e73d7f6950a2b84f563132562ea82f62f4acac907257e233c7e68d85506c9
|
lib/nc-tclgen.sh 5b8e73d7f6950a2b84f563132562ea82f62f4acac907257e233c7e68d85506c9
|
||||||
lib/nc-parse.sh 52fef42d7a4b361534ab0d921deef74586dfeb6c199c941cebb55abcc2c39d4f
|
lib/nc-parse.sh 52fef42d7a4b361534ab0d921deef74586dfeb6c199c941cebb55abcc2c39d4f
|
||||||
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
||||||
|
|||||||
34
larry.sh
34
larry.sh
@ -78,7 +78,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.8.30"
|
LARRY_VERSION="0.8.31"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -350,6 +350,7 @@ nc-xlate.sh|Visualize and explore a Cloverleaf xlate (.xlt) file — the TCL nes
|
|||||||
nc-table.sh|Read and modify Cloverleaf lookup tables (.tbl) — every write is backed up and auditable
|
nc-table.sh|Read and modify Cloverleaf lookup tables (.tbl) — every write is backed up and auditable
|
||||||
#NetConfig (write)
|
#NetConfig (write)
|
||||||
nc-create-thread.sh|High-level: create a new thread in a NetConfig (and optionally wire its route)
|
nc-create-thread.sh|High-level: create a new thread in a NetConfig (and optionally wire its route)
|
||||||
|
nc-set-field.sh|Change ONE settable field (PORT, HOST/IP, PROCESSNAME, ENCODING) on an existing thread — anchored to the right block, journaled, rollback-reversible
|
||||||
nc-insert-protocol.sh|Low-level write side: insert/replace a protocol block in a NetConfig
|
nc-insert-protocol.sh|Low-level write side: insert/replace a protocol block in a NetConfig
|
||||||
nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data replay
|
nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data replay
|
||||||
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
||||||
@ -1816,6 +1817,32 @@ tool_nc_add_route() {
|
|||||||
return $rc
|
return $rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# nc_set_field — change ONE curated settable field (PORT, HOST/IP, PROCESSNAME,
|
||||||
|
# ENCODING) on an existing thread's NetConfig block. JOURNALED + rollback-
|
||||||
|
# reversible (same foundation as nc_insert_protocol / nc_add_route). The edit is
|
||||||
|
# anchored to the right thread's block and the right field via nc-parse — NOT a
|
||||||
|
# global sed — so a shared port/host on another thread is never touched. Rejects
|
||||||
|
# any non-curated field. dry_run previews before→after without writing; confirm
|
||||||
|
# defaults to 'yes' from the API path (the journal makes it reversible).
|
||||||
|
tool_nc_set_field() {
|
||||||
|
local thread="$1" field="$2" value="$3" site="${4:-}" netconfig="${5:-}"
|
||||||
|
local dry_run="${6:-0}" confirm="${7:-yes}" hciroot="${8:-${HCIROOT:-}}"
|
||||||
|
_lib_err_if_missing || return
|
||||||
|
[ -n "$thread" ] && [ -n "$field" ] && [ -n "$value" ] \
|
||||||
|
|| { echo "ERROR: nc_set_field needs thread, field, and value"; return 1; }
|
||||||
|
local args=("$thread" "$field" "$value")
|
||||||
|
[ -n "$site" ] && args+=(--site "$site")
|
||||||
|
[ -n "$netconfig" ] && args+=(--netconfig "$netconfig")
|
||||||
|
[ -n "$hciroot" ] && args+=(--hciroot "$hciroot")
|
||||||
|
if [ "$dry_run" = "1" ]; then
|
||||||
|
args+=(--dry-run)
|
||||||
|
elif [ "$confirm" = "yes" ]; then
|
||||||
|
args+=(--confirm yes)
|
||||||
|
fi
|
||||||
|
LARRY_SESSION_ID="${LARRY_SESSION_ID:-$SESSION_ID}" \
|
||||||
|
"$LARRY_LIB_DIR/nc-set-field.sh" "${args[@]}" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
tool_nc_regression() {
|
tool_nc_regression() {
|
||||||
local scope="$1" count="$2" env_a="$3" site_a="$4" env_b="$5" site_b="$6" out_dir="$7"
|
local scope="$1" count="$2" env_a="$3" site_a="$4" env_b="$5" site_b="$6" out_dir="$7"
|
||||||
local route_cmd="${8:-}" ignore="${9:-MSH.7}" phase="${10:-all}" dry_run="${11:-0}"
|
local route_cmd="${8:-}" ignore="${9:-MSH.7}" phase="${10:-all}" dry_run="${11:-0}"
|
||||||
@ -4382,6 +4409,10 @@ execute_tool() {
|
|||||||
nc_find) tool_nc_find "$(J '.mode')" "$(J '.query')" "$(J '.format // "table"')" "$(J '.hciroot // ""')" ;;
|
nc_find) tool_nc_find "$(J '.mode')" "$(J '.query')" "$(J '.format // "table"')" "$(J '.hciroot // ""')" ;;
|
||||||
nc_insert_protocol) tool_nc_insert_protocol "$(J '.netconfig')" "$(J '.block')" "$(J '.mode // "end"')" "$(J '.anchor // ""')" ;;
|
nc_insert_protocol) tool_nc_insert_protocol "$(J '.netconfig')" "$(J '.block')" "$(J '.mode // "end"')" "$(J '.anchor // ""')" ;;
|
||||||
nc_add_route) tool_nc_add_route "$(J '.netconfig')" "$(J '.protocol_name')" "$(J '.route')" ;;
|
nc_add_route) tool_nc_add_route "$(J '.netconfig')" "$(J '.protocol_name')" "$(J '.route')" ;;
|
||||||
|
nc_set_field) tool_nc_set_field "$(J '.thread')" "$(J '.field')" "$(J '.value')" \
|
||||||
|
"$(J '.site // ""')" "$(J '.netconfig // ""')" \
|
||||||
|
"$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" \
|
||||||
|
"$(J '.confirm // "yes"')" "$(J '.hciroot // ""')" ;;
|
||||||
hl7_diff) tool_hl7_diff "$(J '.left')" "$(J '.right')" "$(J '.ignore // "MSH.7"')" "$(J '.include // ""')" "$(J '.format // "text"')" ;;
|
hl7_diff) tool_hl7_diff "$(J '.left')" "$(J '.right')" "$(J '.ignore // "MSH.7"')" "$(J '.include // ""')" "$(J '.format // "text"')" ;;
|
||||||
nc_diff_interface) tool_nc_diff_interface "$(J '.interface')" "$(J '.left')" "$(J '.right')" "$(J '.out // ""')" \
|
nc_diff_interface) tool_nc_diff_interface "$(J '.interface')" "$(J '.left')" "$(J '.right')" "$(J '.out // ""')" \
|
||||||
"$(J '.include_tables // 0' | sed "s/false/0/;s/true/1/")" \
|
"$(J '.include_tables // 0' | sed "s/false/0/;s/true/1/")" \
|
||||||
@ -4444,6 +4475,7 @@ TOOLS_JSON=$(cat <<'TOOLS_END'
|
|||||||
{"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}},
|
{"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}},
|
||||||
{"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}},
|
{"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}},
|
||||||
{"name":"nc_add_route","description":"Splice a route entry into an existing protocol's DATAXLATE block. Used to add a new DEST to an inbound's routing (e.g. wiring the OLD inbound to also route to the new linux_<tag>_out jump thread). ALL WRITES GO THROUGH THE JOURNAL.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"protocol_name":{"type":"string","description":"The existing protocol to modify."},"route":{"type":"string","description":"The route entry text (an inner `{ ... }` object with CACHEMSG, ROUTE_DETAILS, TRXID, etc.). Get from nc_make_jump's route_add output."}},"required":["netconfig","protocol_name","route"]}},
|
{"name":"nc_add_route","description":"Splice a route entry into an existing protocol's DATAXLATE block. Used to add a new DEST to an inbound's routing (e.g. wiring the OLD inbound to also route to the new linux_<tag>_out jump thread). ALL WRITES GO THROUGH THE JOURNAL.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"protocol_name":{"type":"string","description":"The existing protocol to modify."},"route":{"type":"string","description":"The route entry text (an inner `{ ... }` object with CACHEMSG, ROUTE_DETAILS, TRXID, etc.). Get from nc_make_jump's route_add output."}},"required":["netconfig","protocol_name","route"]}},
|
||||||
|
{"name":"nc_set_field","description":"Change ONE settable field on an EXISTING thread's NetConfig protocol block — Bryan's top write feature: changing PORT numbers and HOST/IP addresses on a live interface, safely. JOURNALED + ROLLBACK-REVERSIBLE on the same foundation as nc_insert_protocol/nc_add_route (snapshot + atomic write; undo with larry-rollback.sh, view with larry_rollback_list). The edit is ANCHORED to the named thread's block and the named field via the native parser — NOT a global sed — so a port/host value SHARED by another thread is never touched. CURATED SAFE SET ONLY (anything else is rejected with a clear error; it will NOT blind-edit arbitrary tokens, and it will NOT create a missing field): PORT (the nested PROTOCOL.PORT), HOST (the nested PROTOCOL.HOST; alias IP), PROCESSNAME (top-level), ENCODING (top-level, must already exist). USE THIS for 'change the port on thread X to N', 'point X at a new IP/host', 'move X to process P', 'set the encoding'. SAFETY: set dry_run=true to preview the before→after WITHOUT writing; otherwise confirm defaults to yes (the journal keeps it reversible). Resolves the NetConfig from thread+site under $HCIROOT (or pass netconfig explicitly). Thread may be a bare name (resolved in site/$HCISITE) or 'thread.site'.","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"Thread/protocol name to edit. Bare name (resolved in site or $HCISITE) or 'thread.site' for cross-site."},"field":{"type":"string","enum":["PORT","HOST","IP","PROCESSNAME","ENCODING"],"description":"Which curated field to change. PORT/HOST(=IP) live in the nested PROTOCOL{} block; PROCESSNAME/ENCODING are top-level. Any other field is rejected."},"value":{"type":"string","description":"The new value, e.g. 39600 (PORT) or 10.34.48.11 (HOST)."},"site":{"type":"string","description":"Site (the NetConfig's parent dir). Optional alt to thread.site; defaults to $HCISITE."},"netconfig":{"type":"string","description":"Explicit NetConfig path. Overrides site-based resolution."},"dry_run":{"type":"boolean","description":"true = show the before→after WITHOUT writing. Default false."},"confirm":{"type":"string","description":"'yes' (default) skips the interactive y/N prompt — still journaled/reversible. Set anything else to require an interactive confirm (only meaningful in a tty)."},"hciroot":{"type":"string","description":"Override $HCIROOT for site resolution."}},"required":["thread","field","value"]}},
|
||||||
{"name":"larry_rollback_list","description":"List journal entries — every write that's gone through nc_insert_protocol, nc_add_route, or write_file (once journaled write_file is enabled). Shows session-id, sequence, target, timestamp. Use larry-rollback.sh from the shell to actually roll back.","input_schema":{"type":"object","properties":{"session":{"type":"string","description":"Optional. Limit to one session id."}},"required":[]}},
|
{"name":"larry_rollback_list","description":"List journal entries — every write that's gone through nc_insert_protocol, nc_add_route, or write_file (once journaled write_file is enabled). Shows session-id, sequence, target, timestamp. Use larry-rollback.sh from the shell to actually roll back.","input_schema":{"type":"object","properties":{"session":{"type":"string","description":"Optional. Limit to one session id."}},"required":[]}},
|
||||||
|
|
||||||
{"name":"lesson_record","description":"Append a lesson to local capture at $LARRY_HOME/lessons/<date>.md. Use when Bryan teaches you something new (a correction, a pattern, a quirk, a gotcha) so the home-Larry can be updated later. Lessons stay LOCAL; Bryan exports them with `lessons.sh export` and pastes back to home-Larry when he can. CALL THIS WHEN: Bryan corrects a misunderstanding, reveals a site-specific convention, points out a bug, requests a behavior change, or shares a workflow detail you should remember next time.","input_schema":{"type":"object","properties":{"text":{"type":"string","description":"The lesson content. Markdown. Include enough context that home-Larry can act on it without re-deriving."},"topic":{"type":"string","description":"Short topic tag, e.g. \"NetConfig parsing\", \"jump-thread naming\", \"site conventions\"."},"site":{"type":"string","description":"Site this lesson is scoped to, if any. Default: current $HCISITE."},"severity":{"type":"string","enum":["info","warn","fix"],"description":"info=general learning, warn=behavior I should change, fix=Bryan called out a bug."}},"required":["text"]}},
|
{"name":"lesson_record","description":"Append a lesson to local capture at $LARRY_HOME/lessons/<date>.md. Use when Bryan teaches you something new (a correction, a pattern, a quirk, a gotcha) so the home-Larry can be updated later. Lessons stay LOCAL; Bryan exports them with `lessons.sh export` and pastes back to home-Larry when he can. CALL THIS WHEN: Bryan corrects a misunderstanding, reveals a site-specific convention, points out a bug, requests a behavior change, or shares a workflow detail you should remember next time.","input_schema":{"type":"object","properties":{"text":{"type":"string","description":"The lesson content. Markdown. Include enough context that home-Larry can act on it without re-deriving."},"topic":{"type":"string","description":"Short topic tag, e.g. \"NetConfig parsing\", \"jump-thread naming\", \"site conventions\"."},"site":{"type":"string","description":"Site this lesson is scoped to, if any. Default: current $HCISITE."},"severity":{"type":"string","enum":["info","warn","fix"],"description":"info=general learning, warn=behavior I should change, fix=Bryan called out a bug."}},"required":["text"]}},
|
||||||
|
|||||||
350
lib/nc-set-field.sh
Executable file
350
lib/nc-set-field.sh
Executable file
@ -0,0 +1,350 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# nc-set-field.sh — change ONE settable field on an existing thread's NetConfig
|
||||||
|
# protocol block, JOURNALED so it's rollback-reversible.
|
||||||
|
#
|
||||||
|
# Bryan's top-requested write feature: changing port numbers and IP addresses on
|
||||||
|
# an existing interface, safely and auditably. Built on the SAME journal/atomic-
|
||||||
|
# write foundation proven byte-identical-reversible by the v0.8.30 write/mutate
|
||||||
|
# pass — identical pattern to nc-table.sh / nc-insert-protocol.sh.
|
||||||
|
#
|
||||||
|
# Invocation:
|
||||||
|
# nc-set-field <thread>[.<site>] <field> <value> [opts]
|
||||||
|
#
|
||||||
|
# <thread> bare thread → resolved in $HCISITE
|
||||||
|
# <thread>.<site> cross-site form (e.g. ADTto_CodaMetrix.ancout)
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --site SITE site override (alt to thread.site form)
|
||||||
|
# --dry-run show the before→after WITHOUT writing
|
||||||
|
# --confirm yes skip the interactive y/N prompt (still JOURNALED)
|
||||||
|
# --netconfig PATH explicit NetConfig path (overrides site resolution)
|
||||||
|
# --hciroot PATH override $HCIROOT for site resolution
|
||||||
|
# --completion emit a bash-completion snippet (thread names + field enum)
|
||||||
|
#
|
||||||
|
# SUPPORTED FIELDS (curated + explicit — anything else is REJECTED with a clear
|
||||||
|
# error; this tool NEVER blind-edits an arbitrary token):
|
||||||
|
# PORT → the nested PROTOCOL { … PORT <v> … } block's PORT
|
||||||
|
# HOST → the nested PROTOCOL { … HOST <v> … } block's HOST (alias: IP)
|
||||||
|
# IP → alias for HOST
|
||||||
|
# PROCESSNAME → the protocol block's top-level PROCESSNAME
|
||||||
|
# ENCODING → the protocol block's top-level ENCODING (must already exist)
|
||||||
|
#
|
||||||
|
# The edit is ANCHORED to the right thread's block and the right field via
|
||||||
|
# nc-parse.sh (protocol-block range + the field's canonical one-line render).
|
||||||
|
# It replaces ONLY that value token, preserves the surrounding TCL brace
|
||||||
|
# structure, and re-verifies balanced braces before the journal write. If the
|
||||||
|
# field is not present in the thread's block it DIES (it will not CREATE the
|
||||||
|
# field — that is nc_add_route / nc-insert-protocol territory).
|
||||||
|
#
|
||||||
|
# Every write goes through journal.sh (snapshot + diff + atomic write). Undo:
|
||||||
|
# larry-rollback.sh --target <netconfig> # newest-first
|
||||||
|
# larry-rollback.sh --entry <entry-id> # one specific write
|
||||||
|
#
|
||||||
|
# Exit codes: 0 OK, 1 generic error, 2 usage, 3 target not found, 4 no 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-set-field: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# v0.8.31: shared CR-safety primitives + the tty-gated control-byte sanitizer.
|
||||||
|
# awk-emitted line numbers feed head/tail arithmetic; Cygwin awk.exe can taint
|
||||||
|
# them with a trailing CR. _sanitize_ctl_tty keeps human-readable output from
|
||||||
|
# corrupting a terminal while passing raw on a pipe.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$LIB_DIR/cygwin-safe.sh"
|
||||||
|
else
|
||||||
|
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; }
|
||||||
|
_sanitize_ctl_tty() { cat; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source journal so we can call journal_write directly (same idiom as
|
||||||
|
# nc-insert-protocol.sh) — keeps entries in the running session.
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -r "$JOURNAL" ] && . "$JOURNAL"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# FIELD MAP — friendly name → exact NetConfig location.
|
||||||
|
# nested : value lives at PROTOCOL.<FIELD> inside the inner { PROTOCOL { … } }
|
||||||
|
# toplevel: value lives as a depth-1 { FIELD value } in the protocol block
|
||||||
|
# Anything not in this map is rejected; we never blind-edit arbitrary tokens.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Returns "<scope> <real_field>" on stdout, non-zero if unsupported.
|
||||||
|
resolve_field() {
|
||||||
|
case "$1" in
|
||||||
|
PORT) printf 'nested PORT' ;;
|
||||||
|
HOST|IP) printf 'nested HOST' ;;
|
||||||
|
PROCESSNAME) printf 'toplevel PROCESSNAME' ;;
|
||||||
|
ENCODING) printf 'toplevel ENCODING' ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block line range for a protocol: "START,END" (1-based, inclusive). Reuses
|
||||||
|
# nc-parse's _blocks via protocol-line + brace-balanced end walk so the edit is
|
||||||
|
# scoped to exactly this thread.
|
||||||
|
block_range() {
|
||||||
|
local nc="$1" name="$2" start end
|
||||||
|
start=$("$NCP" protocol-line "$nc" "$name" 2>/dev/null)
|
||||||
|
start=$(coerce_int "$start" 0)
|
||||||
|
[ "$start" -gt 0 ] || return 1
|
||||||
|
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")
|
||||||
|
end=$(coerce_int "$end" 0)
|
||||||
|
[ "$end" -ge "$start" ] || return 1
|
||||||
|
printf '%s,%s\n' "$start" "$end"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Locate the exact line number of the target field WITHIN [start,end].
|
||||||
|
# scope=nested → inside the inner { PROTOCOL { … } } sub-block, the canonical
|
||||||
|
# one-line `{ FIELD value }` at that depth.
|
||||||
|
# scope=toplevel → the depth-1 `{ FIELD value }` directly under `protocol N {`.
|
||||||
|
# Prints the absolute line number, or nothing if not found.
|
||||||
|
field_line() {
|
||||||
|
local nc="$1" start="$2" end="$3" scope="$4" field="$5"
|
||||||
|
awk -v S="$start" -v E="$end" -v SCOPE="$scope" -v F="$field" '
|
||||||
|
BEGIN { depth = 0; in_proto = 0; proto_depth = 0 }
|
||||||
|
NR < S { next }
|
||||||
|
NR > E { exit }
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
prev = depth
|
||||||
|
# Detect entry into the nested { PROTOCOL { ... } } sub-block. At NR==S the
|
||||||
|
# protocol-block opener `protocol N {` set depth to 1 conceptually; we track
|
||||||
|
# depth deltas from there. A line ` { PROTOCOL {` is the nested opener.
|
||||||
|
is_proto_open = (line ~ /^[[:space:]]+\{ PROTOCOL \{$/)
|
||||||
|
|
||||||
|
if (SCOPE == "toplevel") {
|
||||||
|
# depth-1 field statement: ` { FIELD value }` entirely on one line,
|
||||||
|
# at the protocol-block top level (one indent in). prev-depth bookkeeping
|
||||||
|
# mirrors nc-parse cmd_protocol_field (baseline 1).
|
||||||
|
if (prev == 1 && line ~ ("^[[:space:]]+\\{ " F " ")) { print NR; found=1; exit }
|
||||||
|
} else {
|
||||||
|
# nested: only consider lines once we are inside { PROTOCOL { ... } }.
|
||||||
|
if (in_proto && line ~ ("^[[:space:]]+\\{ " F " ")) { print NR; found=1; exit }
|
||||||
|
}
|
||||||
|
|
||||||
|
n_open = gsub(/\{/, "{", line)
|
||||||
|
n_close = gsub(/\}/, "}", line)
|
||||||
|
depth += n_open - n_close
|
||||||
|
|
||||||
|
if (is_proto_open && !in_proto) { in_proto = 1; proto_depth = prev + 1 }
|
||||||
|
else if (in_proto && depth < proto_depth) { in_proto = 0 }
|
||||||
|
}
|
||||||
|
END { if (!found) exit 1 }
|
||||||
|
' "$nc"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the current value token from a ` { FIELD value }` line.
|
||||||
|
# Strips the leading `{ FIELD ` and trailing ` }`. Leaves the raw value
|
||||||
|
# (which may itself be `{}` or a braced list — we replace the whole token).
|
||||||
|
extract_value() {
|
||||||
|
local line="$1" field="$2"
|
||||||
|
line="${line#"${line%%[![:space:]]*}"}" # ltrim
|
||||||
|
line="${line#\{ "$field" }" # drop "{ FIELD "
|
||||||
|
line="${line%"${line##*[![:space:]]}"}" # rtrim (drops space before })
|
||||||
|
line="${line%\}}" # drop the single trailing "}"
|
||||||
|
line="${line%"${line##*[![:space:]]}"}" # rtrim again (space between value and })
|
||||||
|
printf '%s' "$line"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a replacement line preserving the original indentation + brace shape:
|
||||||
|
# `<indent>{ FIELD <newval> }`
|
||||||
|
build_line() {
|
||||||
|
local orig="$1" field="$2" newval="$3" indent
|
||||||
|
indent="${orig%%[![:space:]]*}" # leading whitespace
|
||||||
|
printf '%s{ %s %s }' "$indent" "$field" "$newval"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify balanced braces across the whole file (open == close). Cheap structural
|
||||||
|
# guard run AFTER the edit, BEFORE the journal write.
|
||||||
|
braces_balanced() {
|
||||||
|
awk '{ o += gsub(/\{/, "{"); c += gsub(/\}/, "}") } END { exit (o==c)?0:1 }' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_completion() {
|
||||||
|
cat <<'COMP'
|
||||||
|
# bash completion for `larry tools nc-set-field` and a standalone `nc-set-field`.
|
||||||
|
# Source this file (or `eval "$(larry tools nc-set-field --completion)"`).
|
||||||
|
# Completes: thread names (from the resolved NetConfig) for arg 1, and the
|
||||||
|
# curated field enum for arg 2.
|
||||||
|
_nc_set_field_complete() {
|
||||||
|
local cur prev words cword
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
local nfields="PORT HOST IP PROCESSNAME ENCODING"
|
||||||
|
# Find the lib dir (next to larry.sh) to call nc-parse for thread names.
|
||||||
|
local lib="${LARRY_LIB_DIR:-}"
|
||||||
|
[ -z "$lib" ] && lib="$(dirname "$(command -v nc-set-field.sh 2>/dev/null || echo .)")"
|
||||||
|
local nc="${HCIROOT:-}/${HCISITE:-}/NetConfig"
|
||||||
|
case "$COMP_CWORD" in
|
||||||
|
1) # thread name (optionally bare; thread.site also fine)
|
||||||
|
if [ -f "$nc" ] && [ -x "$lib/nc-parse.sh" ]; then
|
||||||
|
local threads; threads=$("$lib/nc-parse.sh" list-protocols "$nc" 2>/dev/null)
|
||||||
|
COMPREPLY=( $(compgen -W "$threads" -- "$cur") )
|
||||||
|
fi ;;
|
||||||
|
2) COMPREPLY=( $(compgen -W "$nfields" -- "$cur") ) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
complete -F _nc_set_field_complete nc-set-field nc-set-field.sh
|
||||||
|
COMP
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
THREAD=""; FIELD_IN=""; VALUE=""
|
||||||
|
SITE="${HCISITE:-}"; NC=""; HCIROOT_OVR="${HCIROOT:-}"
|
||||||
|
DRY=0; CONFIRM=""
|
||||||
|
|
||||||
|
POSARGS=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--site) shift; SITE="$1" ;;
|
||||||
|
--netconfig) shift; NC="$1" ;;
|
||||||
|
--hciroot) shift; HCIROOT_OVR="$1" ;;
|
||||||
|
--dry-run) DRY=1 ;;
|
||||||
|
--confirm) shift; CONFIRM="$1" ;;
|
||||||
|
--completion) emit_completion; exit 0 ;;
|
||||||
|
-h|--help) sed -n '2,43p' "$NC_SELF"; exit 0 ;;
|
||||||
|
--*) die "unknown flag: $1" ;;
|
||||||
|
*) POSARGS+=("$1") ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "${#POSARGS[@]}" -ge 3 ] || { echo "usage: nc-set-field <thread>[.<site>] <field> <value> [--site S] [--dry-run] [--confirm yes] [--netconfig PATH] [--hciroot PATH]" >&2; exit 2; }
|
||||||
|
THREAD="${POSARGS[0]}"; FIELD_IN="${POSARGS[1]}"; VALUE="${POSARGS[2]}"
|
||||||
|
|
||||||
|
# Parse thread.site form (the dot-suffix wins over a bare --site default, but a
|
||||||
|
# site supplied via thread.site AND --site that disagree is an error).
|
||||||
|
if [[ "$THREAD" == *.* ]]; then
|
||||||
|
dot_site="${THREAD##*.}"
|
||||||
|
THREAD="${THREAD%.*}"
|
||||||
|
if [ -n "$SITE" ] && [ "$SITE" != "${HCISITE:-}" ] && [ "$SITE" != "$dot_site" ]; then
|
||||||
|
die "site conflict: thread says '.$dot_site' but --site says '$SITE'"
|
||||||
|
fi
|
||||||
|
SITE="$dot_site"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Field validation (curated set ONLY — explicit reject otherwise).
|
||||||
|
FIELD_IN_UC=$(printf '%s' "$FIELD_IN" | tr '[:lower:]' '[:upper:]')
|
||||||
|
RESOLVED=$(resolve_field "$FIELD_IN_UC") \
|
||||||
|
|| die "unsupported field: '$FIELD_IN'. Supported: PORT, HOST (alias IP), PROCESSNAME, ENCODING. (This tool only edits a curated safe set; it will not blind-edit arbitrary NetConfig tokens.)"
|
||||||
|
SCOPE="${RESOLVED%% *}"
|
||||||
|
REAL_FIELD="${RESOLVED##* }"
|
||||||
|
|
||||||
|
[ -n "$VALUE" ] || die "value is required (got empty)"
|
||||||
|
|
||||||
|
# Resolve the NetConfig path.
|
||||||
|
if [ -z "$NC" ]; then
|
||||||
|
[ -n "$SITE" ] || die "no site: pass <thread>.<site>, --site SITE, or set \$HCISITE (or give --netconfig PATH)"
|
||||||
|
[ -n "$HCIROOT_OVR" ] || die "no \$HCIROOT (and no --hciroot / --netconfig). Cannot resolve the site's NetConfig."
|
||||||
|
NC="$HCIROOT_OVR/$SITE/NetConfig"
|
||||||
|
fi
|
||||||
|
[ -f "$NC" ] || { printf 'nc-set-field: no such NetConfig: %s\n' "$NC" >&2; exit 3; }
|
||||||
|
|
||||||
|
# Confirm the thread exists.
|
||||||
|
"$NCP" list-protocols "$NC" 2>/dev/null | grep -qx "$THREAD" \
|
||||||
|
|| { printf 'nc-set-field: no such thread in %s: %s\n' "$NC" "$THREAD" >&2; exit 3; }
|
||||||
|
|
||||||
|
# Find the thread's protocol-block line range.
|
||||||
|
RANGE=$(block_range "$NC" "$THREAD") || die "could not determine block range for thread $THREAD"
|
||||||
|
B_START="${RANGE%,*}"; B_END="${RANGE#*,}"
|
||||||
|
|
||||||
|
# Locate the exact field line within that block.
|
||||||
|
TARGET_LINE=$(field_line "$NC" "$B_START" "$B_END" "$SCOPE" "$REAL_FIELD")
|
||||||
|
TARGET_LINE=$(coerce_int "$TARGET_LINE" 0)
|
||||||
|
if [ "$TARGET_LINE" -le 0 ]; then
|
||||||
|
case "$SCOPE" in
|
||||||
|
nested) die "field $REAL_FIELD not found in thread $THREAD's nested PROTOCOL{} block (looked in $NC lines $B_START-$B_END). This tool changes EXISTING fields only — it will not create one." ;;
|
||||||
|
toplevel) die "top-level field $REAL_FIELD not found in thread $THREAD's protocol block (looked in $NC lines $B_START-$B_END). This tool changes EXISTING fields only — it will not create one." ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORIG_LINE=$(sed -n "${TARGET_LINE}p" "$NC")
|
||||||
|
OLD_VALUE=$(extract_value "$ORIG_LINE" "$REAL_FIELD")
|
||||||
|
NEW_LINE=$(build_line "$ORIG_LINE" "$REAL_FIELD" "$VALUE")
|
||||||
|
|
||||||
|
# Display form of the old value: an empty value renders as the Tcl empty list
|
||||||
|
# `{}` (computed separately — inlining `${OLD_VALUE:-{}}` mis-parses the braces).
|
||||||
|
OLD_DISP="$OLD_VALUE"
|
||||||
|
[ -z "$OLD_DISP" ] && OLD_DISP='{}'
|
||||||
|
|
||||||
|
# No-op guard.
|
||||||
|
if [ "$OLD_VALUE" = "$VALUE" ]; then
|
||||||
|
printf 'nc-set-field: %s.%s %s is already %s — nothing to change.\n' \
|
||||||
|
"$THREAD" "$SITE" "$FIELD_IN_UC" "$VALUE" | _sanitize_ctl_tty
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show the before→after (always — for dry-run AND the confirm prompt).
|
||||||
|
{
|
||||||
|
printf '\n=== %s (%s) %s ===\n' "$THREAD" "${SITE:-<site>}" "$NC"
|
||||||
|
printf 'field: %s (%s %s, line %s)\n' "$FIELD_IN_UC" "$SCOPE" "$REAL_FIELD" "$TARGET_LINE"
|
||||||
|
printf 'before: %s\n' "$ORIG_LINE"
|
||||||
|
printf 'after: %s\n' "$NEW_LINE"
|
||||||
|
printf ' %s → %s\n\n' "$OLD_DISP" "$VALUE"
|
||||||
|
} | _sanitize_ctl_tty
|
||||||
|
|
||||||
|
if [ "$DRY" = "1" ]; then
|
||||||
|
printf '(dry-run; no write made)\n' | _sanitize_ctl_tty
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the candidate file: lines 1..(target-1) + new line + (target+1)..EOF.
|
||||||
|
TMP=$(mktemp)
|
||||||
|
head -n $((TARGET_LINE - 1)) "$NC" > "$TMP"
|
||||||
|
printf '%s\n' "$NEW_LINE" >> "$TMP"
|
||||||
|
tail -n +$((TARGET_LINE + 1)) "$NC" >> "$TMP"
|
||||||
|
|
||||||
|
# Structural guard: braces must remain balanced (open == close).
|
||||||
|
if ! braces_balanced "$TMP"; then
|
||||||
|
rm -f "$TMP"
|
||||||
|
die "post-edit brace check FAILED (open != close) — refusing to write. The edit would have broken the TCL structure; nothing was changed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm unless --confirm yes.
|
||||||
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
|
printf 'Apply this change? [y/N]: ' | _sanitize_ctl_tty
|
||||||
|
ans=""
|
||||||
|
if declare -f read_clean >/dev/null 2>&1; then
|
||||||
|
read_clean ans
|
||||||
|
else
|
||||||
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
|
fi
|
||||||
|
if ! [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
|
rm -f "$TMP"
|
||||||
|
printf 'aborted (no change)\n' | _sanitize_ctl_tty
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Journaled atomic write.
|
||||||
|
if declare -f journal_write >/dev/null 2>&1; then
|
||||||
|
ENTRY_ID=$(journal_write "$NC" "$TMP")
|
||||||
|
rm -f "$TMP"
|
||||||
|
{
|
||||||
|
printf '\nset %s.%s %s = %s (was %s)\n' "$THREAD" "${SITE:-?}" "$FIELD_IN_UC" "$VALUE" "$OLD_DISP"
|
||||||
|
printf 'journal entry: %s\n' "$ENTRY_ID"
|
||||||
|
printf 'rollback: larry-rollback.sh --entry %s OR larry-rollback.sh --target %s\n' "$ENTRY_ID" "$NC"
|
||||||
|
} | _sanitize_ctl_tty
|
||||||
|
else
|
||||||
|
# No journal available — degrade with a plain backup (mirrors nc-table.sh).
|
||||||
|
_ts=$(date +%s | tr -cd '0-9')
|
||||||
|
cp -p "$NC" "${NC}.larry-bak.${_ts:-0}"
|
||||||
|
mv -f "$TMP" "$NC"
|
||||||
|
printf '(no journal available; backup at %s.larry-bak.%s)\n' "$NC" "${_ts:-0}" | _sanitize_ctl_tty
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue
Block a user