portable-agent-layer 0.112.0 → 0.113.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 CHANGED
@@ -77,7 +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. Install asks once whether to do this on its own: when a session closes PAL checks for a release (at most hourly) and installs it, so opening PAL is never slowed. A failed update waits a day before retrying. The switch lives in the control room under Settings → Updates. On a repo install an update waits while the clone has uncommitted changes |
80
+ | `pal cli update` | Update PAL (git pull or npm update) and reinstall hooks. Install asks once whether to do this on its own: when a session closes PAL checks for a release (at most hourly) and installs it, so opening PAL is never slowed. A failed update waits a day before retrying. The switch lives in the control room under Settings → Updates. On a repo install an update waits while the clone has uncommitted changes. `--canary` installs the latest prerelease from the `canary` branch instead; auto-update always follows the stable release |
81
81
  | `pal cli export` | Export user state (telos, memory) to a zip |
82
82
  | `pal cli import` | Import user state from a zip |
83
83
  | `pal cli status` | Show current PAL configuration |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.112.0",
3
+ "version": "0.113.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -49,7 +49,7 @@ const tree = cliTree({
49
49
  process.exit(await install(resolveTargets(argv), argv));
50
50
  },
51
51
  uninstall: ({ argv }) => uninstall(argv),
52
- update: () => update(),
52
+ update: ({ values }) => update(values.canary === true),
53
53
  export: ({ argv }) => exportState(argv),
54
54
  import: ({ argv }) => importState(argv),
55
55
  status: () => status(),
@@ -553,15 +553,23 @@ async function importState(args: string[]) {
553
553
  log.info("Run 'pal cli install' to re-register hooks.");
554
554
  }
555
555
 
556
- async function update() {
557
- const { checkForUpdate, clearUpdateCache } = await import(
556
+ async function update(canary: boolean) {
557
+ const { checkCanary, checkForUpdate, clearUpdateCache, isRepoMode } = await import(
558
558
  "../hooks/handlers/update-check"
559
559
  );
560
560
  const { reinstallInFreshProcess } = await import("./reinstall");
561
- const { runUpdate } = await import("./update-flow");
561
+ const { CANARY_NEEDS_PACKAGE, runUpdate, updateCheck } = await import("./update-flow");
562
+ const check = updateCheck(canary, isRepoMode(), {
563
+ stable: () => checkForUpdate(true),
564
+ canary: checkCanary,
565
+ });
566
+ if (!check) {
567
+ log.error(CANARY_NEEDS_PACKAGE);
568
+ process.exit(1);
569
+ }
562
570
  process.exit(
563
571
  await runUpdate({
564
- check: () => checkForUpdate(true),
572
+ check,
565
573
  reinstall: (env) => {
566
574
  clearUpdateCache();
567
575
  return reinstallInFreshProcess(process.argv[1], env);
package/src/cli/tree.ts CHANGED
@@ -109,7 +109,17 @@ function adminCommands(admin: AdminHandlers): Record<string, Leaf | Group> {
109
109
  options: agentOptions("Uninstall from"),
110
110
  run: admin.uninstall,
111
111
  }),
112
- update: leaf({ summary: "Update PAL (git pull or bun add -g)", run: admin.update }),
112
+ update: leaf({
113
+ summary: "Update PAL (git pull or bun add -g)",
114
+ options: {
115
+ canary: {
116
+ type: "boolean",
117
+ description:
118
+ "Install the latest canary prerelease instead of the stable release",
119
+ },
120
+ },
121
+ run: admin.update,
122
+ }),
113
123
  export: leaf({
114
124
  summary: "Export your state to a zip",
115
125
  args: "[path]",
@@ -33,6 +33,21 @@ export interface UpdateSteps {
33
33
  run?: Run;
34
34
  }
35
35
 
36
+ export const CANARY_NEEDS_PACKAGE =
37
+ "--canary installs from npm. A repo install follows its git checkout: check out the canary branch there instead.";
38
+
39
+ type Check = UpdateSteps["check"];
40
+
41
+ /** Null when --canary is asked of a repo install, which has no npm channel to follow. */
42
+ export function updateCheck(
43
+ canary: boolean,
44
+ repoMode: boolean,
45
+ checks: { stable: Check; canary: Check }
46
+ ): Check | null {
47
+ if (!canary) return checks.stable;
48
+ return repoMode ? null : checks.canary;
49
+ }
50
+
36
51
  /** `bun update -g` keeps the caret range, and on 0.x a caret allows only patch bumps. */
37
52
  export function downloadCommand(
38
53
  update: UpdateCache
@@ -15,16 +15,22 @@ export interface WhatsNew {
15
15
 
16
16
  const SHOWN = 3;
17
17
 
18
- function parts(version: string): number[] {
19
- return version.replace(/^v/, "").split(".").map(Number);
18
+ function bare(version: string): string {
19
+ return version.replace(/^v/, "");
20
20
  }
21
21
 
22
22
  export function newer(a: string, b: string): boolean {
23
- const [x, y] = [parts(a), parts(b)];
24
- for (let i = 0; i < 3; i++) {
25
- if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
26
- }
27
- return false;
23
+ return Bun.semver.order(bare(a), bare(b)) === 1;
24
+ }
25
+
26
+ function isPrerelease(version: string): boolean {
27
+ return bare(version).includes("-");
28
+ }
29
+
30
+ /** A stable release's notes already cover the canaries before it, so a stable update skips them. */
31
+ function inRange(tag: string, from: string, to: string): boolean {
32
+ if (isPrerelease(tag) && !isPrerelease(to)) return false;
33
+ return newer(tag, from) && !newer(tag, to);
28
34
  }
29
35
 
30
36
  /** "* **rating:** confirm corrections ([abc](url))" reads "Confirm corrections". */
@@ -71,7 +77,7 @@ export function summarizeReleases(
71
77
  slug: string
72
78
  ): WhatsNew | null {
73
79
  const items = releases
74
- .filter((r) => newer(r.tag_name, from) && !newer(r.tag_name, to))
80
+ .filter((r) => inRange(r.tag_name, from, to))
75
81
  .flatMap((r) => releaseNotes(r.body ?? ""));
76
82
  if (items.length === 0) return null;
77
83
  return {
@@ -139,12 +139,18 @@ export function isNewerRelease(latest: string, current: string): boolean {
139
139
  return Bun.semver.order(latest, current) === 1;
140
140
  }
141
141
 
142
- async function checkNpm(): Promise<UpdateCache> {
142
+ /** latest is the stable release from main; canary is the prerelease from the canary branch. */
143
+ export type Channel = "latest" | "canary";
144
+
145
+ export async function checkNpm(
146
+ channel: Channel = "latest",
147
+ fetchImpl: typeof fetch = fetch
148
+ ): Promise<UpdateCache> {
143
149
  const current = getInstalledVersion();
144
150
 
145
151
  try {
146
- const response = await fetch(
147
- "https://registry.npmjs.org/portable-agent-layer/latest",
152
+ const response = await fetchImpl(
153
+ `https://registry.npmjs.org/portable-agent-layer/${channel}`,
148
154
  { signal: AbortSignal.timeout(5000) }
149
155
  );
150
156
 
@@ -170,6 +176,11 @@ async function checkNpm(): Promise<UpdateCache> {
170
176
  }
171
177
  }
172
178
 
179
+ /** The canary check is never cached: the cache feeds the stable update notice. */
180
+ export function checkCanary(fetchImpl: typeof fetch = fetch): Promise<UpdateCache> {
181
+ return checkNpm("canary", fetchImpl);
182
+ }
183
+
173
184
  /** Run update check. Caches result. Use force=true to skip cache (e.g. for `pal cli update`). */
174
185
  export async function checkForUpdate(force = false): Promise<UpdateCache> {
175
186
  if (!force) {