package main import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) type Project struct { Name string Path string Description string HasReadme bool } // ListProjects returns a sorted list of projects from the given directory. // Only directories are considered projects. Hidden directories (starting with .) // are excluded. func ListProjects(projectsPath string) ([]Project, error) { entries, err := os.ReadDir(projectsPath) if err != nil { return nil, err } var projects []Project for _, e := range entries { if !e.IsDir() { continue } name := e.Name() if strings.HasPrefix(name, ".") { continue } p := Project{ Name: name, Path: filepath.Join(projectsPath, name), } // Try to read description from README.md first line readmePath := filepath.Join(p.Path, "README.md") if data, err := os.ReadFile(readmePath); err == nil { p.HasReadme = true lines := strings.SplitN(string(data), "\n", 3) for _, line := range lines { line = strings.TrimSpace(line) line = strings.TrimLeft(line, "# ") if line != "" { p.Description = line break } } } projects = append(projects, p) } sort.Slice(projects, func(i, j int) bool { return projects[i].Name < projects[j].Name }) return projects, nil } var validProjectName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*$`) // CreateProject creates a new empty project directory. // Name must contain only alphanumeric characters, hyphens, and underscores. func CreateProject(projectsPath, name string) error { if name == "" { return fmt.Errorf("ime projekta ne može biti prazno") } if !validProjectName.MatchString(name) { return fmt.Errorf("ime projekta može sadržati samo slova, brojeve, '-' i '_'") } path := filepath.Join(projectsPath, name) if _, err := os.Stat(path); err == nil { return fmt.Errorf("projekat '%s' već postoji", name) } return os.MkdirAll(path, 0755) }