Add an update command that reads, diffs, and writes - #17
Conversation
Dan: "we should have an apply update method, that looks up the existing ones, and then just applies the deltas... just pull the current state, run an update against it, then update the controller, its that simple." The three-step export/plan/apply flow exists so a plan can be reviewed before it is applied. That is worth having, but it is not what most changes need, and it fails the README's own motivating example. Asking for the top row to be green aborts outright if any one of those knobs is already green, because `createPatchPlan` treats a no-op as a contract violation — which is right for `plan`, where the contract is "the state I described is the state I found", and wrong for an update. `update` reads the controller, works out what actually differs, prints it, and writes only that with --yes. Already-correct values are reported as unchanged and skipped; an update where everything is already set succeeds having done nothing. Verified against hardware: the four-knob request above reports two unchanged and two changes, where `plan` refuses at the first no-op. No plan file changes hands, so there is nothing to forge and nothing to replay. That is why this needs neither the journal nor the single-use machinery `apply` carries, and why it is not behind the #14 gate: it keeps no hidden state, and the backup is a file you name. `planUpdate` rejects two --set operations for one field rather than picking a winner. It delegates to `createPatchPlan` once the no-ops are filtered, so both paths build frames through exactly the same code. Refs #14 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95449ff177
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const connection = backend.connectForApply(device); | ||
| try { | ||
| const result = await applyPatchPlan(connection, device, plan, { | ||
| journalPath: resolve(dirname(backupPath), "mft-updates.ndjson"), |
There was a problem hiding this comment.
Keep the journal separate from the requested backup
When --backup names <directory>/mft-updates.ndjson, saveBackup first writes the JSON snapshot there and then applyPatchPlan appends journal records to the same path. The command reports this file as the backup, but it is no longer valid JSON and cannot be used for restoration. Since the CLI permits any backup filename and does not reserve this name, reject this collision or store the journal at a path that cannot overlap the requested backup.
Useful? React with 👍 / 👎.
Read the controller, work out what differs, write only that.
The existing export/plan/apply flow exists so a plan can be reviewed before applying. Worth having, but it is not what most changes need — and it fails the README's own motivating example:
Asking for the top row to be green aborts if any one of those knobs already is. That is correct for
plan, where the contract is "the state I described is the state I found". It is wrong for an update.Verified against real hardware.
Why this is not behind the #14 gate
No plan file changes hands, so there is nothing to forge and nothing to replay — the entire class of defect in #14 comes from serialising a plan, handing it to someone else, and reading it back.
updatetherefore needs neither the journal nor the single-use machinery, keeps no hidden state, and writes a backup file you name. 0d, the cwd-relative state root thatapplyis gated on, does not apply to it.It still gets every device-side guard: fresh export, precondition check on each value, firmware eligibility recomputed from the device, frames derived and verified, and full read-back.
Notes
planUpdaterejects two--sets for one field rather than picking a winnercreatePatchPlanonce no-ops are filtered, so both paths build frames through identical code🤖 Generated with Claude Code