portable-agent-layer 0.6.2 → 0.7.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/package.json +1 -1
- package/src/cli/index.ts +12 -0
- package/src/hooks/handlers/update-check.ts +189 -0
- package/src/hooks/lib/security.ts +1 -0
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -122,6 +122,18 @@ async function session(sessionArgs: string[]) {
|
|
|
122
122
|
// Silently ignore summary errors
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Check for updates and display notice
|
|
126
|
+
try {
|
|
127
|
+
const { checkForUpdate, getUpdateNotice } = await import(
|
|
128
|
+
"../hooks/handlers/update-check"
|
|
129
|
+
);
|
|
130
|
+
await checkForUpdate();
|
|
131
|
+
const notice = getUpdateNotice();
|
|
132
|
+
if (notice) console.log(`\n${notice}`);
|
|
133
|
+
} catch {
|
|
134
|
+
// Non-critical
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
process.exit(exitCode);
|
|
126
138
|
}
|
|
127
139
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update checker — detects if a newer version of PAL is available.
|
|
3
|
+
*
|
|
4
|
+
* Repo mode (.palroot exists): git fetch + compare HEAD vs origin/main
|
|
5
|
+
* Package mode: fetch npm registry for latest version vs installed
|
|
6
|
+
*
|
|
7
|
+
* Caches result in state/update-available.json. Checked at most once per hour.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { logDebug } from "../lib/log";
|
|
13
|
+
import { ensureDir, palPkg, paths } from "../lib/paths";
|
|
14
|
+
|
|
15
|
+
interface UpdateCache {
|
|
16
|
+
checkedAt: string;
|
|
17
|
+
available: boolean;
|
|
18
|
+
current: string;
|
|
19
|
+
latest: string;
|
|
20
|
+
mode: "repo" | "package";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
24
|
+
|
|
25
|
+
function cachePath(): string {
|
|
26
|
+
return resolve(ensureDir(paths.state()), "update-available.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readCache(): UpdateCache | null {
|
|
30
|
+
try {
|
|
31
|
+
const fp = cachePath();
|
|
32
|
+
if (!existsSync(fp)) return null;
|
|
33
|
+
const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
|
|
34
|
+
if (Date.now() - new Date(cache.checkedAt).getTime() < CACHE_TTL_MS) return cache;
|
|
35
|
+
return null; // expired
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeCache(cache: UpdateCache): void {
|
|
42
|
+
try {
|
|
43
|
+
writeFileSync(cachePath(), JSON.stringify(cache, null, 2), "utf-8");
|
|
44
|
+
} catch {
|
|
45
|
+
/* non-critical */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRepoMode(): boolean {
|
|
50
|
+
return existsSync(resolve(palPkg(), ".palroot"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getInstalledVersion(): string {
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(resolve(palPkg(), "package.json"), "utf-8"));
|
|
56
|
+
return pkg.version || "0.0.0";
|
|
57
|
+
} catch {
|
|
58
|
+
return "0.0.0";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function checkRepo(): Promise<UpdateCache> {
|
|
63
|
+
const repoDir = palPkg();
|
|
64
|
+
const current = getInstalledVersion();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const fetch = Bun.spawn(["git", "fetch", "--quiet"], {
|
|
68
|
+
cwd: repoDir,
|
|
69
|
+
stdout: "ignore",
|
|
70
|
+
stderr: "ignore",
|
|
71
|
+
});
|
|
72
|
+
await fetch.exited;
|
|
73
|
+
|
|
74
|
+
const local = Bun.spawn(["git", "rev-parse", "HEAD"], {
|
|
75
|
+
cwd: repoDir,
|
|
76
|
+
stdout: "pipe",
|
|
77
|
+
stderr: "ignore",
|
|
78
|
+
});
|
|
79
|
+
const localHash = (await new Response(local.stdout).text()).trim();
|
|
80
|
+
|
|
81
|
+
const remote = Bun.spawn(["git", "rev-parse", "origin/main"], {
|
|
82
|
+
cwd: repoDir,
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "ignore",
|
|
85
|
+
});
|
|
86
|
+
const remoteHash = (await new Response(remote.stdout).text()).trim();
|
|
87
|
+
|
|
88
|
+
const available = localHash !== remoteHash && remoteHash.length > 0;
|
|
89
|
+
|
|
90
|
+
// Get remote version from package.json on origin/main
|
|
91
|
+
let latest = current;
|
|
92
|
+
if (available) {
|
|
93
|
+
try {
|
|
94
|
+
const show = Bun.spawn(["git", "show", "origin/main:package.json"], {
|
|
95
|
+
cwd: repoDir,
|
|
96
|
+
stdout: "pipe",
|
|
97
|
+
stderr: "ignore",
|
|
98
|
+
});
|
|
99
|
+
const remotePkg = JSON.parse(await new Response(show.stdout).text());
|
|
100
|
+
latest = remotePkg.version || current;
|
|
101
|
+
} catch {
|
|
102
|
+
latest = `${remoteHash.slice(0, 7)} (ahead)`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
checkedAt: new Date().toISOString(),
|
|
108
|
+
available,
|
|
109
|
+
current,
|
|
110
|
+
latest,
|
|
111
|
+
mode: "repo",
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
checkedAt: new Date().toISOString(),
|
|
116
|
+
available: false,
|
|
117
|
+
current,
|
|
118
|
+
latest: current,
|
|
119
|
+
mode: "repo",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function checkNpm(): Promise<UpdateCache> {
|
|
125
|
+
const current = getInstalledVersion();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(
|
|
129
|
+
"https://registry.npmjs.org/portable-agent-layer/latest",
|
|
130
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
134
|
+
const data = (await response.json()) as { version?: string };
|
|
135
|
+
const latest = data.version || current;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
checkedAt: new Date().toISOString(),
|
|
139
|
+
available: latest !== current,
|
|
140
|
+
current,
|
|
141
|
+
latest,
|
|
142
|
+
mode: "package",
|
|
143
|
+
};
|
|
144
|
+
} catch {
|
|
145
|
+
return {
|
|
146
|
+
checkedAt: new Date().toISOString(),
|
|
147
|
+
available: false,
|
|
148
|
+
current,
|
|
149
|
+
latest: current,
|
|
150
|
+
mode: "package",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Run update check on Stop (non-blocking). Caches result for greeting. */
|
|
156
|
+
export async function checkForUpdate(): Promise<void> {
|
|
157
|
+
const cached = readCache();
|
|
158
|
+
if (cached) {
|
|
159
|
+
logDebug("update-check", "Using cached result");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = isRepoMode() ? await checkRepo() : await checkNpm();
|
|
164
|
+
writeCache(result);
|
|
165
|
+
|
|
166
|
+
if (result.available) {
|
|
167
|
+
logDebug(
|
|
168
|
+
"update-check",
|
|
169
|
+
`Update available: ${result.current} → ${result.latest} (${result.mode})`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read cached update status for greeting display. Returns null if no update. */
|
|
175
|
+
export function getUpdateNotice(): string | null {
|
|
176
|
+
try {
|
|
177
|
+
const fp = cachePath();
|
|
178
|
+
if (!existsSync(fp)) return null;
|
|
179
|
+
const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
|
|
180
|
+
if (!cache.available) return null;
|
|
181
|
+
|
|
182
|
+
if (cache.mode === "repo") {
|
|
183
|
+
return `📦 Update available: ${cache.current} → ${cache.latest} (git pull)`;
|
|
184
|
+
}
|
|
185
|
+
return `📦 Update available: ${cache.current} → ${cache.latest} (bun update -g portable-agent-layer)`;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|