Files
j3ssie 77bad65cd9 feat: v5.1.0 — orgs, npm distribution, bundled skills, platform vendoring
Org (tenant) layer
- New Org model with org_uuid denormalized onto workspaces, assets,
  vulnerabilities and runs so cross-workspace queries need no join
- Automatic attribution via BeforeAppendModel hooks; importers stay org-unaware
- Read semantics: empty org means no filter (backward compatible)
  Write semantics: empty org coerced to the default org
- Migration backfills every pre-existing row into the default org
- CLI: osmedeus org create/show/assign/use/rename/delete
- API: /osm/api/orgs CRUD plus ?org= on assets, vulns, runs and workspaces

npm distribution
- npm install -g @j3ssie/osmedeus ships the Go binary through npm
- One npm name with version-suffixed platform builds pulled in as aliased
  optionalDependencies, so an install downloads exactly one binary
- Binary ships gzipped and is decompressed on first run into a
  version-scoped cache, so an upgrade can never exec a stale binary
- make bump-version is the single source of truth for the version constant

Bundled agent skills
- public/skills/ embedded in the binary, installed via osmedeus skills install
- Filesystem-driven discovery: a new bundle needs no code change
- make sync-skills mirrors bundles out to the standalone skills repo

Platform sub-projects
- Vendor dashboard, registry and workflow under platform/ so they version
  with the engine they talk to; make sync-platform publishes them out
- Rebuild the embedded UI in public/ui/
2026-08-08 22:26:09 +08:00

137 lines
3.4 KiB
Go

package cli
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/database"
)
// activeOrgFileName is the file under the base folder holding the org selected by
// `osmedeus org use`.
const activeOrgFileName = ".active-org"
var (
resolvedOrgUUID string
resolveOrgOnce sync.Once
resolveOrgErr error
)
// activeOrgFilePath returns the path of the persisted active-org file.
func activeOrgFilePath() string {
base := ""
if cfg := config.Get(); cfg != nil {
base = cfg.BaseFolder
}
if base == "" {
base = baseFolder
}
if base == "" {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
base = filepath.Join(home, "osmedeus-base")
}
return filepath.Join(base, activeOrgFileName)
}
// readActiveOrg returns the persisted active org reference, or "" if none is set.
func readActiveOrg() string {
path := activeOrgFilePath()
if path == "" {
return ""
}
data, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
// writeActiveOrg persists the active org reference.
func writeActiveOrg(orgUUID string) error {
path := activeOrgFilePath()
if path == "" {
return fmt.Errorf("cannot determine base folder for active org file")
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, []byte(orgUUID+"\n"), 0o644)
}
// clearActiveOrg removes the persisted active org, returning to the unfiltered
// "all orgs" view.
func clearActiveOrg() error {
path := activeOrgFilePath()
if path == "" {
return nil
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// orgRef returns the raw org reference (name or UUID) from the highest-priority
// source that has one, without touching the database. Resolution order:
//
// 1. --org flag
// 2. $OSMEDEUS_ORG_UUID
// 3. $OSMEDEUS_ORG
// 4. the active-org file written by `osmedeus org use`
// 5. "" — meaning no org was selected
func orgRef() string {
if v := strings.TrimSpace(globalOrg); v != "" {
return v
}
if v := strings.TrimSpace(os.Getenv("OSMEDEUS_ORG_UUID")); v != "" {
return v
}
if v := strings.TrimSpace(os.Getenv("OSMEDEUS_ORG")); v != "" {
return v
}
return readActiveOrg()
}
// resolveOrgUUID resolves the selected org to a UUID, once per process.
//
// The memo matters: a multi-target run resolves the org once per target, and all
// targets share one constant reference.
//
// An empty return means no org was selected. Read paths must treat that as "no
// filter" so a database with no orgs behaves exactly as it did before the org
// layer existed; write paths must run it through database.NormalizeOrgUUID so
// rows land in the default org rather than being stranded under an empty string.
//
// Requires a connected database; callers that may run without one should check
// database.GetDB() first.
func resolveOrgUUID(ctx context.Context) (string, error) {
resolveOrgOnce.Do(func() {
ref := orgRef()
if ref == "" {
return
}
orgUUID, err := database.ResolveOrgUUID(ctx, ref)
if err != nil {
resolveOrgErr = fmt.Errorf("failed to resolve --org %q: %w", ref, err)
return
}
resolvedOrgUUID = orgUUID
})
return resolvedOrgUUID, resolveOrgErr
}
// resetOrgResolution clears the cached resolution. Tests only.
func resetOrgResolution() {
resolveOrgOnce = sync.Once{}
resolvedOrgUUID = ""
resolveOrgErr = nil
}