portable-agent-layer 0.6.2 → 0.8.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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/index.ts +56 -0
- package/src/hooks/handlers/update-check.ts +193 -0
- package/src/hooks/lib/security.ts +1 -0
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ pal cli status # check your setup
|
|
|
77
77
|
| `pal cli init` | Scaffold PAL home directory and install hooks |
|
|
78
78
|
| `pal cli install` | Register hooks/skills for targets |
|
|
79
79
|
| `pal cli uninstall` | Remove hooks/skills for targets |
|
|
80
|
+
| `pal cli update` | Update PAL (git pull or npm update) and reinstall hooks |
|
|
80
81
|
| `pal cli export` | Export user state (telos, memory) to a zip |
|
|
81
82
|
| `pal cli import` | Import user state from a zip |
|
|
82
83
|
| `pal cli status` | Show current PAL configuration |
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* init Scaffold PAL home, install hooks for all targets
|
|
11
11
|
* install [--claude] [--opencode] Register hooks/skills for targets
|
|
12
12
|
* uninstall [--claude] [--opencode] Remove hooks/skills for targets
|
|
13
|
+
* update Update PAL (git pull or npm update)
|
|
13
14
|
* export [path] [--dry-run] Export user state to zip
|
|
14
15
|
* import [path] [--dry-run] Import user state from zip
|
|
15
16
|
* status Show current PAL configuration
|
|
@@ -122,6 +123,18 @@ async function session(sessionArgs: string[]) {
|
|
|
122
123
|
// Silently ignore summary errors
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// Check for updates and display notice
|
|
127
|
+
try {
|
|
128
|
+
const { checkForUpdate, getUpdateNotice } = await import(
|
|
129
|
+
"../hooks/handlers/update-check"
|
|
130
|
+
);
|
|
131
|
+
await checkForUpdate();
|
|
132
|
+
const notice = getUpdateNotice();
|
|
133
|
+
if (notice) console.log(`\n${notice}`);
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-critical
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
process.exit(exitCode);
|
|
126
139
|
}
|
|
127
140
|
|
|
@@ -145,6 +158,9 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
145
158
|
case "import":
|
|
146
159
|
await importState(args);
|
|
147
160
|
break;
|
|
161
|
+
case "update":
|
|
162
|
+
await update();
|
|
163
|
+
break;
|
|
148
164
|
case "status":
|
|
149
165
|
await status();
|
|
150
166
|
break;
|
|
@@ -184,6 +200,7 @@ function showHelp() {
|
|
|
184
200
|
pal cli init [--claude] [--opencode] Scaffold and install (default: all)
|
|
185
201
|
pal cli install [--claude] [--opencode] Register hooks for targets
|
|
186
202
|
pal cli uninstall [--claude] [--opencode] Remove hooks for targets
|
|
203
|
+
pal cli update Update PAL (git pull or npm update)
|
|
187
204
|
pal cli export [path] [--dry-run] Export state to zip
|
|
188
205
|
pal cli import [path] [--dry-run] Import state from zip
|
|
189
206
|
pal cli status Show PAL configuration
|
|
@@ -562,6 +579,45 @@ async function importState(args: string[]) {
|
|
|
562
579
|
}
|
|
563
580
|
}
|
|
564
581
|
|
|
582
|
+
async function update() {
|
|
583
|
+
const { checkForUpdate } = await import("../hooks/handlers/update-check");
|
|
584
|
+
const result = await checkForUpdate(true);
|
|
585
|
+
|
|
586
|
+
log.info(`Current: ${result.current} (${result.mode} mode)`);
|
|
587
|
+
|
|
588
|
+
if (!result.available) {
|
|
589
|
+
log.success("Already up to date.");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
log.info(`Available: ${result.latest}`);
|
|
594
|
+
|
|
595
|
+
const pkg = palPkg();
|
|
596
|
+
if (result.mode === "repo") {
|
|
597
|
+
log.info("Pulling updates...");
|
|
598
|
+
const pull = spawnSync("git", ["pull", "--ff-only"], { cwd: pkg, stdio: "inherit" });
|
|
599
|
+
if (pull.status !== 0) {
|
|
600
|
+
log.error("git pull failed. You may have local changes — try pulling manually.");
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
log.info("Updating via npm...");
|
|
605
|
+
const up = spawnSync("bun", ["update", "-g", "portable-agent-layer"], {
|
|
606
|
+
stdio: "inherit",
|
|
607
|
+
});
|
|
608
|
+
if (up.status !== 0) {
|
|
609
|
+
log.error("Update failed. Try: bun update -g portable-agent-layer");
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const newPkg = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
|
|
615
|
+
log.success(`Updated: ${result.current} → ${newPkg.version}`);
|
|
616
|
+
|
|
617
|
+
log.info("Reinstalling...");
|
|
618
|
+
await install(resolveTargets([]));
|
|
619
|
+
}
|
|
620
|
+
|
|
565
621
|
async function status() {
|
|
566
622
|
const home = palHome();
|
|
567
623
|
const pkg = palPkg();
|
|
@@ -0,0 +1,193 @@
|
|
|
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. Caches result. Use force=true to skip cache (e.g. for `pal cli update`). */
|
|
156
|
+
export async function checkForUpdate(force = false): Promise<UpdateCache> {
|
|
157
|
+
if (!force) {
|
|
158
|
+
const cached = readCache();
|
|
159
|
+
if (cached) {
|
|
160
|
+
logDebug("update-check", "Using cached result");
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = isRepoMode() ? await checkRepo() : await checkNpm();
|
|
166
|
+
writeCache(result);
|
|
167
|
+
|
|
168
|
+
if (result.available) {
|
|
169
|
+
logDebug(
|
|
170
|
+
"update-check",
|
|
171
|
+
`Update available: ${result.current} → ${result.latest} (${result.mode})`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Read cached update status for greeting display. Returns null if no update. */
|
|
179
|
+
export function getUpdateNotice(): string | null {
|
|
180
|
+
try {
|
|
181
|
+
const fp = cachePath();
|
|
182
|
+
if (!existsSync(fp)) return null;
|
|
183
|
+
const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
|
|
184
|
+
if (!cache.available) return null;
|
|
185
|
+
|
|
186
|
+
if (cache.mode === "repo") {
|
|
187
|
+
return `📦 Update available: ${cache.current} → ${cache.latest} (git pull)`;
|
|
188
|
+
}
|
|
189
|
+
return `📦 Update available: ${cache.current} → ${cache.latest} (bun update -g portable-agent-layer)`;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|