sap-devs resources — Design Specification
Goal
Add sap-devs resources with three subcommands — list, search, and open — so developers can discover and access curated SAP documentation, samples, and community links directly from their terminal.
Commands
sap-devs resources list Browse curated resources (profile-filtered)
sap-devs resources search <query> Full-text search across all resources
sap-devs resources open <id> Open a resource URL in the default browserContent Schema
Each pack may contain a resources.yaml file. The Resource struct already exists in internal/content/pack.go:
type Resource struct {
ID string `yaml:"id"`
Title string `yaml:"title"`
URL string `yaml:"url"`
Type string `yaml:"type"`
Tags []string `yaml:"tags"`
Advocate string `yaml:"advocate,omitempty"`
PackID string // set at load time, not in YAML
}resources.yaml is already parsed by LoadPack into pack.Resources. The PackID field is not in YAML — it must be set by LoadPack after unmarshalling, by assigning meta.ID to each resource's PackID.
Change to LoadPack: After yaml.Unmarshal(data, &pack.Resources), add:
for i := range pack.Resources {
pack.Resources[i].PackID = pack.ID
}Architecture
What already exists (do not re-implement)
Resourcestruct — ininternal/content/pack.goresources.yamlloading — inLoadPack(populatespack.Resources)- Layer deduplication —
LoadPacksalready handles this (later layers override by pack ID) LoadPacks(nil)— loads all packs unfiltered across all layers (nil profile = no weight filtering)
New code: internal/content/resources.go
Three pure helper functions operating on an already-loaded []*Pack slice:
// FlattenResources collects all resources from all packs into a single slice.
func FlattenResources(packs []*Pack) []Resource
// FilterResources returns resources whose title, type, or any tag contains query
// (case-insensitive substring match).
func FilterResources(resources []Resource, query string) []Resource
// FindResource returns the first resource with an exact ID match, or nil.
func FindResource(resources []Resource, id string) *ResourceNo filesystem access. No new loading. These operate on data already in memory.
New code: cmd/resources.go
Thin presentation layer only. All data logic stays in internal/content.
Subcommand Behaviour
resources list
- Load active profile:
config.LoadProfile→loader.FindProfile - No profile set (empty ID) → print
"No profile set. Run 'sap-devs profile set <name>' first."and exit 1. Profile ID configured but not found (FindProfilereturnsnil, nil) → print"Profile '<id>' not found. Run 'sap-devs sync' to refresh content."and exit 1. (Both cases intentionally diverge fromtip.gowhich runs without a profile.listis profile-filtered by definition — showing all resources with a missing/unknown profile would be misleading.) - Call
loader.LoadPacks(activeProfile)— returns profile-weighted packs - Call
content.FlattenResources(packs) - Print aligned table (no
PACKcolumn):
ID TYPE TITLE
cap/docs-official official-docs CAP Documentation
cap/samples-github sample CAP Samples on GitHub
btp-core/discovery-center official-docs SAP Discovery CenterIf no resources found: "No resources found for your current profile.".
resources search <query>
- Call
loader.LoadPacks(nil)— all packs, no profile filtering - Call
content.FlattenResources(packs)thencontent.FilterResources(resources, query) - Print aligned table with
PACKcolumn added:
ID PACK TYPE TITLE
cap/docs-official cap official-docs CAP Documentation
abap/adt-guide abap official-docs ABAP Development Tools GuideIf no matches: "No resources found matching '<query>'.".
Query argument is required; Cobra prints usage automatically if missing.
Note: list and search use separate table formatters — list has 3 columns, search has 4. They do not share a renderer.
resources open <id>
- Call
loader.LoadPacks(nil)— all packs, not profile-filtered (user should not be blocked by profile when they know an ID) - Call
content.FindResource(content.FlattenResources(packs), id)— exact ID match - If not found →
"Resource '<id>' not found. Use 'sap-devs resources list' or 'sap-devs resources search' to browse."and exit 1 - Open URL using
github.com/pkg/browser - Print
"Opening: <title> — <url>"
Dependencies
New: github.com/pkg/browser — opens URLs in the default system browser (xdg-open on Linux, open on macOS, rundll32 url.dll,FileProtocolHandler on Windows). No CGO.
Error Handling
- Missing
resources.yamlin a pack: already silently skipped byLoadPack(existing behaviour) - Malformed YAML: already silently skipped by
LoadPack(existing behaviour —_ = yaml.Unmarshal(...)) - Browser launch failure: print
"Could not open browser: <err>. URL: <url>"and exit 0 (non-fatal)
Testing
Tests in internal/content/resources_test.go:
TestFlattenResources— two packs, verifies all resources collected andPackIDsetTestFilterResources_TitleMatch— substring in title returns resourceTestFilterResources_TagMatch— substring in a tag returns resourceTestFilterResources_CaseInsensitive— uppercase query matches lowercase titleTestFilterResources_NoMatch— returns empty slice, no errorTestFindResource_Found— exact ID returns correct resourceTestFindResource_NotFound— unknown ID returns nil
The open command's browser launch is not unit-tested (side effect). Lookup logic is covered by TestFindResource_*.
Files
- Modify:
internal/content/pack.go— addPackID stringfield toResource; set it inLoadPackafter unmarshallingresources.yaml - Create:
internal/content/resources.go—FlattenResources,FilterResources,FindResource - Create:
internal/content/resources_test.go - Create:
cmd/resources.go - Modify:
go.mod/go.sum— addgithub.com/pkg/browser