// Path: Backend/internal/bootstrap/project_disk_helpers.go package bootstrap import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "unicode" ) func samePath(left, right string) bool { cleanLeft := filepath.Clean(left) cleanRight := filepath.Clean(right) if cleanLeft == cleanRight { return true } leftInfo, leftErr := os.Stat(cleanLeft) rightInfo, rightErr := os.Stat(cleanRight) if leftErr == nil && rightErr == nil { return os.SameFile(leftInfo, rightInfo) } return false } func normalizePOSIXSlug(value string) string { trimmed := strings.TrimSpace(strings.ToLower(value)) if trimmed == "" { return "untitled" } var builder strings.Builder lastDash := false for _, r := range trimmed { switch { case unicode.IsLetter(r) || unicode.IsDigit(r): builder.WriteRune(r) lastDash = false case r == '-' || r == '_' || unicode.IsSpace(r): if !lastDash && builder.Len() > 0 { builder.WriteByte('-') lastDash = true } } } slug := strings.Trim(builder.String(), "-") if slug == "" { return "untitled" } return slug } func fallbackFolderLabel(path string) string { base := filepath.Base(filepath.FromSlash(path)) trimmed := strings.TrimPrefix(base, "folder-") parts := strings.FieldsFunc(trimmed, func(r rune) bool { return r == '-' || r == '_' }) for index, part := range parts { if part != "" { parts[index] = strings.ToUpper(part[:1]) + part[1:] } } label := strings.Join(parts, " ") if label == "" { return base } return label } func fallbackItemLabel(path string) string { base := filepath.Base(filepath.FromSlash(path)) trimmed := strings.TrimPrefix(base, "item-") parts := strings.FieldsFunc(trimmed, func(r rune) bool { return r == '-' || r == '_' }) for index, part := range parts { if part != "" { parts[index] = strings.ToUpper(part[:1]) + part[1:] } } label := strings.Join(parts, " ") if label == "" { return base } return label } func normalizeProjectTreeItemType(itemType string) string { switch strings.TrimSpace(strings.ToLower(itemType)) { case "", "board", "core.board", "core.board.kanban", "kanban": return "core.board.kanban" case "core.doc", "doc", "document": return "core.doc" case "core.board.list", "list", "list-board": return "core.board.list" default: return strings.TrimSpace(itemType) } } func defaultProjectTreeItemSchema(itemType string) map[string]any { return map[string]any{"type": "object", "itemType": normalizeProjectTreeItemType(itemType)} } func defaultProjectTreeItemData(itemType, name string) map[string]any { return map[string]any{"title": strings.TrimSpace(name), "itemType": normalizeProjectTreeItemType(itemType)} } func slugDir(prefix, slug string) string { trimmedSlug := strings.TrimSpace(slug) if trimmedSlug == "" { return prefix } return fmt.Sprintf("%s-%s", prefix, trimmedSlug) } func writeJSONFile(path string, payload any) error { parentDir := filepath.Dir(path) if err := os.MkdirAll(parentDir, 0o755); err != nil { return err } data, err := json.MarshalIndent(payload, "", " ") if err != nil { return err } data = append(data, '\n') return os.WriteFile(path, data, 0o644) } func readJSONFileMap(path string) map[string]any { data, err := os.ReadFile(path) if err != nil { return map[string]any{} } var payload map[string]any if err := json.Unmarshal(data, &payload); err != nil || payload == nil { return map[string]any{} } return payload }