setup

package
v0.3.0-alpha.6 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package setup implements the interactive first-run wizard for Gramaton and the non-interactive bootstrap fallback that both entry points (`gramaton init` in a terminal, `gramaton init --non-interactive` in scripts/CI) share.

Why a wizard at all

The pre-OSS target audience is "tech-capable users" — developers who can run `go install`. Even for them, a complete Gramaton install wires together several moving parts: config file, data directory, embedding model, LLM provider + API key, MCP client registration, agent-usage instructions, automatic-capture hooks. A bare `gramaton init` that drops out after creating the data dir leaves most of those steps undocumented in the quickstart and invisible in `--help`. New users end up with a half-wired install and assume Gramaton "doesn't really work."

The wizard walks the user through each of those steps once, with a plain-English explanation at every decision point, auto-detection where it's possible (MCP clients, installed tools), and a validation pass at the end. It's the single highest-leverage UX investment we can make before the first public push.

Decision captured in Memory record "Gramaton pre-OSS target audience and wave-prioritization decision" (2026-04-22). The wizard is an explicit Wave-1 requirement; no-code/low-code polish (pre-built binaries, install script, GUI) is Wave-2+ and explicitly deferred.

Why `internal/setup/` (not `cli/wizard/`)

The wizard is a sequence of pure operations on config, filesystem, and the MCP-client's config files. None of it is CLI-flag-specific. Putting it in an internal package (a) keeps cli/ thin, (b) makes every step unit-testable with mock stdin/stdout, (c) leaves the door open for a future non-CLI driver (gramaton doctor --fix could reuse the MCP injection logic; a future desktop app, if one ever ships, could drive the same setup package).

The trade-off: an extra package boundary. Worth it for the testability alone. No shared state with cli/; imports go one way (cli -> setup).

Wizard shape

The public entry point is Run. It expects a Prompter and a Writer injected from the caller; cli/ passes terminal-backed implementations, tests pass scripted mocks.

wiz := setup.New(prompter, writer, cfg, cfgPath, configDir)
if err := wiz.Run(ctx); err != nil { ... }

Steps (see wizard.go Run):

  1. Welcome + route branch: fresh install, import, or attach a shared read-only store
  2. Knowledge-store bootstrap (config, data dir, embedding provider, model download)
  3. LLM provider (optional but strongly recommended) + API key + test call + cost caps
  4. MCP client auto-detect (harness registry: Claude Code, kiro-cli, Codex, Cursor) + config injection
  5. Agent-usage instructions installer (per-client guidance files)
  6. Hooks installer (auto-capture) for detected clients

followed by an unnumbered verification + next-steps wrap-up.

The read-only route (Step 0 [3], step_readonly.go) replaces Steps 1-5 entirely: it attaches a shared store as a frozen named store via store.Attach (shared with `gramaton store attach`), registers per-store MCP entries, and installs the read-only guidance variant. It skips identity, LLM, and hooks -- a read-only consumer never writes.

Each step is idempotent and safe to re-run: re-running the wizard against an existing install offers a menu to reconfigure individual parts rather than clobbering state.

Non-interactive mode

When the caller detects no TTY (or the user passes --non-interactive), the wizard runs in a defaults-only path that completes Step 1 only (bootstrap + BERT model download) and prints instructions for completing Steps 2-5 manually. This preserves backward compatibility with existing `gramaton init` invocations in scripts/CI and keeps the --non-interactive exit behavior predictable.

Index

Constants

View Source
const NoAutostartEnv = "GRAMATON_NO_AUTOSTART"

NoAutostartEnv, when set to "1" in a gramaton process's environment, suppresses the CLI client's auto-start of a background server (cli/client.go serverURL). Uninstall sets it on every vendor-CLI invocation -- list AND remove -- because `claude mcp list` health-checks each configured stdio entry by SPAWNING it, and a spawned `gramaton mcp` would otherwise auto-start the very server uninstall just stopped (or, on a dry run, one that was never running at all).

Variables

View Source
var ErrAborted = errors.New("setup aborted by user")

ErrAborted is returned when the user explicitly aborts the wizard (currently: ctrl+C mid-prompt, which surfaces as io.EOF from stdin and we translate here for callers to handle gracefully).

View Source
var ErrInputTooLong = errors.New("input too long")

ErrInputTooLong is returned when a single prompt receives more than maxLineBytes bytes of input. Defensive cap against pathological paste: a user who accidentally paste-bombs the terminal (very long file dropped into the prompt by mistake, escaped ANSI sequences, etc.) would otherwise force us to buffer the whole blob. With the cap we fail fast and the caller can re-prompt.

View Source
var ErrNoMCPBackend = errors.New("no MCP backend configured")

ErrNoMCPBackend is returned by a Wizard whose MCPBackend field was explicitly cleared (by a test, for example) and who then tried to run Step 3. Should never fire in production.

Functions

func HarnessRegistrations

func HarnessRegistrations(ctx context.Context, backend MCPBackend) map[string][]string

HarnessRegistrations surveys every detected harness and returns a map from gramaton-owned MCP entry name to the display names of the harnesses that currently have it registered (enumerated by naming convention). Best-effort: a harness whose enumeration fails is skipped rather than failing the whole survey. Used by `store list` to show which stores are wired and by `store sync-harness` to find orphaned entries.

func OSAccountName

func OSAccountName() string

OSAccountName returns the current OS account's human name for use as the default author identity: the account's full/display name when the platform provides one, the username otherwise, "" when the lookup fails. Exported because cli/init.go's non-interactive path applies the same fallback without a wizard.

On Unix the full name comes from the GECOS field, which may carry comma-separated subfields (office, phone); everything after the first comma is dropped, matching git's handling of the same field.

func StoreEntryName

func StoreEntryName(storeName string) string

StoreEntryName is storeEntryName exported for callers (store list / sync-harness) that need a store's expected MCP entry name without duplicating the naming convention.

func UninstallInventory

func UninstallInventory(ctx context.Context, configDir string, targets []*Harness) (*UninstallPlan, []UninstallResult)

UninstallInventory probes every uninstall surface for the selected harnesses without modifying anything, and returns the plan a subsequent UninstallApply consumes plus the per-surface inventory results. Present surfaces come back with UninstallPresent; surfaces that would be skipped or refused report that up front so the user sees it before confirming.

Types

type ClientSyncResult

type ClientSyncResult struct {
	// Client is the harness display name ("Claude Code").
	Client string
	// Action is what happened for this harness.
	Action SyncAction
	// Err carries the failure when Action is SyncFailed; nil otherwise.
	// Per-harness failures never abort the whole sync (warn-and-continue,
	// matching the wizard's install posture), so the caller inspects
	// this to report or escalate.
	Err error
}

ClientSyncResult is one harness's outcome from a sync.

type DefaultHookBackend

type DefaultHookBackend struct{}

DefaultHookBackend is the production implementation.

func (DefaultHookBackend) Materialize

func (DefaultHookBackend) Materialize(client string, configDir string) ([]string, error)

Materialize writes the proxy scripts for `client` into <configDir>/hooks/<client>/. configDir is typically ~/.gramaton. Creates directories with 0o700 and scripts with 0o755. The exec bit is ignored on Windows but preserved on Unix.

Pre-Phase-2 this function extracted real hook logic from embedded .sh files; as of Phase 2 the scripts are one-line proxies generated from Go templates. User customizations of proxy files are overwritten on re-run — documented in the wizard output.

func (DefaultHookBackend) RegisterHooks

func (DefaultHookBackend) RegisterHooks(ctx context.Context, client string, scriptPaths []string) (bool, error)

RegisterHooks dispatches to the harness's hook-wiring strategy (mirrors DefaultMCPBackend.Register). Clients without a WireHooks strategy are a programming error here -- stepHooks only calls RegisterHooks when the registry entry has one.

type DefaultMCPBackend

type DefaultMCPBackend struct{}

DefaultMCPBackend is the production implementation. Uses exec to detect binaries and shell out to each client's CLI for registration.

func (DefaultMCPBackend) Detect

func (DefaultMCPBackend) Detect() []DetectedClient

Detect probes every harness in the registry (PATH binary or config-dir presence, per entry). Order of the returned slice matches registry order -- it's stable for a given machine, which matters for the wizard's display.

func (DefaultMCPBackend) ListEntries

func (DefaultMCPBackend) ListEntries(ctx context.Context, client DetectedClient) ([]string, error)

ListEntries dispatches to the harness's enumeration strategy. A harness without one (the registry allows nil) reports no entries rather than erroring, so a survey never fails on an un-enumerable harness.

func (DefaultMCPBackend) Register

func (DefaultMCPBackend) Register(ctx context.Context, client DetectedClient) (bool, error)

Register dispatches to the harness's registration strategy. Unknown clients are a programming error (Detect returned something the registry doesn't know) so we surface that loudly.

func (DefaultMCPBackend) RegisterStore

func (DefaultMCPBackend) RegisterStore(ctx context.Context, client DetectedClient, storeName string) (bool, error)

RegisterStore dispatches to the harness's per-store registration strategy, mirroring Register.

func (DefaultMCPBackend) RemoveStore

func (DefaultMCPBackend) RemoveStore(ctx context.Context, client DetectedClient, storeName string) (bool, error)

RemoveStore removes the store's MCP entry from the client's config, reusing uninstall's enumerate-by-convention-then-remove machinery scoped to a single entry (removeStoreEntry). client.Binary is the path Detect resolved (empty for dir-detected Cursor, whose list/remove strategies ignore it).

type DetectedClient

type DetectedClient struct {
	Name   string
	Binary string
}

DetectedClient describes one MCP client installed on this system that Step 3 can register Gramaton against. Name is a human- readable label ("Claude Code", "kiro-cli") used in wizard output; Binary is the absolute path returned by exec.LookPath, kept so we run the same binary we detected (not whatever PATH resolves to at registration time).

type EntryState

type EntryState int

EntryState is the desired state of a store's MCP entry across the detected harnesses.

const (
	// EntryPresent registers the store's MCP entry (idempotent:
	// re-registering an existing entry reports "already registered").
	EntryPresent EntryState = iota
	// EntryAbsent removes the store's MCP entry (idempotent: removing
	// an absent entry reports "not present", not an error).
	EntryAbsent
)

type Harness

type Harness struct {
	// Name is the human-readable label shown in wizard output AND
	// the identifier used for registry lookups.
	Name string

	// DetectBinary is the PATH binary whose presence marks this
	// harness as installed. Empty when the harness has no CLI
	// binary (e.g. a GUI IDE) — DetectDir is consulted instead.
	DetectBinary string

	// DetectDir is a home-relative directory whose existence marks
	// the harness as installed, for harnesses without a PATH
	// binary. Ignored when DetectBinary is non-empty.
	DetectDir string

	// RegisterMCP registers gramaton as an MCP server with this
	// harness. bin is the detected binary path (empty for
	// dir-detected harnesses). Returns alreadyRegistered=true when
	// the entry was already present (a soft success the wizard
	// reports differently). Nil means Step 3 cannot register this
	// harness programmatically.
	RegisterMCP func(ctx context.Context, bin string) (alreadyRegistered bool, err error)

	// RegisterMCPStore registers an attached named store's MCP entry
	// with this harness: entry storeMCPEntryName(store), running
	// `gramaton --store <store> mcp`. Used by the wizard's read-only
	// attach route. Nil means that route cannot register this
	// harness programmatically.
	RegisterMCPStore func(ctx context.Context, bin, storeName string) (alreadyRegistered bool, err error)

	// ManualMCPHint is the one-line manual-registration hint shown
	// when no clients are detected or the user skips Step 3.
	ManualMCPHint string

	// VerifyMCPRegistered reports whether a gramaton entry is
	// present in this harness's MCP config, for the wizard's
	// unnumbered verification pass (and registration idempotency
	// probes where the vendor CLI lacks replace-on-add). bin is the
	// detected binary path, empty for dir-detected harnesses. Nil
	// means the registration state can't be surveyed (kiro-cli:
	// list-output format unverified) and the check is skipped.
	VerifyMCPRegistered func(ctx context.Context, bin string) (bool, error)

	// InstructionsRelPath locates the agent-guidance file this
	// harness reads, as path elements relative to the user's home
	// directory. Empty means Step 4 (instructions) cannot target
	// this harness.
	InstructionsRelPath []string

	// ConfigRootEnv names an environment variable that relocates
	// this harness's config root (Codex's CODEX_HOME). When the
	// variable is set and non-empty, its value replaces the FIRST
	// element of InstructionsRelPath -- the home-relative config
	// dir -- when resolving the instructions path. Requires
	// len(InstructionsRelPath) >= 2.
	ConfigRootEnv string

	// InstructionsLayout selects the install strategy for the
	// guidance file: fenced block inside a shared user-owned file,
	// or a whole file Gramaton owns end to end.
	InstructionsLayout instructionsLayout

	// InstructionsHeader, when non-nil, returns a preamble prepended
	// verbatim to the rendered guidance body on install (Cursor's
	// SKILL.md YAML frontmatter + version stamp). Only meaningful
	// for wholeFileOwned layouts -- the fenced layout carries its
	// stamp in the fence line and never needs a header. A func, not
	// a string, because the stamp interpolates
	// templates.GuidanceVersion.
	InstructionsHeader func() string

	// Addendum is the harness-specific guidance block substituted
	// at the CLIENT_ADDENDUM marker in the base template. Empty
	// strips the marker (graceful default for harnesses with no
	// divergent guidance).
	Addendum string

	// ReconnectHint fills {{mcp_reconnect_hint}} in the guidance
	// prose: a short clause telling the user how to re-attach a
	// disconnected MCP server in this harness (e.g. "for Claude
	// Code: `/mcp` in the prompt"). Required when
	// InstructionsRelPath is set — an empty hint would leave the
	// variable unfilled in the installed file.
	ReconnectHint string

	// HookEmbedDir is the directory name under <configDir>/hooks/
	// that Materialize writes this harness's proxy scripts into
	// (matching the historical hooks/ layout at repo root). Empty
	// means no hook scripts are bundled for this harness.
	HookEmbedDir string

	// HookEvents are the lifecycle events Gramaton wires for this
	// harness. Order is stable for deterministic Materialize
	// output and test assertions.
	HookEvents []hookEventSpec

	// ProxyStyle selects which proxy-script variants Materialize
	// writes: .sh always (Claude Code bundles Git Bash on Windows),
	// .sh-or-.cmd by host OS (kiro: native Windows, no bash), or
	// both variants on every host (Codex: its hook config carries
	// command + commandWindows and picks at runtime).
	ProxyStyle proxyStyle

	// WireHooks patches this harness's hook configuration to route
	// lifecycle events at the materialized scripts, preserving
	// non-gramaton entries. Nil means Step 5 cannot wire hooks
	// automatically -- the wizard prints the script paths with
	// manual wiring guidance instead.
	WireHooks func(ctx context.Context, scriptPaths []string) (unchanged bool, err error)

	// HookConfigPathHint resolves the location WireHooks patches,
	// for wizard success copy. A func (resolved at print time)
	// because some locations depend on the environment — Codex's
	// hooks.json moves with CODEX_HOME, and printing the default
	// path while writing the relocated one would mislead. Required
	// when WireHooks is set.
	HookConfigPathHint func() string

	// ListMCPEntries enumerates the gramaton-owned MCP entry names
	// currently registered with this harness: the default "gramaton"
	// entry plus any "gramaton-<store>" attached-store entries
	// (storeMCPEntryName). Enumeration is by naming convention from
	// the harness's own config — never from a record of what init
	// wrote — so hand-added entries and stale entries for
	// since-deleted stores are caught too. bin is the detected
	// binary path (empty for dir-detected harnesses). Nil means
	// uninstall cannot enumerate this harness's MCP entries.
	ListMCPEntries func(ctx context.Context, bin string) ([]string, error)

	// RemoveMCPEntries removes the named MCP entries from this
	// harness's config in one pass. Returns the entries actually
	// removed (a prefix of the input when a mid-run failure stops
	// the loop) and, for harnesses that rewrite a config file
	// directly (Cursor), the backup written before the rewrite.
	// Required when ListMCPEntries is set.
	RemoveMCPEntries func(ctx context.Context, bin string, entries []string) (removed []string, backupPath string, err error)

	// ManualMCPRemoveHint returns the exact command (or edit) a user
	// can perform by hand to remove one MCP entry — shown when the
	// harness binary has gone missing or enumeration fails. Required
	// when ListMCPEntries is set.
	ManualMCPRemoveHint func(entry string) string

	// MCPBestEffort marks a harness whose MCP integration is
	// best-effort (kiro: registration syntax unverified, integration
	// parked). Uninstall downgrades an MCP removal failure to an
	// informative skip instead of failing the run, mirroring the
	// wizard's warn-and-continue install behavior.
	MCPBestEffort bool

	// UnwireHooks strips gramaton-owned entries from this harness's
	// hook config: the strip-only counterpart of WireHooks, sharing
	// its ownership rule (isGramatonHookCommand). scriptPaths
	// carries ownership paths so hooks materialized under a
	// relocated config dir are matched (#83). apply=false probes
	// without touching the file. Existence-driven — runs whenever
	// the config file exists, regardless of whether the harness
	// binary is still installed. Required when WireHooks is set, so
	// uninstall coverage automatically tracks hook-wiring coverage.
	UnwireHooks func(ctx context.Context, scriptPaths []string, apply bool) (changed bool, backupPath string, err error)

	// OwnsInstructionsDir marks a wholeFileOwned harness whose
	// instructions file lives in a gramaton-dedicated directory
	// (Cursor's ~/.cursor/skills/gramaton/). Uninstall removes the
	// directory after the file — but only when empty, so user-added
	// siblings survive. Meaningless for fenced layouts.
	OwnsInstructionsDir bool
}

Harness declaratively describes one supported AI harness (MCP client): how to detect it, how to register Gramaton as an MCP server with it, where its agent-guidance file lives, and how its hook scripts are wired. Wizard steps iterate the registry instead of switching on client names, so adding a harness is an entry here plus an addendum template (plus, occasionally, a bespoke RegisterMCP strategy).

The registry deliberately holds data and strategy references, not behavior: install mechanics (fenced merges, settings.json patching, proxy-script synthesis) stay in their existing homes and read the per-harness facts from here.

func UninstallTargets

func UninstallTargets(slug string) ([]*Harness, error)

UninstallTargets resolves the CLI's --harness flag value to registry entries: "" selects every harness; otherwise the value must be a hook embed-dir slug (claude-code, kiro, codex, cursor). Unknown slugs error, listing the valid values.

type HookBackend

type HookBackend interface {
	// Materialize generates the proxy scripts for `client` into a
	// canonical on-disk location (typically
	// ~/.gramaton/hooks/<client>/) and returns the absolute paths of
	// the installed scripts. Idempotent: re-running overwrites with
	// the current proxy template so upgrades propagate when users
	// re-run the wizard.
	Materialize(client string, configDir string) (scriptPaths []string, err error)

	// RegisterHooks wires the materialized scripts into the named
	// client's hook configuration; client is the hook embed-dir
	// name, same as Materialize. Existing gramaton-owned hook
	// entries (commands under ~/.gramaton/hooks/) are replaced in
	// place; other hook entries (user's own, other tools') are left
	// untouched. Returns (true, nil) when our entries were already
	// present and unchanged, (false, nil) on a successful update,
	// (false, err) on failure.
	RegisterHooks(ctx context.Context, client string, scriptPaths []string) (unchanged bool, err error)
}

HookBackend is the test seam for Step 5. Production uses DefaultHookBackend; tests inject a fake to exercise the wizard orchestration without touching the real filesystem or the user's hook configs.

type MCPBackend

type MCPBackend interface {
	// Detect returns the clients found on this system. Empty slice if
	// nothing was detected; never nil.
	Detect() []DetectedClient

	// Register adds Gramaton to the named client's config. Returns
	// (true, nil) if Gramaton was already registered (a soft success
	// the wizard can report differently), (false, nil) on a new
	// registration, or (false, err) on failure.
	Register(ctx context.Context, client DetectedClient) (alreadyRegistered bool, err error)

	// RegisterStore adds an attached named store's MCP entry to the
	// client's config: entry storeMCPEntryName(storeName) running
	// `gramaton --store <storeName> mcp`. Same result semantics as
	// Register. Used by the wizard's read-only attach route.
	RegisterStore(ctx context.Context, client DetectedClient, storeName string) (alreadyRegistered bool, err error)

	// RemoveStore removes a store's MCP entry from the client's config:
	// the default "gramaton" entry when storeName is empty, else the
	// "gramaton-<storeName>" entry. It enumerates by naming convention
	// first, so a foreign entry is never removed and an already-absent
	// entry is a no-op: removed=false, err=nil. The store-scoped
	// counterpart to Register/RegisterStore, used by SyncStoreHarness.
	RemoveStore(ctx context.Context, client DetectedClient, storeName string) (removed bool, err error)

	// ListEntries returns the gramaton-owned MCP entry names currently
	// registered with the client, enumerated by naming convention
	// ("gramaton" + "gramaton-<store>"). Used to report registration
	// state (store list) and find orphaned entries (store
	// sync-harness). Empty when the client has none.
	ListEntries(ctx context.Context, client DetectedClient) ([]string, error)
}

MCPBackend is the test seam for Step 3. Production uses DefaultMCPBackend; tests inject a fake to exercise the wizard's orchestration without running real clients.

type Prompter

type Prompter interface {
	// Text reads a line, trims whitespace, and returns it. If the user
	// presses Enter without input and def is non-empty, def is
	// returned. The prompt is whatever the caller wants; Prompter does
	// not print it (the caller prints, the prompter reads). This
	// separation keeps output formatting in Writer and input reading
	// here.
	Text(def string) (string, error)

	// Secret reads a line with echo disabled (if the input is a TTY).
	// No default -- secrets are explicitly typed or skipped by pressing
	// Enter. If the stream is not a TTY (piped input), Secret falls
	// back to plain ReadString; this is acceptable because piped
	// secrets are already flowing through something that could log
	// them, and refusing to read them would break scripted --non-
	// interactive setups.
	Secret() (string, error)

	// Choice presents the options as "  [n] label" lines on the Writer
	// out-of-band (caller prints; Prompter just reads the digit). It
	// parses a 1-indexed digit, validates it's in [1, maxChoice], and
	// returns the 0-indexed result. If the user presses Enter without
	// input and def != -1, def is returned. Any other input is an
	// error the caller should surface and re-prompt.
	Choice(maxChoice, def int) (int, error)

	// YesNo reads a single [y/n] answer. defaultYes controls which
	// letter is returned on a blank Enter. Accepts y, Y, yes, YES, n,
	// N, no, NO.
	YesNo(defaultYes bool) (bool, error)
}

Prompter is the test seam for reading user input during the wizard. The terminal implementation (NewTerminalPrompter) reads from stdin and honours TTY conventions (hidden input for Secret, line-buffered input otherwise). Tests pass ScriptedPrompter to drive the wizard with a canned sequence of answers.

Why an interface (not a concrete terminal reader everywhere):

  • Unit-testing the wizard without driving a real terminal.
  • Keeps the wizard logic independent of *os.File assumptions; the wizard never calls term.IsTerminal directly, it just receives a Prompter that already committed to a mode.

type ScriptedPrompter

type ScriptedPrompter struct {
	Answers []string
	// contains filtered or unexported fields
}

ScriptedPrompter is the test implementation. It yields pre-loaded answers in order; an out-of-answers read returns ErrAborted so tests fail loudly rather than hanging.

func NewScriptedPrompter

func NewScriptedPrompter(answers ...string) *ScriptedPrompter

NewScriptedPrompter accepts the canned answer sequence.

func (*ScriptedPrompter) Choice

func (s *ScriptedPrompter) Choice(maxChoice, def int) (int, error)

func (*ScriptedPrompter) Secret

func (s *ScriptedPrompter) Secret() (string, error)

func (*ScriptedPrompter) Text

func (s *ScriptedPrompter) Text(def string) (string, error)

func (*ScriptedPrompter) YesNo

func (s *ScriptedPrompter) YesNo(defaultYes bool) (bool, error)

type SyncAction

type SyncAction string

SyncAction classifies what happened for one harness during a sync.

const (
	SyncRegistered        SyncAction = "registered"
	SyncAlreadyRegistered SyncAction = "already registered"
	SyncRemoved           SyncAction = "removed"
	SyncNotPresent        SyncAction = "not present"
	SyncFailed            SyncAction = "failed"
)

type SyncReport

type SyncReport struct {
	// Entry is the MCP entry name reconciled ("gramaton" or
	// "gramaton-<store>").
	Entry string
	// Want is the desired state that produced this report.
	Want EntryState
	// Clients holds one result per detected harness, in detection order.
	// Empty when no harnesses were detected.
	Clients []ClientSyncResult
}

SyncReport is the outcome of reconciling one store's MCP entry across every detected harness.

func RenameStoreHarness

func RenameStoreHarness(ctx context.Context, backend MCPBackend, oldName, newName string) (newRep, oldRep *SyncReport)

RenameStoreHarness moves harness registration for a store rename: registers the NEW entry FIRST, then removes the OLD one. Ordering is load-bearing -- a mid-run failure after the register but before the remove leaves the agent reachable through the new entry rather than orphaned. oldName/newName use the empty string for the default store, so a default<->named rename correctly flips "gramaton" <-> "gramaton-<name>". Returns both sub-reports (new first) for the caller to render and to detect failures.

func SyncStoreHarness

func SyncStoreHarness(ctx context.Context, backend MCPBackend, storeName string, want EntryState) *SyncReport

SyncStoreHarness reconciles the MCP entry for storeName (empty = default store) across all detected harnesses toward want. It is non-interactive: per-harness failures are captured in the returned report rather than aborting, so one broken or best-effort harness never strands the rest. Detection, registration, and removal all go through backend, so tests inject a fake and production passes DefaultMCPBackend{}.

func (*SyncReport) Failures

func (r *SyncReport) Failures() []ClientSyncResult

Failures returns the per-harness results that failed. Empty when the sync was clean.

func (*SyncReport) JSON

func (r *SyncReport) JSON() map[string]any

JSON buckets the report by action for a command's structured output. Only non-empty buckets are included; a report with no detected harnesses yields an empty map.

func (*SyncReport) Registered

func (r *SyncReport) Registered() []string

Registered returns the harnesses whose entry is present after the sync (newly registered or already registered).

func (*SyncReport) Removed

func (r *SyncReport) Removed() []string

Removed returns the harnesses whose entry was removed by the sync.

type TerminalPrompter

type TerminalPrompter struct {
	// contains filtered or unexported fields
}

TerminalPrompter is the production Prompter backed by os.Stdin and golang.org/x/term for secret input. It is NOT safe for concurrent use (the wizard is single-threaded by design).

func NewTerminalPrompter

func NewTerminalPrompter() *TerminalPrompter

NewTerminalPrompter returns a Prompter that reads from os.Stdin. Secret uses term.ReadPassword when stdin is a TTY; otherwise it falls back to ReadString (piped input case).

func (*TerminalPrompter) Choice

func (p *TerminalPrompter) Choice(maxChoice, def int) (int, error)

func (*TerminalPrompter) Secret

func (p *TerminalPrompter) Secret() (string, error)

func (*TerminalPrompter) Text

func (p *TerminalPrompter) Text(def string) (string, error)

func (*TerminalPrompter) YesNo

func (p *TerminalPrompter) YesNo(defaultYes bool) (bool, error)

type TerminalWriter

type TerminalWriter struct {
	// contains filtered or unexported fields
}

TerminalWriter is the production Writer that renders to an io.Writer (usually os.Stdout). ANSI styling is deliberately minimal: the checkmark/warning/error prefixes are plain-ASCII-plus-UTF8 (not ANSI colour codes) so output is readable when piped to a log file or redirected by an IDE. If richer styling is desired later, the prefixes can be upgraded in one place here.

func NewTerminalWriter

func NewTerminalWriter() *TerminalWriter

NewTerminalWriter returns a TerminalWriter that writes to os.Stdout. Tests and alternate drivers can construct one directly with a different writer via the NewWriter helper.

func NewWriter

func NewWriter(out io.Writer) *TerminalWriter

NewWriter is for tests and alternate frontends that need to inject a specific io.Writer (e.g., a bytes.Buffer to capture output).

func (*TerminalWriter) Blank

func (w *TerminalWriter) Blank()

func (*TerminalWriter) Check

func (w *TerminalWriter) Check(msg string)

func (*TerminalWriter) ErrorLine

func (w *TerminalWriter) ErrorLine(msg string)

func (*TerminalWriter) Paragraph

func (w *TerminalWriter) Paragraph(lines ...string)

func (*TerminalWriter) ProgressEnd

func (w *TerminalWriter) ProgressEnd()

func (*TerminalWriter) ProgressStart

func (w *TerminalWriter) ProgressStart(label string)

func (*TerminalWriter) ProgressUpdate

func (w *TerminalWriter) ProgressUpdate(done, total int64)

func (*TerminalWriter) Prompt

func (w *TerminalWriter) Prompt(text string)

func (*TerminalWriter) Raw

func (w *TerminalWriter) Raw(line string)

func (*TerminalWriter) Section

func (w *TerminalWriter) Section(title string)

func (*TerminalWriter) StepHeader

func (w *TerminalWriter) StepHeader(n, of int, title string)

func (*TerminalWriter) Warn

func (w *TerminalWriter) Warn(msg string)

type UninstallOutcome

type UninstallOutcome string

UninstallOutcome classifies one surface's result.

const (
	// UninstallPresent is the inventory-only outcome: the surface
	// exists and an apply pass would remove it.
	UninstallPresent UninstallOutcome = "present"

	// UninstallRemoved: the surface existed and was removed.
	UninstallRemoved UninstallOutcome = "removed"

	// UninstallNotPresent: nothing to do for this surface.
	UninstallNotPresent UninstallOutcome = "not present"

	// UninstallNote: informational only -- the surface could not be
	// verified (the harness binary is not on PATH, so its MCP
	// registration can't be checked). Notes never affect the exit
	// code and never suppress the CLI's "nothing to remove"
	// headline; Detail carries the manual check/removal command.
	UninstallNote UninstallOutcome = "note"

	// UninstallSkipped: the surface could not be handled (failed
	// enumeration, best-effort harness) and needs manual attention;
	// Detail carries the reason and, where possible, the exact
	// manual command. Skips are warnings, not failures.
	UninstallSkipped UninstallOutcome = "skipped"

	// UninstallFailed: removal was attempted and failed, or a config
	// file was refused (unparseable). Any failed result makes the
	// CLI exit non-zero.
	UninstallFailed UninstallOutcome = "failed"
)

type UninstallPlan

type UninstallPlan struct {
	// contains filtered or unexported fields
}

UninstallPlan carries the probed state from UninstallInventory to UninstallApply so the two phases cannot disagree and every vendor CLI is invoked exactly once per harness.

type UninstallResult

type UninstallResult struct {
	// Harness is the registry display name ("Claude Code").
	Harness string
	// Surface is the human-readable surface description, path
	// included where one exists.
	Surface string
	// Outcome classifies what happened (or would happen).
	Outcome UninstallOutcome
	// Detail carries the skip/failure reason or extra context.
	Detail string
	// Backup is the path of the backup written before a rewrite.
	Backup string
}

UninstallResult is one per-surface outcome line.

func UninstallApply

func UninstallApply(ctx context.Context, plan *UninstallPlan) []UninstallResult

UninstallApply removes every uninstall surface captured in the plan. Partial failure is deliberate: a failed surface is reported and the walk continues, so one broken config file doesn't strand the rest of the cleanup.

type Wizard

type Wizard struct {
	// contains filtered or unexported fields
}

Wizard orchestrates the interactive first-run setup. A single Wizard instance runs once per `gramaton init` invocation; the type is not concurrency-safe and is not intended to be.

Construction injects three things:

  • Prompter: how the wizard reads input (terminal in production, scripted in tests).
  • Writer: how the wizard prints output (stdout in production, buffer in tests).
  • cfg / cfgPath / configDir: the target state the wizard mutates. The wizard writes to cfg in memory and persists via config.Save at well-defined checkpoints; callers passing a partially- populated cfg (e.g., pre-loaded from defaults + a partial user-provided override) is supported.

Why inject the Prompter/Writer instead of reading os.Stdin / writing os.Stdout directly in the wizard code:

  • Every step becomes unit-testable.
  • Steps stay unaware of what "a terminal" is; they just ask the Prompter for a Yes/No or print to the Writer.
  • A future non-CLI driver (`gramaton doctor --fix`, for example, which might want to re-run Step 3 MCP injection headlessly) can swap the Prompter for a non-interactive stub.

func New

func New(prompter Prompter, writer Writer, cfg *config.Config, cfgPath, configDir string) *Wizard

New constructs a Wizard. cfg may be any config.Config (typically config.Defaults() with the DataDir already resolved by the caller). cfgPath is the path we'll eventually write the final config to. configDir is the parent directory of cfgPath and is where the wizard drops ancillary files (API key files, model cache, etc.).

func (*Wizard) Run

func (w *Wizard) Run(ctx context.Context) error

type Writer

type Writer interface {
	// StepHeader prints the "Step N of M: Title" banner that frames
	// each section. The implementation should add surrounding
	// whitespace so sections are visually separated.
	StepHeader(n, of int, title string)

	// Section prints a free-standing header used outside the
	// numbered steps (Welcome, Verification, Next steps).
	Section(title string)

	// Paragraph prints one or more lines of prose with indentation
	// matching the checkmark/warning prefix width. Used for the
	// plain-English explanations above each prompt.
	Paragraph(lines ...string)

	// Prompt prints the final inline prompt that immediately
	// precedes input. No trailing newline -- the user types after.
	Prompt(text string)

	// Check prints "  ✓ <msg>" for a completed sub-step.
	Check(msg string)

	// Warn prints "  ⚠ <msg>" for a non-fatal issue. Use when we
	// continued past a minor hiccup; use Error for fatal stops.
	Warn(msg string)

	// ErrorLine prints "  ✗ <msg>" for a fatal failure within a
	// step. Named ErrorLine (not Error) to avoid shadowing the
	// builtin error type in method receivers.
	ErrorLine(msg string)

	// Blank prints a blank line. Used to separate prose from
	// prompts within a step.
	Blank()

	// Raw prints a line verbatim, no indentation or prefix. Used for
	// feature-map tables, example commands, and the end-of-wizard
	// next-steps block where explicit layout is needed.
	Raw(line string)

	// ProgressStart signals the beginning of a long operation (like
	// an embedding-model download). The implementation can choose to
	// show a spinner, a byte counter, or nothing; the wizard only
	// cares that it's told when the op starts and ends.
	ProgressStart(label string)

	// ProgressUpdate reports incremental progress (bytes or count).
	// Total may be zero if the total isn't known up front.
	ProgressUpdate(done, total int64)

	// ProgressEnd closes out the progress display.
	ProgressEnd()
}

Writer is the test seam for wizard output. Everything the wizard prints to the user (banners, step headers, prompts, checkmarks, warnings, errors, progress) goes through a Writer. The terminal implementation writes to stdout with ANSI-safe formatting; tests capture output into a buffer.

Why not just use fmt.Fprintln everywhere:

  • We want consistent prefix conventions (✓ for success, ⚠ for warnings, ✗ for errors) with no stray emoji elsewhere. A central Writer enforces this.
  • Tests benefit from a structured API (Check, Warn, Error) that lets them assert "did the wizard report success on step N" without substring-matching raw output.
  • A future non-terminal driver (imagine a plain-HTML wizard output for a desktop app) would swap the Writer impl without touching step logic.

Directories

Path Synopsis
Package templates holds the canonical Gramaton agent-guidance prose that `gramaton init` installs into AI harness instruction files, plus the version stamp used to detect outdated installs.
Package templates holds the canonical Gramaton agent-guidance prose that `gramaton init` installs into AI harness instruction files, plus the version stamp used to detect outdated installs.

Jump to

Keyboard shortcuts

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