diff --git a/TODO.md b/TODO.md index c813819..19ac62b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,5 @@ # TODO - Future Improvements -## Quick Wins - -- [ ] Check if revision exists before indexing (skip or require `--force`) - ## Usability - [ ] Progress reporting during indexing ("Fetching nixpkgs... Parsing options... Indexing files...") diff --git a/cmd/nixos-options/main.go b/cmd/nixos-options/main.go index 645b522..9aec61f 100644 --- a/cmd/nixos-options/main.go +++ b/cmd/nixos-options/main.go @@ -50,12 +50,17 @@ func main() { Name: "no-files", Usage: "Skip indexing file contents (faster, disables get_file tool)", }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "Force re-indexing even if revision already exists", + }, }, Action: func(c *cli.Context) error { if c.NArg() < 1 { return fmt.Errorf("revision argument required") } - return runIndex(c, c.Args().First(), !c.Bool("no-files")) + return runIndex(c, c.Args().First(), !c.Bool("no-files"), c.Bool("force")) }, }, { @@ -162,7 +167,7 @@ func runServe(c *cli.Context) error { return server.Run(ctx, os.Stdin, os.Stdout) } -func runIndex(c *cli.Context, revision string, indexFiles bool) error { +func runIndex(c *cli.Context, revision string, indexFiles bool, force bool) error { ctx := context.Background() store, err := openStore(c.String("database")) @@ -178,11 +183,22 @@ func runIndex(c *cli.Context, revision string, indexFiles bool) error { indexer := nixos.NewIndexer(store) fmt.Printf("Indexing revision: %s\n", revision) - result, err := indexer.IndexRevision(ctx, revision) + + var result *nixos.IndexResult + if force { + result, err = indexer.ReindexRevision(ctx, revision) + } else { + result, err = indexer.IndexRevision(ctx, revision) + } if err != nil { return fmt.Errorf("indexing failed: %w", err) } + if result.AlreadyIndexed { + fmt.Printf("Revision already indexed (%d options). Use --force to re-index.\n", result.OptionCount) + return nil + } + fmt.Printf("Indexed %d options in %s\n", result.OptionCount, result.Duration) fmt.Printf("Git hash: %s\n", result.Revision.GitHash) if result.Revision.ChannelName != "" { diff --git a/internal/mcp/handlers.go b/internal/mcp/handlers.go index 0761cb1..9aaf413 100644 --- a/internal/mcp/handlers.go +++ b/internal/mcp/handlers.go @@ -217,6 +217,20 @@ func (s *Server) makeIndexHandler(indexer *nixos.Indexer) ToolHandler { return ErrorContent(fmt.Errorf("indexing failed: %w", err)), nil } + // If already indexed, return early with info + if result.AlreadyIndexed { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Revision already indexed: %s\n", result.Revision.GitHash)) + if result.Revision.ChannelName != "" { + sb.WriteString(fmt.Sprintf("Channel: %s\n", result.Revision.ChannelName)) + } + sb.WriteString(fmt.Sprintf("Options: %d\n", result.OptionCount)) + sb.WriteString(fmt.Sprintf("Indexed at: %s\n", result.Revision.IndexedAt.Format("2006-01-02 15:04"))) + return CallToolResult{ + Content: []Content{TextContent(sb.String())}, + }, nil + } + // Index files by default fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, result.Revision.GitHash) if err != nil { diff --git a/internal/nixos/indexer.go b/internal/nixos/indexer.go index 6da231c..460443e 100644 --- a/internal/nixos/indexer.go +++ b/internal/nixos/indexer.go @@ -35,10 +35,11 @@ func NewIndexer(store database.Store) *Indexer { // IndexResult contains the results of an indexing operation. type IndexResult struct { - Revision *database.Revision - OptionCount int - FileCount int - Duration time.Duration + Revision *database.Revision + OptionCount int + FileCount int + Duration time.Duration + AlreadyIndexed bool // True if revision was already indexed (skipped) } // IndexRevision indexes a nixpkgs revision by git hash or channel name. @@ -55,9 +56,10 @@ func (idx *Indexer) IndexRevision(ctx context.Context, revision string) (*IndexR } if existing != nil { return &IndexResult{ - Revision: existing, - OptionCount: existing.OptionCount, - Duration: time.Since(start), + Revision: existing, + OptionCount: existing.OptionCount, + Duration: time.Since(start), + AlreadyIndexed: true, }, nil } @@ -112,6 +114,25 @@ func (idx *Indexer) IndexRevision(ctx context.Context, revision string) (*IndexR }, nil } +// ReindexRevision forces re-indexing of a revision, deleting existing data first. +func (idx *Indexer) ReindexRevision(ctx context.Context, revision string) (*IndexResult, error) { + ref := resolveRevision(revision) + + // Delete existing revision if present + existing, err := idx.store.GetRevision(ctx, ref) + if err != nil { + return nil, fmt.Errorf("failed to check existing revision: %w", err) + } + if existing != nil { + if err := idx.store.DeleteRevision(ctx, existing.ID); err != nil { + return nil, fmt.Errorf("failed to delete existing revision: %w", err) + } + } + + // Now index fresh + return idx.IndexRevision(ctx, revision) +} + // buildOptions builds options.json for a nixpkgs revision. func (idx *Indexer) buildOptions(ctx context.Context, ref string) (string, func(), error) { // Create temp directory