Skip to content

Commit 89ec51a

Browse files
committed
fix: prevent plugin from hanging when opened in home directory
Add requireProjectMarker config option (default: true) that checks for project markers (.git, package.json, Cargo.toml, etc.) before enabling file watching and auto-indexing. This prevents the plugin from trying to watch/index large non-project directories like the home directory. - Add hasProjectMarker() utility to detect project directories - Guard file watcher and auto-indexing with project marker check - Log warning when skipping due to missing project marker - Add tests for hasProjectMarker function - Update README, AGENTS.md, and TROUBLESHOOTING.md with new option
1 parent 919f879 commit 89ec51a

File tree

7 files changed

+144
-4
lines changed

7 files changed

+144
-4
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Key options:
219219
- `embeddingProvider`: `auto` | `github-copilot` | `openai` | `google` | `ollama`
220220
- `indexing.watchFiles`: Auto-reindex on file changes
221221
- `indexing.semanticOnly`: Skip generic blocks, only index functions/classes
222+
- `indexing.requireProjectMarker`: Require `.git`/`package.json` etc. to enable watching (prevents hanging in home dir)
222223
- `search.hybridWeight`: 0.0 (semantic) to 1.0 (keyword)
223224
- `debug.enabled`: Enable debug logging and metrics collection
224225
- `debug.metrics`: Enable performance metrics (use with `index_metrics` tool)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ Zero-config by default (uses `auto` mode). Customize in `.opencode/codebase-inde
246246
"semanticOnly": false,
247247
"autoGc": true,
248248
"gcIntervalDays": 7,
249-
"gcOrphanThreshold": 100
249+
"gcOrphanThreshold": 100,
250+
"requireProjectMarker": true
250251
},
251252
"search": {
252253
"maxResults": 20,
@@ -279,6 +280,7 @@ Zero-config by default (uses `auto` mode). Customize in `.opencode/codebase-inde
279280
| `autoGc` | `true` | Automatically run garbage collection to remove orphaned embeddings/chunks |
280281
| `gcIntervalDays` | `7` | Run GC on initialization if last GC was more than N days ago |
281282
| `gcOrphanThreshold` | `100` | Run GC after indexing if orphan count exceeds this threshold |
283+
| `requireProjectMarker` | `true` | Require a project marker (`.git`, `package.json`, etc.) to enable file watching and auto-indexing. Prevents accidentally indexing large directories like home. Set to `false` to index any directory. |
282284
| **search** | | |
283285
| `maxResults` | `20` | Maximum results to return |
284286
| `minScore` | `0.1` | Minimum similarity score (0-1). Lower = more results |

TROUBLESHOOTING.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Common issues and solutions for opencode-codebase-index.
44

55
## Table of Contents
66

7+
- [OpenCode Hangs in Home Directory](#opencode-hangs-in-home-directory)
78
- [No Embedding Provider Available](#no-embedding-provider-available)
89
- [Rate Limiting Errors](#rate-limiting-errors)
910
- [Index Corruption / Stale Results](#index-corruption--stale-results)
@@ -14,6 +15,54 @@ Common issues and solutions for opencode-codebase-index.
1415

1516
---
1617

18+
## OpenCode Hangs in Home Directory
19+
20+
**Symptoms:**
21+
- OpenCode becomes unresponsive when opened in home directory (`~`)
22+
- New session starts but nothing happens when typing
23+
- High CPU or memory usage
24+
25+
**Cause:** The plugin's file watcher attempts to watch the entire home directory, which contains hundreds of thousands of files.
26+
27+
**Solutions:**
28+
29+
### Default Behavior (v0.4.1+)
30+
The plugin now requires a project marker (`.git`, `package.json`, `Cargo.toml`, etc.) by default. If no marker is found, file watching and auto-indexing are disabled. You'll see this warning:
31+
```
32+
[codebase-index] Skipping file watching and auto-indexing: no project marker found
33+
```
34+
35+
### If You Need to Index a Non-Project Directory
36+
Set `requireProjectMarker` to `false` in your config:
37+
```json
38+
{
39+
"indexing": {
40+
"requireProjectMarker": false
41+
}
42+
}
43+
```
44+
45+
**Warning:** Only do this for specific directories you intend to index. Never disable this for your home directory.
46+
47+
### Recognized Project Markers
48+
The plugin looks for any of these files/directories:
49+
- `.git`
50+
- `package.json`
51+
- `Cargo.toml`
52+
- `go.mod`
53+
- `pyproject.toml`
54+
- `setup.py`
55+
- `requirements.txt`
56+
- `Gemfile`
57+
- `composer.json`
58+
- `pom.xml`
59+
- `build.gradle`
60+
- `CMakeLists.txt`
61+
- `Makefile`
62+
- `.opencode`
63+
64+
---
65+
1766
## No Embedding Provider Available
1867

1968
**Error message:**
@@ -342,6 +391,7 @@ If none of these solutions work:
342391

343392
| Problem | Quick Fix |
344393
|---------|-----------|
394+
| Hangs in home dir | Update to v0.4.1+ (auto-detects non-project dirs) |
345395
| No provider | `export OPENAI_API_KEY=...` or use Ollama |
346396
| Rate limited | Switch to Ollama for large codebases |
347397
| Stale results | `rm -rf .opencode/index/` and re-index |

src/config/schema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export interface IndexingConfig {
1515
autoGc: boolean;
1616
gcIntervalDays: number;
1717
gcOrphanThreshold: number;
18+
/**
19+
* When true (default), requires a project marker (.git, package.json, Cargo.toml, etc.)
20+
* to be present before enabling file watching and auto-indexing.
21+
* This prevents accidentally watching/indexing large non-project directories like home.
22+
* Set to false to allow indexing any directory.
23+
*/
24+
requireProjectMarker: boolean;
1825
}
1926

2027
export interface SearchConfig {
@@ -96,6 +103,7 @@ function getDefaultIndexingConfig(): IndexingConfig {
96103
autoGc: true,
97104
gcIntervalDays: 7,
98105
gcOrphanThreshold: 100,
106+
requireProjectMarker: true,
99107
};
100108
}
101109

@@ -161,6 +169,7 @@ export function parseConfig(raw: unknown): ParsedCodebaseIndexConfig {
161169
autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
162170
gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
163171
gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold,
172+
requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker,
164173
};
165174

166175
const rawSearch = (input.search && typeof input.search === "object" ? input.search : {}) as Record<string, unknown>;

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
initializeTools,
2020
} from "./tools/index.js";
2121
import { loadCommandsFromDirectory } from "./commands/loader.js";
22+
import { hasProjectMarker } from "./utils/files.js";
2223

2324
function getCommandsDir(): string {
2425
let currentDir = process.cwd();
@@ -64,13 +65,22 @@ const plugin: Plugin = async ({ directory }) => {
6465

6566
const indexer = new Indexer(projectRoot, config);
6667

67-
if (config.indexing.autoIndex) {
68+
const isValidProject = !config.indexing.requireProjectMarker || hasProjectMarker(projectRoot);
69+
70+
if (!isValidProject) {
71+
console.warn(
72+
`[codebase-index] Skipping file watching and auto-indexing: no project marker found in "${projectRoot}". ` +
73+
`Set "indexing.requireProjectMarker": false in config to override.`
74+
);
75+
}
76+
77+
if (config.indexing.autoIndex && isValidProject) {
6878
indexer.initialize().then(() => {
6979
indexer.index().catch(() => {});
7080
}).catch(() => {});
7181
}
7282

73-
if (config.indexing.watchFiles) {
83+
if (config.indexing.watchFiles && isValidProject) {
7484
createWatcherWithIndexer(indexer, projectRoot, config);
7585
}
7686

src/utils/files.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@ import ignore, { Ignore } from "ignore";
22
import { existsSync, readFileSync, promises as fsPromises } from "fs";
33
import * as path from "path";
44

5+
const PROJECT_MARKERS = [
6+
".git",
7+
"package.json",
8+
"Cargo.toml",
9+
"go.mod",
10+
"pyproject.toml",
11+
"setup.py",
12+
"requirements.txt",
13+
"Gemfile",
14+
"composer.json",
15+
"pom.xml",
16+
"build.gradle",
17+
"CMakeLists.txt",
18+
"Makefile",
19+
".opencode",
20+
];
21+
22+
export function hasProjectMarker(projectRoot: string): boolean {
23+
for (const marker of PROJECT_MARKERS) {
24+
if (existsSync(path.join(projectRoot, marker))) {
25+
return true;
26+
}
27+
}
28+
return false;
29+
}
30+
531
export interface SkippedFile {
632
path: string;
733
reason: "too_large" | "excluded" | "gitignore" | "no_match";

tests/files.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
22
import * as fs from "fs";
33
import * as path from "path";
44
import * as os from "os";
5-
import { collectFiles, createIgnoreFilter, shouldIncludeFile } from "../src/utils/files.js";
5+
import { collectFiles, createIgnoreFilter, shouldIncludeFile, hasProjectMarker } from "../src/utils/files.js";
66

77
describe("files utilities", () => {
88
let tempDir: string;
@@ -166,4 +166,46 @@ describe("files utilities", () => {
166166
expect(result.files.some((f) => f.path.endsWith("nested.js"))).toBe(true);
167167
});
168168
});
169+
170+
describe("hasProjectMarker", () => {
171+
it("should return true when .git exists", () => {
172+
fs.mkdirSync(path.join(tempDir, ".git"), { recursive: true });
173+
expect(hasProjectMarker(tempDir)).toBe(true);
174+
});
175+
176+
it("should return true when package.json exists", () => {
177+
fs.writeFileSync(path.join(tempDir, "package.json"), "{}");
178+
expect(hasProjectMarker(tempDir)).toBe(true);
179+
});
180+
181+
it("should return true when Cargo.toml exists", () => {
182+
fs.writeFileSync(path.join(tempDir, "Cargo.toml"), "[package]");
183+
expect(hasProjectMarker(tempDir)).toBe(true);
184+
});
185+
186+
it("should return true when go.mod exists", () => {
187+
fs.writeFileSync(path.join(tempDir, "go.mod"), "module test");
188+
expect(hasProjectMarker(tempDir)).toBe(true);
189+
});
190+
191+
it("should return true when pyproject.toml exists", () => {
192+
fs.writeFileSync(path.join(tempDir, "pyproject.toml"), "[project]");
193+
expect(hasProjectMarker(tempDir)).toBe(true);
194+
});
195+
196+
it("should return true when .opencode exists", () => {
197+
fs.mkdirSync(path.join(tempDir, ".opencode"), { recursive: true });
198+
expect(hasProjectMarker(tempDir)).toBe(true);
199+
});
200+
201+
it("should return false for empty directory", () => {
202+
expect(hasProjectMarker(tempDir)).toBe(false);
203+
});
204+
205+
it("should return false for directory with only regular files", () => {
206+
fs.writeFileSync(path.join(tempDir, "readme.txt"), "hello");
207+
fs.writeFileSync(path.join(tempDir, "data.json"), "{}");
208+
expect(hasProjectMarker(tempDir)).toBe(false);
209+
});
210+
});
169211
});

0 commit comments

Comments
 (0)