@jmylchreest/aide-plugin 0.0.64 → 0.0.66
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 +3 -3
- package/src/core/session-init.ts +24 -8
- package/src/core/types.ts +10 -0
- package/src/hooks/session-start.ts +26 -2
- package/src/lib/aide-downloader.ts +20 -2
- package/src/lib/project-root.ts +137 -0
- package/src/opencode/index.ts +2 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmylchreest/aide-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"description": "aide plugin for OpenCode and Codex CLI — multi-agent orchestration, memory, skills, and persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/opencode/index.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"cross-spawn": "^7.0.6",
|
|
55
|
-
"smol-toml": "^1.
|
|
56
|
-
"which": "^
|
|
55
|
+
"smol-toml": "^1.6.1",
|
|
56
|
+
"which": "^7.0.0"
|
|
57
57
|
}
|
|
58
58
|
}
|
package/src/core/session-init.ts
CHANGED
|
@@ -186,23 +186,39 @@ export function getProjectName(cwd: string): string {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
* Load config from
|
|
190
|
-
*
|
|
189
|
+
* Load config from ~/.aide/config/aide.json (global).
|
|
190
|
+
* Used before a project root has been resolved (e.g. by the SessionStart
|
|
191
|
+
* hook deciding whether to honour `requireGit`).
|
|
192
|
+
*/
|
|
193
|
+
export function loadGlobalConfig(): AideConfig {
|
|
194
|
+
const configPath = join(homedir(), ".aide", "config", "aide.json");
|
|
195
|
+
if (!existsSync(configPath)) return DEFAULT_CONFIG;
|
|
196
|
+
try {
|
|
197
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(configPath, "utf-8")) };
|
|
198
|
+
} catch {
|
|
199
|
+
return DEFAULT_CONFIG;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load config layered as: defaults → global (~/.aide/config/aide.json) →
|
|
205
|
+
* project (`<cwd>/.aide/config/aide.json`). Project values override global.
|
|
191
206
|
* Does NOT create a default config file — only user-set values are persisted.
|
|
192
207
|
*/
|
|
193
208
|
export function loadConfig(cwd: string): AideConfig {
|
|
194
|
-
const
|
|
209
|
+
const global = loadGlobalConfig();
|
|
210
|
+
const projectPath = join(cwd, ".aide", "config", "aide.json");
|
|
195
211
|
|
|
196
|
-
if (existsSync(
|
|
212
|
+
if (existsSync(projectPath)) {
|
|
197
213
|
try {
|
|
198
|
-
const
|
|
199
|
-
return { ...DEFAULT_CONFIG, ...
|
|
214
|
+
const project = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
215
|
+
return { ...DEFAULT_CONFIG, ...global, ...project };
|
|
200
216
|
} catch {
|
|
201
|
-
return
|
|
217
|
+
return global;
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
|
|
205
|
-
return
|
|
221
|
+
return global;
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
/**
|
package/src/core/types.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
// =============================================================================
|
|
10
10
|
|
|
11
11
|
export interface AideConfig {
|
|
12
|
+
/**
|
|
13
|
+
* When true (default), AIDE refuses to bootstrap if no `.git/` or `.aide/`
|
|
14
|
+
* marker is found walking up from the launched cwd. This prevents the hook
|
|
15
|
+
* from planting an orphan `.aide/` folder in an arbitrary subdirectory of a
|
|
16
|
+
* git repo when `claude` is launched there. Set to false in
|
|
17
|
+
* `~/.aide/config/aide.json` to allow init in non-git directories.
|
|
18
|
+
* Only the global-config value is consulted (the project layer is moot
|
|
19
|
+
* because, if a project root was found, the gate has already passed).
|
|
20
|
+
*/
|
|
21
|
+
requireGit?: boolean;
|
|
12
22
|
share?: {
|
|
13
23
|
/** Auto-import shared data from .aide/shared/ on session start (default: false) */
|
|
14
24
|
autoImport?: boolean;
|
|
@@ -26,10 +26,12 @@ import { homedir } from "os";
|
|
|
26
26
|
import { Logger, debug, setDebugCwd } from "../lib/logger.js";
|
|
27
27
|
import { readStdin, detectPlatform } from "../lib/hook-utils.js";
|
|
28
28
|
import { findAideBinary, ensureAideBinary } from "../lib/aide-downloader.js";
|
|
29
|
+
import { findProjectRoot } from "../lib/project-root.js";
|
|
29
30
|
import { recordTokenEvent } from "../core/read-tracking.js";
|
|
30
31
|
import {
|
|
31
32
|
ensureDirectories as coreEnsureDirectories,
|
|
32
33
|
loadConfig as coreLoadConfig,
|
|
34
|
+
loadGlobalConfig as coreLoadGlobalConfig,
|
|
33
35
|
initializeSession as coreInitializeSession,
|
|
34
36
|
cleanupStaleStateFiles as coreCleanupStaleStateFiles,
|
|
35
37
|
resetHudState as coreResetHudState,
|
|
@@ -340,13 +342,35 @@ async function main(): Promise<void> {
|
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
const data: HookInput = JSON.parse(input);
|
|
343
|
-
const
|
|
345
|
+
const launchedCwd = data.cwd || process.cwd();
|
|
344
346
|
const sessionId = data.session_id || "unknown";
|
|
345
347
|
|
|
348
|
+
// Resolve the project root so we never plant a sibling .aide/ in a
|
|
349
|
+
// subdirectory of a git repo. Mirrors the Go binary's findProjectRoot().
|
|
350
|
+
const { root: resolvedRoot, hasMarker } = findProjectRoot(launchedCwd);
|
|
351
|
+
if (!hasMarker) {
|
|
352
|
+
const requireGit = coreLoadGlobalConfig().requireGit ?? true;
|
|
353
|
+
if (requireGit) {
|
|
354
|
+
process.stderr.write(
|
|
355
|
+
`[aide] No .git/ or .aide/ found walking up from ${launchedCwd}. ` +
|
|
356
|
+
`Set \`requireGit\`: false in ~/.aide/config/aide.json to allow ` +
|
|
357
|
+
`init in arbitrary directories. Skipping AIDE bootstrap.\n`,
|
|
358
|
+
);
|
|
359
|
+
console.log(JSON.stringify({ continue: true }));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
process.stderr.write(
|
|
363
|
+
`[aide] No project root found, falling back to ${launchedCwd} (requireGit=false).\n`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const cwd = hasMarker ? resolvedRoot : launchedCwd;
|
|
367
|
+
|
|
346
368
|
// Switch debug logging to project-local logs
|
|
347
369
|
setDebugCwd(cwd);
|
|
348
370
|
|
|
349
|
-
debugLog(
|
|
371
|
+
debugLog(
|
|
372
|
+
`Parsed input: cwd=${cwd}, launchedCwd=${launchedCwd}, sessionId=${sessionId.slice(0, 8)}`,
|
|
373
|
+
);
|
|
350
374
|
|
|
351
375
|
// Initialize logger
|
|
352
376
|
log = new Logger("session-start", cwd);
|
|
@@ -28,6 +28,8 @@ import { Readable, Transform } from "stream";
|
|
|
28
28
|
import { pipeline } from "stream/promises";
|
|
29
29
|
// Canonical binary finder — import for local use, re-export for backward compat
|
|
30
30
|
import { findAideBinary } from "./hook-utils.js";
|
|
31
|
+
import { findProjectRoot } from "./project-root.js";
|
|
32
|
+
import { loadGlobalConfig } from "../core/session-init.js";
|
|
31
33
|
export { findAideBinary };
|
|
32
34
|
|
|
33
35
|
export interface DownloadResult {
|
|
@@ -544,8 +546,24 @@ Downloads the aide binary from GitHub releases.
|
|
|
544
546
|
}
|
|
545
547
|
destDir = join(pluginRoot, "bin");
|
|
546
548
|
} else if (!destDir) {
|
|
547
|
-
// Default to
|
|
548
|
-
|
|
549
|
+
// Default: resolve to the project root rather than blindly using cwd.
|
|
550
|
+
// Matches the SessionStart hook so the CLI fallback never plants an
|
|
551
|
+
// orphan .aide/bin/ in a subdirectory of a git repo.
|
|
552
|
+
const { root, hasMarker } = findProjectRoot(process.cwd());
|
|
553
|
+
if (!hasMarker) {
|
|
554
|
+
const requireGit = loadGlobalConfig().requireGit ?? true;
|
|
555
|
+
if (requireGit) {
|
|
556
|
+
console.error(
|
|
557
|
+
`[aide] No .git/ or .aide/ found walking up from ${process.cwd()}. ` +
|
|
558
|
+
`Set \`requireGit\`: false in ~/.aide/config/aide.json or pass --cwd / --dest to install anyway.`,
|
|
559
|
+
);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
console.error(
|
|
563
|
+
`[aide] No project root found, installing into ${process.cwd()} (requireGit=false).`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
destDir = join(hasMarker ? root : process.cwd(), ".aide", "bin");
|
|
549
567
|
}
|
|
550
568
|
|
|
551
569
|
const result = await downloadAideBinary(destDir, { force, quiet: false });
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project root resolution for the AIDE plugin.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Go binary's findProjectRoot() in aide/cmd/aide/main.go so that
|
|
5
|
+
* the TypeScript hook layer and the Go binary always agree on where `.aide/`
|
|
6
|
+
* lives. Without this, the hook would plant a sibling `.aide/` in whatever
|
|
7
|
+
* subdirectory `claude` was launched from, while the Go binary would walk up
|
|
8
|
+
* and use the real one at the repo root.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order, matching main.go:findProjectRoot:
|
|
11
|
+
* 1. AIDE_PROJECT_ROOT env override (must be an existing directory).
|
|
12
|
+
* 2. Walk up from cwd. At each level:
|
|
13
|
+
* a. .aide/ — return this dir. Skip ~/.aide/ unless cwd === $HOME.
|
|
14
|
+
* b. .git/ directory — return this dir.
|
|
15
|
+
* c. .git/ file (worktree pointer) — resolve to the main repo root.
|
|
16
|
+
* 3. No marker found: return { root: cwd, hasMarker: false }.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { basename, dirname, join, resolve } from "path";
|
|
20
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
|
|
23
|
+
export interface ProjectRootResult {
|
|
24
|
+
root: string;
|
|
25
|
+
hasMarker: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the AIDE project root for a given cwd.
|
|
30
|
+
*
|
|
31
|
+
* `hasMarker` is true when an actual `.aide/` or `.git/` marker was found
|
|
32
|
+
* (or when AIDE_PROJECT_ROOT is set to an existing directory). When false,
|
|
33
|
+
* `root` is just the input cwd — callers should decide whether to fall
|
|
34
|
+
* back to it (e.g. via the `requireGit` config) or refuse to bootstrap.
|
|
35
|
+
*/
|
|
36
|
+
export function findProjectRoot(cwd: string): ProjectRootResult {
|
|
37
|
+
const override = process.env.AIDE_PROJECT_ROOT;
|
|
38
|
+
if (override) {
|
|
39
|
+
try {
|
|
40
|
+
const abs = resolve(override);
|
|
41
|
+
const stat = statSync(abs);
|
|
42
|
+
if (stat.isDirectory()) {
|
|
43
|
+
return { root: abs, hasMarker: true };
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall through to the walk-up.
|
|
47
|
+
}
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
`aide: AIDE_PROJECT_ROOT=${JSON.stringify(override)} is not a directory; falling back to walk-up\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const startCwd = resolve(cwd);
|
|
54
|
+
const home = homedir();
|
|
55
|
+
|
|
56
|
+
let dir = startCwd;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const aidePath = join(dir, ".aide");
|
|
59
|
+
if (existsSync(aidePath)) {
|
|
60
|
+
// Skip ~/.aide/ unless cwd is $HOME itself. ~/.aide/ is the global
|
|
61
|
+
// config dir, not a project marker.
|
|
62
|
+
if (!(home && dir === home && startCwd !== home)) {
|
|
63
|
+
return { root: dir, hasMarker: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gitPath = join(dir, ".git");
|
|
68
|
+
if (existsSync(gitPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const stat = statSync(gitPath);
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
return { root: dir, hasMarker: true };
|
|
73
|
+
}
|
|
74
|
+
if (stat.isFile()) {
|
|
75
|
+
const mainRoot = resolveWorktreeGitFile(gitPath);
|
|
76
|
+
if (mainRoot) {
|
|
77
|
+
return { root: mainRoot, hasMarker: true };
|
|
78
|
+
}
|
|
79
|
+
return { root: dir, hasMarker: true };
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
return { root: dir, hasMarker: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parent = dirname(dir);
|
|
87
|
+
if (parent === dir) {
|
|
88
|
+
return { root: startCwd, hasMarker: false };
|
|
89
|
+
}
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Walk up from `startDir` looking for `.aide/` or `.git/` markers.
|
|
96
|
+
* Returns the resolved root directory, or null when nothing is found.
|
|
97
|
+
*
|
|
98
|
+
* Thin wrapper around findProjectRoot for callers that want a nullable
|
|
99
|
+
* result rather than the {root,hasMarker} shape (e.g. the OpenCode plugin
|
|
100
|
+
* which has its own fallback chain).
|
|
101
|
+
*/
|
|
102
|
+
export function walkUpForProjectRoot(startDir: string): string | null {
|
|
103
|
+
const { root, hasMarker } = findProjectRoot(startDir);
|
|
104
|
+
return hasMarker ? root : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read a .git worktree file ("gitdir: <path>") and return the main repo root.
|
|
109
|
+
*
|
|
110
|
+
* Mirrors aide/cmd/aide/main.go:resolveWorktreeRoot(). The file's gitdir
|
|
111
|
+
* normally points at "<main>/.git/worktrees/<name>"; we walk up that path
|
|
112
|
+
* until we find a component named ".git" and return its parent.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync(gitFilePath, "utf-8").trim();
|
|
117
|
+
if (!content.startsWith("gitdir:")) return null;
|
|
118
|
+
|
|
119
|
+
let gitdir = content.slice("gitdir:".length).trim();
|
|
120
|
+
if (!gitdir.startsWith("/")) {
|
|
121
|
+
gitdir = resolve(dirname(gitFilePath), gitdir);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let candidate = gitdir;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const parent = dirname(candidate);
|
|
127
|
+
if (parent === candidate) break;
|
|
128
|
+
if (basename(candidate) === ".git") {
|
|
129
|
+
return parent;
|
|
130
|
+
}
|
|
131
|
+
candidate = parent;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/opencode/index.ts
CHANGED
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { dirname, join, resolve } from "path";
|
|
40
40
|
import { fileURLToPath } from "url";
|
|
41
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
42
41
|
import { createHooks } from "./hooks.js";
|
|
43
42
|
import { isDebugEnabled } from "../lib/logger.js";
|
|
43
|
+
import { walkUpForProjectRoot } from "../lib/project-root.js";
|
|
44
44
|
import type { Plugin, PluginInput, Hooks } from "./types.js";
|
|
45
45
|
|
|
46
46
|
// Resolve the plugin package root so we can find bundled skills.
|
|
@@ -121,84 +121,6 @@ function resolveProjectRoot(ctx: PluginInput): {
|
|
|
121
121
|
return { root: directory || "/", hasProjectRoot: false };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* Walk up from `startDir` looking for .aide/ or .git/ directories.
|
|
126
|
-
* Returns the project root path, or null if none found.
|
|
127
|
-
*
|
|
128
|
-
* For git worktrees, .git is a file containing "gitdir: <path>".
|
|
129
|
-
* We follow it to the main repo root, matching the Go binary's
|
|
130
|
-
* resolveWorktreeRoot() behavior.
|
|
131
|
-
*/
|
|
132
|
-
function walkUpForProjectRoot(startDir: string): string | null {
|
|
133
|
-
let dir = resolve(startDir);
|
|
134
|
-
for (;;) {
|
|
135
|
-
if (existsSync(join(dir, ".aide"))) {
|
|
136
|
-
return dir;
|
|
137
|
-
}
|
|
138
|
-
const gitPath = join(dir, ".git");
|
|
139
|
-
if (existsSync(gitPath)) {
|
|
140
|
-
try {
|
|
141
|
-
const stat = statSync(gitPath);
|
|
142
|
-
if (stat.isDirectory()) {
|
|
143
|
-
// Normal git repo
|
|
144
|
-
return dir;
|
|
145
|
-
}
|
|
146
|
-
if (stat.isFile()) {
|
|
147
|
-
// Worktree: .git is a file containing "gitdir: <path>"
|
|
148
|
-
// Follow it to the main repo root.
|
|
149
|
-
const mainRoot = resolveWorktreeGitFile(gitPath);
|
|
150
|
-
if (mainRoot) return mainRoot;
|
|
151
|
-
// Fallback to current dir if resolution fails
|
|
152
|
-
return dir;
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
return dir;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const parent = resolve(dir, "..");
|
|
159
|
-
if (parent === dir) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
dir = parent;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Read a .git worktree file and resolve to the main repository root.
|
|
168
|
-
* Mirrors the Go binary's resolveWorktreeRoot() in main.go.
|
|
169
|
-
*
|
|
170
|
-
* The file contains "gitdir: /path/to/repo/.git/worktrees/<name>".
|
|
171
|
-
* We walk up from that gitdir path to find the .git directory,
|
|
172
|
-
* then return its parent.
|
|
173
|
-
*/
|
|
174
|
-
function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
175
|
-
try {
|
|
176
|
-
const content = readFileSync(gitFilePath, "utf-8").trim();
|
|
177
|
-
if (!content.startsWith("gitdir:")) return null;
|
|
178
|
-
|
|
179
|
-
let gitdir = content.slice("gitdir:".length).trim();
|
|
180
|
-
// Make absolute if relative
|
|
181
|
-
if (!gitdir.startsWith("/")) {
|
|
182
|
-
gitdir = resolve(dirname(gitFilePath), gitdir);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Walk up from .git/worktrees/<name> to find the .git directory,
|
|
186
|
-
// then return its parent as the repo root.
|
|
187
|
-
let candidate = gitdir;
|
|
188
|
-
for (;;) {
|
|
189
|
-
const parent = dirname(candidate);
|
|
190
|
-
if (parent === candidate) break;
|
|
191
|
-
if (basename(candidate) === ".git") {
|
|
192
|
-
return parent;
|
|
193
|
-
}
|
|
194
|
-
candidate = parent;
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
} catch {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
124
|
export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
|
|
203
125
|
// Log raw plugin input BEFORE any resolution for diagnostics.
|
|
204
126
|
// This is the key to understanding what OpenCode actually passes.
|