Files
keyfunc/internal/cli/cli.go
T
sneak 332bc909ce
check / check (push) Successful in 39s
Clean up the agent socket and working files when a signal ends the tool (closes #17)
Main ran the command tree on a background context, so SIGINT, SIGTERM or
SIGHUP killed the process before the deferred cleanup ran: the agent's
temporary directory and socket, and the install working directory, were
left behind.

Main now runs the tree on a signal.NotifyContext for those three signals.
A signal cancels the context, which ends the child ssh or sftp started
with exec.CommandContext, and the deferred cleanup then runs. The exit
status after a signal stays 1 unless ssh reported one of its own.

For "ssh to" the child is cancelled with SIGTERM rather than the default
kill, so ssh restores the terminal before it goes.

Model: opus-4-8
2026-09-21 14:07:40 +00:00

98 lines
2.6 KiB
Go

// Package cli builds the command tree and runs it.
package cli
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"runtime/debug"
"syscall"
"git.eeqj.de/sneak/keyfunc/internal/cli/age"
"git.eeqj.de/sneak/keyfunc/internal/cli/mnemonic"
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
"git.eeqj.de/sneak/keyfunc/internal/cli/ssh"
"github.com/spf13/cobra"
)
// devVersion is what Version holds until a build stamps a real one.
const devVersion = "dev"
// Version is what --version prints. make build stamps it with -ldflags.
//
//nolint:gochecknoglobals // set at build time with -ldflags
var Version = devVersion
// resolveVersion chooses what --version reports. A value stamped at
// build time wins. Otherwise, for a binary from go install, the module
// version recorded in the build info is used, unless that is empty or
// the "(devel)" of a local build. When neither names a version, the
// "dev" fallback stays.
func resolveVersion(stamped string, info *debug.BuildInfo) string {
if stamped != devVersion {
return stamped
}
if info != nil && info.Main.Version != "" &&
info.Main.Version != "(devel)" {
return info.Main.Version
}
return devVersion
}
// Root returns the whole command tree.
func Root() *cobra.Command {
info, _ := debug.ReadBuildInfo()
root := &cobra.Command{
Use: "keyfunc",
Short: "derive key pairs from a BIP-39 mnemonic",
Long: "keyfunc turns a BIP-39 mnemonic into key pairs that can " +
"be recreated from that mnemonic at any time. The same " +
"mnemonic, key type and index always give the same key.",
Version: resolveVersion(Version, info),
SilenceUsage: true,
SilenceErrors: true,
}
options.Add(root)
root.AddCommand(ssh.Command(), age.Command(), mnemonic.Command())
return root
}
// Main runs the tool and returns the status the process should exit
// with. An error ends the tool with status 1, except when it carries a
// status of its own, which "ssh to" uses to hand on the status ssh
// ended with. ssh has already said whatever it had to say in that
// case, so nothing more is printed.
//
// SIGINT, SIGTERM and SIGHUP cancel the command's context instead of
// killing the process outright, so the child ssh or sftp ends and the
// deferred cleanup that removes the agent socket and the install
// working directory still runs.
func Main() int {
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP,
)
defer stop()
err := Root().ExecuteContext(ctx)
if err == nil {
return 0
}
var passed ssh.StatusError
if errors.As(err, &passed) {
return passed.Status
}
fmt.Fprintln(os.Stderr, "keyfunc: "+err.Error())
return 1
}