replay-doctor 0.6.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 +21 -0
- package/bin/replay-doctor.js +113 -0
- package/lib/release.mjs +58 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# replay-doctor (npm shim)
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
npx replay-doctor diff ~/.claude/projects/
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Replay Doctor names the turn your prompt cache broke on and what it cost. It reads the transcripts your coding agent already keeps on disk; nothing leaves your machine.
|
|
8
|
+
|
|
9
|
+
npm installs the one platform package that matches your machine (`@replay-doctor/darwin-arm64`, `linux-x64`, and so on, pinned to this exact version as optional dependencies), so the binary arrives through npm with the lockfile's integrity hash and provenance covering it. No postinstall script, no network at run time. If the platform package is missing (`--no-optional`), the launcher says so and stops; it does not fetch anything unless you tell it to (below). The package version and the binary version are the same tag. macOS and Linux, amd64 and arm64.
|
|
10
|
+
|
|
11
|
+
## `REPLAY_DOCTOR_ALLOW_FETCH`
|
|
12
|
+
|
|
13
|
+
The launcher goes to the network only when this variable is exactly `1`:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
REPLAY_DOCTOR_ALLOW_FETCH=1 npx replay-doctor version
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
With it set, a missing platform package makes the launcher fetch the release tarball for your platform from the GitHub release that matches the package version, verify its sha256 against the release's `checksums.txt`, cache it once per version, and run it. Any other value, or no value, is a refusal: the launcher exits non-zero and prints the package it could not find, this variable, and the two other routes to the same binary, `go install github.com/RedRobotKK/Replay/cmd/replay@latest` and the release page at <https://github.com/RedRobotKK/Replay/releases>.
|
|
20
|
+
|
|
21
|
+
Documentation, the other install routes and the signature check: <https://replay.doctor/install/>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npx replay-doctor <args>
|
|
4
|
+
*
|
|
5
|
+
* Resolves the binary from the platform package npm installed alongside this
|
|
6
|
+
* one (@replay-doctor/<os>-<cpu>, an optionalDependency pinned to this exact
|
|
7
|
+
* version) and execs it. That is the whole path for a normal install: no
|
|
8
|
+
* network, no postinstall, the lockfile's integrity hash covers the binary.
|
|
9
|
+
*
|
|
10
|
+
* When the platform package is absent (--no-optional, an unusual installer, a
|
|
11
|
+
* mirror that dropped it) the launcher stops and says so on stderr. It fetches
|
|
12
|
+
* the goreleaser tarball for this version from the GitHub release, verifies it
|
|
13
|
+
* against checksums.txt, caches it and execs that only when the environment
|
|
14
|
+
* variable REPLAY_DOCTOR_ALLOW_FETCH is exactly "1". A run that went to the
|
|
15
|
+
* network on its own would be a different promise than the one on the package
|
|
16
|
+
* page, so the network needs the user's word first (PRD E6).
|
|
17
|
+
*/
|
|
18
|
+
import { createReadStream, existsSync, mkdirSync, renameSync, rmSync, writeFileSync, chmodSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
21
|
+
import { join, dirname } from 'node:path';
|
|
22
|
+
import { homedir, tmpdir } from 'node:os';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { target, archiveName, assetURL, expectedHash, releaseVersion } from '../lib/release.mjs';
|
|
26
|
+
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
29
|
+
const version = releaseVersion(pkg.version);
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
function fromPlatformPackage() {
|
|
33
|
+
const t = target();
|
|
34
|
+
if (!t) return null;
|
|
35
|
+
const name = `@replay-doctor/${process.platform}-${process.arch}`;
|
|
36
|
+
try {
|
|
37
|
+
const p = join(dirname(require.resolve(`${name}/package.json`)), 'bin', 'replay');
|
|
38
|
+
return existsSync(p) ? p : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cacheDir() {
|
|
45
|
+
if (process.env.REPLAY_DOCTOR_CACHE) return process.env.REPLAY_DOCTOR_CACHE;
|
|
46
|
+
if (process.env.XDG_CACHE_HOME) return join(process.env.XDG_CACHE_HOME, 'replay-doctor');
|
|
47
|
+
return process.platform === 'darwin' ? join(homedir(), 'Library', 'Caches', 'replay-doctor') : join(homedir(), '.cache', 'replay-doctor');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function sha256(path) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const h = createHash('sha256');
|
|
53
|
+
createReadStream(path).on('data', (d) => h.update(d)).on('end', () => resolve(h.digest('hex'))).on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function download(url, to) {
|
|
58
|
+
const r = await fetch(url, { headers: { 'user-agent': `replay-doctor npm shim ${version}` } });
|
|
59
|
+
if (!r.ok) throw new Error(`${r.status} fetching ${url}`);
|
|
60
|
+
writeFileSync(to, Buffer.from(await r.arrayBuffer()));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function fromRelease() {
|
|
64
|
+
const t = target();
|
|
65
|
+
if (!t) {
|
|
66
|
+
console.error(`replay-doctor: ${process.platform}/${process.arch} is not supported. macOS and Linux on amd64 and arm64 are; Windows is not.`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
const dir = join(cacheDir(), version, `${t.os}_${t.cpu}`);
|
|
70
|
+
const bin = join(dir, 'replay');
|
|
71
|
+
if (existsSync(bin)) return bin;
|
|
72
|
+
const name = archiveName(version, t);
|
|
73
|
+
const work = join(tmpdir(), `replay-doctor-${process.pid}`);
|
|
74
|
+
mkdirSync(work, { recursive: true });
|
|
75
|
+
try {
|
|
76
|
+
console.error(`replay-doctor: the platform package @replay-doctor/${process.platform}-${process.arch} is not installed; fetching ${name} from the v${version} release instead (once per version)`);
|
|
77
|
+
await download(assetURL(version, name), join(work, name));
|
|
78
|
+
await download(assetURL(version, 'checksums.txt'), join(work, 'checksums.txt'));
|
|
79
|
+
const want = expectedHash(readFileSync(join(work, 'checksums.txt'), 'utf8'), name);
|
|
80
|
+
if (!want) throw new Error(`${name} is not listed in checksums.txt for v${version}`);
|
|
81
|
+
const got = await sha256(join(work, name));
|
|
82
|
+
if (got !== want) throw new Error(`sha256 mismatch for ${name}: release says ${want}, downloaded ${got}. Nothing was installed.`);
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
execFileSync('tar', ['-xzf', join(work, name), '-C', work, 'replay']);
|
|
85
|
+
chmodSync(join(work, 'replay'), 0o755);
|
|
86
|
+
renameSync(join(work, 'replay'), bin);
|
|
87
|
+
console.error(`replay-doctor: verified against checksums.txt (sha256 ${want.slice(0, 12)}). Signature check: https://replay.doctor/install/`);
|
|
88
|
+
return bin;
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(work, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refuseFetch() {
|
|
95
|
+
const t = target();
|
|
96
|
+
if (!t) {
|
|
97
|
+
console.error(`replay-doctor: ${process.platform}/${process.arch} is not supported. macOS and Linux on amd64 and arm64 are; Windows is not.`);
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
const name = `@replay-doctor/${process.platform}-${process.arch}`;
|
|
101
|
+
console.error(
|
|
102
|
+
`replay-doctor: the platform package ${name} is not installed, and this launcher does not go to the network on its own.\n` +
|
|
103
|
+
` Reinstall without --no-optional so npm brings in ${name}, or take one of these:\n` +
|
|
104
|
+
` REPLAY_DOCTOR_ALLOW_FETCH=1 npx replay-doctor ... fetches ${archiveName(version, t)} from the v${version} GitHub release, verified against its checksums.txt\n` +
|
|
105
|
+
` go install github.com/RedRobotKK/Replay/cmd/replay@latest\n` +
|
|
106
|
+
` Release page: https://github.com/RedRobotKK/Replay/releases/tag/v${version}`
|
|
107
|
+
);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const bin = fromPlatformPackage() || (process.env.REPLAY_DOCTOR_ALLOW_FETCH === '1' ? await fromRelease() : refuseFetch());
|
|
112
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' });
|
|
113
|
+
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });
|
package/lib/release.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the npm shim, kept apart from the launcher so they can be
|
|
3
|
+
* tested without touching the network or the filesystem.
|
|
4
|
+
*
|
|
5
|
+
* The shim does one thing: fetch the goreleaser tarball for this platform from
|
|
6
|
+
* the GitHub release that matches the package version, verify its sha256
|
|
7
|
+
* against the release's checksums.txt, unpack it once into a cache, and exec
|
|
8
|
+
* the binary. The package itself contains no binary, so `npm install` is
|
|
9
|
+
* instant and the bytes that run are the bytes the release signed. The
|
|
10
|
+
* checksums file is the same one cosign signs; the shim verifies the hash and
|
|
11
|
+
* tells you how to verify the signature, because sigstore in Node would be a
|
|
12
|
+
* second implementation of the thing the installer already does.
|
|
13
|
+
*/
|
|
14
|
+
export const REPO = 'RedRobotKK/Replay';
|
|
15
|
+
|
|
16
|
+
/** Map Node's platform/arch to goreleaser's names. Returns null when unsupported. */
|
|
17
|
+
export function target(platform = process.platform, arch = process.arch) {
|
|
18
|
+
const os = { darwin: 'darwin', linux: 'linux' }[platform];
|
|
19
|
+
const cpu = { x64: 'amd64', arm64: 'arm64' }[arch];
|
|
20
|
+
if (!os || !cpu) return null;
|
|
21
|
+
return { os, cpu };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The tarball name goreleaser produced for a version and target. */
|
|
25
|
+
export function archiveName(version, t) {
|
|
26
|
+
return `replay_${version}_${t.os}_${t.cpu}.tar.gz`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assetURL(version, name) {
|
|
30
|
+
return `https://github.com/${REPO}/releases/download/v${version}/${name}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Find the expected sha256 for a file in checksums.txt (goreleaser's "hash name" lines). */
|
|
34
|
+
export function expectedHash(checksums, name) {
|
|
35
|
+
for (const line of checksums.split('\n')) {
|
|
36
|
+
const m = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/);
|
|
37
|
+
if (m && m[2] === name) return m[1];
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The version the launcher will fetch, which must be a real release tag.
|
|
44
|
+
*
|
|
45
|
+
* The checked-in package.json carries a placeholder that the publish workflow
|
|
46
|
+
* replaces from the tag. A prefix match let "0.0.0-set-by-release-workflow"
|
|
47
|
+
* through as 0.0.0, which would have sent a launcher run from the repository
|
|
48
|
+
* to fetch a release that does not exist. Whole-string semver, with an
|
|
49
|
+
* optional pre-release suffix for release candidates, and the placeholder is
|
|
50
|
+
* refused by name.
|
|
51
|
+
*/
|
|
52
|
+
export function releaseVersion(pkgVersion) {
|
|
53
|
+
const v = String(pkgVersion || '');
|
|
54
|
+
if (v.includes('set-by-release-workflow') || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/.test(v)) {
|
|
55
|
+
throw new Error(`package version "${v}" is not a released version; the publish workflow sets it from the tag`);
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "replay-doctor",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Replay Doctor: names the turn your prompt cache broke on and what it cost. This package fetches the signed release binary for your platform, verifies it against checksums.txt, and runs it. macOS and Linux.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"homepage": "https://replay.doctor",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/RedRobotKK/Replay.git",
|
|
10
|
+
"directory": "packaging/npm"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/RedRobotKK/Replay/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"prompt-cache",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"codex",
|
|
19
|
+
"coding-agent",
|
|
20
|
+
"token-cost",
|
|
21
|
+
"cache-break",
|
|
22
|
+
"llm-observability"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"replay-doctor": "bin/replay-doctor.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"lib",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"optionalDependencies": {
|
|
34
|
+
"@replay-doctor/darwin-x64": "0.6.0",
|
|
35
|
+
"@replay-doctor/darwin-arm64": "0.6.0",
|
|
36
|
+
"@replay-doctor/linux-x64": "0.6.0",
|
|
37
|
+
"@replay-doctor/linux-arm64": "0.6.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"darwin",
|
|
44
|
+
"linux"
|
|
45
|
+
],
|
|
46
|
+
"cpu": [
|
|
47
|
+
"x64",
|
|
48
|
+
"arm64"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"test": "node --test test.mjs"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"provenance": true
|
|
56
|
+
}
|
|
57
|
+
}
|