package infra import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // VaultManifestEntry is a single vault entry parsed from a projects//vaults/vault.yaml. type VaultManifestEntry struct { ProjectID string // basename of projects//, inferred from manifest path Name string // vault name as declared in vault.yaml Description string // human description Path string // absolute path to the vault directory Tags []string // tags declared in vault.yaml ManifestFile string // absolute path to the vault.yaml this entry came from } // vaultYAML mirrors the vault.yaml schema (only the fields we care about). type vaultYAML struct { Vaults []struct { Name string `yaml:"name"` Description string `yaml:"description"` Path string `yaml:"path"` Tags []string `yaml:"tags"` } `yaml:"vaults"` } // VaultManifestRead globs all projects/*/vaults/vault.yaml under repoRoot, parses each // manifest and returns a flat slice of VaultManifestEntry. // // Rules: // - If a manifest fails to parse, an error is returned immediately with the file path. // - If no manifests are found, an empty slice is returned (not an error). // - ProjectID is inferred from the directory component between "projects/" and "/vaults/". func VaultManifestRead(repoRoot string) ([]VaultManifestEntry, error) { pattern := filepath.Join(repoRoot, "projects", "*", "vaults", "vault.yaml") matches, err := filepath.Glob(pattern) if err != nil { return nil, fmt.Errorf("vault_manifest_read: glob %q: %w", pattern, err) } var out []VaultManifestEntry for _, manifestPath := range matches { entries, err := parseVaultManifest(manifestPath) if err != nil { return nil, err } out = append(out, entries...) } return out, nil } func parseVaultManifest(manifestPath string) ([]VaultManifestEntry, error) { data, err := os.ReadFile(manifestPath) if err != nil { return nil, fmt.Errorf("vault_manifest_read: read %q: %w", manifestPath, err) } var raw vaultYAML if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("vault_manifest_read: parse %q: %w", manifestPath, err) } projectID := inferProjectID(manifestPath) entries := make([]VaultManifestEntry, 0, len(raw.Vaults)) for _, v := range raw.Vaults { entries = append(entries, VaultManifestEntry{ ProjectID: projectID, Name: v.Name, Description: v.Description, Path: v.Path, Tags: v.Tags, ManifestFile: manifestPath, }) } return entries, nil } // inferProjectID extracts the project basename from a path of the form // .../projects//vaults/vault.yaml. func inferProjectID(manifestPath string) string { // Normalize separators and split. parts := strings.Split(filepath.ToSlash(manifestPath), "/") // Walk backwards: vault.yaml -> vaults -> -> projects -> ... for i, p := range parts { if p == "projects" && i+1 < len(parts) { return parts[i+1] } } return "" }