claude-web-chat/files.go
djuka 3283888738
All checks were successful
Tests / unit-tests (push) Successful in 51s
Inicijalna implementacija Claude Web Chat (Faza 1 - CLI mod)
- Login sa session cookie autentifikacijom
- Lista projekata iz filesystem-a
- Chat sa Claude CLI preko WebSocket-a
- Streaming NDJSON parsiranje iz CLI stdout-a
- Sesija zivi nezavisno od browsera (reconnect replay)
- Sidebar sa .md fajlovima i markdown renderovanjem
- Dark tema, htmx + Go templates
- 47 unit testova

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:03:40 +00:00

112 lines
2.4 KiB
Go

package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/yuin/goldmark"
)
type FileInfo struct {
Name string
Path string
RelPath string
IsDir bool
Size int64
}
// ListMarkdownFiles returns all .md files in the project directory (non-recursive).
func ListMarkdownFiles(projectDir string) ([]FileInfo, error) {
entries, err := os.ReadDir(projectDir)
if err != nil {
return nil, err
}
var files []FileInfo
for _, e := range entries {
if e.IsDir() {
continue
}
if !strings.HasSuffix(strings.ToLower(e.Name()), ".md") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
files = append(files, FileInfo{
Name: e.Name(),
Path: filepath.Join(projectDir, e.Name()),
RelPath: e.Name(),
Size: info.Size(),
})
}
// Also check docs/ subdirectory
docsDir := filepath.Join(projectDir, "docs")
if docEntries, err := os.ReadDir(docsDir); err == nil {
for _, e := range docEntries {
if e.IsDir() {
continue
}
if !strings.HasSuffix(strings.ToLower(e.Name()), ".md") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
files = append(files, FileInfo{
Name: "docs/" + e.Name(),
Path: filepath.Join(docsDir, e.Name()),
RelPath: "docs/" + e.Name(),
Size: info.Size(),
})
}
}
sort.Slice(files, func(i, j int) bool {
return files[i].Name < files[j].Name
})
return files, nil
}
// ReadFileContent reads a file and returns its content.
// It validates that the file is within the project directory (path traversal protection).
func ReadFileContent(projectDir, relPath string) (string, error) {
absPath := filepath.Join(projectDir, relPath)
absPath = filepath.Clean(absPath)
// Path traversal protection
if !strings.HasPrefix(absPath, filepath.Clean(projectDir)) {
return "", fmt.Errorf("path traversal detected")
}
data, err := os.ReadFile(absPath)
if err != nil {
return "", err
}
return string(data), nil
}
// RenderMarkdownFile reads a markdown file and returns rendered HTML.
func RenderMarkdownFile(projectDir, relPath string) (string, error) {
content, err := ReadFileContent(projectDir, relPath)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := goldmark.Convert([]byte(content), &buf); err != nil {
return "", fmt.Errorf("render markdown: %w", err)
}
return buf.String(), nil
}