package cli import ( "context" "errors" "fmt" "strings" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // The tree can be built from the web, which mints ids like proj-3kq7..., or // from `keys init`, which makes slugs. A command that wanted the id made the // operator copy one off a page, and every --project defaulted to "default", // which a tree built from the web does not have. These take what a person // would type instead: an id, a path, or a name that is unique. // normPath lowercases a path and spaces its separators the way the store joins // them, so "acme/platform/gateway" matches "Acme / platform / gateway". func normPath(p string) string { parts := strings.Split(p, "/") for i := range parts { parts[i] = strings.ToLower(strings.TrimSpace(parts[i])) } return strings.Join(parts, " / ") } type candidate struct{ id, path string } // pick finds the one candidate an argument names. func pick(kind, arg string, all []candidate) (candidate, error) { if arg == "" { if len(all) == 1 { return all[0], nil } if len(all) == 0 { return candidate{}, fmt.Errorf("there is no %s yet; create one with `llmgw org add %s`", kind, kind) } return candidate{}, fmt.Errorf("which %s? give an id, a path or a name:\n%s", kind, listCandidates(all)) } for _, c := range all { if c.id == arg { return c, nil } } want := normPath(arg) var matches []candidate for _, c := range all { full := normPath(c.path) segs := strings.Split(full, " / ") if full == want || segs[len(segs)-1] == want { matches = append(matches, c) } } switch len(matches) { case 1: return matches[0], nil case 0: return candidate{}, fmt.Errorf("no %s %q; there is:\n%s", kind, arg, listCandidates(all)) default: return candidate{}, fmt.Errorf("%q names more than one %s; give its path:\n%s", arg, kind, listCandidates(matches)) } } func listCandidates(cs []candidate) string { var b strings.Builder for _, c := range cs { fmt.Fprintf(&b, " %-40s %s\n", c.path, c.id) } return strings.TrimRight(b.String(), "\n") } func resolveProject(ctx context.Context, s *store.DB, arg string) (store.ProjectPath, error) { all, err := s.AllProjects(ctx) if err != nil { return store.ProjectPath{}, err } cs := make([]candidate, len(all)) for i, p := range all { cs[i] = candidate{p.ID, p.Path} } c, err := pick("project", arg, cs) return store.ProjectPath{ID: c.id, Path: c.path}, err } func resolveTeam(ctx context.Context, s *store.DB, arg string) (store.TeamPath, error) { all, err := s.AllTeams(ctx) if err != nil { return store.TeamPath{}, err } cs := make([]candidate, len(all)) for i, t := range all { cs[i] = candidate{t.ID, t.OrgName + " / " + t.Name} } c, err := pick("team", arg, cs) if err != nil { return store.TeamPath{}, err } for _, t := range all { if t.ID == c.id { return t, nil } } return store.TeamPath{}, store.ErrNotFound } func resolveOrg(ctx context.Context, s *store.DB, arg string) (store.Org, error) { all, err := s.AllOrgs(ctx) if err != nil { return store.Org{}, err } cs := make([]candidate, len(all)) for i, o := range all { cs[i] = candidate{o.ID, o.Name} } c, err := pick("org", arg, cs) return store.Org{ID: c.id, Name: c.path}, err } // resolveUser takes an account's id or its email. func resolveUser(ctx context.Context, s *store.DB, arg string) (store.User, error) { if u, err := s.UserByID(ctx, arg); err == nil { return u, nil } u, err := s.UserByEmail(ctx, arg) if errors.Is(err, store.ErrNotFound) { return u, fmt.Errorf("no account with id or email %q; `llmgw users list` shows them", arg) } return u, err }