@jmylchreest/aide-plugin 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmylchreest/aide-plugin",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "description": "aide plugin for OpenCode — multi-agent orchestration, memory, skills, and persistence",
5
5
  "type": "module",
6
6
  "main": "./src/opencode/index.ts",
@@ -126,6 +126,48 @@ function isObviousComment(line: string): boolean {
126
126
  return OBVIOUS_PATTERNS.some((p) => p.test(line));
127
127
  }
128
128
 
129
+ /**
130
+ * Track /* ... * / block comment state.
131
+ * Returns [shouldSkip, newInBlockState].
132
+ */
133
+ function isInBlockComment(
134
+ trimmed: string,
135
+ inBlock: boolean,
136
+ ): [boolean, boolean] {
137
+ if (trimmed.startsWith("/*")) {
138
+ // Single-line block comments: /* ... */
139
+ const closed = trimmed.includes("*/");
140
+ return [true, !closed];
141
+ }
142
+ if (inBlock) {
143
+ const closed = trimmed.includes("*/");
144
+ return [true, !closed];
145
+ }
146
+ return [false, false];
147
+ }
148
+
149
+ /**
150
+ * Track Python/Ruby docstring state (triple quotes).
151
+ * Returns [shouldSkip, newInDocstringState].
152
+ */
153
+ function isInDocstring(
154
+ trimmed: string,
155
+ ext: string,
156
+ inDocstring: boolean,
157
+ ): [boolean, boolean] {
158
+ if (ext !== ".py" && ext !== ".rb") {
159
+ return [false, inDocstring];
160
+ }
161
+ const tripleQuoteCount = (trimmed.match(/"""|'''/g) || []).length;
162
+ if (tripleQuoteCount === 1) {
163
+ return [true, !inDocstring];
164
+ }
165
+ if (inDocstring) {
166
+ return [true, inDocstring];
167
+ }
168
+ return [false, inDocstring];
169
+ }
170
+
129
171
  /**
130
172
  * Extract comment lines from code content.
131
173
  * Returns only single-line comments (// or #), not block comments or docstrings.
@@ -137,39 +179,23 @@ function extractCommentLines(
137
179
  const lines = content.split("\n");
138
180
  const comments: { line: string; lineNumber: number }[] = [];
139
181
  const usesHash = HASH_COMMENT_EXTENSIONS.has(ext);
140
- let inBlockComment = false;
141
- let inDocstring = false;
182
+ let inBlock = false;
183
+ let inDoc = false;
142
184
 
143
185
  for (let i = 0; i < lines.length; i++) {
144
186
  const trimmed = lines[i].trim();
145
187
 
146
- // Track block comments (/* ... */)
147
- if (!inDocstring) {
148
- if (trimmed.startsWith("/*")) {
149
- inBlockComment = true;
150
- // Single-line block comments
151
- if (trimmed.includes("*/")) {
152
- inBlockComment = false;
153
- }
154
- continue;
155
- }
156
- if (inBlockComment) {
157
- if (trimmed.includes("*/")) {
158
- inBlockComment = false;
159
- }
160
- continue;
161
- }
188
+ // Track block comments (/* ... */), but not inside docstrings
189
+ if (!inDoc) {
190
+ const [skipBlock, newBlock] = isInBlockComment(trimmed, inBlock);
191
+ inBlock = newBlock;
192
+ if (skipBlock) continue;
162
193
  }
163
194
 
164
195
  // Track Python/Ruby docstrings
165
- if (ext === ".py" || ext === ".rb") {
166
- const tripleQuoteCount = (trimmed.match(/"""|'''/g) || []).length;
167
- if (tripleQuoteCount === 1) {
168
- inDocstring = !inDocstring;
169
- continue;
170
- }
171
- if (inDocstring) continue;
172
- }
196
+ const [skipDoc, newDoc] = isInDocstring(trimmed, ext, inDoc);
197
+ inDoc = newDoc;
198
+ if (skipDoc) continue;
173
199
 
174
200
  // Single-line comments
175
201
  if (trimmed.startsWith("//")) {
@@ -351,7 +351,7 @@ export async function ensureAideBinary(cwd?: string): Promise<EnsureResult> {
351
351
 
352
352
  // Step 1: Check for existing binary
353
353
  let binaryPath = findAideBinary(cwd);
354
- let binaryVersion = binaryPath ? getBinaryVersion(binaryPath) : null;
354
+ const binaryVersion = binaryPath ? getBinaryVersion(binaryPath) : null;
355
355
 
356
356
  // Step 2: Check version match
357
357
  // Skip download if the binary is a dev build with base version >= plugin version
@@ -407,7 +407,6 @@ export async function ensureAideBinary(cwd?: string): Promise<EnsureResult> {
407
407
 
408
408
  if (result.success && result.path) {
409
409
  binaryPath = result.path;
410
- binaryVersion = getBinaryVersion(result.path);
411
410
  downloaded = true;
412
411
  } else {
413
412
  // Download failed - return error with manual instructions
@@ -36,9 +36,9 @@
36
36
  * ```
37
37
  */
38
38
 
39
- import { dirname, join, resolve } from "path";
39
+ import { basename, dirname, join, resolve } from "path";
40
40
  import { fileURLToPath } from "url";
41
- import { existsSync, statSync } from "fs";
41
+ import { existsSync, readFileSync, statSync } from "fs";
42
42
  import { createHooks } from "./hooks.js";
43
43
  import type { Plugin, PluginInput, Hooks } from "./types.js";
44
44
 
@@ -62,8 +62,16 @@ if (!process.env.AIDE_PLUGIN_ROOT) {
62
62
  * ctx.project.worktree — `dirname(git rev-parse --git-common-dir)` (main repo root)
63
63
  *
64
64
  * Priority:
65
- * 1. ctx.worktree — the git sandbox root (correct for both normal repos and worktrees)
66
- * 2. ctx.directorywhere OpenCode was invoked (fallback for non-git)
65
+ * 1. ctx.project.worktree — the main repo root (shared across all worktrees)
66
+ * 2. ctx.worktreethe git sandbox root (fallback if project.worktree missing)
67
+ * 3. ctx.directory — where OpenCode was invoked (fallback for non-git)
68
+ *
69
+ * IMPORTANT: We use ctx.project.worktree (git common dir parent) rather than
70
+ * ctx.worktree (show-toplevel) so that all git worktrees resolve to the SAME
71
+ * main repository root. This matches the Go binary's findProjectRoot() which
72
+ * follows .git worktree pointers to the main repo. Without this, each worktree
73
+ * would create its own .aide/ directory while the Go binary opens the database
74
+ * in the main repo's .aide/, causing BoltDB/SQLite lock contention.
67
75
  *
68
76
  * Both ctx.worktree and ctx.directory are "/" for non-git projects, so we
69
77
  * detect that case and skip initialization.
@@ -72,18 +80,23 @@ function resolveProjectRoot(ctx: PluginInput): {
72
80
  root: string;
73
81
  hasProjectRoot: boolean;
74
82
  } {
75
- // ctx.worktree is the git working tree root (from `git rev-parse --show-toplevel`).
76
- // For non-git projects, OpenCode sets this to "/".
77
- const worktree = ctx.worktree;
78
83
  const directory = ctx.directory;
79
84
 
80
- // OpenCode sets worktree to "/" for non-git projects — treat as no project.
81
- // Also guard against empty strings.
85
+ // Prefer ctx.project.worktree this is dirname(git rev-parse --git-common-dir),
86
+ // i.e. the main repo root that is shared across all worktrees.
87
+ // This matches the Go binary's resolveWorktreeRoot() behavior.
88
+ const projectWorktree = ctx.project?.worktree;
89
+ if (projectWorktree && projectWorktree !== "/") {
90
+ return { root: resolve(projectWorktree), hasProjectRoot: true };
91
+ }
92
+
93
+ // Fallback: ctx.worktree is git rev-parse --show-toplevel (per-worktree root).
94
+ // For normal (non-worktree) repos, this equals the main repo root anyway.
95
+ // For non-git projects, OpenCode sets this to "/".
96
+ const worktree = ctx.worktree;
82
97
  const isNonGitWorktree = !worktree || worktree === "/";
83
98
 
84
99
  if (!isNonGitWorktree) {
85
- // The worktree is a valid git root — use it directly.
86
- // No need to walk up the filesystem; OpenCode already resolved it.
87
100
  return { root: resolve(worktree), hasProjectRoot: true };
88
101
  }
89
102
 
@@ -110,6 +123,10 @@ function resolveProjectRoot(ctx: PluginInput): {
110
123
  /**
111
124
  * Walk up from `startDir` looking for .aide/ or .git/ directories.
112
125
  * Returns the project root path, or null if none found.
126
+ *
127
+ * For git worktrees, .git is a file containing "gitdir: <path>".
128
+ * We follow it to the main repo root, matching the Go binary's
129
+ * resolveWorktreeRoot() behavior.
113
130
  */
114
131
  function walkUpForProjectRoot(startDir: string): string | null {
115
132
  let dir = resolve(startDir);
@@ -121,8 +138,16 @@ function walkUpForProjectRoot(startDir: string): string | null {
121
138
  if (existsSync(gitPath)) {
122
139
  try {
123
140
  const stat = statSync(gitPath);
124
- // .git can be a directory (normal repo) or a file (worktree pointer)
125
- if (stat.isDirectory() || stat.isFile()) {
141
+ if (stat.isDirectory()) {
142
+ // Normal git repo
143
+ return dir;
144
+ }
145
+ if (stat.isFile()) {
146
+ // Worktree: .git is a file containing "gitdir: <path>"
147
+ // Follow it to the main repo root.
148
+ const mainRoot = resolveWorktreeGitFile(gitPath);
149
+ if (mainRoot) return mainRoot;
150
+ // Fallback to current dir if resolution fails
126
151
  return dir;
127
152
  }
128
153
  } catch {
@@ -137,6 +162,42 @@ function walkUpForProjectRoot(startDir: string): string | null {
137
162
  }
138
163
  }
139
164
 
165
+ /**
166
+ * Read a .git worktree file and resolve to the main repository root.
167
+ * Mirrors the Go binary's resolveWorktreeRoot() in main.go.
168
+ *
169
+ * The file contains "gitdir: /path/to/repo/.git/worktrees/<name>".
170
+ * We walk up from that gitdir path to find the .git directory,
171
+ * then return its parent.
172
+ */
173
+ function resolveWorktreeGitFile(gitFilePath: string): string | null {
174
+ try {
175
+ const content = readFileSync(gitFilePath, "utf-8").trim();
176
+ if (!content.startsWith("gitdir:")) return null;
177
+
178
+ let gitdir = content.slice("gitdir:".length).trim();
179
+ // Make absolute if relative
180
+ if (!gitdir.startsWith("/")) {
181
+ gitdir = resolve(dirname(gitFilePath), gitdir);
182
+ }
183
+
184
+ // Walk up from .git/worktrees/<name> to find the .git directory,
185
+ // then return its parent as the repo root.
186
+ let candidate = gitdir;
187
+ for (;;) {
188
+ const parent = dirname(candidate);
189
+ if (parent === candidate) break;
190
+ if (basename(candidate) === ".git") {
191
+ return parent;
192
+ }
193
+ candidate = parent;
194
+ }
195
+ return null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
140
201
  export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
141
202
  // Log raw plugin input BEFORE any resolution for diagnostics.
142
203
  // This is the key to understanding what OpenCode actually passes.