contextecf-codeledger 0.10.79 → 0.10.81
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/bin/codeledger.js +95 -63
- package/package.json +1 -1
- package/scripts/postinstall.js +228 -139
package/bin/codeledger.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* CodeLedger npm launcher — thin shim.
|
|
4
4
|
*
|
|
5
|
-
* This is the
|
|
6
|
-
*
|
|
5
|
+
* This file is the ONLY JavaScript that ships to npm under @codeledger/cli.
|
|
6
|
+
* It contains no engine code, no scoring logic, no formulas. All CodeLedger
|
|
7
|
+
* functionality lives inside the hardened binary that is downloaded during
|
|
8
|
+
* `postinstall` from the GitHub Release for this exact version, verified
|
|
9
|
+
* via SHA256, and cached under ~/.cache/codeledger/bin/.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* postinstall soft-fails (see scripts/postinstall.js) rather than blocking
|
|
12
|
+
* `npm install`, so the binary may not be cached yet the first time this
|
|
13
|
+
* shim runs. When that happens, this shim first looks for a repo-local
|
|
14
|
+
* runtime (an already-initialized repo's `.codeledger/bin/`) before making
|
|
15
|
+
* one synchronous, on-demand download attempt itself.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
'use strict';
|
|
16
19
|
|
|
17
20
|
const { spawnSync } = require('node:child_process');
|
|
18
|
-
const { existsSync } = require('node:fs');
|
|
21
|
+
const { existsSync, realpathSync, statSync } = require('node:fs');
|
|
19
22
|
const { dirname, join, resolve } = require('node:path');
|
|
20
23
|
const { homedir, platform } = require('node:os');
|
|
21
24
|
|
|
@@ -37,6 +40,11 @@ function die(msg, code = 1) {
|
|
|
37
40
|
process.exit(code);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
// Repo-local fallback: an already-`codeledger init`'d repo vendors its own
|
|
44
|
+
// runtime under .codeledger/bin/. Checked before attempting a network
|
|
45
|
+
// download since it's instant and needs no connectivity — the global cache
|
|
46
|
+
// miss just means this particular machine hasn't installed globally yet,
|
|
47
|
+
// not that the user has no working CodeLedger available to them.
|
|
40
48
|
function findRepoLocalRuntime(startDir) {
|
|
41
49
|
if (process.env.CODELEDGER_GLOBAL_VERSION_PROBE === '1') return null;
|
|
42
50
|
let current = resolve(startDir);
|
|
@@ -67,7 +75,6 @@ function runRepoLocal(runtime) {
|
|
|
67
75
|
stdio: 'inherit',
|
|
68
76
|
cwd: runtime.repoRoot,
|
|
69
77
|
env: { ...process.env, CODELEDGER_DELEGATED: '1' },
|
|
70
|
-
shell: runtime.kind === 'wrapper',
|
|
71
78
|
});
|
|
72
79
|
if (result.error) {
|
|
73
80
|
die(`failed to spawn repo-local runtime: ${result.error.message}`);
|
|
@@ -75,64 +82,89 @@ function runRepoLocal(runtime) {
|
|
|
75
82
|
process.exit(result.status ?? 1);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
// One-shot, on-demand fetch, used only when the binary isn't cached yet and
|
|
86
|
+
// no repo-local runtime was found. Bounded well below postinstall's own
|
|
87
|
+
// window since a live invocation is blocked on this, not a background
|
|
88
|
+
// install step.
|
|
89
|
+
const ON_DEMAND_DEADLINE_MS = 30_000;
|
|
90
|
+
|
|
91
|
+
function launchDirectory() {
|
|
92
|
+
const args = process.argv.slice(2);
|
|
93
|
+
let target;
|
|
94
|
+
for (let i = 0; i < args.length && args[i] !== '--'; i++) {
|
|
95
|
+
if (args[i].startsWith('--cwd=')) target = args[i].slice(6);
|
|
96
|
+
else if (args[i] === '--cwd') {
|
|
97
|
+
target = args[++i];
|
|
98
|
+
if (!target || target.startsWith('--')) die('--cwd requires a repository directory.', 2);
|
|
99
|
+
}
|
|
82
100
|
}
|
|
101
|
+
if (target === undefined && args[0] === 'mcp') target = process.env.CODELEDGER_REPO_ROOT;
|
|
102
|
+
if (target !== undefined && !target.trim()) die('Repository target cannot be empty.', 2);
|
|
103
|
+
try {
|
|
104
|
+
const root = realpathSync(resolve(target ?? process.cwd()));
|
|
105
|
+
if (!statSync(root).isDirectory()) throw new Error('Not a directory');
|
|
106
|
+
return root;
|
|
107
|
+
} catch { die('Repository target must be an existing directory. Check --cwd or CODELEDGER_REPO_ROOT.', 2); }
|
|
108
|
+
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
} else {
|
|
108
|
-
die(
|
|
109
|
-
[
|
|
110
|
-
`ContextECF CodeLedger hardened binary for v${version} not found at ${BINARY_PATH}.`,
|
|
111
|
-
'',
|
|
112
|
-
'The postinstall step may have been skipped (e.g. --ignore-scripts).',
|
|
113
|
-
'This safe alias installs ContextECF CodeLedger. Avoid: npm install -g codeledger.',
|
|
114
|
-
'',
|
|
115
|
-
'Recover with one of:',
|
|
116
|
-
` 1. Upgrade: npm install -g contextecf-codeledger@latest`,
|
|
117
|
-
` 2. Retry this install: npm rebuild contextecf-codeledger`,
|
|
118
|
-
` 3. node "${installerPath}"`,
|
|
119
|
-
` 4. Re-run the install without --ignore-scripts`,
|
|
120
|
-
` 5. In an initialized repo, use: ./.codeledger/bin/codeledger ready`,
|
|
121
|
-
'',
|
|
122
|
-
'Or download the binary manually from:',
|
|
123
|
-
` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
|
|
124
|
-
].join('\n'),
|
|
110
|
+
async function main() {
|
|
111
|
+
if (!existsSync(BINARY_PATH)) {
|
|
112
|
+
const repoLocal = findRepoLocalRuntime(launchDirectory());
|
|
113
|
+
if (repoLocal) {
|
|
114
|
+
runRepoLocal(repoLocal);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { arch } = require('node:os');
|
|
118
|
+
const platformKey = `${platform()}-${arch()}`;
|
|
119
|
+
const installerPath = join(__dirname, '..', 'scripts', 'postinstall.js');
|
|
120
|
+
|
|
121
|
+
const { PLATFORM_MAP, downloadAndVerifyBinary, unsupportedPlatformMessage } = require('../scripts/postinstall.js');
|
|
122
|
+
if (!PLATFORM_MAP[platformKey]) {
|
|
123
|
+
// Don't waste time on a download attempt that can only ever fail —
|
|
124
|
+
// getBinaryPaths() would reject this platformKey immediately anyway.
|
|
125
|
+
// See postinstall.js's matching message for why this is "no
|
|
126
|
+
// committed timeline", not "try again later".
|
|
127
|
+
die(unsupportedPlatformMessage(platformKey));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`codeledger: hardened binary for v${version} not cached locally — downloading now (one-time)...\n`,
|
|
125
132
|
);
|
|
133
|
+
try {
|
|
134
|
+
await downloadAndVerifyBinary({ deadlineMs: ON_DEMAND_DEADLINE_MS });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
die(
|
|
137
|
+
[
|
|
138
|
+
`hardened binary for v${version} not found at ${BINARY_PATH}, and the on-demand download just failed: ${err.message}`,
|
|
139
|
+
'',
|
|
140
|
+
'The postinstall step may have been skipped (e.g. --ignore-scripts).',
|
|
141
|
+
'Confirm the official scoped package is installed: @codeledger/cli.',
|
|
142
|
+
'Avoid the unrelated unscoped npm package: codeledger.',
|
|
143
|
+
'',
|
|
144
|
+
'Recover with one of:',
|
|
145
|
+
' 1. Retry: codeledger --version',
|
|
146
|
+
' 2. Reinstall: npm install -g @codeledger/cli@latest',
|
|
147
|
+
` 3. node "${installerPath}"`,
|
|
148
|
+
' 4. Re-run the install without --ignore-scripts',
|
|
149
|
+
' 5. In an initialized repo, use: ./.codeledger/bin/codeledger ready',
|
|
150
|
+
'',
|
|
151
|
+
'Or download the binary manually from:',
|
|
152
|
+
` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
|
|
153
|
+
].join('\n'),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
126
156
|
}
|
|
127
|
-
}
|
|
128
157
|
|
|
129
|
-
const result = spawnSync(BINARY_PATH, process.argv.slice(2), {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
158
|
+
const result = spawnSync(BINARY_PATH, process.argv.slice(2), {
|
|
159
|
+
stdio: 'inherit',
|
|
160
|
+
env: process.env,
|
|
161
|
+
});
|
|
133
162
|
|
|
134
|
-
if (result.error) {
|
|
135
|
-
|
|
163
|
+
if (result.error) {
|
|
164
|
+
die(`failed to spawn binary: ${result.error.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
process.exit(result.status ?? 1);
|
|
136
168
|
}
|
|
137
169
|
|
|
138
|
-
|
|
170
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextecf-codeledger",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.81",
|
|
4
4
|
"mcpName": "io.github.codeledgerECF/codeledger",
|
|
5
5
|
"description": "ContextECF CodeLedger — deterministic context selection for AI coding agents. Safe unscoped alias for @codeledger/cli; avoid the unrelated codeledger package.",
|
|
6
6
|
"type": "commonjs",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* CodeLedger postinstall — download + verify the hardened binary.
|
|
4
4
|
*
|
|
5
|
-
* Runs automatically on `npm install -g
|
|
5
|
+
* Runs automatically on `npm install -g @codeledger/cli`. Downloads the
|
|
6
6
|
* platform-specific hardened binary from the GitHub Release for this
|
|
7
7
|
* exact version, verifies its SHA256 against SHA256SUMS-v<version>.txt
|
|
8
8
|
* (also downloaded from the release), and writes it to the cache dir.
|
|
9
9
|
*
|
|
10
|
+
* This is best-effort and always exits 0: if the download can't complete
|
|
11
|
+
* within the propagation window (e.g. installing a version whose release
|
|
12
|
+
* assets haven't landed yet, or aren't going to), npm must NOT treat the
|
|
13
|
+
* install as failed — a failed postinstall can roll back an existing
|
|
14
|
+
* working install. bin/codeledger.js retries the same download on-demand,
|
|
15
|
+
* synchronously, the first time the binary is actually needed.
|
|
16
|
+
*
|
|
10
17
|
* Environment variables (all optional):
|
|
11
18
|
* CODELEDGER_SKIP_POSTINSTALL=1 — skip entirely (air-gapped installs)
|
|
19
|
+
* CODELEDGER_RUN_UPGRADE=1 — after the binary download succeeds,
|
|
20
|
+
* also run `codeledger upgrade`
|
|
12
21
|
* CODELEDGER_BINARY_URL=<url> — override the release download base URL
|
|
13
22
|
* (for corporate mirrors / internal CDNs).
|
|
14
23
|
* Must serve the same filenames and a
|
|
15
24
|
* SHA256SUMS-v<version>.txt sibling file.
|
|
16
25
|
* CODELEDGER_CACHE_DIR=<path> — override the cache directory
|
|
17
26
|
* (default: ~/.cache/codeledger/bin)
|
|
27
|
+
* CODELEDGER_INSTALL_DEADLINE_MS=<ms> — override the download propagation
|
|
28
|
+
* window (default: 150000 = 2.5 min)
|
|
18
29
|
*
|
|
19
30
|
* This file contains no engine logic. It only fetches, verifies, and writes
|
|
20
31
|
* bytes. Safe to inspect, safe to mirror, safe to audit.
|
|
@@ -37,107 +48,79 @@ const { homedir, platform, arch } = require('node:os');
|
|
|
37
48
|
|
|
38
49
|
const { version } = require('../package.json');
|
|
39
50
|
|
|
40
|
-
// ───
|
|
41
|
-
|
|
42
|
-
if (process.env.CODELEDGER_SKIP_POSTINSTALL === '1') {
|
|
43
|
-
console.log(
|
|
44
|
-
'codeledger: CODELEDGER_SKIP_POSTINSTALL=1 set, skipping ContextECF CodeLedger hardened binary download.',
|
|
45
|
-
);
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Skip when running inside this source workspace (dev mode). The presence of
|
|
50
|
-
// pnpm-workspace.yaml two directories up is a reliable signal that this is not
|
|
51
|
-
// a real end-user install, and the release binary for this version may not
|
|
52
|
-
// exist yet.
|
|
53
|
-
try {
|
|
54
|
-
const path = require('node:path');
|
|
55
|
-
const fs = require('node:fs');
|
|
56
|
-
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
|
|
57
|
-
if (fs.existsSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'))) {
|
|
58
|
-
console.log(
|
|
59
|
-
'codeledger: in-workspace dev install detected, skipping ContextECF CodeLedger hardened binary download.',
|
|
60
|
-
);
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
// fall through to normal install
|
|
65
|
-
}
|
|
51
|
+
// ─── platform / path resolution (pure — no I/O, no process.exit) ──────────
|
|
66
52
|
|
|
67
|
-
//
|
|
68
|
-
// Mac runner on the release CI plan as of 2026-04-09. Intel Mac
|
|
69
|
-
// users hit the "unsupported platform" soft-fail below, which tells
|
|
70
|
-
// them to download manually from the GitHub Release. Re-add when a
|
|
71
|
-
// working Intel Mac binary is available.
|
|
53
|
+
// Only these native asset names are wired into this installer.
|
|
72
54
|
const PLATFORM_MAP = {
|
|
73
55
|
'darwin-arm64': 'darwin-arm64',
|
|
74
56
|
'linux-x64': 'linux-x64',
|
|
75
57
|
};
|
|
76
58
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`codeledger: unsupported platform ${platformKey}. Supported: ${Object.keys(
|
|
100
|
-
PLATFORM_MAP,
|
|
101
|
-
).join(', ')}`,
|
|
102
|
-
);
|
|
103
|
-
console.error(
|
|
104
|
-
`codeledger: v${version} does not provide a hardened binary for ${platformKey}.`,
|
|
105
|
-
);
|
|
106
|
-
console.error(
|
|
107
|
-
'codeledger: this installer is for ContextECF CodeLedger; you can still use the published source package or a supported release asset.',
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
// Fail softly: don't block `npm install` for unsupported platforms. The
|
|
111
|
-
// bin shim will tell the user what happened when they first run `codeledger`.
|
|
112
|
-
process.exit(0);
|
|
59
|
+
function unsupportedPlatformMessage(platformKey) {
|
|
60
|
+
return [
|
|
61
|
+
`${platformKey} has no pre-built CodeLedger hardened binary.`,
|
|
62
|
+
`Supported native routes: ${Object.keys(PLATFORM_MAP).join(', ')}.`,
|
|
63
|
+
'A successful npm install does not establish a working runtime or MCP connection.',
|
|
64
|
+
'For an already initialized repository with a trusted vendored runtime:',
|
|
65
|
+
' node .codeledger/bin/codeledger-standalone.cjs ready',
|
|
66
|
+
'For a fresh repository, use a supported host or a supported linux-x64 environment.',
|
|
67
|
+
'Skipping postinstall does not bootstrap a missing runtime; codeledger init cannot fix that loop.',
|
|
68
|
+
'Intel Macs cannot run an Apple Silicon binary through Rosetta.',
|
|
69
|
+
'Source-build prerequisites and limitations: https://github.com/codeledgerECF/codeledger/blob/main/docs/security/ENTERPRISE_ASSURANCE.md',
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPlatformKey() {
|
|
74
|
+
return `${platform()}-${arch()}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getCacheDir() {
|
|
78
|
+
return (
|
|
79
|
+
process.env.CODELEDGER_CACHE_DIR || join(homedir(), '.cache', 'codeledger', 'bin')
|
|
80
|
+
);
|
|
113
81
|
}
|
|
114
82
|
|
|
115
83
|
// Binaries are hosted on the public CodeLedger GitHub Releases page so
|
|
116
84
|
// `npm install -g @codeledger/cli` can fetch them without authentication.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
85
|
+
function getBinaryPaths(platformKey) {
|
|
86
|
+
const platformSuffix = PLATFORM_MAP[platformKey];
|
|
87
|
+
if (!platformSuffix) return null;
|
|
88
|
+
|
|
89
|
+
const releaseBase =
|
|
90
|
+
process.env.CODELEDGER_BINARY_URL ||
|
|
91
|
+
`https://github.com/codeledgerECF/codeledger/releases/download/v${version}`;
|
|
92
|
+
const binaryRemoteName = `codeledger-hardened-${platformSuffix}-v${version}`;
|
|
93
|
+
const cacheDir = getCacheDir();
|
|
120
94
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
95
|
+
return {
|
|
96
|
+
binaryUrl: `${releaseBase}/${binaryRemoteName}`,
|
|
97
|
+
checksumsUrl: `${releaseBase}/SHA256SUMS-v${version}.txt`,
|
|
98
|
+
binaryRemoteName,
|
|
99
|
+
cacheDir,
|
|
100
|
+
binaryLocalPath: join(cacheDir, `codeledger-hardened-v${version}`),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const BINARY_LOCAL_PATH = join(CACHE_DIR, BINARY_LOCAL_NAME);
|
|
104
|
+
function isBinaryCached() {
|
|
105
|
+
const paths = getBinaryPaths(getPlatformKey());
|
|
106
|
+
return Boolean(paths && existsSync(paths.binaryLocalPath));
|
|
107
|
+
}
|
|
130
108
|
|
|
131
109
|
// ─── http ────────────────────────────────────────────────────────────────────
|
|
132
110
|
|
|
133
|
-
|
|
111
|
+
const FETCH_TIMEOUT_MS = 60_000;
|
|
112
|
+
|
|
113
|
+
function fetchBufferOnce(url, redirectDepth = 0, expiresAt = Date.now() + FETCH_TIMEOUT_MS) {
|
|
134
114
|
if (redirectDepth > 5) {
|
|
135
115
|
return Promise.reject(new Error(`too many redirects fetching ${url}`));
|
|
136
116
|
}
|
|
137
117
|
return new Promise((resolve, reject) => {
|
|
118
|
+
const remaining = Math.min(FETCH_TIMEOUT_MS, expiresAt - Date.now());
|
|
119
|
+
if (remaining <= 0) { reject(new Error('Binary download deadline exceeded.')); return; }
|
|
138
120
|
const req = https.get(
|
|
139
121
|
url,
|
|
140
122
|
{
|
|
123
|
+
timeout: FETCH_TIMEOUT_MS,
|
|
141
124
|
headers: {
|
|
142
125
|
'User-Agent': `codeledger-installer/${version}`,
|
|
143
126
|
Accept: 'application/octet-stream',
|
|
@@ -153,7 +136,7 @@ function fetchBufferOnce(url, redirectDepth = 0) {
|
|
|
153
136
|
) {
|
|
154
137
|
res.resume();
|
|
155
138
|
const next = new URL(res.headers.location, url).toString();
|
|
156
|
-
fetchBufferOnce(next, redirectDepth + 1).then(resolve, reject);
|
|
139
|
+
fetchBufferOnce(next, redirectDepth + 1, expiresAt).then(resolve, reject);
|
|
157
140
|
return;
|
|
158
141
|
}
|
|
159
142
|
if (res.statusCode !== 200) {
|
|
@@ -169,34 +152,51 @@ function fetchBufferOnce(url, redirectDepth = 0) {
|
|
|
169
152
|
res.on('error', reject);
|
|
170
153
|
},
|
|
171
154
|
);
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
const error = new Error('Binary download deadline exceeded.');
|
|
157
|
+
req.destroy(error);
|
|
158
|
+
reject(error);
|
|
159
|
+
}, remaining);
|
|
172
160
|
req.on('error', reject);
|
|
173
|
-
req.
|
|
174
|
-
req.destroy(new Error(`timeout fetching ${url}`));
|
|
175
|
-
});
|
|
161
|
+
req.once('close', () => clearTimeout(timer));
|
|
176
162
|
});
|
|
177
163
|
}
|
|
178
164
|
|
|
165
|
+
// Default propagation window for the postinstall-time attempt. Deliberately
|
|
166
|
+
// a wall-clock deadline, not a fixed attempt count: the question is "how
|
|
167
|
+
// long is it plausible for a just-published release's assets to still be
|
|
168
|
+
// propagating," not "how many times did we try." A version whose GitHub
|
|
169
|
+
// Release will never exist (npm published ahead of a working release — the
|
|
170
|
+
// actual incident this retry policy is designed around) now fails within
|
|
171
|
+
// this window instead of retrying for ~8 minutes that were never going to
|
|
172
|
+
// succeed.
|
|
173
|
+
const DEFAULT_DEADLINE_MS = Number(process.env.CODELEDGER_INSTALL_DEADLINE_MS) || 150_000; // 2.5 minutes
|
|
174
|
+
|
|
179
175
|
// Retry wrapper that tolerates the short propagation window where a freshly
|
|
180
176
|
// published package may be visible before all release assets are available.
|
|
181
|
-
// A 404 is retried with exponential backoff
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
// A 404 is retried with exponential backoff bounded by deadlineMs;
|
|
178
|
+
// non-404 errors fail fast.
|
|
179
|
+
async function fetchBuffer(url, deadlineMs = DEFAULT_DEADLINE_MS) {
|
|
180
|
+
const start = Date.now();
|
|
181
|
+
let attempt = 0;
|
|
184
182
|
let lastErr;
|
|
185
|
-
for (
|
|
183
|
+
for (;;) {
|
|
186
184
|
try {
|
|
187
|
-
return await fetchBufferOnce(url);
|
|
185
|
+
return await fetchBufferOnce(url, 0, start + deadlineMs);
|
|
188
186
|
} catch (err) {
|
|
189
187
|
lastErr = err;
|
|
190
|
-
// Only retry on 404 — everything else is a hard failure
|
|
188
|
+
// Only retry on 404 — everything else is a hard failure.
|
|
191
189
|
if (err.statusCode !== 404) throw err;
|
|
192
|
-
|
|
193
|
-
const wait =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
const elapsed = Date.now() - start;
|
|
191
|
+
const wait = Math.min(20_000, 5_000 * 2 ** attempt);
|
|
192
|
+
if (elapsed + wait >= deadlineMs) break;
|
|
193
|
+
attempt += 1;
|
|
194
|
+
const remainingSec = Math.round((deadlineMs - elapsed) / 1000);
|
|
195
|
+
console.error(
|
|
196
|
+
`codeledger: Binary pending — retrying in ${Math.round(wait / 1000)}s ` +
|
|
197
|
+
`(${remainingSec}s left in propagation window).`,
|
|
198
198
|
);
|
|
199
|
-
await new Promise((r) => setTimeout(r, wait
|
|
199
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
throw lastErr;
|
|
@@ -221,72 +221,161 @@ function findExpectedSha(checksumsText, filename) {
|
|
|
221
221
|
return null;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// ───
|
|
224
|
+
// ─── download + verify (the reusable, side-effect-scoped core) ────────────
|
|
225
|
+
|
|
226
|
+
// Downloads, verifies, and caches the hardened binary for the current
|
|
227
|
+
// platform + this package's version. Throws on failure — does not exit the
|
|
228
|
+
// process. Callers decide what "failure" means for them: postinstall.js
|
|
229
|
+
// soft-fails (exit 0, retry later); bin/codeledger.js's on-demand path
|
|
230
|
+
// falls through to a hard failure message since the user is blocked right
|
|
231
|
+
// now.
|
|
232
|
+
async function downloadAndVerifyBinary({ deadlineMs = DEFAULT_DEADLINE_MS } = {}) {
|
|
233
|
+
const platformKey = getPlatformKey();
|
|
234
|
+
const paths = getBinaryPaths(platformKey);
|
|
235
|
+
if (!paths) {
|
|
236
|
+
throw new Error(`unsupported platform ${platformKey}`);
|
|
237
|
+
}
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
console.
|
|
228
|
-
console.log(` source: ${BINARY_URL}`);
|
|
239
|
+
console.error(`codeledger: installing v${version} hardened binary for ${platformKey}`);
|
|
240
|
+
console.error(` source: ${paths.binaryUrl}`);
|
|
229
241
|
|
|
230
242
|
const [checksumsBuf, binary] = await Promise.all([
|
|
231
|
-
fetchBuffer(
|
|
232
|
-
fetchBuffer(
|
|
243
|
+
fetchBuffer(paths.checksumsUrl, deadlineMs),
|
|
244
|
+
fetchBuffer(paths.binaryUrl, deadlineMs),
|
|
233
245
|
]);
|
|
234
246
|
const checksums = checksumsBuf.toString('utf8');
|
|
235
247
|
|
|
236
|
-
const expectedSha = findExpectedSha(checksums,
|
|
248
|
+
const expectedSha = findExpectedSha(checksums, paths.binaryRemoteName);
|
|
237
249
|
if (!expectedSha) {
|
|
238
250
|
throw new Error(
|
|
239
|
-
`${
|
|
251
|
+
`${paths.binaryRemoteName} not listed in SHA256SUMS-v${version}.txt — cannot verify binary integrity`,
|
|
240
252
|
);
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
const actualSha = createHash('sha256').update(binary).digest('hex');
|
|
244
256
|
if (actualSha !== expectedSha) {
|
|
245
257
|
throw new Error(
|
|
246
|
-
`SHA256 mismatch for ${
|
|
258
|
+
`SHA256 mismatch for ${paths.binaryRemoteName}:\n expected ${expectedSha}\n got ${actualSha}`,
|
|
247
259
|
);
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
mkdirSync(
|
|
262
|
+
mkdirSync(paths.cacheDir, { recursive: true });
|
|
251
263
|
|
|
252
264
|
// Atomic-ish write: write to tmp, rename
|
|
253
|
-
const tmpPath = `${
|
|
265
|
+
const tmpPath = `${paths.binaryLocalPath}.tmp`;
|
|
254
266
|
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
255
267
|
writeFileSync(tmpPath, binary);
|
|
256
268
|
chmodSync(tmpPath, 0o755);
|
|
257
|
-
if (existsSync(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
console.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
console.log('codeledger: next step inside your repo:');
|
|
265
|
-
console.log(' codeledger ready');
|
|
266
|
-
console.log('codeledger: ready initializes, scans, and prepares ambient task context.');
|
|
269
|
+
if (existsSync(paths.binaryLocalPath)) unlinkSync(paths.binaryLocalPath);
|
|
270
|
+
require('node:fs').renameSync(tmpPath, paths.binaryLocalPath);
|
|
271
|
+
|
|
272
|
+
console.error(`codeledger: installed ${paths.binaryLocalPath}`);
|
|
273
|
+
console.error(` sha256: ${actualSha}`);
|
|
274
|
+
|
|
275
|
+
return paths.binaryLocalPath;
|
|
267
276
|
}
|
|
268
277
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.error('
|
|
283
|
-
|
|
284
|
-
|
|
278
|
+
// Optional post-download step: run `codeledger upgrade` in the most likely
|
|
279
|
+
// target repo so existing repos pick up refreshed ambient hooks/runtime
|
|
280
|
+
// without a second manual command. Opt-in only — never runs by default.
|
|
281
|
+
function maybeRunUpgrade() {
|
|
282
|
+
if (process.env.CODELEDGER_RUN_UPGRADE === '1') {
|
|
283
|
+
const { spawnSync } = require('node:child_process');
|
|
284
|
+
const path = require('node:path');
|
|
285
|
+
const fs = require('node:fs');
|
|
286
|
+
const codeledgerBin = path.join(__dirname, '..', 'bin', 'codeledger.js');
|
|
287
|
+
const initCwd = process.env['INIT_CWD'];
|
|
288
|
+
const homeCwd = process.env['HOME'] ?? process.env['USERPROFILE'] ?? process.cwd();
|
|
289
|
+
const upgradeCwd =
|
|
290
|
+
initCwd && fs.existsSync(path.join(initCwd, '.codeledger')) ? initCwd : homeCwd;
|
|
291
|
+
console.error('codeledger: CODELEDGER_RUN_UPGRADE=1 set, running upgrade...');
|
|
292
|
+
const upgradeResult = spawnSync(process.execPath, [codeledgerBin, 'upgrade'], {
|
|
293
|
+
stdio: 'inherit',
|
|
294
|
+
cwd: upgradeCwd,
|
|
295
|
+
env: { ...process.env, CODELEDGER_SKIP_AMBIENT_ACTIVATION: '1' },
|
|
296
|
+
});
|
|
297
|
+
if ((upgradeResult.status ?? 0) !== 0) {
|
|
298
|
+
console.error('');
|
|
299
|
+
console.error('codeledger: upgrade step encountered an issue. Run manually:');
|
|
300
|
+
console.error(' codeledger upgrade');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── postinstall entry point ───────────────────────────────────────────────
|
|
306
|
+
// All side-effecting decisions (env skip, dev-workspace skip, platform
|
|
307
|
+
// gating, process.exit) live here, gated behind `require.main === module`
|
|
308
|
+
// below — so requiring this file as a library (from bin/codeledger.js, or
|
|
309
|
+
// from tests) never has side effects.
|
|
310
|
+
|
|
311
|
+
async function runPostinstall() {
|
|
312
|
+
if (process.env.CODELEDGER_SKIP_POSTINSTALL === '1') {
|
|
285
313
|
console.error(
|
|
286
|
-
|
|
314
|
+
'codeledger: CODELEDGER_SKIP_POSTINSTALL=1 set, skipping ContextECF CodeLedger hardened binary download.',
|
|
287
315
|
);
|
|
288
|
-
|
|
289
|
-
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Skip when running inside this source workspace (dev mode). The presence
|
|
320
|
+
// of pnpm-workspace.yaml two directories up is a reliable signal that
|
|
321
|
+
// this is not a real end-user install, and the release binary for this
|
|
322
|
+
// version may not exist yet.
|
|
323
|
+
try {
|
|
324
|
+
const path = require('node:path');
|
|
325
|
+
const fs = require('node:fs');
|
|
326
|
+
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
|
|
327
|
+
if (fs.existsSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'))) {
|
|
328
|
+
console.error(
|
|
329
|
+
'codeledger: in-workspace dev install detected, skipping ContextECF CodeLedger hardened binary download.',
|
|
330
|
+
);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// fall through to normal install
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const platformKey = getPlatformKey();
|
|
338
|
+
|
|
339
|
+
if (!PLATFORM_MAP[platformKey]) {
|
|
340
|
+
// Soft install failure does not create a fresh runtime on this platform.
|
|
341
|
+
console.error(`codeledger: ${unsupportedPlatformMessage(platformKey)}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await downloadAndVerifyBinary();
|
|
347
|
+
console.error('');
|
|
348
|
+
console.error('codeledger: install complete.');
|
|
349
|
+
console.error(' Next: cd your-repo && codeledger ready');
|
|
350
|
+
console.error(' Existing repos can refresh ambient hooks/runtime with: codeledger upgrade');
|
|
351
|
+
console.error('codeledger: ready initializes, scans, and prepares ambient task context.');
|
|
352
|
+
maybeRunUpgrade();
|
|
353
|
+
} catch (err) {
|
|
354
|
+
// Soft-fail, deliberately: exit 0 either way. A failed postinstall
|
|
355
|
+
// makes npm treat the whole install as failed, which can roll back an
|
|
356
|
+
// existing working install (the exact incident this is designed
|
|
357
|
+
// around). bin/codeledger.js retries this same download on-demand,
|
|
358
|
+
// synchronously, the first time it's actually needed.
|
|
359
|
+
console.error(`codeledger: could not pre-fetch the hardened binary — ${err.message}`);
|
|
360
|
+
console.error('codeledger: not fatal — it will download automatically on first "codeledger" invocation.');
|
|
361
|
+
console.error('codeledger: or, in an initialized repo, use: ./.codeledger/bin/codeledger ready');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Only run the postinstall entry point when invoked directly (npm
|
|
366
|
+
// postinstall, node scripts/postinstall.js). When required — from
|
|
367
|
+
// bin/codeledger.js's on-demand download path, or from tests — this file
|
|
368
|
+
// has no side effects; only the exported pure/reusable functions are used.
|
|
369
|
+
if (require.main === module) {
|
|
370
|
+
runPostinstall().then(() => process.exit(0));
|
|
290
371
|
}
|
|
291
372
|
|
|
292
|
-
module.exports = {
|
|
373
|
+
module.exports = {
|
|
374
|
+
findExpectedSha,
|
|
375
|
+
PLATFORM_MAP,
|
|
376
|
+
downloadAndVerifyBinary,
|
|
377
|
+
getPlatformKey,
|
|
378
|
+
getBinaryPaths,
|
|
379
|
+
isBinaryCached,
|
|
380
|
+
unsupportedPlatformMessage,
|
|
381
|
+
};
|