feat(fleet): save a command with its settings, and manage them from the app - #23
Merged
Conversation
…he app Two gaps, both real. A saved command stored the script, the interpreter, sudo, the working directory and the timeout — but not **how many servers at a time** or **whether a failure stops the rest**. Those are the two settings that differ most between commands: "reload nginx" and "upgrade the database tier" want opposite answers, and re-choosing them on every run is how a saved command still gets run wrong. They are part of the command now, stored and restored, with a flag still winning at the point of use. And the desktop could not save a command **at all**. The recipe strip was read-only; only the CLI could write one. Editing anything now offers "Save these settings", and a saved command carries an x to delete it. A shipped recipe has no x — it is copied, not edited, so an upgrade of DiskPush never silently changes a command someone relies on, and the main process refuses a delete aimed at one rather than trusting the window to have hidden the button. Migration 004 adds the two columns with the schema's own defaults, so every existing row reads back exactly as it behaved before. Caught by the type checker on the way through: `copyRecipe` did not carry the new fields, so `fleet commands copy` would have produced a copy that ran the same script at a different speed — the one thing a copy must not do. Caught by running it: the built-in recipes are plain object literals typed as FleetCommand, and nothing parses them through the schema, so a field the schema merely defaults came back `undefined`. That reached the desktop as `setConcurrency(undefined)`. They state every field now, with a test asserting it, because the next field added will have the same problem. Verified end to end against a real store: saved with `--concurrency 2 --stop-on-error --timeout 45`, read back intact, honoured on a run with no flags, overridden by a flag, and a pre-existing row migrating to the defaults. The desktop side was screenshotted: picking a saved command restores sudo, "At once 2", "Timeout 45" and "Stop after a failure" together, not just the text. 508 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two gaps, both real.
A saved command didn't store its pacing. It kept the script, interpreter, sudo, working directory and timeout — but not how many servers at a time or whether a failure stops the rest. Those are the two settings that differ most between commands: "reload nginx" and "upgrade the database tier" want opposite answers, and re-choosing them on every run is how a saved command still gets run wrong.
The desktop couldn't save a command at all. The recipe strip was read-only; only the CLI could write one.
Now
In the app, editing anything offers Save these settings. A saved command carries an × to delete it; a shipped recipe does not — it is copied, not edited, and the main process refuses a delete aimed at one rather than trusting the window to have hidden the button.
Migration
004adds the two columns with the schema's own defaults, so every existing row reads back exactly as it behaved before.Two bugs caught before they shipped
copyRecipedidn't carry the new fields, sofleet commands copywould have produced a copy that ran the same script at a different speed — the one thing a copy must not do.FleetCommand, and nothing parses them through the schema, so a field the schema merely defaults came backundefined. That reached the desktop assetConcurrency(undefined). They state every field now, with a test asserting it, because the next field added will have the same problem.Verified
End to end against a real store: saved with
--concurrency 2 --stop-on-error --timeout 45, read back intact, honoured on a run with no flags, overridden by a flag, and a pre-existing row migrating to the defaults.The desktop side was screenshotted rather than reasoned about — picking a saved command restores sudo ✓, At once 2, Timeout 45, Stop after a failure ✓ together, not just the text. No CSP violations.
508 tests. Typecheck and
pnpm smoke:desktopgreen.Transfer profiles are the other half of this and are not in here:
IPC.profilesSaveis declared with no handler and there is no profiles UI, even though the table and the CLI have worked all along. That is a separate PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4