agent-watch 1.0.0
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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/cli-nMe9-VkJ.d.ts +1 -0
- package/dist/cli.cjs +770 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +771 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/run.d.ts +5 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/detect-BMnM34-m.cjs +177 -0
- package/dist/detect-BMnM34-m.cjs.map +1 -0
- package/dist/detect-BWGm1KGQ.js +122 -0
- package/dist/detect-BWGm1KGQ.js.map +1 -0
- package/dist/detect-B_DDBj5N.cjs +182 -0
- package/dist/detect-B_DDBj5N.cjs.map +1 -0
- package/dist/detect-CPW1RRIq.js +117 -0
- package/dist/detect-CPW1RRIq.js.map +1 -0
- package/dist/detect-Dii2e4wf.cjs +174 -0
- package/dist/detect-Dii2e4wf.cjs.map +1 -0
- package/dist/detect-Pkaqn3YG.js +120 -0
- package/dist/detect-Pkaqn3YG.js.map +1 -0
- package/dist/detect.d.ts +16 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/hooks.d.ts +13 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index-CXIlEXUx.d.ts +51 -0
- package/dist/index.cjs +13 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-BNjXChov.js +164 -0
- package/dist/logger-BNjXChov.js.map +1 -0
- package/dist/logger-CdAUnlsG.cjs +271 -0
- package/dist/logger-CdAUnlsG.cjs.map +1 -0
- package/dist/logger-hSIaaw_k.js +166 -0
- package/dist/logger-hSIaaw_k.js.map +1 -0
- package/dist/logger-oq2Z7oYf.cjs +269 -0
- package/dist/logger-oq2Z7oYf.cjs.map +1 -0
- package/dist/sessions-90kmJrQI.js +360 -0
- package/dist/sessions-90kmJrQI.js.map +1 -0
- package/dist/sessions-Bmk48zTI.js +311 -0
- package/dist/sessions-Bmk48zTI.js.map +1 -0
- package/dist/sessions-BpNk9YjU.cjs +431 -0
- package/dist/sessions-BpNk9YjU.cjs.map +1 -0
- package/dist/sessions-CkCQikpl.cjs +444 -0
- package/dist/sessions-CkCQikpl.cjs.map +1 -0
- package/dist/sessions-Cy-_zIh6.js +315 -0
- package/dist/sessions-Cy-_zIh6.js.map +1 -0
- package/dist/sessions-DZgPENb6.cjs +434 -0
- package/dist/sessions-DZgPENb6.cjs.map +1 -0
- package/dist/sessions-_HBb3nIW.cjs +495 -0
- package/dist/sessions-_HBb3nIW.cjs.map +1 -0
- package/dist/sessions-tBeR9gKG.js +308 -0
- package/dist/sessions-tBeR9gKG.js.map +1 -0
- package/dist/utils/copilot.d.ts +27 -0
- package/dist/utils/copilot.d.ts.map +1 -0
- package/dist/utils/git.d.ts +35 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/gitignore.d.ts +5 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/sessions.d.ts +48 -0
- package/dist/utils/sessions.d.ts.map +1 -0
- package/package.json +79 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as detectAgentFiles, c as loadConfig, f as FILE_SELECTION_PAGE_SIZE, g as SUPPORTED_AI_AGENTS, i as logger, l as saveConfig, m as KNOWN_AGENT_FILES, p as IGNORED_FILE_PATTERNS, r as processNewSessions, s as createDefaultConfig, u as AGENT_WATCH_DIR } from "./sessions-90kmJrQI.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import { checkbox, confirm, select } from "@inquirer/prompts";
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
//#region src/utils/git.ts
|
|
12
|
+
/**
|
|
13
|
+
* Find the git repository root from a given directory.
|
|
14
|
+
* Returns null if not in a git repository.
|
|
15
|
+
*/
|
|
16
|
+
function findGitRoot(cwd = process.cwd()) {
|
|
17
|
+
try {
|
|
18
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
19
|
+
cwd,
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
stdio: [
|
|
22
|
+
"pipe",
|
|
23
|
+
"pipe",
|
|
24
|
+
"pipe"
|
|
25
|
+
]
|
|
26
|
+
}).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the path to the .git/hooks directory.
|
|
33
|
+
*/
|
|
34
|
+
function getGitHooksDir(gitRoot) {
|
|
35
|
+
return join(gitRoot, ".git", "hooks");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if lefthook is installed in the project.
|
|
39
|
+
*/
|
|
40
|
+
function hasLefthook(projectRoot) {
|
|
41
|
+
return existsSync(join(projectRoot, "lefthook.yml")) || existsSync(join(projectRoot, "lefthook.yaml"));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if husky is installed in the project.
|
|
45
|
+
*/
|
|
46
|
+
function hasHusky(projectRoot) {
|
|
47
|
+
return existsSync(join(projectRoot, ".husky"));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the path to lefthook.yml or lefthook.yaml.
|
|
51
|
+
* Returns null if not found.
|
|
52
|
+
*/
|
|
53
|
+
function getLefthookPath(projectRoot) {
|
|
54
|
+
const ymlPath = join(projectRoot, "lefthook.yml");
|
|
55
|
+
const yamlPath = join(projectRoot, "lefthook.yaml");
|
|
56
|
+
if (existsSync(ymlPath)) return ymlPath;
|
|
57
|
+
if (existsSync(yamlPath)) return yamlPath;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if lefthook CLI is available.
|
|
62
|
+
*/
|
|
63
|
+
function isLefthookAvailable() {
|
|
64
|
+
try {
|
|
65
|
+
execSync("npx lefthook version", {
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
timeout: 5e3
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if husky CLI is available.
|
|
76
|
+
*/
|
|
77
|
+
function isHuskyAvailable() {
|
|
78
|
+
try {
|
|
79
|
+
execSync("npx husky --version", {
|
|
80
|
+
stdio: "pipe",
|
|
81
|
+
timeout: 5e3
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/hooks.ts
|
|
91
|
+
const HOOK_MARKER_START = "# >>> agent-watch hook start >>>";
|
|
92
|
+
const HOOK_MARKER_END = "# <<< agent-watch hook end <<<";
|
|
93
|
+
function getHookScript() {
|
|
94
|
+
return `${HOOK_MARKER_START}
|
|
95
|
+
# agent-watch: auto-update agent configuration files
|
|
96
|
+
npx agent-watch run 2>/dev/null || true
|
|
97
|
+
${HOOK_MARKER_END}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validate basic lefthook.yml structure before attempting modification.
|
|
101
|
+
* Checks: not empty, contains colons, no tabs, reasonable line/indentation length.
|
|
102
|
+
*/
|
|
103
|
+
function isValidLefthookStructure(content) {
|
|
104
|
+
if (!content.trim()) return false;
|
|
105
|
+
if (!content.includes(":")) return false;
|
|
106
|
+
if (content.includes(" ")) return false;
|
|
107
|
+
const lines = content.split("\n");
|
|
108
|
+
if (lines.length > 1e3) return false;
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (line.length > 500) return false;
|
|
111
|
+
const leadingSpaces = line.search(/\S/);
|
|
112
|
+
if (leadingSpaces > 50 && leadingSpaces !== -1) return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Remove existing agent-watch hook section from lefthook.yml content.
|
|
118
|
+
* Removes ANY agent-watch: section to prevent duplicates.
|
|
119
|
+
*/
|
|
120
|
+
function removeExistingAgentWatchHook(lines) {
|
|
121
|
+
const result = [];
|
|
122
|
+
let inAgentWatchSection = false;
|
|
123
|
+
let sectionIndentation = 0;
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (trimmed.startsWith("agent-watch:")) {
|
|
127
|
+
inAgentWatchSection = true;
|
|
128
|
+
sectionIndentation = line.search(/\S/);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (inAgentWatchSection) {
|
|
132
|
+
const currentIndent = line.search(/\S/);
|
|
133
|
+
if (currentIndent !== -1 && currentIndent <= sectionIndentation && trimmed.length > 0) {
|
|
134
|
+
inAgentWatchSection = false;
|
|
135
|
+
result.push(line);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
result.push(line);
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Insert agent-watch command into lefthook.yml content.
|
|
145
|
+
* Handles: missing hook section, missing commands section, proper indentation.
|
|
146
|
+
*/
|
|
147
|
+
function insertAgentWatchCommand(lines, hookName) {
|
|
148
|
+
const result = [];
|
|
149
|
+
let inserted = false;
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
result.push(line);
|
|
153
|
+
if (line.trim() === `${hookName}:`) {
|
|
154
|
+
let commandsLineIndex = -1;
|
|
155
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
156
|
+
const trimmedLine = lines[j].trim();
|
|
157
|
+
if (trimmedLine.startsWith("commands:")) {
|
|
158
|
+
commandsLineIndex = j;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (trimmedLine && !lines[j].startsWith(" ")) break;
|
|
162
|
+
}
|
|
163
|
+
if (commandsLineIndex === -1) {
|
|
164
|
+
result.push(" commands:", " agent-watch:", " run: npx agent-watch run");
|
|
165
|
+
inserted = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (let j = i + 1; j <= commandsLineIndex; j++) result.push(lines[j]);
|
|
169
|
+
result.push(" agent-watch:", " run: npx agent-watch run");
|
|
170
|
+
inserted = true;
|
|
171
|
+
i = commandsLineIndex;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!inserted) return {
|
|
175
|
+
success: false,
|
|
176
|
+
error: `Could not find proper insertion point in lefthook.yml for ${hookName}`
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
content: result.join("\n")
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Update lefthook.yml content by adding/replacing agent-watch hook.
|
|
185
|
+
* Coordinates removal and insertion, handles edge cases.
|
|
186
|
+
*/
|
|
187
|
+
function updateLefthookContent(content, hookName) {
|
|
188
|
+
const lines = content.split("\n");
|
|
189
|
+
const hasHookSection = new RegExp(`^${hookName}:\\s*$`, "m").test(content);
|
|
190
|
+
const withoutOldHook = removeExistingAgentWatchHook(lines);
|
|
191
|
+
if (!hasHookSection) {
|
|
192
|
+
const newSection = `
|
|
193
|
+
${hookName}:
|
|
194
|
+
commands:
|
|
195
|
+
agent-watch:
|
|
196
|
+
run: npx agent-watch run
|
|
197
|
+
`;
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
content: withoutOldHook.join("\n") + newSection
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return insertAgentWatchCommand(withoutOldHook, hookName);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Map the trigger name to the git hook file name.
|
|
207
|
+
*/
|
|
208
|
+
function getGitHookName(trigger) {
|
|
209
|
+
switch (trigger) {
|
|
210
|
+
case "commit": return "post-commit";
|
|
211
|
+
case "push": return "pre-push";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Install agent-watch hook into lefthook.yml.
|
|
216
|
+
* Flow: read file → validate → update content → write → run lefthook install
|
|
217
|
+
*/
|
|
218
|
+
function installLefthookHook(projectRoot, hookName) {
|
|
219
|
+
const lefthookPath = getLefthookPath(projectRoot);
|
|
220
|
+
if (!lefthookPath) return {
|
|
221
|
+
success: false,
|
|
222
|
+
message: "lefthook.yml not found",
|
|
223
|
+
method: "manual"
|
|
224
|
+
};
|
|
225
|
+
let currentContent;
|
|
226
|
+
try {
|
|
227
|
+
currentContent = readFileSync(lefthookPath, "utf-8");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: `Cannot read lefthook.yml: ${error instanceof Error ? error.message : String(error)}. Please add manually:\n\n${hookName}:\n commands:\n agent-watch:\n run: npx agent-watch run`,
|
|
232
|
+
method: "manual"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (!(currentContent.trim().length === 0) && !isValidLefthookStructure(currentContent)) return {
|
|
236
|
+
success: true,
|
|
237
|
+
message: `lefthook.yml has complex structure. Please add manually:\n\n${hookName}:\n commands:\n agent-watch:\n run: npx agent-watch run`,
|
|
238
|
+
method: "lefthook"
|
|
239
|
+
};
|
|
240
|
+
const result = updateLefthookContent(currentContent, hookName);
|
|
241
|
+
if (!result.success || !result.content) return {
|
|
242
|
+
success: false,
|
|
243
|
+
message: result.error || "Failed to update lefthook.yml",
|
|
244
|
+
method: "manual"
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
const tempPath = `${lefthookPath}.tmp`;
|
|
248
|
+
writeFileSync(tempPath, result.content, "utf-8");
|
|
249
|
+
renameSync(tempPath, lefthookPath);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
message: `Failed to write lefthook.yml: ${error instanceof Error ? error.message : String(error)}`,
|
|
254
|
+
method: "manual"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (isLefthookAvailable()) try {
|
|
258
|
+
execSync("npx lefthook install", {
|
|
259
|
+
cwd: projectRoot,
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
timeout: 3e4
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
success: true,
|
|
265
|
+
message: `Lefthook hook installed successfully in ${hookName}`,
|
|
266
|
+
method: "lefthook"
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
message: `Hook added to lefthook.yml. Failed to run lefthook install: ${error instanceof Error ? error.message : String(error)}\nPlease run: npx lefthook install`,
|
|
272
|
+
method: "lefthook"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
success: true,
|
|
277
|
+
message: "Hook added to lefthook.yml. Please run: npx lefthook install",
|
|
278
|
+
method: "lefthook"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Install agent-watch hook using husky CLI.
|
|
283
|
+
* Runs: npx husky add .husky/{hookName} "npx agent-watch run"
|
|
284
|
+
*/
|
|
285
|
+
function installHuskyHook(projectRoot, hookName) {
|
|
286
|
+
const huskyDir = join(projectRoot, ".husky");
|
|
287
|
+
if (!existsSync(huskyDir)) return {
|
|
288
|
+
success: false,
|
|
289
|
+
message: ".husky directory not found. Initialize husky first with: npx husky init",
|
|
290
|
+
method: "manual"
|
|
291
|
+
};
|
|
292
|
+
const hookPath = join(huskyDir, hookName);
|
|
293
|
+
if (existsSync(hookPath)) try {
|
|
294
|
+
if (readFileSync(hookPath, "utf-8").includes("agent-watch run")) return {
|
|
295
|
+
success: true,
|
|
296
|
+
message: `Husky hook already configured in .husky/${hookName}`,
|
|
297
|
+
method: "husky"
|
|
298
|
+
};
|
|
299
|
+
} catch {}
|
|
300
|
+
if (isHuskyAvailable()) try {
|
|
301
|
+
execSync(`npx husky add .husky/${hookName} "npx agent-watch run"`, {
|
|
302
|
+
cwd: projectRoot,
|
|
303
|
+
stdio: "pipe",
|
|
304
|
+
timeout: 3e4
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
message: `Husky hook installed successfully in .husky/${hookName}`,
|
|
309
|
+
method: "husky"
|
|
310
|
+
};
|
|
311
|
+
} catch {}
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
message: `Husky detected. Run: npx husky add .husky/${hookName} "npx agent-watch run"`,
|
|
315
|
+
method: "husky"
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Install the agent-watch hook into the git hooks directory.
|
|
320
|
+
* If a hook file already exists, appends our section.
|
|
321
|
+
* If lefthook or husky is detected, provides instructions instead.
|
|
322
|
+
*/
|
|
323
|
+
function installGitHook(projectRoot, gitRoot, trigger) {
|
|
324
|
+
const hookName = getGitHookName(trigger);
|
|
325
|
+
if (hasLefthook(projectRoot)) return installLefthookHook(projectRoot, hookName);
|
|
326
|
+
if (hasHusky(projectRoot)) return installHuskyHook(projectRoot, hookName);
|
|
327
|
+
const hooksDir = getGitHooksDir(gitRoot);
|
|
328
|
+
const hookPath = join(hooksDir, hookName);
|
|
329
|
+
try {
|
|
330
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
331
|
+
let hookContent;
|
|
332
|
+
if (existsSync(hookPath)) {
|
|
333
|
+
const existing = readFileSync(hookPath, "utf-8");
|
|
334
|
+
if (existing.includes(HOOK_MARKER_START)) {
|
|
335
|
+
const regex = new RegExp(`${escapeRegex(HOOK_MARKER_START)}[\\s\\S]*?${escapeRegex(HOOK_MARKER_END)}`);
|
|
336
|
+
hookContent = existing.replace(regex, getHookScript());
|
|
337
|
+
} else hookContent = `${existing.trimEnd()}\n\n${getHookScript()}\n`;
|
|
338
|
+
} else hookContent = `#!/bin/sh\n\n${getHookScript()}\n`;
|
|
339
|
+
writeFileSync(hookPath, hookContent, "utf-8");
|
|
340
|
+
chmodSync(hookPath, 493);
|
|
341
|
+
return {
|
|
342
|
+
success: true,
|
|
343
|
+
message: `Git ${hookName} hook installed successfully.`,
|
|
344
|
+
method: "direct"
|
|
345
|
+
};
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
message: `Failed to install git hook: ${error instanceof Error ? error.message : String(error)}`,
|
|
350
|
+
method: "manual"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function escapeRegex(str) {
|
|
355
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/utils/copilot.ts
|
|
360
|
+
const COPILOT_CONFIG_FILE = join(join(homedir(), ".copilot"), "config.json");
|
|
361
|
+
/**
|
|
362
|
+
* Check if the standalone GitHub Copilot CLI is installed
|
|
363
|
+
*/
|
|
364
|
+
function isCopilotInstalled() {
|
|
365
|
+
try {
|
|
366
|
+
const output = execSync("copilot -v", {
|
|
367
|
+
encoding: "utf-8",
|
|
368
|
+
stdio: "pipe"
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
installed: true,
|
|
372
|
+
version: /(\d+\.\d+\.\d+)/.exec(output)?.[1]
|
|
373
|
+
};
|
|
374
|
+
} catch {
|
|
375
|
+
return { installed: false };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Check if the user is authenticated with GitHub Copilot CLI
|
|
380
|
+
* by reading the config file at ~/.copilot/config.json
|
|
381
|
+
*/
|
|
382
|
+
function getCopilotAuth() {
|
|
383
|
+
try {
|
|
384
|
+
if (!existsSync(COPILOT_CONFIG_FILE)) return { authenticated: false };
|
|
385
|
+
const configContent = readFileSync(COPILOT_CONFIG_FILE, "utf-8");
|
|
386
|
+
const config = JSON.parse(configContent);
|
|
387
|
+
if (config.logged_in_users && config.logged_in_users.length > 0) return {
|
|
388
|
+
authenticated: true,
|
|
389
|
+
username: (config.last_logged_in_user ?? config.logged_in_users[0]).login
|
|
390
|
+
};
|
|
391
|
+
return { authenticated: false };
|
|
392
|
+
} catch {
|
|
393
|
+
return { authenticated: false };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check the status of GitHub Copilot CLI
|
|
398
|
+
*/
|
|
399
|
+
function checkCopilotStatus() {
|
|
400
|
+
const { installed, version } = isCopilotInstalled();
|
|
401
|
+
const { authenticated, username } = getCopilotAuth();
|
|
402
|
+
return {
|
|
403
|
+
installed,
|
|
404
|
+
authenticated,
|
|
405
|
+
version,
|
|
406
|
+
username
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Prompt the user to install GitHub Copilot CLI
|
|
411
|
+
*/
|
|
412
|
+
async function promptInstallCopilot() {
|
|
413
|
+
logger.step("GitHub Copilot CLI is not installed. Please install it first:");
|
|
414
|
+
logger.info(" macOS: brew install gh-copilot");
|
|
415
|
+
logger.info(" npm: npm install -g @githubnext/github-copilot-cli");
|
|
416
|
+
logger.info(" See: https://docs.github.com/en/copilot/how-tos/copilot-cli");
|
|
417
|
+
logger.blank();
|
|
418
|
+
logger.info("After installation, please run 'agent-watch init' again.");
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Authenticate with GitHub Copilot CLI
|
|
423
|
+
*/
|
|
424
|
+
async function authenticateCopilot() {
|
|
425
|
+
try {
|
|
426
|
+
logger.step("Authenticating with GitHub Copilot CLI...");
|
|
427
|
+
logger.info("Follow the prompts to authenticate:");
|
|
428
|
+
execSync("copilot login", { stdio: "inherit" });
|
|
429
|
+
logger.success("GitHub Copilot CLI authenticated successfully!");
|
|
430
|
+
return true;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.error("Failed to authenticate with GitHub Copilot CLI");
|
|
433
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Setup GitHub Copilot CLI - install and configure if needed
|
|
439
|
+
*/
|
|
440
|
+
async function setupGithubCopilotCli() {
|
|
441
|
+
const status = checkCopilotStatus();
|
|
442
|
+
if (!status.installed) return await promptInstallCopilot();
|
|
443
|
+
if (!status.authenticated) {
|
|
444
|
+
logger.warn("GitHub Copilot CLI is not authenticated");
|
|
445
|
+
if (!await authenticateCopilot()) return false;
|
|
446
|
+
} else logger.success(`Copilot CLI authenticated as ${status.username}`);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Create missing agent files using Copilot CLI.
|
|
451
|
+
* For .github/copilot-instructions.md, uses `copilot init`.
|
|
452
|
+
* For other files, uses `copilot -p` to generate content based on the codebase.
|
|
453
|
+
*/
|
|
454
|
+
function createMissingAgentFiles(projectRoot, selectedFiles, detectedFiles) {
|
|
455
|
+
const missingFiles = selectedFiles.filter((filePath) => {
|
|
456
|
+
return !detectedFiles.find((d) => d.pattern.path === filePath)?.exists;
|
|
457
|
+
});
|
|
458
|
+
if (missingFiles.length === 0) return;
|
|
459
|
+
for (const filePath of missingFiles) {
|
|
460
|
+
const label = KNOWN_AGENT_FILES.find((f) => f.path === filePath)?.label ?? filePath;
|
|
461
|
+
try {
|
|
462
|
+
logger.step(`Creating ${label}...`);
|
|
463
|
+
if (filePath === ".github/copilot-instructions.md") execSync("copilot init", {
|
|
464
|
+
cwd: projectRoot,
|
|
465
|
+
stdio: "pipe",
|
|
466
|
+
timeout: 12e4
|
|
467
|
+
});
|
|
468
|
+
else execSync(`copilot -p "Create a ${filePath} file for this project. Analyze the codebase to understand the project structure, tech stack, and conventions. Write the file directly." --allow-all-tools -s`, {
|
|
469
|
+
cwd: projectRoot,
|
|
470
|
+
stdio: "pipe",
|
|
471
|
+
timeout: 12e4
|
|
472
|
+
});
|
|
473
|
+
logger.success(`Created ${label}`);
|
|
474
|
+
} catch {
|
|
475
|
+
logger.warn(`Failed to create ${label}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/utils/gitignore.ts
|
|
482
|
+
/**
|
|
483
|
+
* Add entries to .gitignore if they don't already exist
|
|
484
|
+
*/
|
|
485
|
+
function addToGitignore(projectRoot, entries) {
|
|
486
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
487
|
+
if (!existsSync(projectRoot)) return;
|
|
488
|
+
let content = "";
|
|
489
|
+
if (existsSync(gitignorePath)) content = readFileSync(gitignorePath, "utf-8");
|
|
490
|
+
const lines = content.split("\n");
|
|
491
|
+
const entriesToAdd = [];
|
|
492
|
+
for (const entry of entries) if (!lines.some((line) => {
|
|
493
|
+
const trimmed = line.trim();
|
|
494
|
+
return trimmed === entry || trimmed === `/${entry}`;
|
|
495
|
+
})) entriesToAdd.push(entry);
|
|
496
|
+
if (entriesToAdd.length === 0) return;
|
|
497
|
+
const newContent = `${content.trim()}\n\n# agent-watch\n${entriesToAdd.join("\n")}\n`;
|
|
498
|
+
try {
|
|
499
|
+
writeFileSync(gitignorePath, newContent, "utf-8");
|
|
500
|
+
} catch {}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/commands/init.ts
|
|
505
|
+
async function initCommand() {
|
|
506
|
+
logger.asciiArt();
|
|
507
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
508
|
+
if (!gitRoot) {
|
|
509
|
+
logger.error("Not inside a git repository. Please run this command from a git repository.");
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
const projectRoot = gitRoot;
|
|
513
|
+
if (loadConfig(projectRoot)) {
|
|
514
|
+
if (!await confirm({
|
|
515
|
+
message: "An existing agent-watch configuration was found. Do you want to overwrite it?",
|
|
516
|
+
default: false
|
|
517
|
+
})) {
|
|
518
|
+
logger.info("Init cancelled. Existing configuration preserved.");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const detectedFiles = detectAgentFiles(projectRoot);
|
|
523
|
+
const existingFiles = detectedFiles.filter((f) => f.exists);
|
|
524
|
+
if (existingFiles.length > 0) logger.success(`Found ${existingFiles.length} existing agent file(s)`);
|
|
525
|
+
const selectedFiles = await checkbox({
|
|
526
|
+
message: "Which agent files should agent-watch manage?",
|
|
527
|
+
pageSize: FILE_SELECTION_PAGE_SIZE,
|
|
528
|
+
choices: KNOWN_AGENT_FILES.map((pattern) => {
|
|
529
|
+
const exists = detectedFiles.find((d) => d.pattern.path === pattern.path)?.exists ?? false;
|
|
530
|
+
return {
|
|
531
|
+
name: exists ? ` ${pattern.label} ✓` : ` ${pattern.label}`,
|
|
532
|
+
value: pattern.path,
|
|
533
|
+
checked: exists
|
|
534
|
+
};
|
|
535
|
+
})
|
|
536
|
+
});
|
|
537
|
+
if (selectedFiles.length === 0) {
|
|
538
|
+
logger.warn("No files selected. You can re-run 'agent-watch init' to configure.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
createMissingAgentFiles(projectRoot, selectedFiles, detectedFiles);
|
|
542
|
+
const contextOptions = await checkbox({
|
|
543
|
+
message: "What should agent-watch track?",
|
|
544
|
+
choices: [{
|
|
545
|
+
name: " File changes (git diff, modified files)",
|
|
546
|
+
value: "watchFileChanges",
|
|
547
|
+
checked: true
|
|
548
|
+
}, {
|
|
549
|
+
name: " Chat sessions (Copilot conversation context)",
|
|
550
|
+
value: "includeChatSession",
|
|
551
|
+
checked: true
|
|
552
|
+
}]
|
|
553
|
+
});
|
|
554
|
+
const watchFileChanges = contextOptions.includes("watchFileChanges");
|
|
555
|
+
const includeChatSession = contextOptions.includes("includeChatSession");
|
|
556
|
+
const hookTrigger = await select({
|
|
557
|
+
message: "When should agent-watch trigger?",
|
|
558
|
+
choices: [{
|
|
559
|
+
name: "After git commit (post-commit hook)",
|
|
560
|
+
value: "commit"
|
|
561
|
+
}, {
|
|
562
|
+
name: "Before git push (pre-push hook)",
|
|
563
|
+
value: "push"
|
|
564
|
+
}],
|
|
565
|
+
default: "commit"
|
|
566
|
+
});
|
|
567
|
+
const selectedAgents = await checkbox({
|
|
568
|
+
message: "Which AI agents would you like to configure?",
|
|
569
|
+
choices: SUPPORTED_AI_AGENTS.map((agent) => ({
|
|
570
|
+
name: ` ${agent.name}`,
|
|
571
|
+
value: agent.value,
|
|
572
|
+
checked: true
|
|
573
|
+
}))
|
|
574
|
+
});
|
|
575
|
+
if (selectedAgents.includes("github-copilot-cli")) {
|
|
576
|
+
if (!await setupGithubCopilotCli()) logger.warn("Copilot CLI setup incomplete. You can set it up manually later.");
|
|
577
|
+
}
|
|
578
|
+
saveConfig(projectRoot, createDefaultConfig({
|
|
579
|
+
agentFiles: selectedFiles,
|
|
580
|
+
watchFileChanges,
|
|
581
|
+
includeChatSession,
|
|
582
|
+
hookTrigger,
|
|
583
|
+
agents: selectedAgents
|
|
584
|
+
}));
|
|
585
|
+
addToGitignore(projectRoot, [AGENT_WATCH_DIR]);
|
|
586
|
+
const hookResult = installGitHook(projectRoot, gitRoot, hookTrigger);
|
|
587
|
+
if (hookResult.success) if (hookResult.message.includes("Please run:") || hookResult.message.includes("please add manually")) logger.warn(hookResult.message);
|
|
588
|
+
else logger.success(hookResult.message);
|
|
589
|
+
else logger.error(hookResult.message);
|
|
590
|
+
logger.blank();
|
|
591
|
+
logger.success("Setup complete!");
|
|
592
|
+
logger.info(`Files: ${selectedFiles.join(", ")} • Hook: ${hookTrigger} • Agents: ${selectedAgents.join(", ")}`);
|
|
593
|
+
logger.blank();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/commands/run.ts
|
|
598
|
+
/**
|
|
599
|
+
* Get git commit message
|
|
600
|
+
*/
|
|
601
|
+
function getCommitMessage() {
|
|
602
|
+
try {
|
|
603
|
+
return execSync("git log -1 --pretty=%B", {
|
|
604
|
+
encoding: "utf-8",
|
|
605
|
+
stdio: "pipe"
|
|
606
|
+
}).trim();
|
|
607
|
+
} catch {
|
|
608
|
+
return "";
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get git diff stats (summary, not full diff)
|
|
613
|
+
*/
|
|
614
|
+
function getGitDiffStats() {
|
|
615
|
+
try {
|
|
616
|
+
return execSync("git diff HEAD~1..HEAD --stat", {
|
|
617
|
+
encoding: "utf-8",
|
|
618
|
+
stdio: "pipe"
|
|
619
|
+
}).trim();
|
|
620
|
+
} catch {
|
|
621
|
+
return "";
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get list of modified files
|
|
626
|
+
*/
|
|
627
|
+
function getModifiedFiles() {
|
|
628
|
+
try {
|
|
629
|
+
return execSync("git diff --name-only HEAD~1..HEAD", {
|
|
630
|
+
encoding: "utf-8",
|
|
631
|
+
stdio: "pipe"
|
|
632
|
+
}).trim().split("\n").filter(Boolean);
|
|
633
|
+
} catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Check if a file path should be ignored (doesn't trigger agent-watch analysis)
|
|
639
|
+
*/
|
|
640
|
+
function shouldIgnoreFile(filePath) {
|
|
641
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
642
|
+
for (const pattern of IGNORED_FILE_PATTERNS) {
|
|
643
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
644
|
+
if (normalizedPath === normalizedPattern) return true;
|
|
645
|
+
if (normalizedPath.startsWith(`${normalizedPattern}/`)) return true;
|
|
646
|
+
if (normalizedPath.split("/").pop() === normalizedPattern) return true;
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Check if all modified files should be ignored
|
|
652
|
+
*/
|
|
653
|
+
function shouldSkipAnalysis(modifiedFiles) {
|
|
654
|
+
if (modifiedFiles.length === 0) return true;
|
|
655
|
+
return modifiedFiles.every(shouldIgnoreFile);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Use Copilot CLI to intelligently update agent file content
|
|
659
|
+
*/
|
|
660
|
+
function updateAgentFileWithCopilot(projectRoot, context, agentFilePath, currentContent) {
|
|
661
|
+
try {
|
|
662
|
+
const output = execSync(`copilot -p "${`You are maintaining ${agentFilePath}, a living document of patterns, conventions, and code rules.
|
|
663
|
+
|
|
664
|
+
Your task:
|
|
665
|
+
1. Read the CURRENT content of this file carefully
|
|
666
|
+
2. Analyze the RECENT changes and conversations below
|
|
667
|
+
3. Determine if there are new patterns, rules, or conventions to document
|
|
668
|
+
4. If updates are needed, output the COMPLETE UPDATED file content
|
|
669
|
+
5. If no meaningful updates needed, output exactly: NO_UPDATE
|
|
670
|
+
|
|
671
|
+
Guidelines:
|
|
672
|
+
- Intelligently merge new insights into existing sections
|
|
673
|
+
- Update or refine existing rules if patterns have evolved
|
|
674
|
+
- Remove outdated information
|
|
675
|
+
- Keep it concise - no code snippets unless absolutely necessary
|
|
676
|
+
- Maintain the file's existing structure and tone
|
|
677
|
+
- Focus on patterns, conventions, and learnings - not changelogs
|
|
678
|
+
|
|
679
|
+
CURRENT FILE CONTENT:
|
|
680
|
+
${currentContent}
|
|
681
|
+
|
|
682
|
+
RECENT CONTEXT:
|
|
683
|
+
${context}
|
|
684
|
+
|
|
685
|
+
Output the complete updated file, or NO_UPDATE if nothing significant to add:`.replaceAll("\"", String.raw`\"`)}" -s`, {
|
|
686
|
+
cwd: projectRoot,
|
|
687
|
+
encoding: "utf-8",
|
|
688
|
+
stdio: "pipe",
|
|
689
|
+
timeout: 9e4
|
|
690
|
+
}).trim();
|
|
691
|
+
if (output === "NO_UPDATE" || output.length === 0 || output === currentContent) return null;
|
|
692
|
+
return output;
|
|
693
|
+
} catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Run command - update agent files with extracted patterns and learnings
|
|
699
|
+
*/
|
|
700
|
+
async function runCommand() {
|
|
701
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
702
|
+
if (!gitRoot) {
|
|
703
|
+
logger.error("Not inside a git repository");
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
const config = loadConfig(gitRoot);
|
|
707
|
+
if (!config) {
|
|
708
|
+
logger.error("No agent-watch configuration found. Run 'agent-watch init' first.");
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
if (config.agentFiles.length === 0) {
|
|
712
|
+
logger.warn("No agent files configured");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const contextParts = [];
|
|
716
|
+
if (config.watchFileChanges) {
|
|
717
|
+
const modifiedFiles = getModifiedFiles();
|
|
718
|
+
if (shouldSkipAnalysis(modifiedFiles)) {
|
|
719
|
+
logger.info("Skipping analysis - only config/documentation files changed");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const commitMessage = getCommitMessage();
|
|
723
|
+
const diffStats = getGitDiffStats();
|
|
724
|
+
if (modifiedFiles.length > 0) {
|
|
725
|
+
const fileContext = [
|
|
726
|
+
commitMessage ? `Commit: ${commitMessage}` : "",
|
|
727
|
+
`Files changed: ${modifiedFiles.join(", ")}`,
|
|
728
|
+
diffStats ? `Stats:\n${diffStats}` : ""
|
|
729
|
+
].filter(Boolean).join("\n");
|
|
730
|
+
contextParts.push(fileContext);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (config.includeChatSession) {
|
|
734
|
+
const sessionContext = processNewSessions(gitRoot);
|
|
735
|
+
if (sessionContext) contextParts.push(`Conversations:\n${sessionContext}`);
|
|
736
|
+
}
|
|
737
|
+
if (contextParts.length === 0) {
|
|
738
|
+
logger.info("No new context to analyze");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const context = contextParts.join("\n\n");
|
|
742
|
+
logger.step("Analyzing context for patterns and rules...");
|
|
743
|
+
let updatedCount = 0;
|
|
744
|
+
for (const filePath of config.agentFiles) {
|
|
745
|
+
const fullPath = join(gitRoot, filePath);
|
|
746
|
+
const updatedContent = updateAgentFileWithCopilot(gitRoot, context, filePath, readFileSync(fullPath, "utf-8"));
|
|
747
|
+
if (updatedContent) {
|
|
748
|
+
writeFileSync(fullPath, updatedContent, "utf-8");
|
|
749
|
+
logger.success(`Updated ${filePath}`);
|
|
750
|
+
updatedCount++;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (updatedCount > 0) logger.success(`Updated ${updatedCount} agent file(s) with new insights`);
|
|
754
|
+
else logger.info("No significant patterns found to update");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/cli.ts
|
|
759
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
760
|
+
program.name("agent-watch").description("Keep your AI agent configuration files in sync with your codebase").version(version);
|
|
761
|
+
program.command("init").description("Initialize agent-watch in the current project").action(async () => {
|
|
762
|
+
await initCommand();
|
|
763
|
+
});
|
|
764
|
+
program.command("run").description("Update agent files with recent changes and chat sessions").action(async () => {
|
|
765
|
+
await runCommand();
|
|
766
|
+
});
|
|
767
|
+
program.parse();
|
|
768
|
+
|
|
769
|
+
//#endregion
|
|
770
|
+
export { };
|
|
771
|
+
//# sourceMappingURL=cli.js.map
|