cli

package
v0.1.0-dev.20260905211448 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

Documentation

Overview

Package cli provides shared CLI infrastructure for writ and lore commands.

Index

Constants

View Source
const (
	IndexEventGraph = "graph"
	IndexEventTrace = "trace"
)

IndexEventGraph marks an IndexEntry recording a graph write; IndexEventTrace marks one recording a trace write.

View Source
const (
	ExitOK          = 0  // Success
	ExitError       = 1  // Generic error
	ExitUsage       = 64 // Bad CLI syntax
	ExitDataErr     = 65 // Invalid manifest/config
	ExitNoInput     = 66 // File not found
	ExitUnavailable = 69 // Registry unreachable
	ExitSoftware    = 70 // Internal error (bug)
	ExitCantCreate  = 73 // Can't create file/symlink
	ExitIOErr       = 74 // Read/write failure
	ExitNoPerm      = 77 // Permission denied
)

Exit codes follow BSD sysexits.h conventions for portable process status.

Variables

View Source
var CommonSetFlagNames = []string{"filter", "jq", "output", "store"}

CommonSetFlagNames are the four flags the shared root puts on every command of every program (10-command-line-interface.md §4): a subcommand inherits all four and defines none.

View Source
var ErrManNotAvailable = errors.New("man command not available")

ErrManNotAvailable indicates the man command is not available on this system.

View Source
var ReservedOutputFlagNames = []string{"filter", "format", "jq", "json", "output", "store"}

ReservedOutputFlagNames are the names no command may bind for itself: the common set, and the two the convention bans outright (§4, §14). Cobra lets a leaf shadow an inherited flag silently, which is how `star devlore actions generate -o json` once meant a directory.

Functions

func AddSilentFlag

func AddSilentFlag(cmd *cobra.Command)

AddSilentFlag adds the --silent flag to a root command. The flag value is read by bootstrap (cobra PersistentPreRun) which forks construction of the narrator: silent → sink.Discard, otherwise → sink.Stderr.

func AddVersionFlag

func AddVersionFlag(rootCmd *cobra.Command, info VersionInfo)

AddVersionFlag installs `--version` on a root command, answering in one line.

Cobra generates the flag as soon as cobra.Command.Version is set; the template fixes the wording to docker's — `writ version 0.4.0, build ed6f468` — a single line that contacts nothing and exits. `-v` is deliberately not a shorthand for it: this repository's commands already use `-v` for verbose output, and the collision would be worse than the missing convenience.

Parameters:

  • `rootCmd`: the root command the flag is installed on.
  • `info`: the build-time metadata; `Version` and `Commit` appear in the line.

func BindFlags

func BindFlags(cmd *cobra.Command, toolName string, useSharedConfig bool) error

BindFlags binds all persistent flags from a command to Viper. Do this after defining flags and before Execute().

Flags are bound with the tool's section prefix when using shared config:

  • --repo flag → viper key "writ.repo" (with UseSharedConfig)
  • --repo flag → viper key "repo" (without UseSharedConfig)

func BuildPipeline

func BuildPipeline(opts SinkOptions, w io.Writer) (*result.Pipeline, error)

BuildPipeline composes a result.Pipeline from the populated SinkOptions writing through w. Filters compose in --filter-then--jq order; the formatter is selected by result.FormatterByName. The writer is wrapped in a sink.Sink via sink.New internally.

Returns an error when the formatter name is unknown, the field expressions fail to parse, or the jq expression fails to compile.

func CheckNoOwnOutputFlag

func CheckNoOwnOutputFlag(root *cobra.Command) []string

CheckNoOwnOutputFlag walks the command tree beneath root and reports three shapes: a subcommand that defines a flag whose long name or shorthand an ancestor's persistent flags already carry -- cobra calls the long-name case an override and says nothing, and panics on the shorthand case the first time the command runs; neither is protection, this is (ruled 2026-09-03) -- a command that binds one of ReservedOutputFlagNames on itself, and a subcommand that does not inherit all of CommonSetFlagNames from the root. An empty result is the invariant holding.

It reads each command's raw flag sets rather than cobra's merged views, because the merged views are what panics on a shorthand collision; a flag that is the very ancestor flag, merged in earlier, is told from a shadow by identity.

Parameters:

  • `root`: the program's root command.

Returns:

  • `[]string`: one line per violation, naming the command path and the flag.

func CheckSharedSetOnRoot

func CheckSharedSetOnRoot(root *cobra.Command) []string

CheckSharedSetOnRoot reports whether root carries the common set as the shared root binds it: all four flags present as persistent flags, each with the shared usage text, so a root that hand-rolled the set with the same names is still caught.

Parameters:

  • `root`: the program's root command.

Returns:

  • `[]string`: one line per missing or foreign flag.

func CollectFiles

func CollectFiles(base, dir string) []string

CollectFiles returns all file paths under dir, relative to base.

func CopyDir

func CopyDir(dstRoot fsroot.Dir, src string, dst fsroot.Path) error

CopyDir recursively copies a directory tree from the host filesystem into `dstRoot`.

Used by post-install hooks — `cmd/star` copies its extensions this way — so the destination root is supplied by the hook rather than constructed here (#405, phase 2b).

Parameters:

  • `dstRoot`: the tree the destination belongs to, opened by the caller.
  • `src`: the source directory, outside the root.
  • `dst`: the destination within `dstRoot`.

Returns:

  • `error`: non-nil when the source cannot be read or any destination cannot be written.

func DisplayManPage

func DisplayManPage(cmd *cobra.Command, header *doc.GenManHeader) (err error)

DisplayManPage generates a man page and displays it with the system pager. Returns ErrManNotAvailable if man is not available on this system.

func Emit

func Emit(cmd *cobra.Command, value any) error

Emit renders a command's result to stdout through the shared pipeline, with the options the command's root binds: `--output` selects the rendering, `--filter` and `--jq` narrow it. It is the one render path for every program on the shared root and for the shared commands alike (10-command-line-interface.md §8; ruled 2026-09-03).

Parameters:

  • `cmd`: the running command; its root's options and its output writer are used.
  • `value`: the result.

Returns:

  • `error`: the root was not built by NewRootCmd, the pipeline cannot be built, or the value cannot be rendered.

func Error

func Error(format string, args ...any)

Error prints an error message via the installed narrator. Unlike Failure, this does not return an error — use for non-fatal errors.

func ExitCode

func ExitCode(err error) int

ExitCode extracts the exit code from an error. Returns the wrapped code if present, or ExitError (1) for plain errors.

func ExitWith

func ExitWith(code int, err error) error

ExitWith returns an error that carries a specific exit code.

func Failure

func Failure(format string, args ...any) error

Failure prints an error message via the installed narrator and returns the wrapped error. Use when the operation cannot continue.

func GraphsDir

func GraphsDir() string

GraphsDir returns the directory holding persisted graphs.

Returns:

  • `string`: the absolute graphs directory under the devlore state home.

func IndexPath

func IndexPath() string

IndexPath returns the run index's path at the store root.

Returns:

  • `string`: the absolute path of `index.ndjson` under StoreHome.

func InitViper

func InitViper(cfg ViperConfig) error

InitViper initializes Viper with standard devlore conventions. Do this in PersistentPreRunE of the root command.

Precedence (lowest to highest):

  1. Config file defaults
  2. Config file values
  3. Environment variables (TOOL_KEY_NAME)
  4. Command-line flags

Environment variable mapping:

  • WRIT_REPO → writ.repo (with UseSharedConfig)
  • WRIT_VARS_USER_NAME → writ.vars.user_name
  • Dots become underscores, keys are case-insensitive

func LatestTracePath

func LatestTracePath(graphChecksum string) string

LatestTracePath returns the path to the `latest.yaml` symlink for the graph identified by `graphChecksum`.

Parameters:

Returns:

  • `string`: the absolute path to the graph's latest-trace symlink (which may not exist yet).

func LoadLatestTrace

func LoadLatestTrace(graphChecksum string) (*op.Trace, error)

LoadLatestTrace loads the most recent trace for the graph identified by `graphChecksum`.

Parameters:

Returns:

  • *op.Trace: the most recent trace for that graph.
  • `error`: non-nil if no trace exists for the graph or it cannot be read.

func LoadTrace

func LoadTrace(path string) (*op.Trace, error)

LoadTrace loads a single trace from `path`, verifying its tier-1 checksum.

Every trace read funnels through here into op.LoadTrace — the checksum trust boundary. A trace with a missing or mismatched checksum is refused (docs/architecture/5-graph-trace-integrity.md).

Parameters:

  • `path`: the trace file to read.

Returns:

  • *op.Trace: the deserialized, integrity-verified trace.
  • `error`: non-nil if the file cannot be read, decoded, or verified.

func NewConfigCmd

func NewConfigCmd(info ConfigInfo) *cobra.Command

NewConfigCmd creates the config command with git-style subcommands.

Dot-paths match the config file structure exactly. No implicit prefixing. "writ.repos.0.path" in the CLI reads writ.repos[0].path in the file. "secrets.mode" reads secrets.mode. WYSIWYG.

Usage:

tool config get <key>...                    # Get values
tool config set <key>=<value>...            # Set values
tool config unset <key>...                  # Remove keys
tool config list                            # List all settings
tool config edit                            # Open in $EDITOR
tool config validate                        # Validate against schema
tool config schema                          # Output JSON schema
tool config path                            # Show config file location

func NewHelpCmd

func NewHelpCmd(rootCmd *cobra.Command, header ManHeader) *cobra.Command

NewHelpCmd creates a help command that prefers man pages when available. This follows git's model: if man pages are installed, display them via pager; otherwise fall back to console text output.

func NewManCmd

func NewManCmd(rootCmd *cobra.Command, header ManHeader) *cobra.Command

NewManCmd creates the man command for displaying/installing man pages. Usage:

tool man              # display man page with pager
tool man --install    # install to ~/.local/share/man/man1/
tool man deploy       # display man page for subcommand

func NewRootCmd

func NewRootCmd(cfg RootConfig) *cobra.Command

NewRootCmd creates a root cobra command with all shared flags, metadata commands, and Viper configuration. The caller adds tool-specific flags and subcommands to the returned command.

Parameters:

  • cfg: root command configuration (name, descriptions, version info)

Returns:

  • *cobra.Command: configured root command with shared flags and metadata commands

func NewSelfCmd

func NewSelfCmd(rootCmd *cobra.Command, info SelfInstallInfo) *cobra.Command

NewSelfCmd creates the "self" command group with install, upgrade, and uninstall subcommands.

func NewVersionCmd

func NewVersionCmd(info VersionInfo) *cobra.Command

NewVersionCmd creates the version command, which prints the full build detail.

Output goes through cobra.Command.OutOrStdout rather than to os.Stdout directly, so a caller that redirects the command's output captures this like any other command's.

Parameters:

  • `info`: the build-time metadata to report.

Returns:

  • `*cobra.Command`: the `version` command, carrying its `--short` flag.

func NoDirectStdout

func NoDirectStdout(dirs ...string) ([]string, error)

NoDirectStdout parses every non-test Go file under the directories and reports each write to stdout that bypasses the sink: `fmt.Print*`, `fmt.Fprint*` with `os.Stdout` as its writer, `os.Stdout.Write*`, the `print` and `println` builtins, and any statement that hands `os.Stdout` to something else -- which is how a child process inherits the terminal. Reading `os.Stdout` -- `Fd()`, `Stat()` -- is not a write and is not reported. The one place allowed to hand the terminal over is RunInteractive, the seam every interactive child goes through (10-command-line-interface.md §10).

Parameters:

  • `dirs`: package directories to walk, recursively; `testdata` directories are skipped.

Returns:

  • `[]string`: one line per write, as `file:line: what`.
  • `error`: a file that cannot be read or parsed.

func NoPrivatePipeline

func NoPrivatePipeline(dirs ...string) ([]string, error)

NoPrivatePipeline parses every non-test Go file under the directories and reports each import of pkg/result outside cmd/internal/cli: BuildPipeline is the one way to a rendering, and a second importer is a second convention (10-command-line-interface.md §14).

Parameters:

  • `dirs`: package directories to walk, recursively.

Returns:

  • `[]string`: one line per importer, as `file:line: imports pkg/result`.
  • `error`: a file that cannot be read or parsed.

func Note

func Note(format string, args ...any)

Note prints an informational message via the installed narrator.

func OpenTree

func OpenTree(dir string) (fsroot.Dir, error)

OpenTree creates the directory at `dir` if it is absent, then opens a confined root at it.

CLI-side trees are addressed before they exist — a first install creates its prefix, and the config, state and cache trees appear on first use — but fsroot.OpenExisting is a query: it opens what is there and errors when nothing is. This is the compound operation that reconciles the two, and it is deliberately named for the creation so that opening keeps meaning only opening.

Creation runs through a root rather than os.MkdirAll because a mode is worth nothing on Windows without the access-control list that fsroot applies for it — a directory made 0o700 by os.MkdirAll is inherited-DACL and readable by every account with access to its parent. The chain is made through a root anchored at the volume, so every component is resolved by the kernel and no window opens between creating the path and opening it.

The mode is 0o700 because the XDG Base Directory Specification says so: "If, when attempting to write a file, the destination directory is non-existent an attempt should be made to create it with permission 0700." That is owner-only, which is also what makes it enforceable on Windows.

Parameters:

  • `dir`: the absolute path of the tree to open, created when absent.

Returns:

  • `fsroot.Dir`: a confined root anchored at `dir`. The caller owns it and must Close it.
  • `error`: when `dir` is not absolute on this platform, or the tree cannot be created or opened.

func Print

func Print(format string, args ...any)

Print emits raw text via the installed narrator.

func RunInteractive

func RunInteractive(child *exec.Cmd, alternative string) error

RunInteractive hands the terminal to a child the user drives -- an editor, a pager -- and waits for it.

It is the one seam through which stdout leaves this process for a child (10-command-line-interface.md §10, ruled 2026-09-03). A child run for its output is captured instead, always, and never comes here. With no terminal on both stdin and stdout there is nothing to hand over: the call fails naming the alternative, rather than launching an editor into a pipe.

Parameters:

  • `child`: the command to run; its standard streams are set here.
  • `alternative`: what the user does instead when there is no terminal, as a clause.

Returns:

  • `error`: no terminal, or the child's failure.

func SetStoreRoot

func SetStoreRoot(root string) (func(), error)

SetStoreRoot points the execution store at root, returning a function that restores the previous value.

The root is made absolute against the working directory, because a relative path is the natural thing for a user to type and OpenTree rejects anything else -- deliberately, since a drive-relative path on Windows anchors to whichever drive the process is standing on. Absolutizing here keeps that invariant true for every later reader rather than at each one.

An empty root restores the default XDG state path. The returned restore function makes the change safe to scope to a test.

Parameters:

  • `root`: the store's new root directory, absolute or relative, or empty for the default.

Returns:

  • `func()`: restores the root in effect before this call.
  • `error`: when the working directory cannot be resolved to absolutize a relative root.

func SetUI

func SetUI(n *status.Narrator)

SetUI installs the package-global narrator used by the cli facade functions (Note, Warn, Error, Failure, Success, Print).

Subsequent calls replace the installed narrator.

func SharedConfigPath

func SharedConfigPath() string

SharedConfigPath returns the path to the shared devlore config file.

func StoreHome

func StoreHome() string

StoreHome returns the execution store's root directory.

The store is one anchored tree, not a set of independently located directories. Everything the store owns -- the graphs directory, the traces directory, and the run index -- resolves from here, and the same root anchors the fsroot.Dir the writers open. Relocating a leaf while the anchor stayed behind produced a path that escapes its parent, which is the failure this single accessor exists to prevent.

Returns:

  • `string`: the store root, defaulting to devlore's XDG state home.

func Success

func Success(format string, args ...any)

Success prints a success message via the installed narrator.

func TracesDir

func TracesDir() string

TracesDir returns the directory holding persisted execution traces.

Traces are grouped into a per-graph subdirectory keyed by graph checksum; see the package store overview.

Returns:

  • `string`: the absolute traces directory under the devlore state home.

func UI

func UI() *status.Narrator

UI returns the currently installed narrator.

func Warn

func Warn(format string, args ...any)

Warn prints a warning message via the installed narrator.

func WriteGraph

func WriteGraph(graph *op.Graph) (path string, err error)

WriteGraph persists `graph` under GraphsDir, keyed by its checksum, and returns the file path.

Idempotent: a graph with the same checksum is written once. Subsequent calls observe the existing file and return its path without rewriting — distinct runs of the same plan share one persisted graph. A first write also appends an IndexEventGraph line to the run index, carrying the origin's tool and scope so index readers can filter without opening the document.

Parameters:

  • `graph`: the assembled, immutable graph to persist. Must not be nil.

Returns:

  • `string`: the absolute path the graph is stored at.
  • `error`: non-nil if the directory cannot be created or the graph or its index line cannot be written.

func WriteTrace

func WriteTrace(trace *op.Trace) (path string, err error)

WriteTrace persists `trace` under TracesDir in its graph's subdirectory, updates the per-graph `latest.yaml` symlink to point at it, and appends an IndexEventTrace line to the run index.

Each run writes a distinct timestamped file, so a graph accumulates many traces. The subdirectory is keyed by op.Trace.GraphChecksum; `latest.yaml` is the convenience entry point for drift detection, reconciliation, and pause/restart.

Parameters:

  • `trace`: the captured executor trace to persist. Must not be nil and must carry a GraphChecksum.

Returns:

  • `string`: the absolute path the trace is stored at.
  • `error`: non-nil if the directory cannot be created or the trace/symlink cannot be written.

Types

type ConfigInfo

type ConfigInfo struct {
	Name          string // Tool name (e.g., "lore", "writ")
	Schema        []byte // Embedded JSON schema
	DefaultConfig []byte // Default configuration content
}

ConfigInfo contains configuration metadata for a tool.

type IndexEntry

type IndexEntry struct {

	// At is the UTC moment the store write happened.
	At time.Time `json:"at"`

	// Event is [IndexEventGraph] or [IndexEventTrace].
	Event string `json:"event"`

	// Tool is the producing program's name from the graph's origin; graph events only.
	Tool string `json:"tool,omitempty"`

	// Scope is the planning scope from the graph's origin; graph events only.
	Scope string `json:"scope,omitempty"`

	// GraphChecksum is the graph's canonical "sha256:<hex>" identity — the join key between events.
	GraphChecksum string `json:"graph_checksum"`

	// TraceFile is the trace's filename within its per-graph traces subdirectory; trace events only.
	TraceFile string `json:"trace_file,omitempty"`
}

IndexEntry is one line of the run index.

A graph event carries `Tool` and `Scope` (from the graph's origin) so readers can filter without opening the document; a trace event carries `TraceFile` and joins to its graph event through the shared `GraphChecksum`.

func ReadIndex

func ReadIndex() ([]IndexEntry, error)

ReadIndex reads the run index, tolerating a torn final line.

Lines that fail to parse are skipped — a crash mid-append must not fail every later read. A missing index file is an error (callers distinguish it via os.IsNotExist): per the deploy-family design, `writ reconcile` treats a missing index as a hard error rather than degrading silently.

Returns:

  • `[]IndexEntry`: the parsed entries in append order.
  • `error`: non-nil when the index cannot be opened, including when it does not exist.

type ManHeader

type ManHeader struct {
	Title   string
	Section string
	Source  string
	Manual  string
}

ManHeader contains metadata for man page generation.

type RootConfig

type RootConfig struct {
	Name          string // Command name ("lore", "star", "writ", ...)
	Short         string // One-line description
	Long          string // Multi-line description
	DefaultConfig []byte // Schema default config (e.g., schema.LoreDefaultConfig)
	Version       string // Semantic version, set via ldflags
	Commit        string // Git commit hash, set via ldflags
	BuildDate     string // Build timestamp, set via ldflags

	// What a program installs beyond the binary, its man pages, its completions and its config rides here
	// rather than on a `self` of the program's own: `self` is one command on every program, and a
	// program's additions attach to it (10-command-line-interface.md §2, ruled 2026-09-02). star's
	// extensions are the one user today.
	PostInstallHooks   []func(string) []string // Run after install with the prefix; return installed paths relative to it
	PostUninstallHooks []func(string) error    // Run after uninstall with the prefix
}

RootConfig configures a root CLI command for one of the four programs: devlore-test, lore, star, writ.

type SelfInstallInfo

type SelfInstallInfo struct {
	Name               string                  // Tool name (e.g., "lore", "writ", "star")
	Version            string                  // Semantic version (e.g., "0.4.0"), set via ldflags
	ManHeader          ManHeader               // Man page header metadata
	ConfigInfo         *ConfigInfo             // Config schema and defaults (nil to skip config init)
	PostInstallHooks   []func(string) []string // Hooks run after install; return installed file paths (relative to prefix)
	PostUninstallHooks []func(string) error    // Hooks run after uninstall
}

SelfInstallInfo contains metadata needed for self-installation.

type SinkOptions

type SinkOptions struct {
	Format  string // bound to --output; the field names the concept, the flag names what users type
	Filters []string
	JQ      string
	Store   string
}

SinkOptions captures the populated values from [AddOutputFlags]. The struct is the input to BuildPipeline, which composes a result.Pipeline from the flag values.

type VersionInfo

type VersionInfo struct {
	Version   string // Semantic version (e.g., "0.1.0")
	Commit    string // Git commit hash
	BuildDate string // Build timestamp
}

VersionInfo contains version metadata set at build time.

type ViperConfig

type ViperConfig struct {
	// Name is the tool name (e.g., "lore", "writ")
	Name string

	// EnvPrefix is the environment variable prefix (e.g., "LORE", "WRIT")
	// If empty, defaults to uppercase Name
	EnvPrefix string

	// ConfigName is the config file name without extension (default: "config")
	ConfigName string

	// ConfigType is the config file type (default: "yaml")
	ConfigType string

	// UseSharedConfig uses ~/.config/devlore/config.yaml with tool-specific section
	// When true, config is read from the tool's section (e.g., config.writ.repo)
	UseSharedConfig bool
}

ViperConfig holds configuration for Viper initialization.

Jump to

Keyboard shortcuts

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