Skip to content

Commit 0d9470c

Browse files
NickCirvclaude
andcommitted
fix: ESM require() runtime bug + empty setup suggestions block
## Bug 1 — doctor.checkHook always reported "not found" in production Root cause: three functions in src/doctor/report.ts, two in src/setup/detect.ts, and one in src/setup/wizard.ts used bare require() calls at runtime — but tsup bundles to ESM where require is undefined. The ReferenceError was caught by each function's own catch block and silently treated as "check failed," so: - doctor reported "Sentinel hook not found" even after a successful install-hook (verified live against a fresh tmp project) - detect.ts's Claude Code detection reported configured=false even when the hook was wired - setup --user scope would hit "homedir is not a function" Why tests passed: vitest's test runtime provides CommonJS interop so require() works in test environment. The bundled CLI doesn't. v2.0.1 shipped a similar "CI-passed, prod-broken" pattern for a different reason; this is the same class of bug. Fix: move every require() to a proper top-level ESM import. - report.ts: import readFileSync + cachePath/isNewer at top - detect.ts: import readFileSync at top - wizard.ts: import readFileSync, writeFileSync, mkdirSync, dirname, homedir at top Verified live: $ engram setup -y -p /tmp/fresh-project ✓ Sentinel hook installed ✓ hook Sentinel hook active (via /tmp/fresh-project/.claude/settings.local.json) ## Bug 2 — empty "Next steps for detected IDEs:" block When only Claude Code was detected-but-unconfigured (and there's no suggest entry for Claude Code because install-hook handles it in step 2), the wizard printed "Next steps for detected IDEs:" with no subsequent lines. Fix: collect suggestions first. If the list is empty, either: - skip with "Claude Code hook declined" if Claude Code is the culprit (user declined hook install in step 2) - done with "no additional adapters needed" otherwise The header line only prints when there's at least one actionable suggestion. ## Audit result While auditing for this bug I grepped every new file for `require(` and found 7 offenders across 3 files. All fixed in one commit. No other ESM-interop issues found. Verified: 746/746 tests still pass, lint clean, build clean, live E2E test of setup → doctor → Bash auto-reindex all behave correctly. Bash PostToolUse with ENGRAM_AUTO_REINDEX=1 successfully prunes the graph when the underlying file is rm'd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ecee8bd commit 0d9470c

3 files changed

Lines changed: 42 additions & 28 deletions

File tree

src/doctor/report.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
* network calls. Safe to run on every SessionStart or in CI.
1010
*/
1111
import chalk from "chalk";
12-
import { existsSync, statSync } from "node:fs";
12+
import { existsSync, readFileSync, statSync } from "node:fs";
1313
import { join } from "node:path";
1414
import { homedir, platform, release } from "node:os";
1515
import {
1616
refreshComponentStatus,
1717
type ComponentHealth,
1818
} from "../intercept/component-status.js";
19+
import { cachePath, isNewer } from "../update/check.js";
1920

2021
/** Severity buckets. */
2122
export type Severity = "ok" | "warn" | "fail";
@@ -78,7 +79,6 @@ function checkHook(projectRoot: string): DoctorCheck {
7879
for (const path of candidates) {
7980
if (!existsSync(path)) continue;
8081
try {
81-
const { readFileSync } = require("node:fs") as typeof import("node:fs");
8282
const content = readFileSync(path, "utf-8");
8383
if (content.includes("engram intercept")) {
8484
return {
@@ -128,7 +128,6 @@ function componentToCheck(c: ComponentHealth): DoctorCheck {
128128
/** Check engram CLI version against the last cached registry check. */
129129
function checkVersion(engramVersion: string): DoctorCheck {
130130
try {
131-
const { cachePath } = require("../update/check.js") as typeof import("../update/check.js");
132131
const path = cachePath();
133132
if (!existsSync(path)) {
134133
return {
@@ -137,20 +136,20 @@ function checkVersion(engramVersion: string): DoctorCheck {
137136
detail: `engram v${engramVersion} (no update check cached yet)`,
138137
};
139138
}
140-
const { readFileSync } = require("node:fs") as typeof import("node:fs");
141139
const cached = JSON.parse(readFileSync(path, "utf-8")) as {
142140
latest?: string;
143141
};
144-
if (typeof cached?.latest === "string" && cached.latest !== engramVersion) {
145-
const { isNewer } = require("../update/check.js") as typeof import("../update/check.js");
146-
if (isNewer(cached.latest, engramVersion)) {
147-
return {
148-
name: "version",
149-
severity: "warn",
150-
detail: `engram v${engramVersion} — v${cached.latest} is available`,
151-
remediation: "Run `engram update` to upgrade.",
152-
};
153-
}
142+
if (
143+
typeof cached?.latest === "string" &&
144+
cached.latest !== engramVersion &&
145+
isNewer(cached.latest, engramVersion)
146+
) {
147+
return {
148+
name: "version",
149+
severity: "warn",
150+
detail: `engram v${engramVersion} — v${cached.latest} is available`,
151+
remediation: "Run `engram update` to upgrade.",
152+
};
154153
}
155154
return {
156155
name: "version",

src/setup/detect.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Used by `engram setup` to decide which adapters to offer. Pure
55
* file-existence probes — no network, no shell calls.
66
*/
7-
import { existsSync } from "node:fs";
7+
import { existsSync, readFileSync } from "node:fs";
88
import { join } from "node:path";
99
import { homedir } from "node:os";
1010

@@ -36,7 +36,6 @@ export function detectClaudeCode(projectRoot: string): IdeDetection {
3636

3737
let configured = false;
3838
try {
39-
const { readFileSync } = require("node:fs") as typeof import("node:fs");
4039
configured = settingsCandidates
4140
.filter(existsSync)
4241
.some((p) => readFileSync(p, "utf-8").includes("engram intercept"));
@@ -99,7 +98,6 @@ export function detectContinue(): IdeDetection {
9998
let configured = false;
10099
if (installed) {
101100
try {
102-
const { readFileSync } = require("node:fs") as typeof import("node:fs");
103101
configured = readFileSync(path, "utf-8").includes("engram");
104102
} catch {
105103
configured = false;

src/setup/wizard.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
*/
1818
import chalk from "chalk";
1919
import readline from "node:readline/promises";
20-
import { existsSync } from "node:fs";
21-
import { join, resolve as pathResolve } from "node:path";
20+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
21+
import { dirname, join, resolve as pathResolve } from "node:path";
22+
import { homedir } from "node:os";
2223
import { init } from "../core.js";
2324
import { installEngramHooks } from "../intercept/installer.js";
24-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
25-
import { dirname } from "node:path";
2625
import { detectAllIdes } from "./detect.js";
2726
import { buildReport, formatReport } from "../doctor/report.js";
2827

@@ -117,7 +116,7 @@ async function ensureHookInstalled(
117116
const scope = opts.settingsScope ?? "local";
118117
const settingsPath =
119118
scope === "user"
120-
? join(require("node:os").homedir(), ".claude", "settings.json")
119+
? join(homedir(), ".claude", "settings.json")
121120
: scope === "project"
122121
? join(root, ".claude", "settings.json")
123122
: join(root, ".claude", "settings.local.json");
@@ -202,15 +201,33 @@ async function offerIdeAdapters(
202201
Windsurf: "engram gen-windsurfrules",
203202
Aider: "engram gen-aider",
204203
};
205-
console.log("");
206-
console.log(chalk.dim(" Next steps for detected IDEs:"));
207-
const run: string[] = [];
204+
205+
// Collect suggestions first so we only print the header when there's
206+
// something actionable. Claude Code is handled by install-hook (step 2)
207+
// so an unconfigured Claude Code here means hook-install was declined.
208+
const suggested: Array<{ name: string; cmd: string }> = [];
208209
for (const ide of unconfigured) {
209210
const cmd = suggest[ide.name];
210-
if (cmd) {
211-
console.log(chalk.white(` $ ${cmd}`));
212-
run.push(ide.name);
211+
if (cmd) suggested.push({ name: ide.name, cmd });
212+
}
213+
214+
if (suggested.length === 0) {
215+
// Nothing actionable to print. Note Claude Code coverage if relevant.
216+
const claudeCode = unconfigured.find((d) => d.name === "Claude Code");
217+
if (claudeCode) {
218+
skip("Claude Code hook declined or missing — re-run `engram install-hook`");
219+
} else {
220+
done("no additional adapters needed");
213221
}
222+
return [];
223+
}
224+
225+
console.log("");
226+
console.log(chalk.dim(" Next steps for detected IDEs:"));
227+
const run: string[] = [];
228+
for (const s of suggested) {
229+
console.log(chalk.white(` $ ${s.cmd}`));
230+
run.push(s.name);
214231
}
215232
return run;
216233
}

0 commit comments

Comments
 (0)