// Package docs renders eeco's tracked-tree documentation scaffolds. // // It backs `eeco docs new ` — a one-shot, deterministic write // of a tracked-tree doc (today: VISION.md) at the repository root, // invoked explicitly by the operator. Reuses the precedent set by // `eeco init` for the workspace `.gitignore` line: eeco may write into // the tracked tree on explicit invocation, the operator stages and // commits (Constraint 6). // // The render is template-driven and project-shape-aware: placeholders // fill in the project basename, the eeco version, and which of the // usual companion docs already exist so the scaffold's "See also" // reflects the project rather than a generic stub. package docs import ( "bytes" "embed" "errors" "fmt" "os" "path/filepath" "text/template" ) //go:embed templates/*.tmpl var templatesFS embed.FS // Target enumerates the tracked-tree docs the scaffolder can produce. // Each target maps to one filename at the repository root and one // embedded template file under templates/. type Target string const ( // TargetVision scaffolds VISION.md. TargetVision Target = "vision" // TargetReadme scaffolds README.md. TargetReadme Target = "readme" ) // AllTargets returns every supported target in the order presented in // usage messages. func AllTargets() []Target { return []Target{TargetVision, TargetReadme} } // Filename returns the tracked-tree filename this target scaffolds to. // An empty string means the target is not recognised. func (t Target) Filename() string { switch t { case TargetVision: return "VISION.md" case TargetReadme: return "README.md" } return "" } // Params carries the substitutions a template can reference. Fields // are kept small and deterministic; nothing here depends on wall-clock // time so a re-render of the same project yields byte-identical bytes. type Params struct { // Project is the repository basename (e.g. "eeco"). Project string // Version is the eeco version string the binary was built with. Version string // HasReadme reports whether README.md exists at the repository root. HasReadme bool // HasUsage reports whether docs/USAGE.md exists. HasUsage bool // HasArch reports whether docs/ARCHITECTURE.md exists. HasArch bool } // Scaffold renders target's template into repoRoot/ // and returns the repo-relative path written. If the file already // exists and overwrite is false, Scaffold refuses and returns an error // whose message names the file and the override flag. func Scaffold(target Target, repoRoot string, overwrite bool, p Params) (string, error) { name := target.Filename() if name == "" { return "", fmt.Errorf("unknown target %q", string(target)) } // Filenames are hardcoded per target; this is belt-and-braces against // a future target accidentally introducing an escape. if filepath.IsAbs(name) || filepath.Clean(name) != name { return "", fmt.Errorf("target filename %q is not a safe relative path", name) } full := filepath.Join(repoRoot, name) if !overwrite { if _, err := os.Stat(full); err == nil { return "", fmt.Errorf("%s already exists at the repo root; pass --overwrite to replace", name) } else if !errors.Is(err, os.ErrNotExist) { return "", err } } tmplPath := "templates/" + string(target) + ".md.tmpl" raw, err := templatesFS.ReadFile(tmplPath) if err != nil { return "", fmt.Errorf("read template: %w", err) } tmpl, err := template.New(string(target)).Parse(string(raw)) if err != nil { return "", fmt.Errorf("parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, p); err != nil { return "", fmt.Errorf("render template: %w", err) } if err := os.WriteFile(full, buf.Bytes(), 0o644); err != nil { return "", err } return name, nil } // Render returns the rendered template bytes for target without // touching the filesystem. Useful for tests and any future caller that // wants to inspect the scaffold without writing it. func Render(target Target, p Params) (string, error) { name := target.Filename() if name == "" { return "", fmt.Errorf("unknown target %q", string(target)) } tmplPath := "templates/" + string(target) + ".md.tmpl" raw, err := templatesFS.ReadFile(tmplPath) if err != nil { return "", fmt.Errorf("read template: %w", err) } tmpl, err := template.New(string(target)).Parse(string(raw)) if err != nil { return "", fmt.Errorf("parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, p); err != nil { return "", fmt.Errorf("render template: %w", err) } return buf.String(), nil }