diff --git a/CLAUDE.md b/CLAUDE.md index 1f0957d..83c7d2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,8 +24,12 @@ Snipdeck groups every Snip under exactly one CLI (e.g. `pl-app`, `mpt-app`, `inv - Windows 11 is the real target. Mica requires Win11; on Win10 it falls back to a solid colour. Do not depend on Mica rendering on Win10. - MVVM via `CommunityToolkit.Mvvm`. DI via `Microsoft.Extensions.DependencyInjection`. -- Persistence is a single JSON document via `System.Text.Json`. **Not** SQLite, not - LiteDB — the data is small, and JSON is human-readable and sync-friendly. +- Persistence: the **snip store** (definitions) is a single JSON document via + `System.Text.Json` — **not** SQLite, not LiteDB; the data is small, and JSON is + human-readable and sync-friendly. **Execution history** is the deliberate + exception: run output is *not* small, so it lives in a separate SQLite database + (`history.db`) — see command execution. The JSON-only rule still governs the snip + store; don't migrate definitions to SQLite. - Install + self-update via Velopack, releasing off GitHub releases. ## Architecture @@ -171,17 +175,21 @@ Conventions: Changelog format). `README.md` is the public-facing intro and stays in sync with what the app actually does today, not aspirational. -## Out of scope for v1 — do not build speculatively +## Not building yet — do not build speculatively -- **Command-palette quick picker.** In v1 the hotkey simply foregrounds the main - window. The palette is a strong v2 candidate, not a v1 deliverable. +v1.0.0 has shipped (command execution included). These remain deliberately unbuilt: + +- **Command-palette quick picker.** The global hotkey just foregrounds the main + window. A quick pick-fill-run/copy palette is a strong v2 candidate — more + compelling now that execution exists — but not yet scheduled. - **macOS / cross-platform head.** Preserve the *option* via the view-model purity discipline above, but do **not** add Avalonia scaffolding now. The existing split already buys ~90% of the future-proofing for free. -- **Secret / masked parameters.** Parked. Revisit only if storing credentials in - Snips becomes a real, stated use case. -- **SnipCommand import** (future nicety): a SnipCommand JSON could be imported, and - the CLI auto-suggested from the first token of each command string. + +Two items that used to sit here are resolved: **SnipCommand import** shipped as the +`snipdeck-importer` tool, and **secret / masked parameters** is now a planned +follow-on (per-CLI/Snip environment variables with DPAPI) — see [TODO.md](TODO.md) +for it and the other deferred command-execution work. ## Working agreement diff --git a/TODO.md b/TODO.md index ca8021b..a699dc4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,151 +1,61 @@ # TODO A backlog of ideas worth building but not yet scheduled. Not a commitment — the -canonical list of *parked* v1 features is the "Out of scope for v1" section in +canonical list of *parked* features is the "Not building yet" section in [CLAUDE.md](CLAUDE.md). ---- - -## Execute Snips, not just construct them (with per-CLI shell + execution history) - -**Problem.** Today Snipdeck is a sophisticated clipboard — it builds the command -and you paste it into your own terminal. That round-trip discards the most -valuable artefact of running a CLI: the *output*. There's no way to ask "what -did this Snip return last time I ran it against staging?", to diff two runs, -or to even tell whether the previous invocation succeeded. - -**Idea.** Let Snipdeck *execute* a Snip in its configured shell, stream the -output into a Snipdeck panel as it runs, and persist each execution against -the Snip so the user accumulates a searchable run history. - -**Sketch.** - -- **Per-CLI shell declaration.** `Cli` gains a `Shell` field (enum: - `Cmd` / `PowerShell` / `PwshCore` / `Bash` / `Custom`) and an optional - `CustomShellPath` + `CustomShellArgsTemplate` for the escape hatch. - Default for new CLIs is whatever the platform considers canonical - (`PowerShell` on Windows). A Snip can override its CLI's shell when needed. -- **Per-CLI executable path and working directory.** `Cli` gains optional - `ExecutablePath` and `WorkingDirectory` fields. The CLI editor dialog - gains a **Browse…** for the executable (re-using `IFilePickerService`) - and another for the working directory. Both are **optional** — Snipdeck - must remain useful as a place to author Snips for a CLI that isn't - installed yet on this machine, or one the user will install later. So: - - No validation at save time. An empty `ExecutablePath` is the default - and is fine. - - When the field *is* set, validation happens **only at Run time**: if - the path is missing, the Run action shows a friendly "couldn't find - `` — has it been installed? edit the CLI to fix the path" rather - than throwing. Copy still works unconditionally. - - `WorkingDirectory` defaults at runtime to the executable's parent - directory when `ExecutablePath` is set, otherwise the user's home - directory. A Snip can override the CLI's working directory. - - The executable path is not currently used by Copy — but it's worth - storing now because the user might reasonably expect "show me where - `pl-app` lives" as a small affordance even before Run lands. -- **Per-CLI environment variables.** `Cli` gains an `EnvironmentVariables` - collection of `(Name, Value, IsSecret)` entries; a Snip can add or - override entries on top of its CLI's set. The child process inherits the - OS environment and gets these merged on top — they're additions / - overrides, not a replacement. - - Non-secret values are stored verbatim in the JSON store and shown in - the editor in cleartext. - - Secret values flip the parked "secret / masked parameters" item from - `CLAUDE.md` from "park" to "needed". Store them via Windows DPAPI - (`ProtectedData.Protect`, current-user scope) so the ciphertext is - bound to the local Windows account. The editor masks the value behind - a "Show" toggle and never logs it. - - Trade-off: DPAPI-protected values won't survive a cross-machine sync - of the data folder — the user would need to re-enter secrets on each - machine. That's the right shape; the alternative (plaintext secrets - in a synced JSON document) is the wrong one. -- **Labelled executions.** Each run can carry a free-form list of `Labels` - — short strings like `INC-4567`, `staging-rollback`, `pr-1234-debug`. - Labels are surfaced as chips on the history list and are fully searchable - (`label:INC-4567` filter, or just free-text). The Run dialog has a small - "labels" input next to the resolved-command preview; the history view - has an inline editor so the user can label a past run after the fact. - - **Sticky labels.** A small affordance in the shell (probably a chip in - the title bar) lets the user *pin* one or more labels for the current - investigation. While pinned, every Run pre-fills with those labels. - Clearing the pin reverts to the empty default. This is the - productivity multiplier — during an incident, the user pins - `INC-4567` once and every subsequent Run is automatically tagged. - - Labels are *not* tags on the Snip itself — they belong to the - execution record. A single Snip will accumulate runs labelled with - many different incident / ticket / deployment identifiers over time. -- **A Run action alongside Copy.** Card overflow gains **Run**. Clicking it - walks the same parameter-fill flow as Copy, but on submit we spawn the - configured shell with the resolved command instead of copying to the - clipboard. The parameter-fill dialog is replaced or extended with a - **dry-run preview** of the exact command line plus a final "Run" button — - arbitrary execution is a footgun and the user must see the resolved - string once before it runs. -- **Live output panel.** Output streams into a dedicated content pane — - probably a new content state alongside `HomeViewModel` / `CliViewModel` — - with stdout and stderr distinguished (colour, or two lanes), an exit-code - badge once the process completes, an elapsed timer, and a **Cancel** button - that kills the process group. Lines arrive via `IDispatcher` so we don't - mutate UI state off-thread (this fix's lesson, applied early). -- **Execution history per Snip.** Each run captures: `SnipId`, - `ResolvedCommand`, `StartedAt`, `FinishedAt`, `ExitCode`, - `Cancelled`, `Stdout`, `Stderr` (or an interleaved stream with timestamps), - the parameter values that were used, and the `Labels` the run was - tagged with. New entries append to a per-Snip history log. -- **Searchable history.** A new "History" view (probably a pane footer entry, - next to Settings) lists executions across all Snips, newest-first, with - full-text search over the captured output and command line. Clicking an - entry opens the run in the same output panel as a live run — just with the - Cancel button disabled and a "Run again" button enabled. - -**Storage — the awkward bit.** +> **Command execution** — run a Snip in its configured shell, watch it live with full +> terminal fidelity (colours, spinners, progress bars, interactive prompts), and keep a +> clean, searchable run history — shipped in **v1.0.0**. The items below are the +> follow-ons that were deliberately deferred from that first cut. -The current JSON-store rule (`CLAUDE.md`: "not SQLite, not LiteDB — the data -is small") was made on the assumption of snippet definitions. **Execution -output is not small.** A single `kubectl describe pod` can be tens of KB, and -a power user might rack up thousands of runs. - -Options to weigh when this is scheduled: - -- **Per-Snip history file** (e.g. `/history/.jsonl`, - append-only JSONL, one line per run). Keeps the main snip store small and - fast to load. Pruning is per-file. Cross-Snip search needs to walk all - files — feasible up to maybe ~10k files; degrades after. -- **Per-CLI history file**. Compromise; smaller fan-out, still naturally - partitioned, but a single hot CLI's history can grow unbounded. -- **Bring in SQLite specifically for executions** (NOT for the snip store). - Best query/search story, opens the door to `LIKE` / `MATCH` over output. - Departs from the "one JSON document" principle — but the principle was - scoped to *definitions*, not arbitrary observation data. Worth a real - conversation when this gets picked up; I (Claude) lean toward this option - for any non-trivial history feature. - -Retention defaults probably want to be "last N runs per Snip" (configurable), -matching the existing backup-retention shape. - -**Open questions** to settle when scheduled: - -- **Safety / confirmation.** First-run confirmation per Snip? An allow-list? - A "this Snip has been edited since you last ran it" warning? Worth getting - right — Snipdeck running an unreviewed `rm -rf` is a category of incident - we don't want to ship. -- **Cancellation semantics.** `Process.Kill(entireProcessTree: true)` covers - most shells. PowerShell can be sticky; document and test. -- **Cross-platform shells.** `cmd` and `PowerShell` are Windows-only; - `pwsh` is cross-platform; `bash` on Windows means WSL. The enum needs to - encode both the shell *and* its launcher. -- **Large outputs.** Cap captured-per-run size (configurable; default ~5 MB?) - and truncate with a marker. The UI panel can spool to disk for the live - view if needed. -- **ANSI / colour.** `IShell` should pass output through verbatim; the panel - needs an ANSI escape parser to render colours and clear-line sequences. - Avalonia / WinUI both lack a built-in terminal control — either pull a - community one or render to a `RichTextBlock` with the ANSI stripped / - interpreted. -- **Streaming-to-history vs. capture-then-write.** Stream-to-history (write - each chunk as it arrives) means a crashed Snipdeck still leaves a useful - partial log. Capture-then-write is simpler. Probably stream. +--- -This is a *significant* expansion of Snipdeck's surface area — it crosses -from "snippet manager" into "lightweight runbook executor". Worth doing, -worth doing carefully, and worth a design conversation before the first PR. +## Per-CLI / per-Snip environment variables (including secrets) + +Command execution shipped with per-CLI shell, executable path and working directory, +but **not** environment variables. Add an `EnvironmentVariables` collection of +`(Name, Value, IsSecret)` entries on `Cli`, with a Snip able to add or override entries +on top. The child process inherits the OS environment and gets these merged on top +(additions / overrides, not a replacement) — the runner already builds the child +environment, so this slots in there. + +- Non-secret values: stored verbatim in the JSON store, shown in the CLI editor in + cleartext. +- Secret values: un-parks the "secret / masked parameters" item in `CLAUDE.md`. Store + via Windows DPAPI (`ProtectedData.Protect`, current-user scope) so the ciphertext is + bound to the local Windows account; the editor masks the value behind a "Show" toggle + and never logs it. +- Trade-off (the right shape): DPAPI-protected values don't survive a cross-machine sync + of the data folder, so secrets are re-entered per machine. The alternative — plaintext + secrets in a synced JSON document — is the wrong one. + +## Labelled executions + sticky labels + +Each run can carry a free-form list of `Labels` — short strings like `INC-4567`, +`staging-rollback`, `pr-1234-debug`. Labels belong to the **execution record**, not the +Snip (a Snip accumulates runs labelled with many different incident / ticket / deployment +identifiers over time). + +- Surface them as chips on the History list and make them searchable (`label:INC-4567` + filter, or just free text — the SQLite history store already does substring search). +- The Run dialog gains a small labels input next to the resolved-command preview; the + History view gets an inline editor to label a past run after the fact. +- **Sticky labels** (the productivity multiplier): a chip in the shell lets the user + *pin* one or more labels for the current investigation. While pinned, every Run + pre-fills with them — pin `INC-4567` once during an incident and every run is tagged. + +## Per-Snip shell / working-directory override editor + +The model (`Snip.ShellOverride`, `Snip.WorkingDirectoryOverride`) and the runner already +support per-Snip overrides of the CLI's shell and working directory, but there's no +editor UI yet — only CLI-level configuration is exposed. Add an "advanced" section to the +Snip editor so a Snip can override its CLI's shell / working directory. + +## Run safety hardening (optional) + +The dry-run preview (resolved command + shell + working directory, shown before every +run) is the current safety gate. Worth considering if usage warrants it: a per-Snip +first-run confirmation, an allow-list, or a "this Snip has been edited since you last ran +it" warning — Snipdeck running an unreviewed `rm -rf` is a category of incident worth +guarding against.