Files
codexis/main.go
2026-04-15 08:33:38 -04:00

154 lines
3.4 KiB
Go

package main
import (
"context"
"database/sql"
_ "embed"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime/pprof"
"strings"
"time"
_ "modernc.org/sqlite"
"codexis/db"
"codexis/indexer"
)
//go:embed db/schema.sql
var schemaSQL string
const dbDir = ".codexis"
const dbFileName = "index.db"
func main() {
force := flag.Bool("force", false, "Force full re-index (ignore file hashes)")
output := flag.String("o", "", "Output database path (default: <root>/.codexis/index.db)")
cpuprofile := flag.String("cpuprofile", "", "Write CPU profile to file")
flag.Parse()
// CPU profiling
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating profile: %v\n", err)
os.Exit(1)
}
pprof.StartCPUProfile(f)
// Flush profile on interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
go func() {
<-sigCh
fmt.Fprintf(os.Stderr, "\nInterrupted, flushing CPU profile...\n")
pprof.StopCPUProfile()
f.Close()
os.Exit(1)
}()
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
}
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
absRoot, err := filepath.Abs(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
dbDirPath := filepath.Join(absRoot, dbDir)
if err := os.MkdirAll(dbDirPath, 0755); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
dbPath := filepath.Join(dbDirPath, dbFileName)
if *output != "" {
dbPath = *output
}
if err := run(absRoot, dbPath, *force); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run(root, dbPath string, force bool) error {
ctx := context.Background()
sqlDB, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)")
if err != nil {
return fmt.Errorf("opening database: %w", err)
}
defer sqlDB.Close()
// Create schema
if err := createSchema(ctx, sqlDB); err != nil {
return fmt.Errorf("creating schema: %w", err)
}
queries := db.New(sqlDB)
idx := indexer.New(sqlDB, queries, root, force)
isTTY := fileIsTTY(os.Stderr)
idx.OnProgress = func(current, total int, path string) {
if !isTTY {
return
}
pct := current * 100 / total
barWidth := 30
filled := barWidth * current / total
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
display := path
if len(display) > 40 {
display = "..." + display[len(display)-37:]
}
fmt.Fprintf(os.Stderr, "\r %s %3d%% (%d/%d) %s\033[K", bar, pct, current, total, display)
if current == total {
fmt.Fprintf(os.Stderr, "\r\033[K")
}
}
start := time.Now()
fmt.Fprintf(os.Stderr, "Indexing %s...\n", root)
stats, err := idx.Index(ctx)
if err != nil {
return fmt.Errorf("indexing: %w", err)
}
elapsed := time.Since(start)
fmt.Fprintf(os.Stderr, "Done in %s\n", elapsed.Round(time.Millisecond))
fmt.Fprintf(os.Stderr, " Files: %d total, %d indexed, %d unchanged\n",
stats.FilesTotal, stats.FilesIndexed, stats.FilesSkipped)
fmt.Fprintf(os.Stderr, " Symbols: %d\n", stats.SymbolsTotal)
fmt.Fprintf(os.Stderr, " Output: %s\n", dbPath)
return nil
}
func fileIsTTY(f *os.File) bool {
fi, err := f.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
}
func createSchema(ctx context.Context, sqlDB *sql.DB) error {
_, err := sqlDB.ExecContext(ctx, schemaSQL)
return err
}