fix: smart uninstaller cleanup + prevent nested .ragcode dirs#48
fix: smart uninstaller cleanup + prevent nested .ragcode dirs#48
Conversation
- Uninstaller derives scan roots from registry, Qdrant file_path payloads, JetBrains/VSCode/AI-IDE configs — no more hardcoded directory guessing - findWorkspaceRootFromFilePath: walk up from Qdrant payload to workspace root - detectIDEProjectParents: reads recentProjects.xml, storage.json, resolveIDEPaths - detector: removed .ragcode from workspace markers (prevents hijacking root detection) - resolver: applyNestedOverride centralized — prevents split-indexing on sub-dirs
There was a problem hiding this comment.
Pull request overview
Improves uninstall completeness by replacing hardcoded directory guesses with data-driven discovery (registry, Qdrant payloads, IDE configs, shallow home scan) and reduces accidental creation of nested .ragcode/ directories by adjusting workspace detection/resolution.
Changes:
- Expand uninstall cleanup strategy to scan authoritative sources (registry + Qdrant + IDE configs + home shallow scan).
- Remove
.ragcodeas a workspace marker and centralize nested-workspace override logic in the resolver. - Update uninstall tests and bump CLI version.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
internal/uninstall/uninstall.go |
New discovery-based scanning for orphaned .ragcode/ dirs; IDE/Qdrant-based root discovery. |
internal/uninstall/uninstall_test.go |
Updates expectations to reflect fallback scan cleanup. |
pkg/workspace/detector/detector.go |
Removes .ragcode from marker list to avoid workspace root “hijacking”. |
pkg/workspace/resolver/resolver.go |
Centralizes nested workspace override via applyNestedOverride. |
cmd/rag-code-mcp/main.go |
Version bump to 2.1.87. |
Comments suppressed due to low confidence (1)
pkg/workspace/detector/detector.go:62
- Removing ".ragcode" from the marker list means detectFromMetadata() can now fail for workspaces that only have the managed .ragcode/root metadata (and no other markers like .git/go.mod). That makes previously indexable “markerless” folders undiscoverable and will return "metadata referenced root ... but no markers found". Consider treating the metadata file itself as sufficient, or adding a dedicated marker check for .ragcode/root without reintroducing .ragcode directory hijacking.
"artisan",
".agent",
".idea",
".vscode",
".vs",
".cursor",
".windsurf",
"AGENTS.md",
"CLAUDE.md",
},
MaxDepth: 10,
MetadataFileName: filepath.Join(".ragcode", "root"),
}
| func extractWorkspaceRootsFromQdrant() []string { | ||
| const qdrantAddr = "http://localhost:6333" | ||
|
|
||
| conn, err := net.DialTimeout("tcp", "127.0.0.1:6333", 2*time.Second) | ||
| if err != nil { | ||
| return nil // Qdrant not running — skip silently | ||
| } | ||
| conn.Close() | ||
|
|
||
| // List all collections. | ||
| cmd := exec.Command("curl", "-s", qdrantAddr+"/collections") | ||
| output, err := cmd.Output() | ||
| if err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| var listResp struct { | ||
| Result struct { | ||
| Collections []struct { | ||
| Name string `json:"name"` | ||
| } `json:"collections"` | ||
| } `json:"result"` | ||
| } | ||
| if err := json.Unmarshal(output, &listResp); err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| seen := make(map[string]struct{}) | ||
| var roots []string | ||
|
|
||
| for _, col := range listResp.Result.Collections { | ||
| if !strings.HasPrefix(col.Name, "ragcode-") { | ||
| continue | ||
| } | ||
|
|
||
| // Scroll 1 point with payload to get a file_path sample. | ||
| scrollPayload := `{"limit":1,"with_payload":true,"with_vector":false}` | ||
| scrollCmd := exec.Command("curl", "-s", "-X", "POST", | ||
| qdrantAddr+"/collections/"+col.Name+"/points/scroll", | ||
| "-H", "Content-Type: application/json", | ||
| "-d", scrollPayload) | ||
| scrollOut, err := scrollCmd.Output() | ||
| if err != nil { |
There was a problem hiding this comment.
extractWorkspaceRootsFromQdrant(): the curl invocations have no timeout/deadline. If Qdrant is reachable but slow/hung, uninstall can block indefinitely on cmd.Output(). Consider using net/http with a client timeout, or add curl timeouts (e.g. --max-time / --connect-timeout) and/or context cancellation.
| func (r *Resolver) applyNestedOverride(ctx context.Context, candidate *contract.WorkspaceCandidate) { | ||
| if r.deps.Registry != nil && candidate.Source != "registry_fallback" { | ||
| if parentRoot, found := r.deps.Registry.FindParentWorkspace(candidate.Root); found { | ||
| r.log(ctx, "nested_workspace_override", map[string]any{ | ||
| "original_root": candidate.Root, | ||
| "parent_root": parentRoot, | ||
| "reason": "candidate root is subdirectory of registered workspace", | ||
| }) | ||
| candidate.Root = parentRoot | ||
| candidate.Source = "nested_workspace_override" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
applyNestedOverride() mutates candidate.Root and sets Source="nested_workspace_override", but callers like handleWorkspaceRoot/handleRoots keep the original Reason/Confidence (e.g. 1.0 for explicit root). This can produce responses that claim ReasonExplicitWorkspaceRoot with high confidence while actually returning the parent workspace. Consider either not applying the override for explicit workspace_root/roots candidates, or when an override occurs also adjust Reason/Confidence and mark it as a fallback source (e.g. via isFallbackSource).
| continue | ||
| } | ||
| // config file's parent dir (the IDE data dir, e.g. ~/.cursor) | ||
| addProject(filepath.Dir(ide.path)) |
There was a problem hiding this comment.
detectIDEProjectParents(): addProject() already takes filepath.Dir(projectPath), so calling addProject(filepath.Dir(ide.path)) adds the grandparent of the IDE config file (and not the IDE data dir described in the comment). If you intended to add the config’s parent dir, call a helper that adds the dir verbatim (or change addProject to accept a parent dir), and if you want “both levels” add both explicitly.
| addProject(filepath.Dir(ide.path)) | |
| dataDir := filepath.Dir(ide.path) | |
| addProject(filepath.Join(dataDir, ".ragcode-ide-sentinel")) |
| } | ||
| rel, _ := filepath.Rel(root, path) | ||
| if strings.Count(rel, string(os.PathSeparator)) > 4 { | ||
| if strings.Count(rel, string(os.PathSeparator)) > maxDepth { |
There was a problem hiding this comment.
scanAndCleanRagcodeDirs(): the depth limit is off-by-one vs the comment. With maxDepth=1 for $HOME, strings.Count(rel, sep) > maxDepth still allows paths two levels deep (e.g. "a/b" has count=1). If the intent is truly “depth 1 only”, adjust the comparison (or compute depth as separators+1) so $HOME scans don’t go deeper than intended.
| if strings.Count(rel, string(os.PathSeparator)) > maxDepth { | |
| depth := 0 | |
| if rel != "." { | |
| depth = strings.Count(rel, string(os.PathSeparator)) + 1 | |
| } | |
| if depth > maxDepth { |
Description
Eliminates incomplete uninstallation caused by hardcoded directory guesses (
~/Projects,~/code, etc.). Replaces with a data-driven strategy that queries every authoritative source available on the system. Also fixes the root cause of nested.ragcode/directories being created when IDEs open sub-directories.Changes:
internal/uninstall/uninstall.go— cleanup from 4 sources: registry, Qdrantfile_pathpayloads, JetBrains/VSCode/AI-IDE configs (resolveIDEPaths), shallow$HOMEscaninternal/uninstall/uninstall_test.go— updated tests for orphan sibling detectionpkg/workspace/detector/detector.go— removed.ragcodefrom workspace markers (prevented hijacking root detection)pkg/workspace/resolver/resolver.go— centralizedapplyNestedOverrideto prevent split-indexing on sub-directory opensType of change
Checklist:
go fmt ./...go test ./...and they pass