roster-cli 0.1.0 → 0.1.2
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/dist/cli.js +4 -1
- package/dist/rules/overlap.js +6 -2
- package/dist/sources/dir.js +11 -1
- package/dist/sources/plugin-cache.js +58 -8
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -169,5 +169,8 @@ export async function main(argv) {
|
|
|
169
169
|
}
|
|
170
170
|
const isDirectRun = process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`;
|
|
171
171
|
if (isDirectRun) {
|
|
172
|
-
main(process.argv.slice(2)).then((code) => process.exit(code))
|
|
172
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
173
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
174
|
+
process.exit(2);
|
|
175
|
+
});
|
|
173
176
|
}
|
package/dist/rules/overlap.js
CHANGED
|
@@ -67,8 +67,12 @@ export const overlapRule = {
|
|
|
67
67
|
pairs.sort((a, b) => b.score - a.score);
|
|
68
68
|
const topPairs = pairs.slice(0, top);
|
|
69
69
|
return topPairs.map(({ i, j, score }) => {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Same name from two sources (e.g. a repo audited via dir that is also
|
|
71
|
+
// installed as a plugin) — disambiguate with the source label so the
|
|
72
|
+
// pair never renders as "x <-> x".
|
|
73
|
+
const sameName = agents[i].name === agents[j].name;
|
|
74
|
+
const a = sameName ? `${agents[i].name} (${agents[i].sourceLabel})` : agents[i].name;
|
|
75
|
+
const b = sameName ? `${agents[j].name} (${agents[j].sourceLabel})` : agents[j].name;
|
|
72
76
|
const critical = failAbove !== undefined && score > failAbove;
|
|
73
77
|
return {
|
|
74
78
|
ruleId: 'overlap',
|
package/dist/sources/dir.js
CHANGED
|
@@ -40,7 +40,17 @@ export const dirSource = {
|
|
|
40
40
|
if (!dir) {
|
|
41
41
|
throw new Error('sources/dir: opts.dir is required');
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
let files;
|
|
44
|
+
try {
|
|
45
|
+
files = await collectMarkdownFiles(dir);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const code = err.code;
|
|
49
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
50
|
+
throw new Error(`sources/dir: "${dir}" is not a readable directory`);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
44
54
|
const agents = await Promise.all(files.map(async (filePath) => {
|
|
45
55
|
const raw = await readFile(filePath, 'utf8');
|
|
46
56
|
return parseAgentMarkdown(raw, filePath, `dir:${dir}`);
|
|
@@ -12,21 +12,70 @@ async function readInstalledPlugins(pluginsRoot) {
|
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
const
|
|
15
|
+
function compareVersions(a, b) {
|
|
16
|
+
const pa = a.split('.').map((n) => parseInt(n, 10));
|
|
17
|
+
const pb = b.split('.').map((n) => parseInt(n, 10));
|
|
18
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
19
|
+
const na = pa[i];
|
|
20
|
+
const nb = pb[i];
|
|
21
|
+
if (Number.isNaN(na ?? NaN) || Number.isNaN(nb ?? NaN)) {
|
|
22
|
+
// Non-numeric segment (git SHAs etc.) — fall back to string compare.
|
|
23
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
24
|
+
}
|
|
25
|
+
if ((na ?? 0) !== (nb ?? 0))
|
|
26
|
+
return (na ?? 0) - (nb ?? 0);
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
function pickPreferred(a, b, cwd) {
|
|
31
|
+
const aCwd = a.projectPath !== undefined && path.resolve(a.projectPath) === cwd;
|
|
32
|
+
const bCwd = b.projectPath !== undefined && path.resolve(b.projectPath) === cwd;
|
|
33
|
+
if (aCwd !== bCwd)
|
|
34
|
+
return aCwd ? a : b;
|
|
35
|
+
const aUser = a.scope === 'user';
|
|
36
|
+
const bUser = b.scope === 'user';
|
|
37
|
+
if (aUser !== bUser)
|
|
38
|
+
return aUser ? a : b;
|
|
39
|
+
return compareVersions(a.version, b.version) >= 0 ? a : b;
|
|
40
|
+
}
|
|
41
|
+
function resolveActivePlugins(data, cwd) {
|
|
42
|
+
const byName = new Map();
|
|
43
|
+
const skipped = [];
|
|
18
44
|
for (const [key, entries] of Object.entries(data.plugins ?? {})) {
|
|
19
45
|
const sepIndex = key.indexOf('@');
|
|
20
46
|
const name = sepIndex === -1 ? key : key.slice(0, sepIndex);
|
|
21
47
|
const marketplace = sepIndex === -1 ? '' : key.slice(sepIndex + 1);
|
|
22
48
|
for (const entry of entries) {
|
|
23
|
-
if (!entry.installPath
|
|
49
|
+
if (!entry.installPath)
|
|
24
50
|
continue;
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
const candidate = {
|
|
52
|
+
name,
|
|
53
|
+
marketplace,
|
|
54
|
+
version: entry.version,
|
|
55
|
+
installPath: entry.installPath,
|
|
56
|
+
scope: entry.scope,
|
|
57
|
+
projectPath: entry.projectPath,
|
|
58
|
+
};
|
|
59
|
+
const current = byName.get(name);
|
|
60
|
+
if (!current) {
|
|
61
|
+
byName.set(name, candidate);
|
|
62
|
+
}
|
|
63
|
+
else if (current.installPath !== candidate.installPath) {
|
|
64
|
+
const winner = pickPreferred(current, candidate, cwd);
|
|
65
|
+
const loser = winner === current ? candidate : current;
|
|
66
|
+
byName.set(name, winner);
|
|
67
|
+
skipped.push(loser);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const loser of skipped) {
|
|
72
|
+
const winner = byName.get(loser.name);
|
|
73
|
+
if (winner && winner.installPath !== loser.installPath) {
|
|
74
|
+
console.error(`sources/plugin-cache: skipped ${loser.name}@${loser.version}` +
|
|
75
|
+
`${loser.projectPath ? ` (pinned by ${loser.projectPath})` : ''} — using ${winner.version}`);
|
|
27
76
|
}
|
|
28
77
|
}
|
|
29
|
-
return
|
|
78
|
+
return [...byName.values()];
|
|
30
79
|
}
|
|
31
80
|
async function collectMarkdownFiles(root) {
|
|
32
81
|
let entries;
|
|
@@ -71,8 +120,9 @@ export const pluginCacheSource = {
|
|
|
71
120
|
console.error(`sources/plugin-cache: ${pluginsRoot}/installed_plugins.json not found — returning empty roster`);
|
|
72
121
|
return [];
|
|
73
122
|
}
|
|
123
|
+
const cwd = path.resolve(opts?.cwd ?? process.cwd());
|
|
74
124
|
const pluginNameFilter = opts?.pluginName;
|
|
75
|
-
const activePlugins = resolveActivePlugins(data).filter((p) => !pluginNameFilter || p.name === pluginNameFilter);
|
|
125
|
+
const activePlugins = resolveActivePlugins(data, cwd).filter((p) => !pluginNameFilter || p.name === pluginNameFilter);
|
|
76
126
|
const agentLists = await Promise.all(activePlugins.map(async (plugin) => {
|
|
77
127
|
const agentsDir = path.join(plugin.installPath, 'agents');
|
|
78
128
|
if (!(await dirExists(agentsDir)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roster-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Zero-runtime-dep static auditor for Claude Code agent rosters \u2014 overlap, harness gaps, routing, cost.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"test": "vitest run",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"prepare": "npm run build"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"typescript": "^5.6.3",
|