Files
iceking2nd 727be3557a feat(cli): list/get 子命令 — 脚本集成 TOTP 获取
- winauth list: tabwriter 列出所有条目(名称/vendor/icon/hotkey)
- winauth get NAME: 输出当前 OTP,-n 不换行,--advance 允许 HOTP
- 加密配置自动 tty 隐藏密码提示(golang.org/x/term)
- --config 改为 PersistentFlag,子命令可继承
- HOTP get 默认拒绝,需 --advance 显式确认并保存配置
- CLI 不运行 GUI、不持有单实例锁
2026-06-12 09:50:07 +08:00

121 lines
3.8 KiB
Go

package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"git.wxccs.org/iceking2nd/winauth-go/internal/config"
"git.wxccs.org/iceking2nd/winauth-go/internal/global"
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
"git.wxccs.org/iceking2nd/winauth-go/internal/logging"
"git.wxccs.org/iceking2nd/winauth-go/internal/ui"
"git.wxccs.org/iceking2nd/winauth-go/internal/version"
"git.wxccs.org/iceking2nd/winauth-go/internal/win32"
)
const (
// singleInstanceMutex is the name used by AcquireInstanceLock. The
// "Global\" prefix would make the lock cross-session; without it
// each Windows user sees their own copy, which is what we want
// (config.yaml is per-user anyway).
singleInstanceMutex = "winauth-go-single-instance-mutex"
// windowTitle is what AcquireInstanceLock's duplicate path tries to
// foreground. Must stay in sync with ui.Run's app.Title option.
windowTitle = "WinAuth"
)
func main() {
var (
console bool
logLevel string
logFile string
configPath string
)
root := &cobra.Command{
Use: "winauth-go",
Short: "WinAuth (Go port) — TOTP/HOTP authenticator",
RunE: func(cmd *cobra.Command, args []string) error {
if console {
logging.AttachConsole()
}
closer, err := logging.Init(logging.Options{
Level: logLevel,
File: logFile,
Console: console,
})
if err != nil {
return err
}
if closer != nil {
defer func() { _ = closer.Close() }()
}
const fn = "cmd.winauth.main"
global.Log.WithField("func", fn).
WithField("version", version.String()).Info("winauth-go starting")
release, alreadyRunning, err := win32.AcquireInstanceLock(singleInstanceMutex)
if err != nil {
global.Log.WithField("func", fn).WithError(err).Warn("instance lock failed; continuing without it")
}
if alreadyRunning {
global.Log.WithField("func", fn).Info("another instance already running; activating it")
if err := win32.ActivateOtherInstance(windowTitle); err != nil {
global.Log.WithField("func", fn).WithError(err).Warn("could not activate existing window")
}
return nil
}
if release != nil {
defer release()
}
// Peek the config (without password) to recover the user's
// last language choice before initialising i18n. Encrypted
// configs still expose Language at the YAML top level so we
// can localise the password prompt itself.
lang := preferredLanguage(configPath)
if err := i18n.Init(lang); err != nil {
global.Log.WithField("func", fn).WithError(err).Warn("i18n init failed; falling back to keys")
}
return ui.Run(configPath)
},
}
root.Flags().BoolVar(&console, "console", false, "Show console window (Windows GUI builds)")
root.Flags().StringVar(&logLevel, "log-level", "info",
"Log level: panic|fatal|error|warn|info|debug|trace (or 0..6)")
root.Flags().StringVar(&logFile, "log-file", "",
"If set, also write logs to this file in addition to the console")
root.PersistentFlags().StringVar(&configPath, "config", "",
"Path to the YAML config file (default: %APPDATA%\\winauth-go\\config.yaml or $XDG_CONFIG_HOME/winauth-go/config.yaml)")
root.AddCommand(newListCmd())
root.AddCommand(newGetCmd())
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
// preferredLanguage reads the user's saved language tag out of the YAML
// config without requiring a passphrase. We resolve the default config
// path when configPath is blank and silently return "" on any failure;
// callers fall back to the i18n default in that case. Encrypted configs
// still expose the top-level Language field so this works for them too.
func preferredLanguage(configPath string) string {
if configPath == "" {
configPath = config.DefaultPath()
}
cfg, _ := config.LoadYAML(configPath, nil)
if cfg == nil {
return ""
}
return cfg.Language
}