commands

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyRunning = errors.New("limbo run: another instance is already running for this tree")

ErrAlreadyRunning is returned by AcquireLock when another limbo process already holds the run lock for the given directory. Callers should use errors.Is to match this sentinel rather than comparing directly:

if errors.Is(err, commands.ErrAlreadyRunning) {
    os.Exit(4)
}

The sentinel is always returned wrapped via fmt.Errorf("...: %w", ...), so direct equality (err == ErrAlreadyRunning) will not match — use errors.Is.

View Source
var Version = "dev"

Version is set at build time via -ldflags "-X github.com/simonspoon/limbo/internal/commands.Version=..."

Functions

func AcquireLock added in v0.7.0

func AcquireLock(dir string) (func(), error)

AcquireLock takes an exclusive, non-blocking advisory lock on "run.lock" inside dir and returns an unlock closure plus any error.

On success, the returned unlock closure releases the lock and closes the underlying file descriptor. The lock file itself is intentionally left on disk after unlock; callers must not remove it. Removing the lock file while another process may have it open is an inode-race footgun — two processes could end up with file locks on different inodes at the same path and both believe they hold the lock.

The lock is advisory and enforced by the kernel via flock(2) on Unix and LockFileEx on Windows. On Unix, per-open-file-description semantics apply: the lock is held on the opened file, not on the inode, and is released automatically when the process dies (normal exit, crash, SIGKILL) — there is no stale-lock-file failure mode. On Windows, LockFileEx locks are also released on handle close / process termination.

The caller MUST ensure dir already exists. AcquireLock does not call MkdirAll; if dir is missing, the underlying os.OpenFile call will fail.

Exit code contract for callers wiring this into "limbo run":

0 - run completed normally
1 - generic error
2 - user stop (e.g. SIGINT)
3 - blocked / no voice input available
4 - lock contention (errors.Is(err, ErrAlreadyRunning))

Use ExitCodeFor to translate an error from AcquireLock into the documented exit code.

func BottomUpCleanup added in v0.7.0

func BottomUpCleanup(store *storage.Storage, tasks []models.Task) error

BottomUpCleanup walks the pre-loaded task slice in post-order and promotes non-leaf tasks whose children are all StatusDone. Promotion mutates the in-memory slice in place (via pointers into the slice) AND persists via store.SaveTask, so a subsequent FindNextLeaf on the same slice observes the new statuses without re-reading from disk.

Rules:

  • Leaves (no children in the slice) are never touched.
  • Nodes already StatusDone are skipped (idempotent; respects manually-done nodes with bespoke outcomes).
  • A non-leaf whose children are all StatusDone is promoted: Status is set to StatusDone and Outcome to "All subtasks completed".

A visited-set guards against parent-pointer cycles in the slice; cycles return an error rather than infinite-looping.

func Execute

func Execute()

Execute runs the root command

func ExitCodeFor added in v0.7.0

func ExitCodeFor(err error) int

ExitCodeFor maps an error from AcquireLock (or any wrapped caller) to the documented "limbo run" exit code. nil → 0, ErrAlreadyRunning → 4, anything else → 1. Codes 2 (user stop) and 3 (blocked) are not produced by this package and must be returned directly by their respective wiring.

func FindNextLeaf added in v0.7.0

func FindNextLeaf(tasks []models.Task) *models.Task

FindNextLeaf returns the earliest-Created eligible leaf in tasks, or nil if none qualifies. It is pure: no store access, no disk I/O. Callers that want cleanup-aware results should run BottomUpCleanup on the same slice first.

A task is an eligible leaf when:

  • It has zero children within the provided slice (strict leaf).
  • Its Status is not StatusDone.
  • Its ManualBlockReason is empty.
  • Every BlockedBy entry either refers to an ID not present in the slice (dangling blockers treated as resolved, matching storage.IsBlocked semantics) or refers to a task in the slice with Status == StatusDone.

Ties on Created.UnixNano are broken by lexicographic ID ascending so that the result is deterministic even when generateRandomAlphaID produces two tasks in the same nanosecond.

func LoadSubtree added in v0.7.0

func LoadSubtree(store *storage.Storage, rootID string) ([]models.Task, error)

LoadSubtree returns all transitive descendants of rootID (excluding rootID itself) by walking the parent->children graph built from storage.LoadAllIndex.

A single disk read is performed (metadata only, no context files). A visited set guards against parent-pointer cycles: if one is detected, an error is returned instead of an infinite loop. If rootID is not present in the store, an error is returned.

The returned slice contains value copies of the descendant tasks.

func SetupRunSignals added in v0.7.0

func SetupRunSignals(ctx context.Context) (context.Context, context.CancelFunc)

SetupRunSignals wires SIGINT and SIGTERM into a cancellable context for the `limbo run` tick loop. Both signals are routed through signal.NotifyContext so that the caller's deferred cleanup (notably AcquireLock's unlock func from run_lock.go) runs on normal return. An os.Exit-based hard-exit path would strand run.lock; graceful ctx cancellation is the ONLY path where deferred unlock actually fires.

Caller MUST defer the returned CancelFunc — otherwise signal.NotifyContext leaks a signal relay goroutine and keeps SIGINT/SIGTERM trapped for the remainder of the process's lifetime.

Exit code mapping applied by the caller AFTER SetupRunSignals returns normally (helper itself never calls os.Exit):

0  completed successfully
1  error (cobra default)
2  user-stop (SIGINT or SIGTERM delivered)
3  blocked-no-vox (task blocked, no interactive override)
4  lock-contention (another limbo run already active)

Mirrors the signal.NotifyContext pattern used in watch.go (around line 61), extended with syscall.SIGTERM for supervisor compatibility (systemd, docker).

Types

type WatchEvent

type WatchEvent struct {
	Type      string        `json:"type"`
	Task      *models.Task  `json:"task,omitempty"`
	Tasks     []models.Task `json:"tasks,omitempty"`
	TaskID    string        `json:"taskId,omitempty"`
	Timestamp time.Time     `json:"timestamp"`
}

WatchEvent represents a change event for JSON output

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL