Files
beads/cmd/bd/context.go

586 lines
14 KiB
Go

// Package main provides the CommandContext struct that consolidates runtime state.
// This addresses the code smell of 20+ global variables in main.go by grouping
// related state into a single struct for better testability and clearer ownership.
package main
import (
"context"
"os"
"sync"
"time"
"github.com/steveyegge/beads/internal/hooks"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
)
// CommandContext holds all runtime state for command execution.
// This consolidates the previously scattered global variables for:
// - Better testability (can inject mock contexts)
// - Clearer state ownership (all state in one place)
// - Reduced global count (20+ globals → 1 context)
// - Thread safety (mutexes grouped with the data they protect)
type CommandContext struct {
// Configuration (derived from flags and config)
DBPath string
Actor string
JSONOutput bool
NoDaemon bool
SandboxMode bool
AllowStale bool
NoDb bool
ReadonlyMode bool
LockTimeout time.Duration
Verbose bool
Quiet bool
// Runtime state
Store storage.Storage
DaemonClient *rpc.Client
DaemonStatus DaemonStatus
RootCtx context.Context
RootCancel context.CancelFunc
HookRunner *hooks.Runner
// Auto-flush state (grouped with protecting mutex)
FlushManager *FlushManager
AutoFlushEnabled bool
FlushMutex sync.Mutex
StoreMutex sync.Mutex // Protects Store access from background goroutine
StoreActive bool // Tracks if Store is available
FlushFailureCount int // Consecutive flush failures
LastFlushError error // Last flush error for debugging
SkipFinalFlush bool // Set by sync command to prevent re-export
// Auto-import state
AutoImportEnabled bool
// Version tracking
VersionUpgradeDetected bool
PreviousVersion string
UpgradeAcknowledged bool
// Profiling
ProfileFile *os.File
TraceFile *os.File
}
// cmdCtx is the global CommandContext instance.
// Commands access state through this single point instead of scattered globals.
var cmdCtx *CommandContext
// testModeUseGlobals when true forces accessor functions to use legacy globals.
// This ensures backward compatibility with tests that manipulate globals directly.
var testModeUseGlobals bool
// initCommandContext creates and initializes a new CommandContext.
// Called from PersistentPreRun to set up runtime state.
func initCommandContext() {
cmdCtx = &CommandContext{
AutoFlushEnabled: true,
AutoImportEnabled: true,
}
}
// GetCommandContext returns the current CommandContext.
// Returns nil if called before initialization (e.g., during init() or help).
func GetCommandContext() *CommandContext {
return cmdCtx
}
// resetCommandContext clears the CommandContext for testing.
// This ensures tests that manipulate globals directly work correctly.
// Only call this in tests, never in production code.
func resetCommandContext() {
cmdCtx = nil
}
// enableTestModeGlobals forces accessor functions to use legacy globals.
// This ensures backward compatibility with tests that manipulate globals directly.
func enableTestModeGlobals() {
testModeUseGlobals = true
cmdCtx = nil
}
// shouldUseGlobals returns true if accessor functions should use globals.
func shouldUseGlobals() bool {
return testModeUseGlobals || cmdCtx == nil
}
// The following accessor functions provide backward-compatible access
// to the CommandContext fields. Commands can use these during the
// migration period, and they can be gradually replaced with direct
// cmdCtx access as files are updated.
// getStore returns the current storage backend (daemon client or direct SQLite).
// This is the primary way commands should access storage.
func getStore() storage.Storage {
if shouldUseGlobals() {
return store // fallback to legacy global during transition
}
return cmdCtx.Store
}
// setStore updates the storage backend in the CommandContext.
func setStore(s storage.Storage) {
if cmdCtx != nil {
cmdCtx.Store = s
}
store = s // keep legacy global in sync during transition
}
// getActor returns the current actor name for audit trail.
func getActor() string {
if shouldUseGlobals() {
return actor
}
return cmdCtx.Actor
}
// setActor updates the actor name in the CommandContext.
func setActor(a string) {
if cmdCtx != nil {
cmdCtx.Actor = a
}
actor = a
}
// getDaemonClient returns the RPC client for daemon mode, or nil in direct mode.
func getDaemonClient() *rpc.Client {
if shouldUseGlobals() {
return daemonClient
}
return cmdCtx.DaemonClient
}
// setDaemonClient updates the daemon client in the CommandContext.
func setDaemonClient(c *rpc.Client) {
if cmdCtx != nil {
cmdCtx.DaemonClient = c
}
daemonClient = c
}
// isJSONOutput returns true if JSON output mode is enabled.
func isJSONOutput() bool {
if shouldUseGlobals() {
return jsonOutput
}
return cmdCtx.JSONOutput
}
// setJSONOutput updates the JSON output flag.
func setJSONOutput(j bool) {
if cmdCtx != nil {
cmdCtx.JSONOutput = j
}
jsonOutput = j
}
// getDBPath returns the database path.
func getDBPath() string {
if shouldUseGlobals() {
return dbPath
}
return cmdCtx.DBPath
}
// setDBPath updates the database path.
func setDBPath(p string) {
if cmdCtx != nil {
cmdCtx.DBPath = p
}
dbPath = p
}
// getRootContext returns the signal-aware root context.
// Returns context.Background() if the root context is nil (e.g., before CLI initialization).
func getRootContext() context.Context {
var ctx context.Context
if shouldUseGlobals() {
ctx = rootCtx
} else {
ctx = cmdCtx.RootCtx
}
if ctx == nil {
return context.Background()
}
return ctx
}
// setRootContext updates the root context and cancel function.
func setRootContext(ctx context.Context, cancel context.CancelFunc) {
if cmdCtx != nil {
cmdCtx.RootCtx = ctx
cmdCtx.RootCancel = cancel
}
rootCtx = ctx
rootCancel = cancel
}
// getHookRunner returns the hook runner instance.
func getHookRunner() *hooks.Runner {
if shouldUseGlobals() {
return hookRunner
}
return cmdCtx.HookRunner
}
// setHookRunner updates the hook runner.
func setHookRunner(h *hooks.Runner) {
if cmdCtx != nil {
cmdCtx.HookRunner = h
}
hookRunner = h
}
// isAutoFlushEnabled returns true if auto-flush is enabled.
func isAutoFlushEnabled() bool {
if shouldUseGlobals() {
return autoFlushEnabled
}
return cmdCtx.AutoFlushEnabled
}
// setAutoFlushEnabled updates the auto-flush flag.
func setAutoFlushEnabled(enabled bool) {
if cmdCtx != nil {
cmdCtx.AutoFlushEnabled = enabled
}
autoFlushEnabled = enabled
}
// isAutoImportEnabled returns true if auto-import is enabled.
func isAutoImportEnabled() bool {
if shouldUseGlobals() {
return autoImportEnabled
}
return cmdCtx.AutoImportEnabled
}
// setAutoImportEnabled updates the auto-import flag.
func setAutoImportEnabled(enabled bool) {
if cmdCtx != nil {
cmdCtx.AutoImportEnabled = enabled
}
autoImportEnabled = enabled
}
// getFlushManager returns the flush manager instance.
func getFlushManager() *FlushManager {
if shouldUseGlobals() {
return flushManager
}
return cmdCtx.FlushManager
}
// setFlushManager updates the flush manager.
func setFlushManager(fm *FlushManager) {
if cmdCtx != nil {
cmdCtx.FlushManager = fm
}
flushManager = fm
}
// getDaemonStatus returns the current daemon status.
func getDaemonStatus() DaemonStatus {
if shouldUseGlobals() {
return daemonStatus
}
return cmdCtx.DaemonStatus
}
// setDaemonStatus updates the daemon status.
func setDaemonStatus(ds DaemonStatus) {
if cmdCtx != nil {
cmdCtx.DaemonStatus = ds
}
daemonStatus = ds
}
// isNoDaemon returns true if daemon mode is disabled.
func isNoDaemon() bool {
if shouldUseGlobals() {
return noDaemon
}
return cmdCtx.NoDaemon
}
// setNoDaemon updates the no-daemon flag.
func setNoDaemon(nd bool) {
if cmdCtx != nil {
cmdCtx.NoDaemon = nd
}
noDaemon = nd
}
// isReadonlyMode returns true if read-only mode is enabled.
func isReadonlyMode() bool {
if shouldUseGlobals() {
return readonlyMode
}
return cmdCtx.ReadonlyMode
}
// getLockTimeout returns the SQLite lock timeout.
func getLockTimeout() time.Duration {
if shouldUseGlobals() {
return lockTimeout
}
return cmdCtx.LockTimeout
}
// isSkipFinalFlush returns true if final flush should be skipped.
func isSkipFinalFlush() bool {
if shouldUseGlobals() {
return skipFinalFlush
}
return cmdCtx.SkipFinalFlush
}
// setSkipFinalFlush updates the skip final flush flag.
func setSkipFinalFlush(skip bool) {
if cmdCtx != nil {
cmdCtx.SkipFinalFlush = skip
}
skipFinalFlush = skip
}
// lockStore acquires the store mutex for thread-safe access.
func lockStore() {
if cmdCtx != nil {
cmdCtx.StoreMutex.Lock()
} else {
storeMutex.Lock()
}
}
// unlockStore releases the store mutex.
func unlockStore() {
if cmdCtx != nil {
cmdCtx.StoreMutex.Unlock()
} else {
storeMutex.Unlock()
}
}
// isStoreActive returns true if the store is currently available.
func isStoreActive() bool {
if cmdCtx != nil {
return cmdCtx.StoreActive
}
return storeActive
}
// setStoreActive updates the store active flag.
func setStoreActive(active bool) {
if cmdCtx != nil {
cmdCtx.StoreActive = active
}
storeActive = active
}
// lockFlush acquires the flush mutex for thread-safe flush operations.
func lockFlush() {
if cmdCtx != nil {
cmdCtx.FlushMutex.Lock()
} else {
flushMutex.Lock()
}
}
// unlockFlush releases the flush mutex.
func unlockFlush() {
if cmdCtx != nil {
cmdCtx.FlushMutex.Unlock()
} else {
flushMutex.Unlock()
}
}
// isVerbose returns true if verbose mode is enabled.
func isVerbose() bool {
if shouldUseGlobals() {
return verboseFlag
}
return cmdCtx.Verbose
}
// isQuiet returns true if quiet mode is enabled.
func isQuiet() bool {
if shouldUseGlobals() {
return quietFlag
}
return cmdCtx.Quiet
}
// isNoDb returns true if no-db mode is enabled.
func isNoDb() bool {
if shouldUseGlobals() {
return noDb
}
return cmdCtx.NoDb
}
// setNoDb updates the no-db flag.
func setNoDb(nd bool) {
if cmdCtx != nil {
cmdCtx.NoDb = nd
}
noDb = nd
}
// isSandboxMode returns true if sandbox mode is enabled.
func isSandboxMode() bool {
if shouldUseGlobals() {
return sandboxMode
}
return cmdCtx.SandboxMode
}
// setSandboxMode updates the sandbox mode flag.
func setSandboxMode(sm bool) {
if cmdCtx != nil {
cmdCtx.SandboxMode = sm
}
sandboxMode = sm
}
// isVersionUpgradeDetected returns true if a version upgrade was detected.
func isVersionUpgradeDetected() bool {
if shouldUseGlobals() {
return versionUpgradeDetected
}
return cmdCtx.VersionUpgradeDetected
}
// setVersionUpgradeDetected updates the version upgrade detected flag.
func setVersionUpgradeDetected(detected bool) {
if cmdCtx != nil {
cmdCtx.VersionUpgradeDetected = detected
}
versionUpgradeDetected = detected
}
// getPreviousVersion returns the previous bd version.
func getPreviousVersion() string {
if shouldUseGlobals() {
return previousVersion
}
return cmdCtx.PreviousVersion
}
// setPreviousVersion updates the previous version.
func setPreviousVersion(v string) {
if cmdCtx != nil {
cmdCtx.PreviousVersion = v
}
previousVersion = v
}
// isUpgradeAcknowledged returns true if the upgrade notification was shown.
func isUpgradeAcknowledged() bool {
if shouldUseGlobals() {
return upgradeAcknowledged
}
return cmdCtx.UpgradeAcknowledged
}
// setUpgradeAcknowledged updates the upgrade acknowledged flag.
func setUpgradeAcknowledged(ack bool) {
if cmdCtx != nil {
cmdCtx.UpgradeAcknowledged = ack
}
upgradeAcknowledged = ack
}
// getProfileFile returns the CPU profile file handle.
func getProfileFile() *os.File {
if shouldUseGlobals() {
return profileFile
}
return cmdCtx.ProfileFile
}
// setProfileFile updates the CPU profile file handle.
func setProfileFile(f *os.File) {
if cmdCtx != nil {
cmdCtx.ProfileFile = f
}
profileFile = f
}
// getTraceFile returns the trace file handle.
func getTraceFile() *os.File {
if shouldUseGlobals() {
return traceFile
}
return cmdCtx.TraceFile
}
// setTraceFile updates the trace file handle.
func setTraceFile(f *os.File) {
if cmdCtx != nil {
cmdCtx.TraceFile = f
}
traceFile = f
}
// isAllowStale returns true if staleness checks should be skipped.
func isAllowStale() bool {
if shouldUseGlobals() {
return allowStale
}
return cmdCtx.AllowStale
}
// syncCommandContext copies all legacy global values to the CommandContext.
// This is called after initialization is complete to ensure cmdCtx has all values.
// During the transition period, this keeps cmdCtx in sync with globals.
func syncCommandContext() {
if shouldUseGlobals() {
return
}
// Configuration
cmdCtx.DBPath = dbPath
cmdCtx.Actor = actor
cmdCtx.JSONOutput = jsonOutput
cmdCtx.NoDaemon = noDaemon
cmdCtx.SandboxMode = sandboxMode
cmdCtx.AllowStale = allowStale
cmdCtx.NoDb = noDb
cmdCtx.ReadonlyMode = readonlyMode
cmdCtx.LockTimeout = lockTimeout
cmdCtx.Verbose = verboseFlag
cmdCtx.Quiet = quietFlag
// Runtime state
cmdCtx.Store = store
cmdCtx.DaemonClient = daemonClient
cmdCtx.DaemonStatus = daemonStatus
cmdCtx.RootCtx = rootCtx
cmdCtx.RootCancel = rootCancel
cmdCtx.HookRunner = hookRunner
// Auto-flush state
cmdCtx.FlushManager = flushManager
cmdCtx.AutoFlushEnabled = autoFlushEnabled
cmdCtx.StoreActive = storeActive
cmdCtx.FlushFailureCount = flushFailureCount
cmdCtx.LastFlushError = lastFlushError
cmdCtx.SkipFinalFlush = skipFinalFlush
// Auto-import state
cmdCtx.AutoImportEnabled = autoImportEnabled
// Version tracking
cmdCtx.VersionUpgradeDetected = versionUpgradeDetected
cmdCtx.PreviousVersion = previousVersion
cmdCtx.UpgradeAcknowledged = upgradeAcknowledged
// Profiling
cmdCtx.ProfileFile = profileFile
cmdCtx.TraceFile = traceFile
}