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 marked
local so that, as before, only the commands that define them accept them.
ErrWriter is stdout so usage errors print with their help, as before.
go mod tidy raises testify to v1.12.1, which v3 requires. The
urfave_cli_no_docs build tag is dropped; v3 has no such tag. v3 no longer
refuses a flag given under two of its names, so -v --verbose now gives
debug output, and the test that pinned the refusal becomes a
TestVerboseCount case. Help text follows v3's templates.

Model: opus-5-5
This commit is contained in:
2026-10-04 18:21:42 +00:00
parent 9bb0ab3a03
commit 5987f6edd1
15 changed files with 198 additions and 229 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()