UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

142 lines (141 loc) • 5.79 kB
/** * Install/runtime diagnostics — a paste-able environment fingerprint for bug * reports about extension load failures (#285/#335: `ResolveMessage: Cannot * find package …`). * * This module is deliberately self-contained and dependency-free: it only * RESOLVES specifiers (never imports them), reads package.json, and stats files. * So it loads and reports correctly even when the very deps that failed (e.g. * `typescript`) are unreachable — which is exactly when we need it. Every probe * is best-effort and never throws. */ import { createRequire } from "node:module"; import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const ISSUES_URL = "https://github.com/apmantza/pi-lens/issues"; /** Runtime third-party deps whose resolution is the documented failure surface. */ const CRITICAL_DEPS = [ "typescript", "minimatch", "typebox", "js-yaml", "vscode-jsonrpc", "web-tree-sitter", "@ast-grep/napi", ]; function safe(fn, fallback) { try { return fn(); } catch { return fallback; } } /** Gather the environment fingerprint. Never throws. */ export function collectInstallDiagnostics() { const notes = []; const here = safe(() => path.dirname(fileURLToPath(import.meta.url)), ""); const pkgDir = safe(() => path.resolve(here, ".."), here); // dist/clients -> dist; good enough as anchor // Walk up to the real package root (dir holding our package.json). let root = pkgDir; for (let i = 0; i < 6; i++) { if (safe(() => fs.existsSync(path.join(root, "package.json")), false)) break; const up = path.dirname(root); if (up === root) break; root = up; } const piLensVersion = safe(() => { const pj = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); return String(pj.version ?? "unknown"); }, "unknown"); const bunVersion = globalThis.Bun?.version; const runtime = bunVersion ? `bun ${bunVersion}` : `node ${process.versions.node}`; const platform = `${process.platform}-${process.arch}`; const pkgRealDir = safe(() => fs.realpathSync(root), root); const behindSymlink = pkgRealDir !== root; let packageManager = "unknown"; if (pkgRealDir.includes(`${path.sep}.pnpm${path.sep}`) || behindSymlink) { packageManager = "pnpm-symlinked"; } else if (safe(() => fs.existsSync(path.join(root, "node_modules", "typescript")), false)) { packageManager = "nested"; } else { packageManager = "npm-hoisted"; } const deps = CRITICAL_DEPS.map((name) => { try { return { name, resolved: true, resolvedPath: require.resolve(name) }; } catch (err) { return { name, resolved: false, error: `${err?.code ?? ""} ${err?.message ?? String(err)}`.trim(), }; } }); const astGrepCli = safe(() => { const cliPkg = require.resolve("@ast-grep/cli/package.json"); const dir = path.dirname(cliPkg); return ["ast-grep", "ast-grep.exe", "sg", "sg.exe"].some((b) => fs.existsSync(path.join(dir, b))); }, false); const grammars = safe(() => { let dir = path.dirname(require.resolve("web-tree-sitter")); while (path.basename(dir) !== "web-tree-sitter" && dir !== path.dirname(dir)) { dir = path.dirname(dir); } return fs.existsSync(path.join(dir, "grammars", "tree-sitter-typescript.wasm")); }, false); if (deps.some((d) => !d.resolved)) { notes.push("One or more runtime dependencies did not resolve. This is the #285/#335 failure mode — usually a package-manager layout (pnpm symlink store / nested install) the runtime's resolver can't traverse, or an outdated runtime."); } if (!astGrepCli || !grammars) { notes.push("ast-grep CLI and/or tree-sitter grammars are missing — pnpm/bun skip lifecycle scripts by default, so the postinstall that fetches them did not run."); } return { piLensVersion, runtime, platform, packageManager, pkgDir: root, pkgRealDir, behindSymlink, deps, astGrepCli, grammars, notes, }; } /** Render a compact, paste-able diagnostic block for a bug report. */ export function formatInstallDiagnostics(diag = collectInstallDiagnostics(), cause) { const lines = []; lines.push("──────── pi-lens install diagnostics ────────"); if (cause) { const msg = cause?.message ?? String(cause); lines.push(`LOAD ERROR: ${msg}`); } lines.push(`pi-lens: ${diag.piLensVersion}`); lines.push(`runtime: ${diag.runtime}`); lines.push(`platform: ${diag.platform}`); lines.push(`install: ${diag.packageManager}${diag.behindSymlink ? " (behind symlink)" : ""}`); lines.push(`pkg dir: ${diag.pkgDir}`); if (diag.behindSymlink) lines.push(`real dir: ${diag.pkgRealDir}`); lines.push("deps:"); for (const d of diag.deps) { lines.push(` ${d.resolved ? "ok " : "FAIL"} ${d.name}${d.error ? ` — ${d.error}` : ""}`); } lines.push(`assets: ast-grep-cli=${diag.astGrepCli ? "ok" : "MISSING"} grammars=${diag.grammars ? "ok" : "MISSING"}`); for (const n of diag.notes) lines.push(`note: ${n}`); lines.push(`Please paste this into a report at ${ISSUES_URL}`); lines.push("─────────────────────────────────────────────"); return lines.join("\n"); }