Documentation
¶
Overview ¶
Package cli provides shared CLI infrastructure for writ and lore commands.
Index ¶
- Constants
- Variables
- func AddOutputFlags(cmd *cobra.Command, opts *SinkOptions)
- func AddSilentFlag(cmd *cobra.Command)
- func AddVersionFlag(rootCmd *cobra.Command, info VersionInfo)
- func BindFlags(cmd *cobra.Command, toolName string, useSharedConfig bool) error
- func BuildPipeline(opts SinkOptions, w io.Writer) (*result.Pipeline, error)
- func CollectFiles(base, dir string) []string
- func CopyDir(dstRoot fsroot.Dir, src string, dst fsroot.Path) error
- func DisplayManPage(cmd *cobra.Command, header *doc.GenManHeader) (err error)
- func Error(format string, args ...any)
- func ExitCode(err error) int
- func ExitWith(code int, err error) error
- func Failure(format string, args ...any) error
- func GraphsDir() string
- func IndexPath() string
- func InitViper(cfg ViperConfig) error
- func LatestTracePath(graphChecksum string) string
- func LoadLatestTrace(graphChecksum string) (*op.Trace, error)
- func LoadTrace(path string) (*op.Trace, error)
- func NewConfigCmd(info ConfigInfo) *cobra.Command
- func NewHelpCmd(rootCmd *cobra.Command, header ManHeader) *cobra.Command
- func NewManCmd(rootCmd *cobra.Command, header ManHeader) *cobra.Command
- func NewRootCmd(cfg RootConfig) *cobra.Command
- func NewSelfCmd(rootCmd *cobra.Command, info SelfInstallInfo) *cobra.Command
- func NewVersionCmd(info VersionInfo) *cobra.Command
- func Note(format string, args ...any)
- func OpenTree(dir string) (fsroot.Dir, error)
- func Print(format string, args ...any)
- func SetStoreRoot(root string) (func(), error)
- func SetUI(n *status.Narrator)
- func SharedConfigPath() string
- func StoreHome() string
- func Success(format string, args ...any)
- func TracesDir() string
- func UI() *status.Narrator
- func Warn(format string, args ...any)
- func WriteGraph(graph *op.Graph) (path string, err error)
- func WriteTrace(trace *op.Trace) (path string, err error)
- type ConfigInfo
- type IndexEntry
- type ManHeader
- type RootConfig
- type SelfInstallInfo
- type SinkOptions
- type VersionInfo
- type ViperConfig
Constants ¶
const ( IndexEventGraph = "graph" IndexEventTrace = "trace" )
IndexEventGraph marks an IndexEntry recording a graph write; IndexEventTrace marks one recording a trace write.
const ( ExitOK = 0 // Success ExitError = 1 // Generic error ExitUsage = 64 // Bad CLI syntax ExitDataErr = 65 // Invalid manifest/config ExitNoInput = 66 // File not found 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 ¶
var ErrManNotAvailable = errors.New("man command not available")
ErrManNotAvailable indicates the man command is not available on this system.
Functions ¶
func AddOutputFlags ¶
func AddOutputFlags(cmd *cobra.Command, opts *SinkOptions)
AddOutputFlags binds the common set -- --filter, --jq, --output/-o, and --store -- to opts.
Bound to PersistentFlags, so one call on a program's root command covers every subcommand. All four in-scope programs register the whole set: a user who learns `-o yaml` on one types it on the next without checking. See docs/architecture/10-command-line-interface.md.
Call once during root setup, then call BuildPipeline from a command's RunE to compose the result.Pipeline.
Binding also WIRES the two flags whose meaning does not depend on a command rendering anything, so registering the set and honoring it cannot come apart:
- `--output` is validated. A command that never reaches BuildPipeline used to accept any string, because result.FormatterByName is the only place the value is checked -- `writ status -o bogus` printed its report and exited 0 (#754).
- `--store` is resolved. It used to be read by nothing outside `devlore-test`, so `writ status --store <elsewhere>` folded runs from the DEFAULT store and reported the result as compliance (#753).
Both were one defect wearing two faces: a flag registered on a root that no leaf consumed. Cobra advertised the whole set on every command's help while one command honored it, and a flag that is present and inert is worse than an absent one -- an absent flag errors, and the user tries something else.
The store selection is undone when the command tree finishes. Leaving it set would be harmless in a process that runs one command and exits, and corrupting in a test binary that runs many: the root is a package-level value, so the first command passing `--store` would silently relocate every command after it. A caller needing a narrower scope still calls SetStoreRoot itself, as `devlore-test` does per run.
Parameters:
- `cmd`: the command to bind to, normally a program's root.
- `opts`: the struct the flag values populate.
func AddSilentFlag ¶
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 ¶
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 ¶
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 CollectFiles ¶
CollectFiles returns all file paths under dir, relative to base.
func CopyDir ¶
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 Error ¶
Error prints an error message via the installed narrator. Unlike Failure, this does not return an error — use for non-fatal errors.
func ExitCode ¶
ExitCode extracts the exit code from an error. Returns the wrapped code if present, or ExitError (1) for plain errors.
func Failure ¶
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):
- Config file defaults
- Config file values
- Environment variables (TOOL_KEY_NAME)
- 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 ¶
LatestTracePath returns the path to the `latest.yaml` symlink for the graph identified by `graphChecksum`.
Parameters:
- `graphChecksum`: the graph's checksum (== op.Trace.GraphChecksum).
Returns:
- `string`: the absolute path to the graph's latest-trace symlink (which may not exist yet).
func LoadLatestTrace ¶
LoadLatestTrace loads the most recent trace for the graph identified by `graphChecksum`.
Parameters:
- `graphChecksum`: the graph's checksum (== op.Trace.GraphChecksum).
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 ¶
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 ¶
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 ¶
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 OpenTree ¶
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 SetStoreRoot ¶
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 ¶
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 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 WriteGraph ¶
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 ¶
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 status` 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 RootConfig ¶
type RootConfig struct {
Name string // Command name ("lore" or "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
}
RootConfig configures a root CLI command for lore or 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
// When true, config is read from the tool's section (e.g., config.writ.repo)
UseSharedConfig bool
}
ViperConfig holds configuration for Viper initialization.