Move the CLI to urfave/cli v3 (closes #110)
check / check (push) Failing after 3s

Ported per the library's v2-to-v3 migration guide: the app is a root
cli.Command, actions take a context and the command, and flag environment
variables become value sources. -v, -q and the version flag are local so,
as before, only the commands defining them accept them. ErrWriter is
stdout so usage errors print with their help, as before. The action's
context reaches the manifest download in check, export and list. testify
rises to v1.12.1, which v3 requires; the urfave_cli_no_docs build tag,
which v3 lacks, is dropped. v3 no longer refuses a flag given under two
names, so -v --verbose gives debug output; the test pinning the refusal
becomes a TestVerboseCount case.

Model: opus-5-5
This commit is contained in:
2026-10-04 18:36:04 +00:00
parent 9bb0ab3a03
commit ccd018aef0
15 changed files with 220 additions and 240 deletions
+21 -18
View File
@@ -1,6 +1,7 @@
package cli
import (
"context"
"errors"
"fmt"
"os"
@@ -12,7 +13,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/spf13/afero"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"sneak.berlin/go/mfer/internal/log"
"sneak.berlin/go/mfer/mfer"
)
@@ -88,12 +89,12 @@ func (mfa *CLIApp) collectInputPaths(args cli.Args) ([]string, error) {
}
// buildScannerOptions constructs scanner options from the CLI flags.
func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
output := ctx.String("output")
func (mfa *CLIApp) buildScannerOptions(cmd *cli.Command) *mfer.ScannerOptions {
output := cmd.String("output")
opts := &mfer.ScannerOptions{
IncludeDotfiles: ctx.Bool("include-dotfiles"),
FollowSymLinks: ctx.Bool("follow-symlinks"),
IncludeTimestamps: ctx.Bool("include-timestamps"),
IncludeDotfiles: cmd.Bool("include-dotfiles"),
FollowSymLinks: cmd.Bool("follow-symlinks"),
IncludeTimestamps: cmd.Bool("include-timestamps"),
Fs: mfa.Fs,
// Neither a manifest being replaced nor a temp file left by an
// interrupted run belongs in the new manifest.
@@ -101,14 +102,14 @@ func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
}
// Set seed for deterministic UUID if provided
if seed := ctx.String("seed"); seed != "" {
if seed := cmd.String("seed"); seed != "" {
opts.Seed = seed
log.Infof("using deterministic seed for manifest UUID")
}
// Set up signing options if sign-key is provided
if signKey := ctx.String("sign-key"); signKey != "" {
if signKey := cmd.String("sign-key"); signKey != "" {
opts.SigningOptions = &mfer.SigningOptions{
KeyID: mfer.GPGKeyID(signKey),
}
@@ -173,14 +174,14 @@ func (mfa *CLIApp) cleanupOnSignal(outFile afero.File, tmpPath string) chan os.S
// runEnumeratePhase enumerates all input paths with optional progress
// reporting and logs the totals.
func (mfa *CLIApp) runEnumeratePhase(ctx *cli.Context, s *mfer.Scanner) error {
func (mfa *CLIApp) runEnumeratePhase(cmd *cli.Command, s *mfer.Scanner) error {
// Set up enumeration progress reporting
var (
enumProgress chan mfer.EnumerateStatus
enumWg sync.WaitGroup
)
if ctx.Bool("progress") {
if cmd.Bool("progress") {
enumProgress = make(chan mfer.EnumerateStatus, 1)
enumWg.Add(1)
@@ -188,7 +189,7 @@ func (mfa *CLIApp) runEnumeratePhase(ctx *cli.Context, s *mfer.Scanner) error {
go reportEnumProgress(enumProgress, &enumWg)
}
err := mfa.enumerateInputs(s, ctx.Args(), enumProgress)
err := mfa.enumerateInputs(s, cmd.Args(), enumProgress)
if err != nil {
return err
}
@@ -201,22 +202,24 @@ func (mfa *CLIApp) runEnumeratePhase(ctx *cli.Context, s *mfer.Scanner) error {
return nil
}
func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
func (mfa *CLIApp) generateManifestOperation(
ctx context.Context, cmd *cli.Command,
) error {
log.Debug("generateManifestOperation()")
s := mfer.NewScannerWithOptions(mfa.buildScannerOptions(ctx))
s := mfer.NewScannerWithOptions(mfa.buildScannerOptions(cmd))
// Phase 1: Enumeration - collect paths and stat files
err := mfa.runEnumeratePhase(ctx, s)
err := mfa.runEnumeratePhase(cmd, s)
if err != nil {
return err
}
showProgress := ctx.Bool("progress")
showProgress := cmd.Bool("progress")
// Check if output file exists
outputPath := ctx.String("output")
if exists, _ := afero.Exists(mfa.Fs, outputPath); exists && !ctx.Bool("force") {
outputPath := cmd.String("output")
if exists, _ := afero.Exists(mfa.Fs, outputPath); exists && !cmd.Bool("force") {
return fmt.Errorf("output file %s %w", outputPath, errOutputExists)
}
@@ -259,7 +262,7 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
go reportScanProgress(scanProgress, &scanWg)
}
err = s.ToManifest(ctx.Context, outFile, scanProgress)
err = s.ToManifest(ctx, outFile, scanProgress)
scanWg.Wait()