bootstrap: iniciar bitácora operativa
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
package clusterlog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const Version = "0.1.0"
|
||||
|
||||
const (
|
||||
ExitOK = 0
|
||||
ExitUsage = 2
|
||||
ExitIdentity = 3
|
||||
ExitValidation = 4
|
||||
ExitConflict = 5
|
||||
ExitGit = 6
|
||||
ExitInternal = 10
|
||||
)
|
||||
|
||||
type CLIError struct {
|
||||
Code int
|
||||
Kind string
|
||||
Message string
|
||||
Details any
|
||||
}
|
||||
|
||||
func (e *CLIError) Error() string { return e.Message }
|
||||
|
||||
func NewError(code int, kind, message string, details any) *CLIError {
|
||||
return &CLIError{Code: code, Kind: kind, Message: message, Details: details}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Root string
|
||||
JSON bool
|
||||
SSHKey string
|
||||
Commit bool
|
||||
DryRun bool
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Stdin io.Reader
|
||||
Now func() time.Time
|
||||
Executable string
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Options Options
|
||||
}
|
||||
|
||||
func New(options Options) *App {
|
||||
if options.Stdout == nil {
|
||||
options.Stdout = os.Stdout
|
||||
}
|
||||
if options.Stderr == nil {
|
||||
options.Stderr = os.Stderr
|
||||
}
|
||||
if options.Stdin == nil {
|
||||
options.Stdin = os.Stdin
|
||||
}
|
||||
if options.Now == nil {
|
||||
options.Now = time.Now
|
||||
}
|
||||
if options.Executable == "" {
|
||||
options.Executable = "clusterlog"
|
||||
}
|
||||
return &App{Options: options}
|
||||
}
|
||||
|
||||
type ReviewPolicy struct {
|
||||
Default string `json:"default"`
|
||||
IncludeAuthor bool `json:"include_author"`
|
||||
}
|
||||
|
||||
type SSHKey struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
AddedAt string `json:"added_at,omitempty"`
|
||||
}
|
||||
|
||||
type Admin struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Active bool `json:"active"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
SSHKeys []SSHKey `json:"ssh_keys"`
|
||||
}
|
||||
|
||||
type AdminRegistry struct {
|
||||
Version int `json:"version"`
|
||||
ReviewPolicy ReviewPolicy `json:"review_policy"`
|
||||
Admins []Admin `json:"admins"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
Admin Admin `json:"admin"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
PublicPath string `json:"public_key_path"`
|
||||
SigningKey string `json:"signing_key_path"`
|
||||
}
|
||||
|
||||
type ReviewEvent struct {
|
||||
ID string `json:"id"`
|
||||
EntryID string `json:"entry_id"`
|
||||
AdminID string `json:"admin_id"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
ReviewedAt string `json:"reviewed_at"`
|
||||
ContentSHA256 string `json:"content_sha256"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type ReviewLog struct {
|
||||
Version int `json:"version"`
|
||||
Events []ReviewEvent `json:"events"`
|
||||
}
|
||||
|
||||
type TaskEvent struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
Type string `json:"type"`
|
||||
AdminID string `json:"admin_id"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
At string `json:"at"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Minutes int `json:"minutes,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type TaskLog struct {
|
||||
Version int `json:"version"`
|
||||
Events []TaskEvent `json:"events"`
|
||||
}
|
||||
|
||||
type PageMeta struct {
|
||||
Path string
|
||||
Body string
|
||||
Root map[string]any
|
||||
Taxonomies map[string][]string
|
||||
Extra map[string]any
|
||||
RawFront string
|
||||
ContentHash string
|
||||
}
|
||||
|
||||
func (p PageMeta) String(key string) string {
|
||||
if v, ok := p.Root[key]; ok {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p PageMeta) ExtraString(key string) string {
|
||||
if v, ok := p.Extra[key]; ok {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p PageMeta) ExtraBool(key string) bool {
|
||||
v, ok := p.Extra[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
b, _ := v.(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
func (p PageMeta) ExtraInt(key string) int {
|
||||
v, ok := p.Extra[key]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case int64:
|
||||
return int(n)
|
||||
case float64:
|
||||
return int(n)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p PageMeta) ExtraStrings(key string) []string {
|
||||
v, ok := p.Extra[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
switch values := v.(type) {
|
||||
case []string:
|
||||
return values
|
||||
case []any:
|
||||
out := make([]string, 0, len(values))
|
||||
for _, item := range values {
|
||||
out = append(out, fmt.Sprint(item))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type MemoryRecord struct {
|
||||
EntryID string `json:"entry_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Date string `json:"date"`
|
||||
AuthorID string `json:"author_id"`
|
||||
AuthorFingerprint string `json:"author_fingerprint"`
|
||||
Risk string `json:"risk"`
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
ReviewRequired bool `json:"review_required"`
|
||||
ReviewPolicy string `json:"review_policy"`
|
||||
ReviewerSnapshot bool `json:"reviewer_snapshot"`
|
||||
Reviewers []string `json:"reviewers"`
|
||||
Tags []string `json:"tags"`
|
||||
Systems []string `json:"systems"`
|
||||
Slug string `json:"slug"`
|
||||
Permalink string `json:"permalink"`
|
||||
SourcePath string `json:"source_path"`
|
||||
ContentSHA256 string `json:"content_sha256"`
|
||||
}
|
||||
|
||||
type ReviewQueueItem struct {
|
||||
EntryID string `json:"entry_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Date string `json:"date"`
|
||||
AuthorID string `json:"author_id"`
|
||||
Risk string `json:"risk"`
|
||||
Tags []string `json:"tags"`
|
||||
Systems []string `json:"systems"`
|
||||
Permalink string `json:"permalink"`
|
||||
ContentSHA256 string `json:"content_sha256"`
|
||||
ReviewedBy []string `json:"reviewed_by"`
|
||||
MissingReviewers []string `json:"missing_reviewers"`
|
||||
RequiredCount int `json:"required_count"`
|
||||
ReviewedCount int `json:"reviewed_count"`
|
||||
ConfigurationError string `json:"configuration_error,omitempty"`
|
||||
}
|
||||
|
||||
type TaskState struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Priority string `json:"priority"`
|
||||
ProjectID string `json:"project_id,omitempty"`
|
||||
Assignees []string `json:"assignees"`
|
||||
EstimateMinutes int `json:"estimate_minutes"`
|
||||
ActualMinutes int `json:"actual_minutes"`
|
||||
Due string `json:"due,omitempty"`
|
||||
RunningBy []string `json:"running_by"`
|
||||
Slug string `json:"slug"`
|
||||
Permalink string `json:"permalink"`
|
||||
SourcePath string `json:"source_path"`
|
||||
}
|
||||
|
||||
type ProjectState struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Owners []string `json:"owners"`
|
||||
TargetDate string `json:"target_date,omitempty"`
|
||||
Slug string `json:"slug"`
|
||||
Permalink string `json:"permalink"`
|
||||
SourcePath string `json:"source_path"`
|
||||
}
|
||||
|
||||
type SiteStats struct {
|
||||
ActiveAdmins int `json:"active_admins"`
|
||||
PendingReviews int `json:"pending_reviews"`
|
||||
OpenTasks int `json:"open_tasks"`
|
||||
ActiveProjects int `json:"active_projects"`
|
||||
}
|
||||
|
||||
type SiteState struct {
|
||||
Version int `json:"version"`
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
SourceSHA256 string `json:"source_sha256"`
|
||||
Stats SiteStats `json:"stats"`
|
||||
ReviewQueue []ReviewQueueItem `json:"review_queue"`
|
||||
RecentMemories []MemoryRecord `json:"recent_memories"`
|
||||
Tasks []TaskState `json:"tasks"`
|
||||
Projects []ProjectState `json:"projects"`
|
||||
}
|
||||
|
||||
type ResultEnvelope struct {
|
||||
OK bool `json:"ok"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
func (a *App) printResult(command string, data any, warnings ...string) error {
|
||||
if a.Options.JSON {
|
||||
enc := json.NewEncoder(a.Options.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(ResultEnvelope{OK: true, Command: command, Data: data, Warnings: warnings})
|
||||
}
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
fmt.Fprintln(a.Options.Stdout, v)
|
||||
default:
|
||||
bytes, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(a.Options.Stdout, string(bytes))
|
||||
}
|
||||
for _, warning := range warnings {
|
||||
fmt.Fprintf(a.Options.Stderr, "ADVERTENCIA: %s\n", warning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PrintError(w io.Writer, jsonMode bool, err error) int {
|
||||
var cliErr *CLIError
|
||||
if !errors.As(err, &cliErr) {
|
||||
cliErr = NewError(ExitInternal, "internal", err.Error(), nil)
|
||||
}
|
||||
if jsonMode {
|
||||
payload := ResultEnvelope{OK: false, Error: map[string]any{
|
||||
"kind": cliErr.Kind,
|
||||
"message": cliErr.Message,
|
||||
"details": cliErr.Details,
|
||||
"exit_code": cliErr.Code,
|
||||
}}
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(payload)
|
||||
} else {
|
||||
fmt.Fprintf(w, "ERROR [%s]: %s\n", cliErr.Kind, cliErr.Message)
|
||||
if cliErr.Details != nil {
|
||||
bytes, _ := json.MarshalIndent(cliErr.Details, "", " ")
|
||||
fmt.Fprintln(w, string(bytes))
|
||||
}
|
||||
}
|
||||
return cliErr.Code
|
||||
}
|
||||
|
||||
func (a *App) ResolveRoot() (string, error) {
|
||||
if a.Options.Root != "" {
|
||||
root, err := filepath.Abs(a.Options.Root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "zola.toml")); err != nil {
|
||||
return "", NewError(ExitUsage, "root_not_found", "el directorio raíz no contiene zola.toml", root)
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
current := cwd
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(current, "zola.toml")); err == nil {
|
||||
return current, nil
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
break
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
return "", NewError(ExitUsage, "root_not_found", "no se encontró zola.toml en el directorio actual ni en sus padres", cwd)
|
||||
}
|
||||
Reference in New Issue
Block a user