codemuster 0.0.1 → 0.1.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/README.md +37 -11
- package/bin/codemuster.js +11 -0
- package/lib/launcher.js +256 -0
- package/lib/update.js +16 -0
- package/package.json +37 -5
package/README.md
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
# codemuster
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
of work in a fresh context,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
# codemuster
|
|
2
|
+
|
|
3
|
+
Audit every file and every entry point of a codebase with your coding agent (Claude Code, Codex,
|
|
4
|
+
Gemini CLI, or OpenCode), and get a coverage number you can check. CodeMuster maps the code,
|
|
5
|
+
hands your agent one unit of work at a time in a fresh context, tracks every unit in a local
|
|
6
|
+
ledger, and tries to refute each finding before it reaches the report.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
You need Node.js 22 or later.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install -g codemuster
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
npm installs a self-contained build for your platform, about 45 MB. Mapping C# also needs the
|
|
17
|
+
.NET SDK, and mapping TypeScript needs the repository's own `typescript` package installed.
|
|
18
|
+
|
|
19
|
+
## Get started
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
cd your-repo
|
|
23
|
+
codemuster init --yes
|
|
24
|
+
codemuster doctor
|
|
25
|
+
codemuster skill install --for claude
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then ask your agent to audit the repository with CodeMuster. `codemuster` with no arguments lists
|
|
29
|
+
every command.
|
|
30
|
+
|
|
31
|
+
## Updates
|
|
32
|
+
|
|
33
|
+
`codemuster` checks npm for a newer version at most once a day, downloads it in the background,
|
|
34
|
+
checks its integrity, and uses it from the next run on. It never updates when the `CI` or
|
|
35
|
+
`CODEMUSTER_NO_UPDATE` environment variable is set.
|
|
36
|
+
|
|
37
|
+
Website: https://codemuster.com
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const launcher = require('../lib/launcher');
|
|
5
|
+
|
|
6
|
+
launcher.main(process.argv.slice(2)).then(
|
|
7
|
+
(code) => process.exit(code),
|
|
8
|
+
(error) => {
|
|
9
|
+
process.stderr.write(`codemuster: ${error.message}\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/lib/launcher.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const childProcess = require('node:child_process');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
10
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
11
|
+
const KEEP_VERSIONS = 2;
|
|
12
|
+
const PLATFORMS = new Set(['win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64']);
|
|
13
|
+
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
14
|
+
|
|
15
|
+
function platformPackage(platform, arch) {
|
|
16
|
+
const name = `${platform}-${arch}`;
|
|
17
|
+
if (!PLATFORMS.has(name)) {
|
|
18
|
+
throw new Error(`there is no CodeMuster build for ${name}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `@codemuster/${name}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function binaryName(platform) {
|
|
25
|
+
return platform === 'win32' ? 'codemuster.exe' : 'codemuster';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareVersions(a, b) {
|
|
29
|
+
const [coreA, preA] = splitVersion(a);
|
|
30
|
+
const [coreB, preB] = splitVersion(b);
|
|
31
|
+
for (let i = 0; i < 3; i++) {
|
|
32
|
+
const difference = Number(coreA[i]) - Number(coreB[i]);
|
|
33
|
+
if (difference !== 0) {
|
|
34
|
+
return difference;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (preA === null || preB === null) {
|
|
39
|
+
return preA === preB ? 0 : preA === null ? 1 : -1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const partsA = preA.split('.');
|
|
43
|
+
const partsB = preB.split('.');
|
|
44
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
45
|
+
if (partsA[i] === undefined || partsB[i] === undefined) {
|
|
46
|
+
return partsA[i] === undefined ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const numericA = /^\d+$/.test(partsA[i]);
|
|
50
|
+
const numericB = /^\d+$/.test(partsB[i]);
|
|
51
|
+
if (numericA && numericB && Number(partsA[i]) !== Number(partsB[i])) {
|
|
52
|
+
return Number(partsA[i]) - Number(partsB[i]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (numericA !== numericB) {
|
|
56
|
+
return numericA ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (partsA[i] !== partsB[i]) {
|
|
60
|
+
return partsA[i] < partsB[i] ? -1 : 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitVersion(version) {
|
|
68
|
+
const dash = version.indexOf('-');
|
|
69
|
+
const core = dash < 0 ? version : version.slice(0, dash);
|
|
70
|
+
return [core.split('.'), dash < 0 ? null : version.slice(dash + 1)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function newestBuild({ versionsDir, bundled, platform }) {
|
|
74
|
+
const candidates = bundled ? [{ version: bundled.version, binary: path.join(bundled.dir, 'bin', binaryName(platform)) }] : [];
|
|
75
|
+
for (const version of installedVersions(versionsDir)) {
|
|
76
|
+
candidates.push({ version, binary: path.join(versionsDir, version, 'bin', binaryName(platform)) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const complete = candidates.filter((candidate) => fs.existsSync(candidate.binary));
|
|
80
|
+
complete.sort((x, y) => compareVersions(y.version, x.version));
|
|
81
|
+
return complete[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function installedVersions(versionsDir) {
|
|
85
|
+
try {
|
|
86
|
+
return fs.readdirSync(versionsDir).filter((entry) => VERSION.test(entry));
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shouldCheckForUpdate({ env, now, lastCheck }) {
|
|
93
|
+
return !env.CI && !env.CODEMUSTER_NO_UPDATE && (lastCheck === null || now - lastCheck >= DAY);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readLastCheck(stateDir) {
|
|
97
|
+
try {
|
|
98
|
+
const value = Number(fs.readFileSync(path.join(stateDir, 'last-update-check'), 'utf8'));
|
|
99
|
+
return Number.isFinite(value) ? value : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tar(args) {
|
|
106
|
+
// Git for Windows puts GNU tar first on PATH, and GNU tar reads "C:" in a path as a remote host.
|
|
107
|
+
const command = process.platform === 'win32' ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe') : 'tar';
|
|
108
|
+
childProcess.execFileSync(command, args, { stdio: 'pipe' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getJson(url) {
|
|
112
|
+
const response = await fetch(url, { headers: { accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8' } });
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`GET ${url} returned ${response.status}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return response.json();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function installVersion({ registry, version, versionsDir, platform, arch }) {
|
|
121
|
+
const pkg = platformPackage(platform, arch);
|
|
122
|
+
fs.mkdirSync(versionsDir, { recursive: true });
|
|
123
|
+
const packument = await getJson(`${registry}/${pkg.replace('/', '%2f')}`);
|
|
124
|
+
const dist = packument.versions && packument.versions[version] && packument.versions[version].dist;
|
|
125
|
+
if (!dist) {
|
|
126
|
+
throw new Error(`${pkg}@${version} is not in the registry`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const response = await fetch(dist.tarball);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`GET ${dist.tarball} returned ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tarball = Buffer.from(await response.arrayBuffer());
|
|
135
|
+
const expected = /sha512-([A-Za-z0-9+\/=]+)/.exec(dist.integrity || '');
|
|
136
|
+
if (!expected || crypto.createHash('sha512').update(tarball).digest('base64') !== expected[1]) {
|
|
137
|
+
throw new Error(`integrity check failed for ${pkg}@${version}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const target = path.join(versionsDir, version);
|
|
141
|
+
const staging = fs.mkdtempSync(path.join(versionsDir, `.staging-${version}-`));
|
|
142
|
+
try {
|
|
143
|
+
const file = path.join(staging, 'build.tgz');
|
|
144
|
+
fs.writeFileSync(file, tarball);
|
|
145
|
+
tar(['-xzf', file, '-C', staging]);
|
|
146
|
+
const binary = path.join(staging, 'package', 'bin', binaryName(platform));
|
|
147
|
+
if (!fs.existsSync(binary)) {
|
|
148
|
+
throw new Error(`${pkg}@${version} has no bin/${binaryName(platform)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (platform !== 'win32') {
|
|
152
|
+
fs.chmodSync(binary, 0o755);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
fs.renameSync(path.join(staging, 'package'), target);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!fs.existsSync(target)) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return target;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pruneVersions(versionsDir) {
|
|
170
|
+
const versions = installedVersions(versionsDir).sort((x, y) => compareVersions(y, x));
|
|
171
|
+
for (const version of versions.slice(KEEP_VERSIONS)) {
|
|
172
|
+
try {
|
|
173
|
+
fs.rmSync(path.join(versionsDir, version), { recursive: true, force: true });
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function update({ registry, stateDir, platform, arch, currentVersion, now }) {
|
|
180
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
181
|
+
fs.writeFileSync(path.join(stateDir, 'last-update-check'), String(now));
|
|
182
|
+
const packument = await getJson(`${registry}/codemuster`);
|
|
183
|
+
const latest = (packument['dist-tags'] && packument['dist-tags'].latest) || null;
|
|
184
|
+
if (latest === null || compareVersions(latest, currentVersion) <= 0) {
|
|
185
|
+
return { latest, installed: null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const versionsDir = path.join(stateDir, 'versions');
|
|
189
|
+
await installVersion({ registry, version: latest, versionsDir, platform, arch });
|
|
190
|
+
pruneVersions(versionsDir);
|
|
191
|
+
return { latest, installed: latest };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function runBuild(binary, args) {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const child = childProcess.spawn(binary, args, { stdio: 'inherit' });
|
|
197
|
+
// The terminal already delivers Ctrl+C to the build, which cancels cleanly; the launcher only has to outlive it.
|
|
198
|
+
const handlers = ['SIGINT', 'SIGTERM', 'SIGHUP'].map((signal) => [signal, () => signal !== 'SIGINT' && child.kill(signal)]);
|
|
199
|
+
handlers.forEach(([signal, handler]) => process.on(signal, handler));
|
|
200
|
+
const done = () => handlers.forEach(([signal, handler]) => process.off(signal, handler));
|
|
201
|
+
child.on('error', (error) => {
|
|
202
|
+
done();
|
|
203
|
+
reject(error);
|
|
204
|
+
});
|
|
205
|
+
child.on('exit', (code) => {
|
|
206
|
+
done();
|
|
207
|
+
resolve(code ?? 1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function bundledBuild(pkg) {
|
|
213
|
+
try {
|
|
214
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
215
|
+
return { version: JSON.parse(fs.readFileSync(manifest, 'utf8')).version, dir: path.dirname(manifest) };
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function main(args, env = process.env) {
|
|
222
|
+
const platform = process.platform;
|
|
223
|
+
const arch = process.arch;
|
|
224
|
+
const stateDir = path.join(os.homedir(), '.codemuster');
|
|
225
|
+
const versionsDir = path.join(stateDir, 'versions');
|
|
226
|
+
let build = newestBuild({ versionsDir, bundled: bundledBuild(platformPackage(platform, arch)), platform });
|
|
227
|
+
if (build === null) {
|
|
228
|
+
const version = require('../package.json').version;
|
|
229
|
+
process.stderr.write(`codemuster: downloading CodeMuster ${version} for ${platform}-${arch}\n`);
|
|
230
|
+
await installVersion({ registry: REGISTRY, version, versionsDir, platform, arch });
|
|
231
|
+
build = newestBuild({ versionsDir, bundled: null, platform });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (shouldCheckForUpdate({ env, now: Date.now(), lastCheck: readLastCheck(stateDir) })) {
|
|
235
|
+
childProcess
|
|
236
|
+
.spawn(process.execPath, [path.join(__dirname, 'update.js'), stateDir, build.version], { detached: true, stdio: 'ignore', windowsHide: true })
|
|
237
|
+
.unref();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return runBuild(build.binary, args);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
REGISTRY,
|
|
245
|
+
binaryName,
|
|
246
|
+
compareVersions,
|
|
247
|
+
installVersion,
|
|
248
|
+
main,
|
|
249
|
+
newestBuild,
|
|
250
|
+
platformPackage,
|
|
251
|
+
readLastCheck,
|
|
252
|
+
runBuild,
|
|
253
|
+
shouldCheckForUpdate,
|
|
254
|
+
tar,
|
|
255
|
+
update,
|
|
256
|
+
};
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const launcher = require('./launcher');
|
|
6
|
+
|
|
7
|
+
const [stateDir, currentVersion] = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
launcher
|
|
10
|
+
.update({ registry: launcher.REGISTRY, stateDir, platform: process.platform, arch: process.arch, currentVersion, now: Date.now() })
|
|
11
|
+
.catch((error) => {
|
|
12
|
+
try {
|
|
13
|
+
fs.writeFileSync(path.join(stateDir, 'last-update-error'), String((error && error.stack) || error));
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemuster",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "CodeMuster: full-coverage codebase audits with a coverage number
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CodeMuster: full-coverage codebase audits with a coverage number, driven by your coding agent through one skill.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tim Carter",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/TSCarterJr/CodeMuster.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://
|
|
12
|
-
"keywords": [
|
|
13
|
-
|
|
11
|
+
"homepage": "https://codemuster.com",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"code-audit",
|
|
14
|
+
"code-review",
|
|
15
|
+
"security",
|
|
16
|
+
"coverage",
|
|
17
|
+
"ai",
|
|
18
|
+
"agent-skill",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"codex",
|
|
21
|
+
"gemini-cli",
|
|
22
|
+
"opencode"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"codemuster": "bin/codemuster.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"lib",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/launcher.test.js"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"@codemuster/darwin-arm64": "0.1.0",
|
|
40
|
+
"@codemuster/darwin-x64": "0.1.0",
|
|
41
|
+
"@codemuster/linux-arm64": "0.1.0",
|
|
42
|
+
"@codemuster/linux-x64": "0.1.0",
|
|
43
|
+
"@codemuster/win32-arm64": "0.1.0",
|
|
44
|
+
"@codemuster/win32-x64": "0.1.0"
|
|
45
|
+
}
|
|
14
46
|
}
|