portable-agent-layer 0.85.10 → 0.85.12

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.
@@ -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,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
- }