portable-agent-layer 0.85.9 → 0.85.11

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/src/cli/skill.ts CHANGED
@@ -76,11 +76,6 @@ function isPlainSegment(part: string): boolean {
76
76
  );
77
77
  }
78
78
 
79
- /** Playwright tools ship compiled as .mjs and need Node; everything else is Bun. */
80
- function runtimeFor(file: string): string {
81
- return file.endsWith(".mjs") ? "node" : "bun";
82
- }
83
-
84
79
  function toolFileName(tool: string): string {
85
80
  return new RegExp(/\.[a-z]+$/).test(tool) ? tool : `${tool}.ts`;
86
81
  }
@@ -96,7 +91,7 @@ function runSkillTool(skill: string, tool: string, toolArgs: string[]): number {
96
91
  log.error(`No tool '${file}' in skill '${skill}' — looked in ${path}`);
97
92
  return 1;
98
93
  }
99
- const { status } = spawnSync(runtimeFor(file), [path, ...toolArgs], {
94
+ const { status } = spawnSync("bun", [path, ...toolArgs], {
100
95
  stdio: "inherit",
101
96
  });
102
97
  return status ?? 1;
@@ -79,9 +79,10 @@ function rotateIfNeeded(path: string): void {
79
79
  }
80
80
  }
81
81
 
82
- export interface HookHealth {
83
- totalErrors: number;
84
- lastError: string | null;
82
+ export interface HookErrorGroup {
83
+ source: string;
84
+ count: number;
85
+ last: string;
85
86
  }
86
87
 
87
88
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -107,18 +108,30 @@ function loggedAt(line: string): number {
107
108
  return match ? Date.parse(`${match[1]}T${match[2]}Z`) : 0;
108
109
  }
109
110
 
110
- export function recentHookErrors(now: number = Date.now()): HookHealth {
111
+ function groupBySource(errors: string[]): HookErrorGroup[] {
112
+ const groups = new Map<string, HookErrorGroup>();
113
+ for (const error of errors) {
114
+ const source = /^([^:]+):/.exec(error)?.[1] ?? "unknown";
115
+ const group = groups.get(source) ?? { source, count: 0, last: "" };
116
+ group.count++;
117
+ group.last = error
118
+ .slice(source.length + 1)
119
+ .trim()
120
+ .slice(0, 120);
121
+ groups.set(source, group);
122
+ }
123
+ return [...groups.values()].sort((a, b) => b.count - a.count);
124
+ }
125
+
126
+ export function recentHookErrors(now: number = Date.now()): HookErrorGroup[] {
111
127
  try {
112
128
  const recent = readAllLogs()
113
129
  .split("\n")
114
- .filter((line) => line.includes("] ERROR ") && loggedAt(line) > now - DAY_MS);
115
- const last = recent.at(-1);
116
- return {
117
- totalErrors: recent.length,
118
- lastError: last ? last.replace(/^\[.*?\] ERROR /, "").slice(0, 120) : null,
119
- };
130
+ .filter((line) => line.includes("] ERROR ") && loggedAt(line) > now - DAY_MS)
131
+ .map((line) => line.replace(/^\[.*?\] ERROR /, ""));
132
+ return groupBySource(recent);
120
133
  } catch {
121
- return { totalErrors: 0, lastError: null };
134
+ return [];
122
135
  }
123
136
  }
124
137
 
@@ -572,22 +572,6 @@ export function auditBindings(
572
572
  return issues;
573
573
  }
574
574
 
575
- /**
576
- * Every issue names the command that fixes it. PAL runs inside agents, where a
577
- * hook cannot ask a question — so a suggestion is always a command the user can
578
- * choose to run, never something applied on their behalf.
579
- */
580
- export function describeBindingIssue(issue: BindingIssue): string {
581
- const fix = (name: string) => `run 'project set-path ${name} <path>'`;
582
- if (issue.kind === "unlocatable")
583
- return `${issue.project} — not checked out here (${fix(issue.project)})`;
584
- // "points at" rather than "bound to": the path may equally have come from a
585
- // legacy record's own field, which is not a binding.
586
- if (issue.kind === "missing")
587
- return `${issue.project} — points at ${issue.path}, which does not exist here (${fix(issue.project)})`;
588
- return `${issue.projects.join(" and ")} — both point at ${issue.path}; rebind whichever is wrong (${fix(issue.projects[0])})`;
589
- }
590
-
591
575
  export type BindingProposal = {
592
576
  state: "unbound";
593
577
  confidence: "strong" | "weak";
@@ -1,226 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createReadStream, constants as fsConstants, realpathSync } from "node:fs";
3
- import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
4
- import { createServer } from "node:http";
5
- import { extname, join, resolve } from "node:path";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { PDFDocument } from "pdf-lib";
8
- import { chromium } from "playwright";
9
- const COLOR = {
10
- navy: "#0f172a",
11
- blue: "#1d4ed8",
12
- red: "#dc2626"
13
- };
14
- async function exists(p) {
15
- try {
16
- await access(p, fsConstants.F_OK);
17
- return true;
18
- } catch {
19
- return false;
20
- }
21
- }
22
- function escapeHtml(s) {
23
- return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
24
- }
25
- function slugify(s) {
26
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27
- }
28
- async function logoDataUri(outDir, publicPath) {
29
- if (!publicPath)
30
- return null;
31
- const filePath = join(outDir, publicPath);
32
- if (!await exists(filePath))
33
- return null;
34
- const buf = await readFile(filePath);
35
- const ext = extname(filePath).toLowerCase();
36
- let mime = "image/jpeg";
37
- if (ext === ".svg") {
38
- mime = "image/svg+xml";
39
- } else if (ext === ".png") {
40
- mime = "image/png";
41
- }
42
- return `data:${mime};base64,${buf.toString("base64")}`;
43
- }
44
- async function loadMeta(reportDir) {
45
- const dataPath = join(reportDir, "lib", "report-data.ts");
46
- if (!await exists(dataPath)) {
47
- throw new Error(`lib/report-data.ts not found at ${dataPath}`);
48
- }
49
- const mod = await import(pathToFileURL(dataPath).href);
50
- if (!mod.reportData) {
51
- throw new Error(`lib/report-data.ts must export a named 'reportData' constant`);
52
- }
53
- return mod.reportData;
54
- }
55
- function buildNext(reportDir) {
56
- const result = spawnSync("bun", ["run", "build"], {
57
- cwd: reportDir,
58
- stdio: "inherit",
59
- shell: true
60
- });
61
- if (result.status !== 0) {
62
- throw new Error(`next build failed (exit ${result.status})`);
63
- }
64
- }
65
- function serveStatic(rootDir) {
66
- const MIME = {
67
- ".html": "text/html; charset=utf-8",
68
- ".css": "text/css; charset=utf-8",
69
- ".js": "application/javascript; charset=utf-8",
70
- ".json": "application/json; charset=utf-8",
71
- ".woff": "font/woff",
72
- ".woff2": "font/woff2",
73
- ".svg": "image/svg+xml",
74
- ".png": "image/png",
75
- ".jpg": "image/jpeg",
76
- ".jpeg": "image/jpeg",
77
- ".webp": "image/webp",
78
- ".ico": "image/x-icon"
79
- };
80
- return new Promise((res) => {
81
- const server = createServer((req, response) => {
82
- const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
83
- let filePath = join(rootDir, urlPath === "/" ? "/index.html" : urlPath);
84
- if (filePath.endsWith("/"))
85
- filePath = join(filePath, "index.html");
86
- const ext = extname(filePath).toLowerCase();
87
- response.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
88
- const stream = createReadStream(filePath);
89
- stream.on("error", () => {
90
- response.statusCode = 404;
91
- response.end();
92
- });
93
- stream.pipe(response);
94
- });
95
- server.listen(0, "127.0.0.1", () => {
96
- const addr = server.address();
97
- const port = typeof addr === "object" && addr ? addr.port : 0;
98
- res({ server, url: `http://127.0.0.1:${port}/` });
99
- });
100
- });
101
- }
102
- async function renderPdf(htmlPath, pdfPath, meta) {
103
- const outDir = resolve(htmlPath, "..");
104
- const { server, url } = await serveStatic(outDir);
105
- const browser = await chromium.launch();
106
- try {
107
- const page = await browser.newPage();
108
- await page.goto(url, { waitUntil: "networkidle" });
109
- await page.evaluate(async () => {
110
- await document.fonts?.ready;
111
- const imgs = Array.from(document.querySelectorAll("img"));
112
- await Promise.all(imgs.map((img) => img.complete ? Promise.resolve() : new Promise((res) => {
113
- img.onload = () => res(null);
114
- img.onerror = () => res(null);
115
- setTimeout(() => res(null), 5000);
116
- })));
117
- });
118
- const [clientUri, consultancyUri] = await Promise.all([
119
- logoDataUri(outDir, meta.clientLogoSrc),
120
- logoDataUri(outDir, meta.consultancyLogoSrc)
121
- ]);
122
- const clientSlot = clientUri ? `<img src="${clientUri}" style="height:18px; width:auto; object-fit:contain; display:block;">` : `<span style="font-weight:600; color:${COLOR.navy}; letter-spacing:0.05em;">${escapeHtml(meta.clientName.toUpperCase())}</span>`;
123
- const consultancySlot = consultancyUri ? `<img src="${consultancyUri}" style="height:14px; width:auto; object-fit:contain; display:block;">` : `<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>`;
124
- const header = `
125
- <div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
126
- ${clientSlot}
127
- <span style="color:#94a3b8;">${escapeHtml(meta.reportTitle)}</span>
128
- </div>`;
129
- const footer = `
130
- <div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
131
- ${consultancySlot}
132
- <span style="color:${COLOR.navy};"><span class="pageNumber"></span></span>
133
- </div>`;
134
- const margin = { top: "0.7in", right: "0.7in", bottom: "0.7in", left: "0.7in" };
135
- const tmpCover = `${pdfPath}.cover.tmp.pdf`;
136
- const tmpBody = `${pdfPath}.body.tmp.pdf`;
137
- await Promise.all([
138
- page.pdf({
139
- path: tmpCover,
140
- format: "A4",
141
- pageRanges: "1",
142
- printBackground: true,
143
- displayHeaderFooter: false,
144
- margin: { top: "0", right: "0", bottom: "0", left: "0" },
145
- preferCSSPageSize: false
146
- }),
147
- page.pdf({
148
- path: tmpBody,
149
- format: "A4",
150
- pageRanges: "2-",
151
- printBackground: true,
152
- displayHeaderFooter: true,
153
- headerTemplate: header,
154
- footerTemplate: footer,
155
- margin,
156
- preferCSSPageSize: false
157
- })
158
- ]);
159
- const [coverBytes, bodyBytes] = await Promise.all([
160
- readFile(tmpCover),
161
- readFile(tmpBody)
162
- ]);
163
- const [coverDoc, bodyDoc] = await Promise.all([
164
- PDFDocument.load(coverBytes),
165
- PDFDocument.load(bodyBytes)
166
- ]);
167
- const [coverPage] = await bodyDoc.copyPages(coverDoc, [0]);
168
- bodyDoc.insertPage(0, coverPage);
169
- await writeFile(pdfPath, await bodyDoc.save());
170
- await Promise.all([unlink(tmpCover), unlink(tmpBody)]);
171
- } finally {
172
- await browser.close();
173
- server.close();
174
- }
175
- }
176
- async function generate(opts) {
177
- const dir = resolve(opts.reportDir);
178
- if (!await exists(join(dir, "package.json"))) {
179
- throw new Error(`not a scaffolded report (missing package.json): ${dir}`);
180
- }
181
- if (!opts.skipBuild) {
182
- buildNext(dir);
183
- }
184
- const htmlPath = join(dir, "out", "index.html");
185
- if (!await exists(htmlPath)) {
186
- throw new Error(`static export missing: ${htmlPath} — run without --skip-build`);
187
- }
188
- const meta = await loadMeta(dir);
189
- const slug = `${slugify(meta.clientName)}-${slugify(meta.reportTitle)}-${slugify(new Date().toISOString().slice(0, 10))}`;
190
- const pdfPath = opts.pdfOut ? resolve(opts.pdfOut) : join(dir, `${slug}.pdf`);
191
- await renderPdf(htmlPath, pdfPath, meta);
192
- return { htmlPath, pdfPath };
193
- }
194
- function parseArgs(argv) {
195
- if (argv.length === 0) {
196
- throw new Error("usage: generate-pdf.ts <report-dir> [--pdf <out>] [--skip-build]");
197
- }
198
- const opts = { reportDir: argv[0] };
199
- for (let i = 1;i < argv.length; i++) {
200
- if (argv[i] === "--pdf")
201
- opts.pdfOut = argv[++i];
202
- else if (argv[i] === "--html")
203
- opts.htmlOut = argv[++i];
204
- else if (argv[i] === "--skip-build")
205
- opts.skipBuild = true;
206
- }
207
- return opts;
208
- }
209
- async function run(argv = process.argv.slice(2)) {
210
- const opts = parseArgs(argv);
211
- const { htmlPath, pdfPath } = await generate(opts);
212
- const [htmlStat, pdfStat] = await Promise.all([stat(htmlPath), stat(pdfPath)]);
213
- console.log(`HTML: ${htmlPath} (${(htmlStat.size / 1024).toFixed(1)} KB)`);
214
- console.log(`PDF: ${pdfPath} (${(pdfStat.size / 1024).toFixed(1)} KB)`);
215
- }
216
- function realResolve(p) {
217
- try {
218
- return realpathSync(resolve(p));
219
- } catch {
220
- return resolve(p);
221
- }
222
- }
223
- const isMain = process.argv[1] && realResolve(process.argv[1]) === realResolve(fileURLToPath(import.meta.url));
224
- if (isMain) {
225
- await run();
226
- }
@@ -1,103 +0,0 @@
1
- import { readFile, stat, writeFile } from "node:fs/promises";
2
- import { basename, dirname, extname, resolve } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { marked } from "marked";
5
- import { chromium } from "playwright";
6
- const args = process.argv.slice(2);
7
- if (args.length === 0) {
8
- console.error("usage: md-to-html-pdf.ts <input.md> [--html <out>] [--pdf <out>] [--margin <css>] [--header <html|file>] [--footer <html|file>]");
9
- process.exit(1);
10
- }
11
- const input = resolve(args[0]);
12
- let htmlOut = "";
13
- let pdfOut = "";
14
- let margin = "25mm";
15
- let headerArg = "";
16
- let footerArg = "";
17
- for (let i = 1;i < args.length; i++) {
18
- if (args[i] === "--html")
19
- htmlOut = resolve(args[++i]);
20
- else if (args[i] === "--pdf")
21
- pdfOut = resolve(args[++i]);
22
- else if (args[i] === "--margin")
23
- margin = args[++i];
24
- else if (args[i] === "--header")
25
- headerArg = args[++i];
26
- else if (args[i] === "--footer")
27
- footerArg = args[++i];
28
- }
29
- const stem = basename(input, extname(input));
30
- const dir = dirname(input);
31
- htmlOut ||= resolve(dir, `${stem}.html`);
32
- pdfOut ||= resolve(dir, `${stem}.pdf`);
33
- async function resolveTemplate(value) {
34
- if (!value)
35
- return "";
36
- try {
37
- return await readFile(resolve(value), "utf8");
38
- } catch {
39
- return value;
40
- }
41
- }
42
- const headerTemplate = await resolveTemplate(headerArg);
43
- const footerTemplate = await resolveTemplate(footerArg);
44
- const displayHeaderFooter = Boolean(headerTemplate || footerTemplate);
45
- const md = await readFile(input, "utf8");
46
- marked.setOptions({ gfm: true, breaks: false });
47
- const body = await marked.parse(md);
48
- const css = `
49
- html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
50
- body {
51
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
52
- font-size: 11px; line-height: 1.6; color: #1a1a1a; margin: 0;
53
- }
54
- h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 8px; margin-top: 0; }
55
- h2 { font-size: 17px; margin-top: 1.6em; }
56
- h3 { font-size: 14px; }
57
- h1, h2, h3, h4 { page-break-after: avoid; }
58
- p, li { orphans: 3; widows: 3; }
59
- hr { border: none; border-top: 1px solid #ccc; margin: 20px 0; }
60
- a { color: #0366d6; text-decoration: none; }
61
- blockquote { border-left: 3px solid #666; padding-left: 12px; color: #444; margin: 12px 0; }
62
- code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; font-size: 10px; }
63
- pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }
64
- pre code { background: transparent; padding: 0; }
65
- table { border-collapse: collapse; width: 100%; margin: 12px 0; page-break-inside: avoid; }
66
- th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; font-size: 10px; vertical-align: top; }
67
- th { background: #f0f0f0; }
68
- tr { page-break-inside: avoid; }
69
- ul, ol { padding-left: 1.4em; }
70
- `;
71
- const html = `<!doctype html>
72
- <html>
73
- <head>
74
- <meta charset="utf-8">
75
- <title>${stem}</title>
76
- <style>${css}</style>
77
- </head>
78
- <body>
79
- ${body}
80
- </body>
81
- </html>
82
- `;
83
- await writeFile(htmlOut, html, "utf8");
84
- const browser = await chromium.launch();
85
- try {
86
- const page = await browser.newPage();
87
- await page.goto(pathToFileURL(htmlOut).href, { waitUntil: "networkidle" });
88
- await page.pdf({
89
- path: pdfOut,
90
- format: "A4",
91
- margin: { top: margin, right: margin, bottom: margin, left: margin },
92
- printBackground: true,
93
- preferCSSPageSize: false,
94
- displayHeaderFooter,
95
- headerTemplate: headerTemplate || "<span></span>",
96
- footerTemplate: footerTemplate || "<span></span>"
97
- });
98
- } finally {
99
- await browser.close();
100
- }
101
- const [htmlStat, pdfStat] = await Promise.all([stat(htmlOut), stat(pdfOut)]);
102
- console.log(`HTML: ${htmlOut} (${(htmlStat.size / 1024).toFixed(1)} KB)`);
103
- console.log(`PDF: ${pdfOut} (${(pdfStat.size / 1024).toFixed(1)} KB)`);
@@ -1,133 +0,0 @@
1
- /**
2
- * The doctor's git and gh checks. Neither is required: without git, project
3
- * matching by remote and the update checks skip; without gh, nothing does yet.
4
- */
5
-
6
- import { readFileSync } from "node:fs";
7
- import { findBinaryOnPath } from "../hooks/lib/which";
8
-
9
- export interface DoctorLine {
10
- level: "ok" | "warn" | "info";
11
- text: string;
12
- }
13
-
14
- type LinuxFamily = "debian" | "fedora" | "arch" | "alpine" | "suse";
15
-
16
- interface Host {
17
- platform: NodeJS.Platform;
18
- osRelease: string;
19
- timeoutMs: number;
20
- }
21
-
22
- const GH_LINUX_DOCS = "https://github.com/cli/cli/blob/trunk/docs/install_linux.md";
23
-
24
- function osReleaseIds(osRelease: string): string[] {
25
- const field = (key: string) =>
26
- osRelease.match(new RegExp(String.raw`^${key}="?([^"\n]*)"?$`, "m"))?.[1] ?? "";
27
- return `${field("ID")} ${field("ID_LIKE")}`.split(/\s+/).filter(Boolean);
28
- }
29
-
30
- function linuxFamily(osRelease: string): LinuxFamily | null {
31
- const ids = osReleaseIds(osRelease);
32
- const families: [LinuxFamily, string[]][] = [
33
- ["debian", ["debian", "ubuntu"]],
34
- ["fedora", ["fedora", "rhel", "centos"]],
35
- ["arch", ["arch"]],
36
- ["alpine", ["alpine"]],
37
- ["suse", ["suse", "opensuse"]],
38
- ];
39
- return families.find(([, names]) => names.some((n) => ids.includes(n)))?.[0] ?? null;
40
- }
41
-
42
- const GIT_ON_LINUX: Record<LinuxFamily, string> = {
43
- debian: "sudo apt install git",
44
- fedora: "sudo dnf install git",
45
- arch: "sudo pacman -S git",
46
- alpine: "sudo apk add git",
47
- suse: "sudo zypper install git",
48
- };
49
-
50
- const GH_ON_LINUX: Partial<Record<LinuxFamily, string>> = {
51
- fedora: "sudo dnf install gh",
52
- arch: "sudo pacman -S github-cli",
53
- };
54
-
55
- export function gitInstallHint(platform: NodeJS.Platform, osRelease: string): string {
56
- if (platform === "win32") return "`winget install Git.Git`";
57
- if (platform === "darwin") return "`brew install git` or `xcode-select --install`";
58
- const family = linuxFamily(osRelease);
59
- return family ? `\`${GIT_ON_LINUX[family]}\`` : "install git with your package manager";
60
- }
61
-
62
- export function ghInstallHint(platform: NodeJS.Platform, osRelease: string): string {
63
- if (platform === "win32") return "`winget install GitHub.cli`";
64
- if (platform === "darwin") return "`brew install gh`";
65
- const family = linuxFamily(osRelease);
66
- const command = family && GH_ON_LINUX[family];
67
- return command ? `\`${command}\`` : `see ${GH_LINUX_DOCS}`;
68
- }
69
-
70
- function run(binary: string, args: string[], timeoutMs: number) {
71
- const result = Bun.spawnSync([binary, ...args], {
72
- stdout: "pipe",
73
- stderr: "ignore",
74
- timeout: timeoutMs,
75
- windowsHide: true,
76
- });
77
- return {
78
- answered: !result.exitedDueToTimeout,
79
- ok: result.success,
80
- firstLine: result.stdout.toString().trim().split("\n")[0] ?? "",
81
- };
82
- }
83
-
84
- function installed(name: string, timeoutMs: number) {
85
- const binary = findBinaryOnPath(name);
86
- if (!binary) return null;
87
- const version = run(binary, ["--version"], timeoutMs);
88
- return version.ok ? { binary, version: version.firstLine } : null;
89
- }
90
-
91
- function gitLine(host: Host): DoctorLine {
92
- const git = installed("git", host.timeoutMs);
93
- if (git) return { level: "ok", text: git.version };
94
- return {
95
- level: "warn",
96
- text: `git — not found; project matching by remote, the update check and auto-update skip without it. ${gitInstallHint(host.platform, host.osRelease)}`,
97
- };
98
- }
99
-
100
- function ghLine(host: Host): DoctorLine {
101
- const gh = installed("gh", host.timeoutMs);
102
- if (!gh)
103
- return {
104
- level: "info",
105
- text: `gh — not installed (optional; lets PAL see PR, CI and release state — ${ghInstallHint(host.platform, host.osRelease)})`,
106
- };
107
- const auth = run(gh.binary, ["auth", "status"], host.timeoutMs);
108
- if (!auth.answered)
109
- return { level: "info", text: `${gh.version} — could not check the login in time` };
110
- if (!auth.ok)
111
- return {
112
- level: "warn",
113
- text: `${gh.version} — not logged in; run \`gh auth login\``,
114
- };
115
- return { level: "ok", text: `${gh.version}, logged in` };
116
- }
117
-
118
- function readOsRelease(): string {
119
- try {
120
- return readFileSync("/etc/os-release", "utf-8");
121
- } catch {
122
- return "";
123
- }
124
- }
125
-
126
- export function versionControlLines(host: Partial<Host> = {}): DoctorLine[] {
127
- const resolved: Host = {
128
- platform: host.platform ?? process.platform,
129
- osRelease: host.osRelease ?? (process.platform === "linux" ? readOsRelease() : ""),
130
- timeoutMs: host.timeoutMs ?? 5000,
131
- };
132
- return [gitLine(resolved), ghLine(resolved)];
133
- }