Skip to content

Commit 1538fa6

Browse files
committed
add interactive setup, completion, and self-update support
1 parent 6dcd96e commit 1538fa6

15 files changed

Lines changed: 836 additions & 82 deletions

File tree

‎CLAUDE.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ dependencies rather than 3. On a terminal, `pindrop scan` also offers to install
8686
what is missing rather than only explaining it; in CI nothing prompts and the
8787
behavior is byte-identical to before.
8888

89+
The same `internal/tui` package now drives **interactive setup prompts** via
90+
`huh` (first-run questionnaire, install confirmation, JIT install offer during
91+
scan) and **`pindrop update` confirmation**. All of it renders to stderr; `--yes`
92+
still bypasses every prompt. **`pindrop completion`** exposes Cobra's shell
93+
completion scripts. **`pindrop update`** checks GitHub Releases and replaces the
94+
running binary in-place — non-functional until a first release is published;
95+
checksum verification on the download is still TODO.
96+
8997
Next: [docs/product/roadmap.md](docs/product/roadmap.md).
9098

9199
## Read before changing
@@ -289,8 +297,8 @@ it.** stdout carries the report and must stay pipeable into `jq`. This works onl
289297
because every adapter buffers its child's stdout *and* stderr and OSV runs with
290298
`--verbosity error` — an adapter that lets a child write to stderr would corrupt
291299
every frame. `--log-level debug` therefore forces plain mode, since slog also
292-
writes there. `internal/tui` is the only package allowed to import bubbletea or
293-
lipgloss ([ADR 0011](docs/decisions/0011-bubbletea-for-progress.md)).
300+
writes there. `internal/tui` is the only package allowed to import bubbletea,
301+
lipgloss, or huh ([ADR 0011](docs/decisions/0011-bubbletea-for-progress.md)).
294302

295303
**The progress footer says "raw findings" on purpose.** It sums what each scanner
296304
reported, before cross-tool dedup, so it is legitimately larger than the count the

‎README.md‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@ committed inside the binary. A download that does not match is deleted and never
7676
made executable.
7777

7878
- **~215 MB**, once. It prints the sizes and the hosts and asks before fetching
79-
anything; `--yes` skips the prompt.
79+
anything; `--yes` skips the prompt. On a terminal the confirmation is an
80+
interactive form rather than a bare `[Y/n]` line.
8081
- **First run on a terminal** asks where to store data (`~/.pindrop` by default)
81-
and which scanners to install. A custom data directory is saved in
82-
`~/.config/pindrop/config.json` (or the platform config dir) and used on
83-
every later run unless `PINDROP_HOME` is set.
82+
and which scanners to install, using the same interactive UI. A custom data
83+
directory is saved in `~/.config/pindrop/config.json` (or the platform config
84+
dir) and used on every later run unless `PINDROP_HOME` is set.
8485
- **Idempotent.** A second run installs nothing and makes *no network requests*,
8586
so there is no separate offline mode.
8687
- **Self-contained.** Nothing is written outside `~/.pindrop`, no package manager
@@ -172,8 +173,8 @@ pindrop scan ~/code/some-repo # any directory
172173
```
173174

174175
On a terminal you get a live row per scanner while they run, and — if any are
175-
missing — an offer to install them before the scan starts. In CI nothing prompts
176-
and nothing animates.
176+
missing — an offer to install them before the scan starts (same interactive
177+
confirm as `pindrop setup`). In CI nothing prompts and nothing animates.
177178

178179
```bash
179180
pindrop scan . --format json --out r.json # machine-readable
@@ -185,8 +186,38 @@ pindrop scan . --verify-secrets # prove a leaked key is live (see be
185186

186187
pindrop setup --check # what is installed, and from where
187188
pindrop setup --force # reinstall at the pinned versions
189+
190+
pindrop completion bash # shell completions (see below)
191+
pindrop update # check GitHub for a newer release
192+
```
193+
194+
### Shell completions
195+
196+
Cobra generates these; output goes to **stdout** so you can redirect or `source` it:
197+
198+
```bash
199+
source <(pindrop completion bash) # bash
200+
pindrop completion zsh > "${fpath[1]}/_pindrop" # zsh (then compinit)
201+
pindrop completion fish > ~/.config/fish/completions/pindrop.fish
188202
```
189203

204+
Enum flags such as `--format`, `--min-severity`, and `--progress` complete to
205+
their valid values.
206+
207+
### Updating pindrop
208+
209+
```bash
210+
pindrop update # check GitHub, confirm, replace binary
211+
pindrop update --yes # skip confirmation
212+
```
213+
214+
This queries the latest release from GitHub and atomically replaces the running
215+
executable. It is **non-functional until GoReleaser publishes a first release**
216+
— dev builds (`version=dev`) are rejected, and a hash build with no release yet
217+
reports already up to date or a network error. Checksum verification on the
218+
downloaded archive is not wired yet; treat `pindrop update` as CLI surface for
219+
now, not as strong an integrity guarantee as `pindrop setup`.
220+
190221
### History — did the fix actually land?
191222

192223
Every scan is recorded in `~/.pindrop/pindrop.db` (SQLite), so the next one can

‎go.mod‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ toolchain go1.26.5
66

77
require (
88
github.com/charmbracelet/bubbletea v1.3.10
9+
github.com/charmbracelet/huh v1.0.0
910
github.com/charmbracelet/lipgloss v1.1.0
1011
github.com/pressly/goose/v3 v3.27.3
1112
github.com/spf13/cobra v1.10.2
1213
modernc.org/sqlite v1.56.0
1314
)
1415

1516
require (
17+
github.com/atotto/clipboard v0.1.4 // indirect
1618
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19+
github.com/catppuccin/go v0.3.0 // indirect
20+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
1721
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
1822
github.com/charmbracelet/x/ansi v0.10.1 // indirect
19-
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
23+
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
24+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
2025
github.com/charmbracelet/x/term v0.2.1 // indirect
2126
github.com/dustin/go-humanize v1.0.1 // indirect
2227
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
@@ -27,6 +32,7 @@ require (
2732
github.com/mattn/go-localereader v0.0.1 // indirect
2833
github.com/mattn/go-runewidth v0.0.16 // indirect
2934
github.com/mfridman/interpolate v0.0.2 // indirect
35+
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
3036
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
3137
github.com/muesli/cancelreader v0.2.2 // indirect
3238
github.com/muesli/termenv v0.16.0 // indirect

‎go.sum‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
1+
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
2+
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
3+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
4+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
15
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
26
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
7+
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
8+
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
9+
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
10+
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
11+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
12+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
313
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
414
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
515
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
616
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
17+
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
18+
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
719
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
820
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
921
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
1022
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
11-
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
12-
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
23+
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
24+
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
25+
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
26+
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
27+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
28+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
29+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
30+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
31+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
32+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
1333
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
1434
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
35+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
36+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
37+
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
38+
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
1539
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
40+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
41+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
1642
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1743
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1844
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -37,6 +63,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
3763
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
3864
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
3965
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
66+
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
67+
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
4068
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
4169
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
4270
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=

‎internal/cli/completion.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func newCompletionCommand() *cobra.Command {
10+
return &cobra.Command{
11+
Use: "completion [bash|zsh|fish|powershell]",
12+
Short: "Generate shell completion script",
13+
Long: `Generate a shell completion script for pindrop.
14+
15+
To load completions:
16+
17+
Bash:
18+
$ source <(pindrop completion bash)
19+
20+
# To install permanently (Linux):
21+
$ pindrop completion bash | sudo tee /etc/bash_completion.d/pindrop > /dev/null
22+
23+
# To install permanently (macOS with Homebrew):
24+
$ pindrop completion bash > $(brew --prefix)/etc/bash_completion.d/pindrop
25+
26+
Zsh:
27+
$ source <(pindrop completion zsh)
28+
29+
# To install permanently:
30+
$ pindrop completion zsh > "${fpath[1]}/_pindrop"
31+
# You may need to restart your shell or run: compinit
32+
33+
Fish:
34+
$ pindrop completion fish | source
35+
36+
# To install permanently:
37+
$ pindrop completion fish > ~/.config/fish/completions/pindrop.fish
38+
39+
PowerShell:
40+
PS> pindrop completion powershell | Out-String | Invoke-Expression
41+
42+
# To install permanently, add the output to your PowerShell profile.
43+
`,
44+
DisableFlagsInUseLine: true,
45+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
46+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
47+
RunE: func(cmd *cobra.Command, args []string) error {
48+
switch args[0] {
49+
case "bash":
50+
return cmd.Root().GenBashCompletionV2(os.Stdout, true)
51+
case "zsh":
52+
return cmd.Root().GenZshCompletion(os.Stdout)
53+
case "fish":
54+
return cmd.Root().GenFishCompletion(os.Stdout, true)
55+
case "powershell":
56+
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
57+
}
58+
return nil
59+
},
60+
}
61+
}

‎internal/cli/root.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@ issue keeps its identity across scans even when the surrounding code moves.`),
6060
newStatusCommand(&g),
6161
newHistoryCommand(&g),
6262
newVersionCommand(),
63+
newCompletionCommand(),
64+
newUpdateCommand(),
6365
)
6466

67+
root.RegisterFlagCompletionFunc("log-level", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
68+
return []string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp
69+
})
70+
6571
return root.ExecuteContext(ctx)
6672
}
6773

‎internal/cli/scan.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ directory.`),
130130
f.BoolVar(&opts.noInstall, "no-install", false,
131131
"never offer to install missing scanners, even on a terminal")
132132

133+
cmd.RegisterFlagCompletionFunc("format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
134+
return []string{"table", "json", "sarif", "csv", "markdown"}, cobra.ShellCompDirectiveNoFileComp
135+
})
136+
cmd.RegisterFlagCompletionFunc("min-severity", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
137+
return []string{"info", "low", "medium", "high", "critical"}, cobra.ShellCompDirectiveNoFileComp
138+
})
139+
cmd.RegisterFlagCompletionFunc("fail-on", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
140+
return []string{"info", "low", "medium", "high", "critical"}, cobra.ShellCompDirectiveNoFileComp
141+
})
142+
cmd.RegisterFlagCompletionFunc("progress", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
143+
return tui.ModeNames, cobra.ShellCompDirectiveNoFileComp
144+
})
145+
133146
return cmd
134147
}
135148

0 commit comments

Comments
 (0)