agentsys 5.4.0 → 5.5.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/.claude-plugin/marketplace.json +36 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +158 -17
- package/lib/adapter-transforms.js +1 -366
- package/lib/binary/index.js +398 -0
- package/lib/binary/version.js +16 -0
- package/lib/collectors/git.js +139 -0
- package/lib/collectors/index.js +10 -1
- package/lib/cross-platform/index.js +3 -13
- package/lib/discovery/index.js +1 -48
- package/lib/index.js +2 -0
- package/lib/patterns/cli-enhancers.js +2 -11
- package/lib/platform/state-dir.js +2 -16
- package/package.json +1 -1
- package/scripts/gen-adapters.js +4 -2
- package/scripts/generate-docs.js +94 -25
- package/scripts/validate-cross-platform-docs.js +5 -2
- package/site/content.json +1 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Binary resolver for the agent-analyzer Rust binary.
|
|
5
|
+
*
|
|
6
|
+
* Handles lazy downloading and execution. Since Claude Code plugins have no
|
|
7
|
+
* postinstall hooks, the binary is downloaded at runtime on first use.
|
|
8
|
+
*
|
|
9
|
+
* @module lib/binary
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const cp = require('child_process');
|
|
17
|
+
const { promisify } = require('util');
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(cp.execFile);
|
|
20
|
+
|
|
21
|
+
const { ANALYZER_MIN_VERSION, BINARY_NAME, GITHUB_REPO } = require('./version');
|
|
22
|
+
|
|
23
|
+
const PLATFORM_MAP = {
|
|
24
|
+
'darwin-arm64': 'aarch64-apple-darwin',
|
|
25
|
+
'darwin-x64': 'x86_64-apple-darwin',
|
|
26
|
+
'linux-x64': 'x86_64-unknown-linux-gnu',
|
|
27
|
+
'linux-arm64': 'aarch64-unknown-linux-gnu',
|
|
28
|
+
'win32-x64': 'x86_64-pc-windows-msvc'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Path helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the expected path to the agent-analyzer binary.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function getBinaryPath() {
|
|
40
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
41
|
+
return path.join(os.homedir(), '.agent-sh', 'bin', BINARY_NAME + ext);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the Rust target triple for the current platform.
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
function getPlatformKey() {
|
|
49
|
+
const key = process.platform + '-' + process.arch;
|
|
50
|
+
return PLATFORM_MAP[key] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Version helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compare a version string against a minimum requirement.
|
|
59
|
+
* @param {string} version
|
|
60
|
+
* @param {string} minVersion
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function meetsMinimumVersion(version, minVersion) {
|
|
64
|
+
if (!version) return false;
|
|
65
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
66
|
+
if (!match) return false;
|
|
67
|
+
const parts = match.slice(1).map(Number);
|
|
68
|
+
const req = minVersion.split('.').map(Number);
|
|
69
|
+
if (parts[0] > req[0]) return true;
|
|
70
|
+
if (parts[0] < req[0]) return false;
|
|
71
|
+
if (parts[1] > req[1]) return true;
|
|
72
|
+
if (parts[1] < req[1]) return false;
|
|
73
|
+
return parts[2] >= req[2];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run the binary with --version and return the version string, or null on failure.
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
function getVersion() {
|
|
81
|
+
const binPath = getBinaryPath();
|
|
82
|
+
if (!fs.existsSync(binPath)) return null;
|
|
83
|
+
try {
|
|
84
|
+
const out = cp.execFileSync(binPath, ['--version'], {
|
|
85
|
+
timeout: 5000,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
+
windowsHide: true
|
|
89
|
+
});
|
|
90
|
+
const match = out.trim().match(/(\d+\.\d+\.\d+)/);
|
|
91
|
+
return match ? match[1] : out.trim();
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Availability checks
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sync check: returns true if the binary exists and meets the minimum version.
|
|
103
|
+
* Does NOT download.
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function isAvailable() {
|
|
107
|
+
const binPath = getBinaryPath();
|
|
108
|
+
if (!fs.existsSync(binPath)) return false;
|
|
109
|
+
const ver = getVersion();
|
|
110
|
+
return meetsMinimumVersion(ver, ANALYZER_MIN_VERSION);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Async check: returns true if the binary exists and meets the minimum version.
|
|
115
|
+
* Does NOT download.
|
|
116
|
+
* @returns {Promise<boolean>}
|
|
117
|
+
*/
|
|
118
|
+
async function isAvailableAsync() {
|
|
119
|
+
return isAvailable();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Download
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build the GitHub release download URL.
|
|
128
|
+
* @param {string} ver
|
|
129
|
+
* @param {string} platformKey
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
function buildDownloadUrl(ver, platformKey) {
|
|
133
|
+
const ext = process.platform === 'win32' ? '.zip' : '.tar.gz';
|
|
134
|
+
return 'https://github.com/' + GITHUB_REPO + '/releases/download/v' + ver + '/' + BINARY_NAME + '-' + platformKey + ext;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Download a URL to a Buffer, following up to 5 redirects.
|
|
139
|
+
* Supports GITHUB_TOKEN / GH_TOKEN for auth.
|
|
140
|
+
* @param {string} url
|
|
141
|
+
* @returns {Promise<Buffer>}
|
|
142
|
+
*/
|
|
143
|
+
function downloadToBuffer(url) {
|
|
144
|
+
return new Promise(function(resolve, reject) {
|
|
145
|
+
const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
146
|
+
|
|
147
|
+
function request(reqUrl, redirectCount) {
|
|
148
|
+
if (redirectCount > 5) {
|
|
149
|
+
reject(new Error('Too many redirects fetching from ' + url));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const headers = {
|
|
153
|
+
'User-Agent': 'agent-core/binary-resolver',
|
|
154
|
+
'Accept': 'application/octet-stream'
|
|
155
|
+
};
|
|
156
|
+
if (ghToken) headers['Authorization'] = 'Bearer ' + ghToken;
|
|
157
|
+
|
|
158
|
+
https.get(reqUrl, { headers: headers }, function(res) {
|
|
159
|
+
const sc = res.statusCode;
|
|
160
|
+
if (sc === 301 || sc === 302 || sc === 307 || sc === 308) {
|
|
161
|
+
res.resume();
|
|
162
|
+
request(res.headers.location, redirectCount + 1);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (sc !== 200) {
|
|
166
|
+
res.resume();
|
|
167
|
+
const hint = sc === 403 ? ' (rate limited - set GITHUB_TOKEN env var)' : '';
|
|
168
|
+
reject(new Error('HTTP ' + sc + hint + ' fetching ' + reqUrl));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const chunks = [];
|
|
172
|
+
res.on('data', function(chunk) { chunks.push(chunk); });
|
|
173
|
+
res.on('end', function() { resolve(Buffer.concat(chunks)); });
|
|
174
|
+
res.on('error', reject);
|
|
175
|
+
}).on('error', reject);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
request(url, 0);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Extract a tar.gz buffer into a directory using the system tar command.
|
|
184
|
+
* @param {Buffer} buf
|
|
185
|
+
* @param {string} destDir
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
function extractTarGz(buf, destDir) {
|
|
189
|
+
return new Promise(function(resolve, reject) {
|
|
190
|
+
const tarDest = process.platform === 'win32' ? destDir.replace(/\\/g, '/') : destDir;
|
|
191
|
+
const tar = cp.spawn('tar', ['xz', '-C', tarDest], {
|
|
192
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
193
|
+
});
|
|
194
|
+
let stderr = '';
|
|
195
|
+
tar.stderr.on('data', function(d) { stderr += d; });
|
|
196
|
+
tar.stdin.write(buf);
|
|
197
|
+
tar.stdin.end();
|
|
198
|
+
tar.on('close', function(code) {
|
|
199
|
+
if (code !== 0) {
|
|
200
|
+
reject(new Error('tar extraction failed (code ' + code + '): ' + stderr));
|
|
201
|
+
} else {
|
|
202
|
+
resolve();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
tar.on('error', reject);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extract a zip buffer into a directory using PowerShell Expand-Archive (Windows).
|
|
211
|
+
* @param {Buffer} buf
|
|
212
|
+
* @param {string} destDir
|
|
213
|
+
* @param {string} binaryName
|
|
214
|
+
* @returns {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
function extractZip(buf, destDir, binaryName) {
|
|
217
|
+
return new Promise(function(resolve, reject) {
|
|
218
|
+
const tmpZip = path.join(os.tmpdir(), binaryName + '-' + Date.now() + '.zip');
|
|
219
|
+
fs.writeFileSync(tmpZip, buf);
|
|
220
|
+
const cmd = 'Expand-Archive -Path \'' + tmpZip + '\' -DestinationPath \'' + destDir + '\' -Force';
|
|
221
|
+
const ps = cp.spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', cmd], {
|
|
222
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
223
|
+
});
|
|
224
|
+
let stderr = '';
|
|
225
|
+
ps.stderr.on('data', function(d) { stderr += d; });
|
|
226
|
+
ps.on('close', function(code) {
|
|
227
|
+
try { fs.unlinkSync(tmpZip); } catch (e) { /* ignore */ }
|
|
228
|
+
if (code !== 0) {
|
|
229
|
+
reject(new Error('zip extraction failed (code ' + code + '): ' + stderr));
|
|
230
|
+
} else {
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
ps.on('error', reject);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Download and install the binary for the current platform into ~/.agent-sh/bin/.
|
|
240
|
+
* @param {string} ver
|
|
241
|
+
* @returns {Promise<string>}
|
|
242
|
+
*/
|
|
243
|
+
async function downloadBinary(ver) {
|
|
244
|
+
const platformKey = getPlatformKey();
|
|
245
|
+
if (!platformKey) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'Unsupported platform: ' + process.platform + '-' + process.arch + '. ' +
|
|
248
|
+
'Supported platforms: ' + Object.keys(PLATFORM_MAP).join(', ')
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const url = buildDownloadUrl(ver, platformKey);
|
|
253
|
+
process.stderr.write('Downloading ' + BINARY_NAME + ' v' + ver + ' for ' + platformKey + '...' + '\n');
|
|
254
|
+
|
|
255
|
+
const binPath = getBinaryPath();
|
|
256
|
+
const binDir = path.dirname(binPath);
|
|
257
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
258
|
+
|
|
259
|
+
let buf;
|
|
260
|
+
try {
|
|
261
|
+
buf = await downloadToBuffer(url);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'Failed to download ' + BINARY_NAME + ':\n' +
|
|
265
|
+
' URL: ' + url + '\n' +
|
|
266
|
+
' Error: ' + err.message + '\n\n' +
|
|
267
|
+
'To install manually:\n' +
|
|
268
|
+
' 1. Download: ' + url + '\n' +
|
|
269
|
+
' 2. Extract the binary to: ' + binDir + '\n' +
|
|
270
|
+
' 3. Ensure it is named: ' + path.basename(binPath)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (process.platform === 'win32') {
|
|
275
|
+
await extractZip(buf, binDir, path.basename(binPath));
|
|
276
|
+
} else {
|
|
277
|
+
await extractTarGz(buf, binDir);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (process.platform !== 'win32') {
|
|
281
|
+
fs.chmodSync(binPath, 0o755);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const installedVer = getVersion();
|
|
285
|
+
if (!installedVer) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
BINARY_NAME + ' was downloaded to ' + binPath + ' but could not be executed. ' +
|
|
288
|
+
'Check the file is a valid binary for this platform.'
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return binPath;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Public API
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Ensure the binary exists and meets the minimum version. Downloads if needed.
|
|
301
|
+
* @param {Object} [options]
|
|
302
|
+
* @param {string} [options.version]
|
|
303
|
+
* @returns {Promise<string>}
|
|
304
|
+
*/
|
|
305
|
+
async function ensureBinary(options) {
|
|
306
|
+
const opts = options || {};
|
|
307
|
+
const targetVer = opts.version || ANALYZER_MIN_VERSION;
|
|
308
|
+
const binPath = getBinaryPath();
|
|
309
|
+
|
|
310
|
+
if (fs.existsSync(binPath)) {
|
|
311
|
+
const ver = getVersion();
|
|
312
|
+
if (meetsMinimumVersion(ver, ANALYZER_MIN_VERSION)) {
|
|
313
|
+
return binPath;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return downloadBinary(targetVer);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Sync version of ensureBinary. Downloads if needed via a child node process.
|
|
322
|
+
* Prefer ensureBinary() unless a sync API is strictly required.
|
|
323
|
+
* @param {Object} [options]
|
|
324
|
+
* @param {string} [options.version]
|
|
325
|
+
* @returns {string}
|
|
326
|
+
*/
|
|
327
|
+
function ensureBinarySync(options) {
|
|
328
|
+
const binPath = getBinaryPath();
|
|
329
|
+
|
|
330
|
+
if (fs.existsSync(binPath)) {
|
|
331
|
+
const ver = getVersion();
|
|
332
|
+
if (meetsMinimumVersion(ver, ANALYZER_MIN_VERSION)) {
|
|
333
|
+
return binPath;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const targetVer = (options && options.version) || ANALYZER_MIN_VERSION;
|
|
338
|
+
const selfPath = __filename;
|
|
339
|
+
const helperLines = [
|
|
340
|
+
'var b = require(' + JSON.stringify(selfPath) + ');',
|
|
341
|
+
'b.ensureBinary({ version: ' + JSON.stringify(targetVer) + ' })',
|
|
342
|
+
' .then(function(p) { process.stdout.write(p); })',
|
|
343
|
+
' .catch(function(e) { process.stderr.write(e.message); process.exit(1); });'
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const result = cp.execFileSync(process.execPath, ['-e', helperLines.join('\n')], {
|
|
348
|
+
encoding: 'utf8',
|
|
349
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
350
|
+
timeout: 120000
|
|
351
|
+
});
|
|
352
|
+
return result.trim() || binPath;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
throw new Error('Failed to ensure binary (sync): ' + err.message);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Run agent-analyzer with the given arguments (sync). Downloads binary if needed.
|
|
360
|
+
* @param {string[]} args
|
|
361
|
+
* @param {Object} [options]
|
|
362
|
+
* @returns {string}
|
|
363
|
+
*/
|
|
364
|
+
function runAnalyzer(args, options) {
|
|
365
|
+
const binPath = ensureBinarySync();
|
|
366
|
+
const opts = Object.assign({ encoding: 'utf8', windowsHide: true }, options);
|
|
367
|
+
if (!opts.stdio) opts.stdio = ['pipe', 'pipe', 'pipe'];
|
|
368
|
+
const result = cp.execFileSync(binPath, args, opts);
|
|
369
|
+
return typeof result === 'string' ? result : result.toString('utf8');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Run agent-analyzer with the given arguments asynchronously. Downloads binary if needed.
|
|
374
|
+
* @param {string[]} args
|
|
375
|
+
* @param {Object} [options]
|
|
376
|
+
* @returns {Promise<string>}
|
|
377
|
+
*/
|
|
378
|
+
async function runAnalyzerAsync(args, options) {
|
|
379
|
+
const binPath = await ensureBinary();
|
|
380
|
+
const opts = Object.assign({ encoding: 'utf8', windowsHide: true }, options);
|
|
381
|
+
const result = await execFileAsync(binPath, args, opts);
|
|
382
|
+
return result.stdout;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
ensureBinary,
|
|
387
|
+
ensureBinarySync,
|
|
388
|
+
runAnalyzer,
|
|
389
|
+
runAnalyzerAsync,
|
|
390
|
+
getBinaryPath,
|
|
391
|
+
getVersion,
|
|
392
|
+
getPlatformKey,
|
|
393
|
+
isAvailable,
|
|
394
|
+
isAvailableAsync,
|
|
395
|
+
meetsMinimumVersion,
|
|
396
|
+
buildDownloadUrl,
|
|
397
|
+
PLATFORM_MAP
|
|
398
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Minimum binary version required by this version of agent-core
|
|
4
|
+
const ANALYZER_MIN_VERSION = '0.1.0';
|
|
5
|
+
|
|
6
|
+
// Binary name
|
|
7
|
+
const BINARY_NAME = 'agent-analyzer';
|
|
8
|
+
|
|
9
|
+
// GitHub repo for releases
|
|
10
|
+
const GITHUB_REPO = 'agent-sh/agent-analyzer';
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
ANALYZER_MIN_VERSION,
|
|
14
|
+
BINARY_NAME,
|
|
15
|
+
GITHUB_REPO
|
|
16
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git History Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects git history analysis data using the agent-analyzer binary.
|
|
5
|
+
* Runs a full repo-intel init and extracts key metrics for downstream consumers.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/collectors/git
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const binary = require('../binary');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_OPTIONS = {
|
|
15
|
+
top: 20,
|
|
16
|
+
adjustForAi: false,
|
|
17
|
+
cwd: process.cwd()
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collect git history data for the given repository.
|
|
22
|
+
*
|
|
23
|
+
* Runs agent-analyzer repo-intel init to produce a full map, then extracts
|
|
24
|
+
* key metrics (hotspots, bus factor, AI ratio, etc.) from the result.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} [options={}] - Collection options
|
|
27
|
+
* @param {string} [options.cwd] - Repository path (default: process.cwd())
|
|
28
|
+
* @param {number} [options.top=20] - Number of hotspots to return
|
|
29
|
+
* @param {boolean} [options.adjustForAi=false] - Adjust bus factor for AI commits
|
|
30
|
+
* @returns {Object} Git history metrics
|
|
31
|
+
*/
|
|
32
|
+
function collectGitData(options = {}) {
|
|
33
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
34
|
+
const cwd = opts.cwd || process.cwd();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
binary.ensureBinarySync();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
available: false,
|
|
41
|
+
error: `Binary not available: ${err.message}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let map;
|
|
46
|
+
try {
|
|
47
|
+
const json = binary.runAnalyzer(['repo-intel', 'init', cwd]);
|
|
48
|
+
map = JSON.parse(json);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
available: false,
|
|
52
|
+
error: `Git analysis failed: ${err.message}`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract metrics from the map
|
|
57
|
+
const fileActivity = map.fileActivity || {};
|
|
58
|
+
const contributors = map.contributors || {};
|
|
59
|
+
const aiAttribution = map.aiAttribution || {};
|
|
60
|
+
const conventions = map.conventions || {};
|
|
61
|
+
const releases = map.releases || {};
|
|
62
|
+
|
|
63
|
+
// Hotspots: sort files by change count
|
|
64
|
+
const hotspots = Object.entries(fileActivity)
|
|
65
|
+
.map(([path, activity]) => ({
|
|
66
|
+
path,
|
|
67
|
+
changes: activity.totalChanges || 0,
|
|
68
|
+
recentChanges: activity.recentChanges || 0,
|
|
69
|
+
authors: activity.authors ? Object.keys(activity.authors).length : 0,
|
|
70
|
+
lastChanged: activity.lastChanged || null
|
|
71
|
+
}))
|
|
72
|
+
.sort((a, b) => b.changes - a.changes)
|
|
73
|
+
.slice(0, opts.top);
|
|
74
|
+
|
|
75
|
+
// Contributors summary
|
|
76
|
+
const humans = contributors.humans || {};
|
|
77
|
+
const humanList = Object.entries(humans)
|
|
78
|
+
.map(([name, data]) => ({
|
|
79
|
+
name,
|
|
80
|
+
commits: data.commitCount || 0,
|
|
81
|
+
firstSeen: data.firstSeen || null,
|
|
82
|
+
lastSeen: data.lastSeen || null
|
|
83
|
+
}))
|
|
84
|
+
.sort((a, b) => b.commits - a.commits);
|
|
85
|
+
|
|
86
|
+
// Bus factor: people covering 80% of commits
|
|
87
|
+
const totalCommits = humanList.reduce((sum, c) => sum + c.commits, 0);
|
|
88
|
+
let cumulative = 0;
|
|
89
|
+
let busFactor = 0;
|
|
90
|
+
for (const contributor of humanList) {
|
|
91
|
+
cumulative += contributor.commits;
|
|
92
|
+
busFactor++;
|
|
93
|
+
if (cumulative >= totalCommits * 0.8) break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// AI ratio
|
|
97
|
+
const aiTotal = (aiAttribution.attributed || 0) + (aiAttribution.heuristic || 0);
|
|
98
|
+
const allCommits = map.git?.totalCommitsAnalyzed || totalCommits;
|
|
99
|
+
const aiRatio = allCommits > 0 ? aiTotal / allCommits : 0;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
available: true,
|
|
103
|
+
health: {
|
|
104
|
+
active: humanList.length > 0,
|
|
105
|
+
busFactor,
|
|
106
|
+
aiRatio: Math.round(aiRatio * 100) / 100,
|
|
107
|
+
totalCommits: allCommits,
|
|
108
|
+
totalContributors: humanList.length
|
|
109
|
+
},
|
|
110
|
+
hotspots,
|
|
111
|
+
contributors: humanList.slice(0, 10),
|
|
112
|
+
aiAttribution: {
|
|
113
|
+
ratio: Math.round(aiRatio * 100) / 100,
|
|
114
|
+
attributed: aiAttribution.attributed || 0,
|
|
115
|
+
heuristic: aiAttribution.heuristic || 0,
|
|
116
|
+
none: aiAttribution.none || 0,
|
|
117
|
+
confidence: aiAttribution.confidence || 'low',
|
|
118
|
+
tools: aiAttribution.tools || {}
|
|
119
|
+
},
|
|
120
|
+
busFactor,
|
|
121
|
+
conventions: {
|
|
122
|
+
style: conventions.style || null,
|
|
123
|
+
prefixes: conventions.prefixes || {},
|
|
124
|
+
usesScopes: conventions.usesScopes || false
|
|
125
|
+
},
|
|
126
|
+
releaseInfo: {
|
|
127
|
+
tagCount: releases.tags ? releases.tags.length : 0,
|
|
128
|
+
lastRelease: releases.tags && releases.tags.length > 0
|
|
129
|
+
? releases.tags[releases.tags.length - 1]
|
|
130
|
+
: null,
|
|
131
|
+
cadence: releases.cadence || null
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
collectGitData,
|
|
138
|
+
DEFAULT_OPTIONS
|
|
139
|
+
};
|
package/lib/collectors/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const github = require('./github');
|
|
|
13
13
|
const documentation = require('./documentation');
|
|
14
14
|
const codebase = require('./codebase');
|
|
15
15
|
const docsPatterns = require('./docs-patterns');
|
|
16
|
+
const git = require('./git');
|
|
16
17
|
|
|
17
18
|
const DEFAULT_OPTIONS = {
|
|
18
19
|
collectors: ['github', 'docs', 'code'],
|
|
@@ -50,7 +51,8 @@ function collect(options = {}) {
|
|
|
50
51
|
github: null,
|
|
51
52
|
docs: null,
|
|
52
53
|
code: null,
|
|
53
|
-
docsPatterns: null
|
|
54
|
+
docsPatterns: null,
|
|
55
|
+
git: null
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
// Collect from each enabled collector
|
|
@@ -70,6 +72,10 @@ function collect(options = {}) {
|
|
|
70
72
|
data.docsPatterns = docsPatterns.collect(opts);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
if (collectors.includes('git')) {
|
|
76
|
+
data.git = git.collectGitData(opts);
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
return data;
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -103,6 +109,7 @@ module.exports = {
|
|
|
103
109
|
documentation,
|
|
104
110
|
codebase,
|
|
105
111
|
docsPatterns,
|
|
112
|
+
git,
|
|
106
113
|
|
|
107
114
|
// Re-export commonly used functions for convenience
|
|
108
115
|
scanGitHubState: github.scanGitHubState,
|
|
@@ -121,6 +128,8 @@ module.exports = {
|
|
|
121
128
|
isInternalExport: docsPatterns.isInternalExport,
|
|
122
129
|
isEntryPoint: docsPatterns.isEntryPoint,
|
|
123
130
|
|
|
131
|
+
collectGitData: git.collectGitData,
|
|
132
|
+
|
|
124
133
|
// Constants
|
|
125
134
|
DEFAULT_OPTIONS
|
|
126
135
|
};
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* - Claude Code (Anthropic)
|
|
6
6
|
* - OpenCode (multi-model)
|
|
7
7
|
* - Codex CLI (OpenAI)
|
|
8
|
-
* - Cursor (AI-first editor)
|
|
9
|
-
* - Kiro (AWS agentic IDE)
|
|
10
8
|
*
|
|
11
9
|
* Based on research from official documentation:
|
|
12
10
|
* - Anthropic Claude 4 Best Practices
|
|
@@ -46,9 +44,7 @@ function compareSemver(a, b) {
|
|
|
46
44
|
const PLATFORMS = {
|
|
47
45
|
CLAUDE_CODE: 'claude-code',
|
|
48
46
|
OPENCODE: 'opencode',
|
|
49
|
-
CODEX_CLI: 'codex-cli'
|
|
50
|
-
CURSOR: 'cursor',
|
|
51
|
-
KIRO: 'kiro'
|
|
47
|
+
CODEX_CLI: 'codex-cli'
|
|
52
48
|
};
|
|
53
49
|
|
|
54
50
|
/**
|
|
@@ -58,9 +54,7 @@ const PLATFORMS = {
|
|
|
58
54
|
const STATE_DIRS = {
|
|
59
55
|
[PLATFORMS.CLAUDE_CODE]: '.claude',
|
|
60
56
|
[PLATFORMS.OPENCODE]: '.opencode',
|
|
61
|
-
[PLATFORMS.CODEX_CLI]: '.codex'
|
|
62
|
-
[PLATFORMS.CURSOR]: '.cursor',
|
|
63
|
-
[PLATFORMS.KIRO]: '.kiro'
|
|
57
|
+
[PLATFORMS.CODEX_CLI]: '.codex'
|
|
64
58
|
};
|
|
65
59
|
|
|
66
60
|
/**
|
|
@@ -82,8 +76,6 @@ function detectPlatform() {
|
|
|
82
76
|
const stateDir = process.env.AI_STATE_DIR;
|
|
83
77
|
if (stateDir === '.opencode') return PLATFORMS.OPENCODE;
|
|
84
78
|
if (stateDir === '.codex') return PLATFORMS.CODEX_CLI;
|
|
85
|
-
if (stateDir === '.cursor') return PLATFORMS.CURSOR;
|
|
86
|
-
if (stateDir === '.kiro') return PLATFORMS.KIRO;
|
|
87
79
|
return PLATFORMS.CLAUDE_CODE;
|
|
88
80
|
}
|
|
89
81
|
|
|
@@ -491,9 +483,7 @@ enabled = true
|
|
|
491
483
|
const INSTRUCTION_FILES = {
|
|
492
484
|
[PLATFORMS.CLAUDE_CODE]: ['CLAUDE.md', '.claude/CLAUDE.md'],
|
|
493
485
|
[PLATFORMS.OPENCODE]: ['AGENTS.md', 'CLAUDE.md'],
|
|
494
|
-
[PLATFORMS.CODEX_CLI]: ['AGENTS.md', 'AGENTS.override.md']
|
|
495
|
-
[PLATFORMS.CURSOR]: ['.cursor/rules/*.mdc'],
|
|
496
|
-
[PLATFORMS.KIRO]: ['AGENTS.md', '.kiro/steering/*.md']
|
|
486
|
+
[PLATFORMS.CODEX_CLI]: ['AGENTS.md', 'AGENTS.override.md']
|
|
497
487
|
};
|
|
498
488
|
|
|
499
489
|
/**
|