Documentation
¶
Overview ¶
Package cli wires the U1-U6 building blocks (config, agentsview, snapshot, machineid, summary, render, readme, gitops) into token-profile's cobra subcommands.
Index ¶
- Variables
- func Init(ctx context.Context, deps InitDeps) (err error)
- func NewCleanupCmd() *cobra.Command
- func NewInitCmd() *cobra.Command
- func NewRunCmd() *cobra.Command
- func NewStatusCmd() *cobra.Command
- func Run(ctx context.Context, deps RunDeps) (err error)
- func Status(ctx context.Context, deps StatusDeps) error
- type CleanupDeps
- type CleanupResult
- type InitDeps
- type RunDeps
- type ScheduleDeps
- type ScheduleState
- type StatusDeps
- type WizardDeps
- type WizardResult
Constants ¶
This section is empty.
Variables ¶
var ErrWizardCancelled = errors.New("setup wizard cancelled")
ErrWizardCancelled signals that RunWizard's trailing confirm group was declined -- the wizard's own cancellation signal. huh's accessible-mode Form.Run() discards each field's own RunAccessible error and unconditionally returns nil (see RunWizard's doc comment for the full rationale, KTD2), so no scripted accessible-mode input can ever produce huh.ErrUserAborted; callers must check for ErrWizardCancelled instead.
Functions ¶
func Init ¶
Init performs one-command setup (R10, R11, F3): it scaffolds the target repo's README markers and a scheduling entry — both idempotent, so re-running Init against an already-initialized repo is safe — then delegates to run for the first commit.
Config.TargetRepo == "" fails fast before touching anything, mirroring NewRunCmd's own guard; the check lives here (rather than only in NewInitCmd, as NewRunCmd's does for Run) so this error path is directly unit-testable without going through cobra.
Like Run, Init validates deps.RepoDir is a git working tree and holds the run-lock for the scaffolding steps and the first run (Fix 2, Fix 3), via initLocked, rather than calling the exported Run (which would try to acquire the same lock a second time and immediately self-conflict) — it calls the unlocked run core instead.
The lock is released before the schedule-registration prompt that follows, not held through it: that prompt can block indefinitely on real user input, and holding the lock across an unbounded wait would starve a concurrently-scheduled `run` for as long as the adopter takes to answer (mirroring Cleanup's own confirm-before-lock ordering).
Recording (mirroring Run's own KTD3 defer) wraps requireGitWorkTree and initLocked's scaffolding-plus-first-run — not the earlier errTargetRepoMissing check (which mirrors NewRunCmd's own pre-Run validation, upstream of Run's recording boundary too) and not the trailing offerScheduleRegistration (which never fails Init's own return — a failed schedule install degrades to a warning, KTD17). A panic in that span is recovered, recorded as a failure, and re-panicked, exactly as in Run.
func NewCleanupCmd ¶ added in v1.0.0
NewCleanupCmd builds the `token-profile cleanup` cobra command: a thin wrapper that loads the real config file, then delegates to Cleanup. Mirrors NewRunCmd/NewInitCmd's own wiring pattern.
func NewInitCmd ¶
NewInitCmd builds the `token-profile init` cobra command: a thin wrapper that resolves the config (loading it as-is, or running the guided wizard on a fresh machine — resolveInitConfig) and this machine's cached identity, then delegates the actual scaffolding-plus-first-run flow to Init. Mirrors NewRunCmd's own wiring pattern.
func NewRunCmd ¶
NewRunCmd builds the `token-profile run` cobra command: a thin wrapper that loads the real config file, this machine's cached identity, and the system clock, then delegates the actual refresh flow to Run. Keeping this wiring separate from Run itself is what lets Run be exercised in tests without going through cobra command execution.
func NewStatusCmd ¶ added in v1.1.0
NewStatusCmd builds the `token-profile status` cobra command: a thin wrapper delegating to Status, mirroring NewRunCmd/NewCleanupCmd's own shape. Unlike those commands, it takes no --config flag — both halves of its report resolve from machine-global state (KTD2), with no target repo in play.
func Run ¶
Run executes the end-to-end refresh flow: resolve this machine's usage, write its snapshot, merge every machine's snapshot in deps.RepoDir, compute the summary, render the dashboard card, inject it into the target repo's README, and publish both files to the repo's remote.
Before touching anything, Run validates deps.RepoDir is a real git working tree and acquires this machine's exclusive run-lock (Fix 2, Fix 3) — both fail fast with an actionable error rather than leaving stray files behind or racing a second overlapping invocation.
Each stage's error is wrapped with enough context to identify which stage failed, since a run can fail at many different points.
The named return plus its deferred recordRunOutcome call (KTD3) wrap every exit path — including requireGitWorkTree and acquireRunLock's own preflight failures, not just the inner run pipeline — so a silently failing schedule (deleted target repo, a stuck lock) still shows up in run history. A panic inside run(ctx, deps) is recovered, recorded as a failure, and re-panicked: Go only assigns a named return from `return expr` after expr evaluates without panicking, so without the recover the deferred call would see err's zero value (nil) and misrecord a crash as Success:true; re-panicking preserves the original crash/exit-code behavior once recording is done.
func Status ¶ added in v1.1.0
func Status(ctx context.Context, deps StatusDeps) error
Status reports the schedule's live registration state and the recorded run history, most-recent first, to deps.Stdout. It always returns nil: a failed schedule check or an unreadable history file are reported as data rather than as a command failure (KTD5), mirroring cleanup's own treatment of ScheduleCheckFailed. A nil deps.Stdout silently discards the report rather than panicking, mirroring RunDeps.Stdout's own nil-is-a-no-op convention.
Types ¶
type CleanupDeps ¶ added in v1.0.0
type CleanupDeps struct {
// RepoDir is the target repo's local working-copy path — the same
// Config.TargetRepo value run/init use. Checked for git-repo validity
// strictly before any lock is acquired (KTD5); a blank, missing, or
// non-git RepoDir degrades the repo-side steps to a no-op (KTD6)
// rather than failing the whole command.
RepoDir string
// Schedule bundles CheckScheduleState/RemoveSchedule's dependencies.
// Schedule deregistration is attempted regardless of RepoDir's
// validity or lock state (KTD5, KTD6).
Schedule ScheduleDeps
// Interactive gates the whole command (KTD12): cleanup always shows a
// confirmation prompt and has no non-interactive override, so it fails
// fast when no TTY is present, mirroring init.go's isInteractive/R5
// pattern.
Interactive bool
// Accessible drives the confirmation huh.Confirm field in accessible
// (TTY-free) mode; every test in this package sets it true, mirroring
// WizardDeps.Accessible (KTD2).
Accessible bool
// Input and Output are the confirmation form's IO streams. Nil defers
// to huh's own defaults (the real terminal, in interactive mode);
// tests set both to drive the prompt entirely TTY-free.
Input io.Reader
Output io.Writer
}
CleanupDeps bundles Cleanup's dependencies (F4) as a struct, mirroring InitDeps/WizardDeps's own rationale: RepoDir/Schedule/Input/Output are heterogeneous values a positional signature would invite mixing up.
type CleanupResult ¶ added in v1.0.0
type CleanupResult struct {
// Declined is true when the confirmation prompt was declined (or, in
// production, interrupted via ctrl+c — indistinguishable from a
// decline in accessible mode, see KTD2). No other field is meaningful
// when this is true: nothing was touched.
Declined bool
// RepoValid reports whether RepoDir was confirmed present and a valid
// git working tree. False means the repo-side steps below were
// skipped as a no-op (KTD6): schedule deregistration proceeded
// regardless.
RepoValid bool
// Schedule is the schedule's state as found *before* any removal
// attempt (KTD7): ScheduleRegistered means something was found and
// removed (assuming Cleanup's error is nil), ScheduleNotRegistered
// means there was nothing to remove, and ScheduleCheckFailed means the
// live state couldn't be determined at all. RemoveSchedule's own
// return value can't distinguish "removed" from "already absent" (both
// collapse to ScheduleNotRegistered post-removal) — this field
// preserves that distinction instead (R11, AE3, AE4).
Schedule ScheduleState
// ReadmeStripped is true only if README.md actually had marker
// interior content cleared (idempotent: a already-clean README leaves
// this false, not an error).
ReadmeStripped bool
// DirRemoved is true only if .token-profile/ actually existed and was
// removed (idempotent: an already-absent directory leaves this false).
DirRemoved bool
}
CleanupResult reports what Cleanup actually found and did, letting a caller (NewCleanupCmd's RunE, or a test) inspect structured outcomes instead of scraping printed text.
func Cleanup ¶ added in v1.0.0
func Cleanup(ctx context.Context, deps CleanupDeps) (CleanupResult, error)
Cleanup performs F4: it shows a single confirmation naming exactly what will be touched, then — on confirm — deregisters the schedule (always, regardless of RepoDir's validity, KTD6) and, only when RepoDir is a valid git working tree, strips README.md's marker interior and deletes .token-profile/ under the protection of the same run-lock run/init use.
RepoDir's validity is checked both before inspectFootprint and again immediately after the confirmation prompt returns (KTD5): the prompt is an unbounded wait on real user input, so trusting a pre-prompt snapshot would let acquireRunLock's own os.MkdirAll silently recreate a RepoDir that was deliberately deleted (or otherwise stopped being a valid git working tree) while the prompt was still open — undermining the whole point of degrading a missing repo to a no-op rather than resurrecting it.
type InitDeps ¶
type InitDeps struct {
// Config supplies TargetRepo — checked fail-fast by Init itself, since
// init_test.go needs to exercise that error path directly — plus the
// same breakdown/window settings the first run consumes.
Config config.Config
// Client resolves this machine's local usage for the first run.
Client *agentsview.Client
// MachineID identifies this machine's snapshot file for the first run.
MachineID string
// Now stands in for "the current time" for the first run; see RunDeps.Now.
Now time.Time
// RepoDir is the target repo's working-copy path. Kept separate from
// Config.TargetRepo so tests can point it at a scratch git fixture,
// mirroring RunDeps.RepoDir.
RepoDir string
// ScheduleDest is where the scheduling entry snippet is written.
// Injectable — rather than hardcoded to the real crontab/LaunchAgents
// location — so tests scaffold and assert against a scratch path
// without touching the real machine's schedule.
ScheduleDest string
// BinaryPath is the token-profile executable the scheduling entry
// invokes.
BinaryPath string
// ConfigPath is the --config value the scheduling entry passes to
// `token-profile run`.
ConfigPath string
// PathEnv, when non-empty, is propagated into both the reviewable
// scheduling-entry snippet and the live-installed schedule (see
// ScheduleDeps.PathEnv) — normally the PATH `init` itself ran with,
// since that's the environment that already proved PATH-dependent
// lookups (agentsview) resolve.
PathEnv string
// Stdout receives the same post-publish confirmation as RunDeps.Stdout
// — propagated into the RunDeps Init builds internally below, so init
// gets this output through the same shared run() core, no separate
// implementation.
Stdout io.Writer
// Stdin is the post-init schedule-registration prompt's input source
// (R4) — read via confirmYesNo's bufio-scanner y/N pattern.
Stdin io.Reader
// PromptSchedule gates whether the schedule-registration prompt is
// shown at all. Resolved once by NewInitCmd via isInteractive(os.Stdin)
// rather than Init re-deriving it from Stdin's own concrete type, so
// tests can drive the prompt through a plain strings.Reader — isInteractive
// would otherwise always report false for such a fixture.
PromptSchedule bool
// RegisterSchedule, when true, installs the refresh schedule directly —
// skipping the interactive Y/N prompt entirely, regardless of
// PromptSchedule — so an unattended re-run of init (e.g. from a
// provisioning script, with no TTY present) can still opt into
// InstallSchedule. Set via --register-schedule; false is the default
// no-op, preserving today's prompt-or-skip behavior.
RegisterSchedule bool
// Schedule carries the live schedule-registration's install-time
// parameters not already covered above: PlistPath (the real LaunchAgents
// location InstallSchedule targets, darwin only) plus Launchctl/Crontab
// overrides for tests. Label, BinaryPath, ConfigPath, and Interval are
// filled in by offerScheduleRegistration itself.
Schedule ScheduleDeps
// DryRun propagates into the RunDeps Init builds internally, stopping
// the first run before commit/push (R7-R9), and additionally skips the
// schedule-registration prompt (R4) entirely (R8, AE2) — registering a
// live schedule is exactly the kind of non-reversible step a dry run
// must never reach.
DryRun bool
// HistoryPath is where Init records its scaffolding-plus-first-run
// outcome (R1), the same store `run` records to — an adopter's very
// first invocation is otherwise invisible to `status`. An empty value
// silently disables recording, mirroring RunDeps.HistoryPath's own
// nil-is-a-no-op convention.
HistoryPath string
}
InitDeps bundles the one-command setup flow's (F3) explicit dependencies: everything RunDeps needs to perform the first run, plus where to scaffold the scheduling entry and what command line it should invoke. Kept as plain fields for the same reason as RunDeps — unit-testable without going through cobra command execution.
type RunDeps ¶
type RunDeps struct {
// Config supplies the breakdown mode and trailing window.
Config config.Config
// Client resolves this machine's local usage via agentsview.
Client *agentsview.Client
// MachineID identifies this machine's snapshot directory within RepoDir.
MachineID string
// Now stands in for "the current time" throughout the run: it bounds
// the trailing-window --since cutoff, the streak's "today", and the
// rendered "last updated" timestamp. An explicit field (rather than
// time.Now() called internally) keeps Run deterministic under test.
Now time.Time
// RepoDir is the target repo's working-copy path on disk — where the
// snapshot is written, the README lives, and git operations run.
// Kept separate from Config.TargetRepo so tests can point it at a
// scratch git fixture without constructing a full config file.
RepoDir string
// Stdout receives a one-line confirmation — the headline summary plus
// the just-published commit's short hash — after a successful
// publish. Nil is valid and silently discards this output, so every
// existing RunDeps literal that doesn't set it keeps behaving exactly
// as before.
Stdout io.Writer
// DryRun stops run before gitops.Publish's stage/commit/push (R7, R9):
// usage resolution, the snapshot write, the card render, and the README
// injection all still happen for real, leaving the working tree with
// real, inspectable changes — only the commit and push are skipped, in
// favor of a printed summary of what would have been committed.
DryRun bool
// HistoryPath is where Run records this invocation's outcome (R1). An
// empty value silently disables recording, mirroring Stdout's own
// nil-is-a-no-op convention — every RunDeps literal that predates this
// field keeps behaving exactly as before.
HistoryPath string
}
RunDeps bundles the explicit dependencies the end-to-end refresh flow (F1: solo adopter refresh, F2: multi-machine merge) needs. Keeping these as plain fields, rather than deriving them from cobra flags/env inside Run itself, is what makes Run unit-testable without going through cobra command execution.
type ScheduleDeps ¶ added in v1.0.0
type ScheduleDeps struct {
// GOOS selects the darwin (launchctl) vs. other (crontab) mechanism.
// Empty defaults to runtime.GOOS.
GOOS string
// Label uniquely identifies the scheduled entry: the launchd plist's
// Label key on darwin, or a marker comment tagging the crontab entry
// elsewhere. Mirrors launchdLabel (init.go).
Label string
// BinaryPath is the token-profile executable the scheduled entry
// invokes. Only read by InstallSchedule.
BinaryPath string
// ConfigPath is the --config value the scheduled entry passes to
// `run`. Only read by InstallSchedule.
ConfigPath string
// Interval is the scheduled-run cadence. Assumed already validated
// against config.Config's divisor set (KTD10) by the time it reaches
// here — this package never re-validates it. Only read by
// InstallSchedule.
Interval time.Duration
// PathEnv, when non-empty, is written into the scheduled entry's own
// environment (launchd's EnvironmentVariables PATH, cron's PATH= line)
// — both mechanisms otherwise give a scheduled job a minimal PATH (e.g.
// launchd's default is just /usr/bin:/bin:/usr/sbin:/sbin), which
// doesn't include common install locations like /opt/homebrew/bin,
// breaking PATH-dependent lookups (agentsview) the interactive session
// that ran `init` already resolved fine. Empty leaves the scheduler's
// own default environment in effect. Only read by InstallSchedule.
PathEnv string
// PlistPath is where InstallSchedule writes the launchd job's plist
// before bootstrapping it (darwin only).
PlistPath string
// Launchctl overrides the launchctl executable resolved via
// exec.LookPath. Empty defaults to "launchctl".
Launchctl string
// Crontab overrides the crontab executable resolved via
// exec.LookPath. Empty defaults to "crontab".
Crontab string
}
ScheduleDeps bundles the parameters CheckScheduleState, InstallSchedule, and RemoveSchedule need against either mechanism (launchd on darwin, cron elsewhere). GOOS/Launchctl/Crontab are overridable — mirroring agentsview.Client.BinaryName's "resolved by exec.LookPath, defaults to the bare name" convention — so tests target fixture scripts regardless of which OS actually runs the test.
type ScheduleState ¶ added in v1.0.0
type ScheduleState int
ScheduleState is the live registration state of token-profile's scheduled run, as resolved by CheckScheduleState (KTD7): three distinct outcomes, never collapsing a check failure into "not registered" (AE4).
const ( ScheduleNotRegistered ScheduleState = iota ScheduleRegistered ScheduleCheckFailed )
func CheckScheduleState ¶ added in v1.0.0
func CheckScheduleState(ctx context.Context, deps ScheduleDeps) (ScheduleState, error)
CheckScheduleState resolves the live registration state of the scheduled run: registered, not registered, or check failed (KTD7) — the last one distinct from "not registered" so a real failure (e.g. launchd unreachable, crontab misconfigured) is never silently treated as "nothing to remove"/"safe to install" (AE4).
func InstallSchedule ¶ added in v1.0.0
func InstallSchedule(ctx context.Context, deps ScheduleDeps) (ScheduleState, error)
InstallSchedule idempotently reconciles the scheduled run to deps' current BinaryPath/ConfigPath/Interval/PathEnv: an already-registered job is reloaded (bootout then bootstrap on darwin, replaced in place on cron) rather than left as a silent no-op (KTD13 superseded — a blind no-op let a stale live job keep spawning a since-deleted binary for over a day after its plist file was correctly updated but never reloaded). A failed state check is propagated rather than risking a blind install against unknown live state.
func RemoveSchedule ¶ added in v1.0.0
func RemoveSchedule(ctx context.Context, deps ScheduleDeps) (ScheduleState, error)
RemoveSchedule idempotently deregisters the scheduled run: a state check runs first, so "nothing registered" is a no-op reporting ScheduleNotRegistered without error (R11) rather than failing on launchctl bootout's "not found" error. A failed state check is propagated rather than risking a blind removal against unknown live state.
func (ScheduleState) String ¶ added in v1.0.0
func (s ScheduleState) String() string
type StatusDeps ¶ added in v1.1.0
type StatusDeps struct {
// Schedule is passed to CheckScheduleState as-is. Only Label (plus the
// GOOS/Launchctl/Crontab test overrides) is actually read for this
// read-only check.
Schedule ScheduleDeps
// HistoryPath is where Status reads recorded run outcomes from.
HistoryPath string
// Stdout receives the rendered report. Nil is valid and silently
// discards the report, mirroring RunDeps.Stdout's own nil-is-a-no-op
// convention.
Stdout io.Writer
}
StatusDeps bundles the read-only dependencies Status needs. Unlike Run/Cleanup, status has no target-repo config to load — both the schedule label and the history path are machine-global (KTD2), so StatusDeps carries no config.Config field.
type WizardDeps ¶ added in v1.0.0
type WizardDeps struct {
// GitUserName resolves the operator's global git user.name, used to
// pre-fill RepoName/LocalPath. Injected so tests don't depend on the
// real machine's git config, mirroring bootstrapDeps.GitUserName.
GitUserName func(ctx context.Context) string
// Accessible drives the underlying huh.Form in accessible (TTY-free)
// mode. Production callers leave this false so the form renders
// interactively; every test in this package sets it true (KTD2).
Accessible bool
// Input and Output are the form's IO streams. Nil defers to huh's own
// defaults (the real terminal, in interactive mode); tests set both to
// drive the form entirely TTY-free.
Input io.Reader
Output io.Writer
}
WizardDeps bundles RunWizard's dependencies as a struct -- Input and Output are same-shaped io values a positional signature would invite mixing up, mirroring bootstrapDeps/InitDeps's own rationale.
type WizardResult ¶ added in v1.0.0
type WizardResult struct {
// RepoName is the GitHub username/handle used to construct the
// profile-repo clone URL via the existing username/username
// convention (see profileRepoURL).
RepoName string
// CloneProtocol is the URL scheme to clone RepoName's profile repo
// with.
CloneProtocol config.CloneProtocol
// LocalPath is where the profile repo should be cloned to locally.
LocalPath string
}
WizardResult is the three fields R2's setup wizard collects. A later unit resolves these into an actual clone: RepoName and CloneProtocol build the remote URL (via profileRepoURL), LocalPath is where it's cloned to.
func RunWizard ¶ added in v1.0.0
func RunWizard(ctx context.Context, deps WizardDeps) (WizardResult, error)
RunWizard collects the three fields R2's guided-init wizard needs -- a GitHub username/handle, clone protocol, and local clone path -- via a huh.Form (an input/select/input group plus a trailing confirm group), pre-filled using the same default-guessing logic as the narrow auto-clone shortcut it eventually replaces (gitGlobalUserName, validAutoCloneName, defaultStateFile; see bootstrapConfig).
The trailing confirm group is the form's actual cancellation signal (KTD2, verified against huh v2.0.3's source): huh's accessible-mode Form.Run() (runAccessible in charm.land/huh/v2's form.go) discards each field's own RunAccessible error via a bare `_ =` and always returns nil, so no scripted-input sequence driven through accessible mode can ever produce huh.ErrUserAborted -- only a real interactive ctrl+c, handled by huh's non-accessible tea.Program path, can. RunWizard therefore reads the confirm field's own value after Form.Run() returns and treats a decline as cancellation. The huh.ErrUserAborted check below remains as a secondary, production-only safeguard for that real ctrl+c path; it is not (and cannot be) exercised by this package's accessible-mode tests, which is expected.
RepoName is validated against GitHub's username shape (KTD11) after Form.Run() returns, rather than via the input field's own Validate: an accessible-mode submission of a blank line validates the *empty string* itself (not the pre-filled default it then falls back to per accessibility.PromptString's cmp.Or), so a field-level Validate would never catch a git user.name-guessed default like "John Smith" (a space isn't path-unsafe, so validAutoCloneName already accepts it as a pre-fill) sailing through unedited into a later unit's clone step as a broken URL.