infernoflow 0.36.0 → 0.37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## Unreleased
4
+
5
+ > Changes since v0.10.17
6
+
7
+ ### Added
8
+ - Scarf analytics + telemetry UUID/timezone + feedback Formspree (v0.35.6)
9
+ - cloud session memory sync (v0.35.4)
10
+ - opt-in telemetry system (v0.35.3)
11
+ - stunning switch output (v0.35.2)
12
+ - infernoflow feedback command (v0.35.1)
13
+ - auto-capture git hooks + log --auto/--quiet/--source (v0.35.0)
14
+ - progressive disclosure — 5 core commands in --help, infernoflow commands shows full grouped list (v0.34.4)
15
+ - npm metadata — AI-optimized description + 20 keywords for AI-native discovery (v0.34.3)
16
+ - infernoflow uninstall — remove all infernoflow artifacts from a project (v0.34.1)
17
+ - v0.34.0 — persistent memory layer: switch session-boundary-aware, README rewrite, build fix (Sprint 8+9+10)
18
+ - infernoflow recap — end-of-session summary with session health score, unlogged git topics, and nudges (v0.33.7)
19
+ - infernoflow ask — query session memory by keyword/type, gotchas surface first (v0.33.6)
20
+ - infernoflow stats — value dashboard (memory, tokens injected, coverage, savings estimate) (v0.33.5)
21
+ - end-to-end HTTP chain tracing — resolves outbound calls to route handlers (v0.33.4)
22
+ - Sprint 4 — route discovery, HTTP URL extraction, entry point detection, --suggest (v0.33.3)
23
+ - infernoflow init --lite (3 files) + infernoflow upgrade (lite → full) (v0.33.2)
24
+ - infernoflow switch — AI agent handoff summary (v0.33.1)
25
+ - session memory (infernoflow log) + design system tracker (infernoflow theme) — capture what AI can't infer from code (v0.33.0)
26
+ - graph --html interactive SVG output; VS Code extension v0.5.0 with Capability Graph panel (v0.32.9)
27
+ - VS Code extension v0.4.0 — save-triggered contract sync
28
+ - ai setup numbered menu with env-var auto-detection (v0.32.3)
29
+ - Sprint 18C/D — dogfood capabilities.json, doctor action list, init AI nudge (v0.32.0)
30
+ - Sprint 18 — demo command, AI fallback nudges, test fix (v0.31.0)
31
+ - Sprint 17 — infernoflow test + ai commands (v0.30.0)
32
+ - Sprint 16C — infernoflow explain command (v0.29.0)
33
+ - Sprint 16B — infernoflow scaffold command (v0.28.0)
34
+ - Sprint 15 Liquid Layer + Sprint 16A/16D (v0.26-0.27)
35
+ - auto-configure Claude Code MCP + allowedTools (v0.10.25)
36
+ - add changelog and diff commands for enhanced version tracking
37
+ - add VS Code + Cursor extension
38
+ - add infernoflow publish command
39
+ - bump to 0.10.18, fix duplicate infernoDir in suggest
40
+ - add React-specific scanner for --adopt
41
+
42
+ ### Fixed
43
+ - uninstall now removes all infernoflow artifacts completely (v0.34.2)
44
+ - extension sidebar icon badge — switch to createTreeView so uncovered count shows on activity bar icon
45
+ - graph crashes on toString() — use Map instead of plain object for funcIndex (v0.32.8)
46
+ - check handles bare-array capabilities.json; scenario coverage is a warning not error (v0.32.7)
47
+ - typo 'capabilityy' in explain file-path output
48
+ - explain command now accepts file paths in addition to capability IDs (v0.32.6)
49
+ - await import in non-async loadCapsAtRef causes syntax error (v0.32.5)
50
+ - add missing resolveProvider export to providerRouter.mjs (v0.32.4)
51
+ - Windows path bug in demo/watch/ci/monorepo/notify (v0.32.2)
52
+ - doctor CLI check false positive on Windows (.cmd PATH resolution)
53
+
54
+ ### Changed
55
+ - v0.37.0 — Windows unicode fix, memory-first init, hot files in switch, CLAUDE.md auto-update, health score tips, icon fix, cross-platform postinstall
56
+ - wire Polar.sh Pro checkout — real payments live
57
+ - point homepage to infernoflow.dev (v0.35.9)
58
+ - activate Formspree feedback endpoint (v0.35.8)
59
+ - activate PostHog EU telemetry (v0.35.7)
60
+ - bump to 0.35.5 (0.35.4 already published)
61
+ - rewrite README to reflect all capabilities (v0.32.5)
62
+ - v0.32.1
63
+ - bump to v0.32.0
64
+ - bump version to 0.31.0
65
+ - release 0.10.24
66
+ - release 0.10.23
67
+ - release 0.10.22
68
+ - release 0.10.21
69
+ - release 0.10.20
70
+ - release 0.10.19
71
+
72
+
73
+
3
74
  ## 0.10.25 — 2026-04-22
4
75
 
5
76
  ### Added
@@ -1,4 +1,39 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // ── Windows PowerShell unicode fix ───────────────────────────────────────
4
+ // Default PowerShell can't render box-drawing chars — patch stdout/stderr
5
+ // to replace them with ASCII equivalents before any output happens.
6
+ (function patchUnicodeForWindows() {
7
+ if (process.platform !== "win32") return;
8
+ if (process.env.WT_SESSION) return; // Windows Terminal — supports unicode
9
+ if (process.env.ConEmuPID) return; // ConEmu/Cmder
10
+ if (process.env.TERM_PROGRAM === "vscode") return; // VS Code terminal
11
+
12
+ const MAP = {
13
+ "─": "-", "━": "-", "═": "=",
14
+ "│": "|", "┃": "|", "║": "|",
15
+ "┌": "+", "┐": "+", "└": "+", "┘": "+",
16
+ "├": "+", "┤": "+", "┬": "+", "┴": "+", "┼": "+",
17
+ "·": "*", "→": "->", "←": "<-", "✔": "[OK]", "✓": "[OK]",
18
+ "✘": "[X]", "✗": "[X]", "⚠": "[!]", "ℹ": "[i]",
19
+ };
20
+ const RE = new RegExp(Object.keys(MAP).join("|"), "g");
21
+
22
+ function patch(stream) {
23
+ const orig = stream.write.bind(stream);
24
+ stream.write = function(chunk, ...args) {
25
+ if (typeof chunk === "string") chunk = chunk.replace(RE, c => MAP[c]);
26
+ else if (Buffer.isBuffer(chunk)) {
27
+ const s = chunk.toString("utf8").replace(RE, c => MAP[c]);
28
+ chunk = Buffer.from(s, "utf8");
29
+ }
30
+ return orig(chunk, ...args);
31
+ };
32
+ }
33
+ patch(process.stdout);
34
+ patch(process.stderr);
35
+ })();
36
+
2
37
  import { readFileSync } from "node:fs";
3
38
  import { dirname, join } from "node:path";
4
39
  import { fileURLToPath } from "node:url";
@@ -214,12 +214,98 @@ async function initLite(cwd, force) {
214
214
  console.log();
215
215
  }
216
216
 
217
+ /** Default init: memory-only, asks for first gotcha, 60 seconds */
218
+ async function initMemory(cwd, force, yes) {
219
+ const { bold, cyan, gray, green, yellow } = await import("../ui/output.mjs");
220
+
221
+ const infernoDir = path.join(cwd, "inferno");
222
+ const sessionsFile = path.join(infernoDir, "sessions.jsonl");
223
+ const configFile = path.join(infernoDir, "config.json");
224
+
225
+ if (fs.existsSync(infernoDir) && !force) {
226
+ // Already initialized — just confirm and show commands
227
+ console.log("\n " + bold("🔥 infernoflow") + gray(" — already set up\n"));
228
+ console.log(" " + green("✔") + " inferno/ found\n");
229
+ console.log(" Quick commands:");
230
+ console.log(" " + cyan("infernoflow log \"...\"") + gray(" — remember something"));
231
+ console.log(" " + cyan("infernoflow switch") + gray(" — handoff to next AI"));
232
+ console.log(" " + cyan("infernoflow recap") + gray(" — session summary\n"));
233
+ console.log(gray(" For contracts & CI gates: infernoflow init --mode full\n"));
234
+ return;
235
+ }
236
+
237
+ const projectName = detectProjectName(cwd);
238
+
239
+ console.log("\n " + bold("🔥 infernoflow") + gray(" — let's get you set up (30 seconds)\n"));
240
+ console.log(" Detected: " + cyan(projectName) + "\n");
241
+
242
+ // Create inferno/ directory
243
+ fs.mkdirSync(infernoDir, { recursive: true });
244
+
245
+ // Write minimal config
246
+ fs.writeFileSync(configFile, JSON.stringify({
247
+ project: projectName,
248
+ version: "1",
249
+ mode: "memory",
250
+ created: new Date().toISOString(),
251
+ }, null, 2) + "\n", "utf8");
252
+
253
+ // Create empty sessions.jsonl
254
+ if (!fs.existsSync(sessionsFile)) {
255
+ fs.writeFileSync(sessionsFile, "", "utf8");
256
+ }
257
+
258
+ // Ask for first gotcha (skip if --yes or not TTY)
259
+ let gotcha = "";
260
+ if (!yes && process.stdin.isTTY) {
261
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
262
+ gotcha = await new Promise(resolve => {
263
+ rl.question(
264
+ " " + gray("What should the next AI agent know about this project?\n > "),
265
+ ans => { rl.close(); resolve(ans.trim()); }
266
+ );
267
+ });
268
+ }
269
+
270
+ if (gotcha) {
271
+ const entry = {
272
+ ts: new Date().toISOString(),
273
+ agent: "user",
274
+ type: "gotcha",
275
+ summary: gotcha,
276
+ source: "init",
277
+ };
278
+ fs.appendFileSync(sessionsFile, JSON.stringify(entry) + "\n", "utf8");
279
+ console.log("\n " + green("✔") + " First gotcha logged!");
280
+ }
281
+
282
+ console.log("\n " + green("✔") + " You're set up. Quick commands:\n");
283
+ console.log(" " + cyan("infernoflow log \"...\"") + gray(" — remember something"));
284
+ console.log(" " + cyan("infernoflow switch") + gray(" — generate handoff for next AI"));
285
+ console.log(" " + cyan("infernoflow recap") + gray(" — session summary\n"));
286
+ console.log(gray(" Tip: infernoflow switch --copy puts the handoff on your clipboard.\n"));
287
+ console.log(gray(" Want contracts & CI gates? Run: infernoflow init --mode full\n"));
288
+ }
289
+
217
290
  export async function initCommand(args) {
218
291
  const cwd = process.cwd();
219
292
  const force = args.includes("--force") || args.includes("-f");
220
293
  const yes = args.includes("--yes") || args.includes("-y");
221
294
  const adopt = args.includes("--adopt");
222
295
 
296
+ // ── Memory-first default (no flags) — 60-second onboarding ───────────────
297
+ const modeArg = args.find(a => a.startsWith("--mode="))?.split("=")[1]
298
+ || (args.indexOf("--mode") !== -1 ? args[args.indexOf("--mode") + 1] : null);
299
+
300
+ const isFullMode = modeArg === "full" || modeArg === "contract";
301
+ const hasAdvancedFlag = adopt || args.includes("--template") || args.includes("--cursor-hooks")
302
+ || args.includes("--vscode-copilot-hooks") || args.includes("--lite");
303
+
304
+ if (!isFullMode && !hasAdvancedFlag) {
305
+ await initMemory(cwd, force, yes);
306
+ return;
307
+ }
308
+
223
309
  // ── Lite mode — tiny project, single directory, 3 files only ──────────────
224
310
  if (args.includes("--lite")) {
225
311
  await initLite(cwd, force);
@@ -30,6 +30,67 @@ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
30
30
  const INFERNO_DIR = "inferno";
31
31
  const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
32
32
 
33
+ /** Silently regenerate CLAUDE.md, .cursorrules, and copilot-instructions.md
34
+ * after every log entry so AI agents always have fresh context. */
35
+ function autoUpdateContextFiles() {
36
+ try {
37
+ const sessionsRaw = fs.existsSync(SESSIONS_FILE)
38
+ ? fs.readFileSync(SESSIONS_FILE, "utf8").split("\n").filter(Boolean)
39
+ .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean)
40
+ : [];
41
+
42
+ const gotchas = sessionsRaw.filter(e => e.type === "gotcha");
43
+ const decisions = sessionsRaw.filter(e => e.type === "decision");
44
+ const attempts = sessionsRaw.filter(e => e.type === "attempt" && (e.result === "failed" || e.result === "partial"));
45
+
46
+ const lines = [
47
+ `# Project Context (auto-generated by infernoflow)`,
48
+ ``,
49
+ `> Last updated: ${new Date().toISOString()}`,
50
+ ``,
51
+ ];
52
+
53
+ if (gotchas.length) {
54
+ lines.push(`## ⚠️ Known Gotchas (Read These First)`, ``);
55
+ for (const g of gotchas) lines.push(`- ${g.summary}`);
56
+ lines.push(``);
57
+ }
58
+ if (decisions.length) {
59
+ lines.push(`## ✓ Decisions In Effect`, ``);
60
+ for (const d of decisions) lines.push(`- ${d.summary}`);
61
+ lines.push(``);
62
+ }
63
+ if (attempts.length) {
64
+ lines.push(`## ❌ Things That Don't Work (Don't Try These)`, ``);
65
+ for (const a of attempts) lines.push(`- ${a.summary}`);
66
+ lines.push(``);
67
+ }
68
+
69
+ const content = lines.join("\n");
70
+ const cwd = process.cwd();
71
+
72
+ // Update CLAUDE.md if it already exists (don't create unsolicited)
73
+ const claudeMd = path.join(cwd, "CLAUDE.md");
74
+ if (fs.existsSync(claudeMd)) {
75
+ fs.writeFileSync(claudeMd, content, "utf8");
76
+ }
77
+
78
+ // Update .cursorrules if Cursor is detected
79
+ const cursorrules = path.join(cwd, ".cursorrules");
80
+ if (fs.existsSync(cursorrules) || fs.existsSync(path.join(cwd, ".cursor"))) {
81
+ fs.writeFileSync(cursorrules, content, "utf8");
82
+ }
83
+
84
+ // Update copilot-instructions.md if .github exists
85
+ const copilotInstructions = path.join(cwd, ".github", "copilot-instructions.md");
86
+ if (fs.existsSync(path.join(cwd, ".github"))) {
87
+ fs.writeFileSync(copilotInstructions, content, "utf8");
88
+ }
89
+ } catch {
90
+ // Never surface errors to the user — this is background infrastructure
91
+ }
92
+ }
93
+
33
94
  const VALID_TYPES = ["note","attempt","decision","gotcha","preference","theme","handoff","error"];
34
95
  const VALID_RESULTS = ["worked","failed","partial","unknown"];
35
96
 
@@ -175,6 +236,9 @@ export async function logCommand(args) {
175
236
  const written = appendEntry(entry, { auto: autoFlag, quiet: quietFlag });
176
237
  if (!written) return; // auto mode, no inferno/ — skip silently
177
238
 
239
+ // Silently regenerate CLAUDE.md / .cursorrules / copilot-instructions.md
240
+ autoUpdateContextFiles();
241
+
178
242
  if (!quietFlag) {
179
243
  const typeLabel = type !== "note" ? cyan(` [${type}]`) : "";
180
244
  const resultLabel = result ? gray(` → ${result}`) : "";
@@ -338,11 +338,32 @@ export async function recapCommand(rawArgs = []) {
338
338
  console.log(`${icon} ${ok ? label : gray(label)}`);
339
339
  }
340
340
 
341
- // Nudge if health is low
342
- if (score < 60) {
343
- console.log();
344
- console.log(gray(" To improve: log at least one gotcha and one decision per session."));
345
- console.log(gray(" Gotchas are the highest-value entries — they prevent repeated mistakes."));
341
+ // Actionable improvement tips
342
+ {
343
+ const gotchaCount = sessionEntries.filter(e => e.type === "gotcha").length;
344
+ const decisionCount = sessionEntries.filter(e => e.type === "decision").length;
345
+ const tips = [];
346
+
347
+ if (gotchaCount === 0) {
348
+ tips.push(cyan("infernoflow log \"...\" --type gotcha") + gray(" — adds 35 pts"));
349
+ } else if (gotchaCount < 3 && score < 80) {
350
+ tips.push(gray(` ${3 - gotchaCount} more gotcha(s) would push you higher`));
351
+ }
352
+ if (decisionCount === 0) {
353
+ tips.push(cyan("infernoflow log \"...\" --type decision") + gray(" — adds 25 pts"));
354
+ }
355
+ if (score >= 60 && score < 80) {
356
+ tips.push(gray(" Almost B! One more entry gets you there."));
357
+ }
358
+ if (score >= 80) {
359
+ tips.push(green(" Great session — your handoff will be excellent."));
360
+ }
361
+
362
+ if (tips.length) {
363
+ console.log();
364
+ console.log(gray(" How to improve:"));
365
+ for (const t of tips) console.log(" " + t);
366
+ }
346
367
  }
347
368
 
348
369
  // ── Next session tip ──────────────────────────────────────────────────────
@@ -136,6 +136,31 @@ function getGitDiffStat() {
136
136
  }
137
137
  }
138
138
 
139
+ /** Get most-edited files from git this session */
140
+ function getHotFiles(since) {
141
+ try {
142
+ const sinceArg = since && since.getTime() > 0
143
+ ? `--after="${since.toISOString()}"`
144
+ : "-10";
145
+ const raw = execSync(
146
+ `git log ${sinceArg} --name-only --pretty=format: 2>/dev/null`,
147
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
148
+ ).trim();
149
+ if (!raw) return [];
150
+ const counts = {};
151
+ for (const line of raw.split("\n")) {
152
+ const f = line.trim();
153
+ if (f) counts[f] = (counts[f] || 0) + 1;
154
+ }
155
+ return Object.entries(counts)
156
+ .sort((a, b) => b[1] - a[1])
157
+ .slice(0, 5)
158
+ .map(([file, edits]) => ({ file, edits }));
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
139
164
  /** Get recent git commits in this session */
140
165
  function getGitCommits(since) {
141
166
  try {
@@ -208,6 +233,7 @@ function buildHandoff(toAgent, sinceArg, allFlag) {
208
233
  // Git data
209
234
  const commits = getGitCommits(sessionStart.getTime() > 0 ? sessionStart : null);
210
235
  const diffStat = getGitDiffStat();
236
+ const hotFiles = getHotFiles(sessionStart.getTime() > 0 ? sessionStart : null);
211
237
 
212
238
  // Memory pool
213
239
  const pool = sessions.length > 0 ? sessions : recentFallback;
@@ -289,6 +315,15 @@ function buildHandoff(toAgent, sinceArg, allFlag) {
289
315
  lines.push("");
290
316
  }
291
317
 
318
+ // ── Hot files — auto-detected from git ────────────────────────────────────
319
+ if (hotFiles.length) {
320
+ lines.push("## 📁 Hot Files This Session", "");
321
+ for (const { file, edits } of hotFiles) {
322
+ lines.push(`- \`${file}\` — ${edits} edit${edits !== 1 ? "s" : ""}`);
323
+ }
324
+ lines.push("");
325
+ }
326
+
292
327
  // ── Preferences ───────────────────────────────────────────────────────────
293
328
  if (prefs.length) {
294
329
  lines.push("## Developer preferences", "");
@@ -437,6 +472,7 @@ export async function switchCommand(args) {
437
472
  const theme = readJSON(THEME_FILE);
438
473
  const contract = readJSON(CONTRACT_FILE) || {};
439
474
  const commits = getGitCommits(sessionStartNow.getTime() > 0 ? sessionStartNow : null);
475
+ const hotFilesNow = getHotFiles(sessionStartNow.getTime() > 0 ? sessionStartNow : null);
440
476
  const ide = detectIde();
441
477
  const pool = sessionEntries.length > 0 ? sessionEntries : allEntriesNow.slice(-5);
442
478
  const openThreads = findOpenThreads(pool);
@@ -454,6 +490,9 @@ export async function switchCommand(args) {
454
490
  console.log(" Memory " + sessionEntries.length + " entries this session (total: " + allEntriesNow.length + ")");
455
491
  if (openThreads.length) console.log(" Open threads " + yellow(openThreads.length + " unresolved"));
456
492
  if (commits.length) console.log(" Git commits " + commits.length + " this session");
493
+ if (hotFilesNow.length) {
494
+ console.log(" Hot files " + hotFilesNow.map(f => cyan(f.file)).join(", "));
495
+ }
457
496
  console.log(" Capabilities " + (contract.capabilities || []).length + " registered");
458
497
  if (theme?.fonts?.primary) console.log(" Font " + theme.fonts.primary);
459
498
  if (theme?.colors?.mode) console.log(" Color mode " + theme.colors.mode);
@@ -1,5 +1,28 @@
1
1
  // Zero-dependency color/output utilities using ANSI codes
2
2
 
3
+ // ── Terminal capability detection ─────────────────────────────────────────
4
+ function supportsUnicode() {
5
+ if (process.platform === "win32") {
6
+ // Windows Terminal sets WT_SESSION
7
+ if (process.env.WT_SESSION) return true;
8
+ // ConEmu / Cmder
9
+ if (process.env.ConEmuPID) return true;
10
+ // VS Code integrated terminal
11
+ if (process.env.TERM_PROGRAM === "vscode") return true;
12
+ // Default cmd.exe / PowerShell — no unicode box drawing
13
+ return false;
14
+ }
15
+ return true; // Mac/Linux always support it
16
+ }
17
+
18
+ export const CHARS = supportsUnicode()
19
+ ? { h: "─", v: "│", tl: "┌", tr: "┐", bl: "└", br: "┘",
20
+ dot: "·", arrow: "→", check: "✔", cross: "✘", warn: "⚠", fire: "🔥" }
21
+ : { h: "-", v: "|", tl: "+", tr: "+", bl: "+", br: "+",
22
+ dot: "*", arrow: "->", check: "[OK]", cross: "[X]", warn: "[!]", fire: "**" };
23
+
24
+ export const HR = CHARS.h.repeat(50);
25
+
3
26
  const c = {
4
27
  reset: "\x1b[0m",
5
28
  bold: "\x1b[1m",
@@ -38,17 +61,17 @@ function strip(str) {
38
61
  }
39
62
 
40
63
  export function header(text) {
41
- const title = boldOrange("🔥 infernoflow") + gray(" — " + text);
64
+ const title = boldOrange(CHARS.fire + " infernoflow") + gray(" — " + text);
42
65
  console.log("\n" + title);
43
- console.log(gray("─".repeat(50)));
66
+ console.log(gray(HR));
44
67
  }
45
68
 
46
- export function ok(msg) { console.log(" " + green("✔") + " " + msg); }
69
+ export function ok(msg) { console.log(" " + green(CHARS.check) + " " + msg); }
47
70
  export function fail(msg, hint) {
48
- console.log(" " + red("✘") + " " + red(msg));
49
- if (hint) console.log(" " + gray(" " + hint));
71
+ console.log(" " + red(CHARS.cross) + " " + red(msg));
72
+ if (hint) console.log(" " + gray(CHARS.arrow + " " + hint));
50
73
  }
51
- export function warn(msg) { console.log(" " + yellow("⚠") + " " + yellow(msg)); }
74
+ export function warn(msg) { console.log(" " + yellow(CHARS.warn) + " " + yellow(msg)); }
52
75
  export function info(msg) { console.log(" " + cyan("ℹ") + " " + msg); }
53
76
  export function section(title) { console.log("\n" + bold(white(title))); }
54
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "dist/templates",
16
16
  "README.md",
17
17
  "CHANGELOG.md",
18
- "dist/lib/templates"
18
+ "dist/lib/templates",
19
+ "scripts/postinstall.js"
19
20
  ],
20
21
  "scripts": {
21
22
  "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
@@ -23,7 +24,7 @@
23
24
  "build": "node build.mjs",
24
25
  "prepublishOnly": "node build.mjs",
25
26
  "inferno:promote-draft": "node scripts/inferno-promote-draft.mjs",
26
- "postinstall": "node -e \"try{require('@scarf/scarf')}catch(e){}\" 2>/dev/null; exit 0"
27
+ "postinstall": "node scripts/postinstall.js"
27
28
  },
28
29
  "dependencies": {
29
30
  "@scarf/scarf": "^1.3.0"
@@ -0,0 +1,2 @@
1
+ // Cross-platform postinstall — works on Windows, Mac, and Linux
2
+ try { require('@scarf/scarf'); } catch (e) { /* silent — analytics are optional */ }