v1.2.1
Patch release. Three small bugs found by exercising v1.2.0 against a real OpenWrt 25.12.4 router, plus one polish item (an honest error code when the daemon you're configuring isn't installed).
Fixed
packages/installedalways returned[]on apk-tools 3.x.list_installed()calledapk info --installed, a flag that exists on apk-tools 2.x but not 3.x (which OpenWrt 25 ships). Plainapk infois the right command; it prints one installed package name per line. The new implementation also filters output to the package-name regex so diagnostic lines never end up surfacing as fake packages.network/interfacesipaddrsurfaced as an array on modern uci. OpenWrt 25 useslist ipaddrfor static-proto multi-address interfaces (loopback ships aslist ipaddr '127.0.0.1/8'); uapi was returning the raw uci value, so a GET on a list-form interface returned"ipaddr": ["127.0.0.1/8"]instead of the schema-declared string.fromUcinow surfaces both forms additively:ipaddris always the first address as a string (preserving the v1.0/v1.1 contract); a newipaddrsarray carries the full list.toUciprefersipaddrswhen present and falls back toipaddr. validate accepts either.- Makefile
lint-emdashscannedbuild/sdk/(OpenWrt feeds checkouts contain em-dashes in upstream package READMEs / test fixtures we don't author or control).grep --exclude-dir=sdknow skips it.
Added
-
New error code
503 init_script_missingwith a pre-flight check. Before any uci write,transaction()now confirms each/etc/init.d/<svc>listed inreload_servicesactually exists; missing → fail-fast with a 503 carrying the missing path inmessage, no uci change. Motivates this: live-router testing of v1.2.0 showed that POST/sqm/queueson a box without sqm-scripts produced500 reload_failed_unrecovered. The cause: step-5 reload returned exit 127 (script not found); the snapshot-restore worked but the SECOND reload attempt also returned 127 (same missing script), so uapi recorded both errors and surfaced "unrecovered" - misleading: uci IS in a known state, only the daemon reload couldn't run because the daemon isn't installed. The new pre-flight makes the two scenarios distinct on the wire:503 init_script_missing: daemon not installed; uci state unchanged.500 reload_failed_restored: daemon installed but reload exited non-zero; uci state restored from snapshot.500 reload_failed_unrecovered: as before, only for the genuinely unrecoverable case (snapshot import / restore-reload both threw or returned errors).
Step numbering in the atomic-transaction recipe shifts by one in CLAUDE.md: pre-flight is step 0, flock moves to step 1, etc.
Tests
- Unit: 422 → 431 (+9 covering
ipaddr/ipaddrssemantics,init_script_missingpre-flight against absent paths and unsafe names, emptyreload_servicespass-through). - Integration: +1 file (
30_init_script_missing_test.sh) verifying live behavior on a router without sqm-scripts: 503 returned, uci unchanged, healthy resources unaffected.
Live verification
End-to-end run against a real OpenWrt 25.12.4 router (apk-tools 3.0.5) with the locally-built APK installed:
- 6 test phases, 131 assertions: 131 pass, 0 fail.
GET /packages/installednow lists 188 packages (was 0).GET /network/interfaces/loopbackreturns"ipaddr": "127.0.0.1/8"(schema-conformant string) and"ipaddrs": ["127.0.0.1/8"].POST /sqm/queueson the unpatched router returned500 reload_failed_unrecovered; on 1.2.1 it returns503 init_script_missingwith"init script /etc/init.d/sqm not found (is the daemon installed?)"and leaves uci untouched.