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 CHANGED
@@ -1,21 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * ContextECF CodeLedger npm launcher — thin shim (safe unscoped alias).
3
+ * CodeLedger npm launcher — thin shim.
4
4
  *
5
- * This is the safe unscoped alias of @codeledger/cli. Both packages install
6
- * the exact same hardened binary. You can install either:
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
- * npm install -g contextecf-codeledger@latest (this package)
9
- * npm install -g @codeledger/cli@latest (scoped original)
10
- *
11
- * This file contains no engine code. All CodeLedger functionality lives
12
- * inside the hardened binary downloaded during postinstall.
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
- if (!existsSync(BINARY_PATH)) {
79
- const repoLocal = findRepoLocalRuntime(process.cwd());
80
- if (repoLocal) {
81
- runRepoLocal(repoLocal);
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
- const { arch } = require('node:os');
85
- const platformKey = `${platform()}-${arch()}`;
86
- const installerPath = join(__dirname, '..', 'scripts', 'postinstall.js');
87
-
88
- if (platformKey === 'darwin-x64') {
89
- die(
90
- [
91
- `ContextECF CodeLedger hardened binary for v${version} not found at ${BINARY_PATH}.`,
92
- '',
93
- 'You are on an Intel Mac (darwin-x64). Pre-built binaries are',
94
- 'available for Apple Silicon (darwin-arm64) only.',
95
- '',
96
- 'Options:',
97
- ' 1. Use Rosetta 2 (recommended):',
98
- ' arch -arm64 npm install -g contextecf-codeledger@latest',
99
- ' 2. If this repo was already initialized, use the vendored CLI:',
100
- ' npm install -g contextecf-codeledger@latest',
101
- ' ./.codeledger/bin/codeledger ready',
102
- ' node .codeledger/bin/codeledger-standalone.cjs ready',
103
- ' 3. Download a binary manually from:',
104
- ` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
105
- ].join('\n'),
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
- stdio: 'inherit',
131
- env: process.env,
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
- die(`failed to spawn binary: ${result.error.message}`);
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
- process.exit(result.status ?? 1);
170
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextecf-codeledger",
3
- "version": "0.10.79",
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",
@@ -1,20 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * ContextECF CodeLedger postinstall — download + verify the hardened binary.
3
+ * CodeLedger postinstall — download + verify the hardened binary.
4
4
  *
5
- * Runs automatically on `npm install -g contextecf-codeledger`. Downloads the
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
- // ─── config ──────────────────────────────────────────────────────────────────
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
- // darwin-x64 is temporarily absent from v0.10.6 — no working Intel
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
- const platformKey = `${platform()}-${arch()}`;
78
- const platformSuffix = PLATFORM_MAP[platformKey];
79
-
80
- if (!platformSuffix) {
81
- if (platformKey === 'darwin-x64') {
82
- console.log('');
83
- console.log('codeledger: ContextECF CodeLedger Intel Mac (darwin-x64) detected.');
84
- console.log('codeledger: Pre-built binaries are available for Apple Silicon (darwin-arm64) only.');
85
- console.log('');
86
- console.log('Options:');
87
- console.log(' Safe alias: contextecf-codeledger. Official package: @codeledger/cli. Avoid: codeledger.');
88
- console.log(' 1. Use Rosetta 2 (recommended):');
89
- console.log(' arch -arm64 npm install -g contextecf-codeledger@latest');
90
- console.log(' 2. If this repo was already initialized, use the vendored CLI:');
91
- console.log(' ./.codeledger/bin/codeledger ready');
92
- console.log(' node .codeledger/bin/codeledger-standalone.cjs ready');
93
- console.log(' 3. For a new repo without a vendored CLI, use a supported platform or Docker.');
94
- console.log(' 4. Download a binary manually from:');
95
- console.log(` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`);
96
- console.log('');
97
- } else {
98
- console.error(
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
- const RELEASE_BASE =
118
- process.env.CODELEDGER_BINARY_URL ||
119
- `https://github.com/codeledgerECF/codeledger/releases/download/v${version}`;
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
- const BINARY_REMOTE_NAME = `codeledger-hardened-${platformSuffix}-v${version}`;
122
- const BINARY_URL = `${RELEASE_BASE}/${BINARY_REMOTE_NAME}`;
123
- const CHECKSUMS_URL = `${RELEASE_BASE}/SHA256SUMS-v${version}.txt`;
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
- const CACHE_DIR =
126
- process.env.CODELEDGER_CACHE_DIR ||
127
- join(homedir(), '.cache', 'codeledger', 'bin');
128
- const BINARY_LOCAL_NAME = `codeledger-hardened-v${version}`;
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
- function fetchBufferOnce(url, redirectDepth = 0) {
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.setTimeout(60_000, () => {
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; non-404 errors fail fast.
182
- async function fetchBuffer(url) {
183
- const backoffs = [10, 20, 40, 60, 60, 60, 60, 60, 60, 60]; // seconds
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 (let i = 0; i <= backoffs.length; i++) {
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
- if (i === backoffs.length) break;
193
- const wait = backoffs[i];
194
- console.log(
195
- `codeledger: Binary pending — new release detected, retrying in ${wait}s ` +
196
- `(attempt ${i + 1}/${backoffs.length + 1}). ` +
197
- `This is normal for the first ~10 minutes after a release.`,
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 * 1000));
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
- // ─── main ────────────────────────────────────────────────────────────────────
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
- async function main() {
227
- console.log(`codeledger: installing v${version} hardened binary for ${platformKey}`);
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(CHECKSUMS_URL),
232
- fetchBuffer(BINARY_URL),
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, BINARY_REMOTE_NAME);
248
+ const expectedSha = findExpectedSha(checksums, paths.binaryRemoteName);
237
249
  if (!expectedSha) {
238
250
  throw new Error(
239
- `${BINARY_REMOTE_NAME} not listed in SHA256SUMS-v${version}.txt — cannot verify binary integrity`,
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 ${BINARY_REMOTE_NAME}:\n expected ${expectedSha}\n got ${actualSha}`,
258
+ `SHA256 mismatch for ${paths.binaryRemoteName}:\n expected ${expectedSha}\n got ${actualSha}`,
247
259
  );
248
260
  }
249
261
 
250
- mkdirSync(CACHE_DIR, { recursive: true });
262
+ mkdirSync(paths.cacheDir, { recursive: true });
251
263
 
252
264
  // Atomic-ish write: write to tmp, rename
253
- const tmpPath = `${BINARY_LOCAL_PATH}.tmp`;
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(BINARY_LOCAL_PATH)) unlinkSync(BINARY_LOCAL_PATH);
258
- // rename via fs for portability
259
- require('node:fs').renameSync(tmpPath, BINARY_LOCAL_PATH);
260
-
261
- console.log(`codeledger: installed ${BINARY_LOCAL_PATH}`);
262
- console.log(` sha256: ${actualSha}`);
263
- console.log('');
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
- // Only run main() when invoked directly (npm postinstall, node scripts/postinstall.js).
270
- // When require()'d from tests, just export the helper functions.
271
- if (require.main === module) {
272
- main().catch((err) => {
273
- console.error(`codeledger: postinstall failed — ${err.message}`);
274
- console.error('');
275
- console.error('Recovery options:');
276
- console.error(' 1. Upgrade: npm install -g contextecf-codeledger@latest');
277
- console.error(' 2. Retry this install: npm rebuild contextecf-codeledger');
278
- console.error(' 3. Air-gapped install:');
279
- console.error(' CODELEDGER_SKIP_POSTINSTALL=1 npm install -g contextecf-codeledger@latest');
280
- console.error(' then manually place the binary at:');
281
- console.error(` ${BINARY_LOCAL_PATH}`);
282
- console.error(' 4. Use a mirror:');
283
- console.error(' CODELEDGER_BINARY_URL=https://your-mirror/path npm install -g contextecf-codeledger@latest');
284
- console.error('');
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
- `Manual download: https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
314
+ 'codeledger: CODELEDGER_SKIP_POSTINSTALL=1 set, skipping ContextECF CodeLedger hardened binary download.',
287
315
  );
288
- process.exit(1);
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 = { findExpectedSha, PLATFORM_MAP };
373
+ module.exports = {
374
+ findExpectedSha,
375
+ PLATFORM_MAP,
376
+ downloadAndVerifyBinary,
377
+ getPlatformKey,
378
+ getBinaryPaths,
379
+ isBinaryCached,
380
+ unsupportedPlatformMessage,
381
+ };