@alphacoder-v0/pi-loops 0.22.0 → 0.22.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to pi-loops are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.22.1] - 2026-09-16
9
+
10
+ ### Fixed
11
+ - **The launcher survives the package being installed by the other route.** The `sh` script
12
+ `install-launcher` writes named one entry file and nothing else, so `pi install npm:…` over a
13
+ `git:` copy — or the reverse — left every `pi-loops` ending in Node's `Cannot find module
14
+ …/src/cli-entry.mjs` stack trace, `pi-loops upgrade` included; the comment claiming it "survives
15
+ the package moving" was the only thing that did. It now carries both places pi installs this
16
+ package to (`npm/node_modules/<name>` and `git/<host>/<owner>/<repo>`, under the agent directory
17
+ it was written with) as literal paths and runs the first one that is there, falls back to the
18
+ `node` on `PATH` when the node it recorded has gone too, and when no copy is in any of them says
19
+ so in one line — write the launcher again with `install-launcher` — instead of a stack trace.
20
+
8
21
  ## [0.22.0] - 2026-09-16
9
22
 
10
23
  ### Added
package/README.md CHANGED
@@ -38,7 +38,7 @@ the first sign of credentials that do not work should not be an empty inbox tomo
38
38
 
39
39
  ```bash
40
40
  pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
41
- pi install git:github.com/alphacoder-v0/pi-loops@v0.22.0 # or from GitHub, at a pinned tag
41
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # or from GitHub, at a pinned tag
42
42
  pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
43
43
  pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
44
44
  ```
package/README.zh-CN.md CHANGED
@@ -45,7 +45,7 @@ pi-loops 自己没有任何运行时依赖。
45
45
 
46
46
  ```bash
47
47
  pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
48
- pi install git:github.com/alphacoder-v0/pi-loops@v0.22.0 # 或者从 GitHub 装,钉住一个 tag
48
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # 或者从 GitHub 装,钉住一个 tag
49
49
  pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
50
50
  ```
51
51
 
package/docs/cli.md CHANGED
@@ -169,8 +169,13 @@ node src/cli-entry.mjs install-launcher
169
169
  node src/cli-entry.mjs install-launcher --dir ~/bin
170
170
  ```
171
171
 
172
- It writes a two-line `sh` script that names the node you ran it with and the package it lives in —
173
- a launcher rather than a symlink, so it keeps working if either moves for the other's reason.
172
+ It writes a small `sh` script that names the node you ran it with and the package it lives in — a
173
+ launcher rather than a symlink. Neither path is trusted for ever: if the package is not where it
174
+ was, the launcher looks in both of the directories above (the ones pi installs into, under the agent
175
+ directory this was written with), so installing again by the other route does not break it, and if
176
+ that node has gone it falls back to the `node` on your `PATH`. When no copy is in any of them it
177
+ prints one line saying to run `install-launcher` again — from inside pi, or from wherever pi has put
178
+ the package now — instead of a Node stack trace about a missing file.
174
179
 
175
180
  ## Upgrading
176
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -579,12 +579,21 @@ function packageRoot(): string {
579
579
  return path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
580
580
  }
581
581
 
582
- /** Where this copy came from and what it is called, so an upgrade goes back to the same place. */
583
- function manifest(): { name?: string; repositoryUrl?: string } {
582
+ /**
583
+ * Where this copy came from and what it is called, so an upgrade goes back to the same place —
584
+ * plus the `host/owner/repo` inside that URL, which is how pi spells the same repository in a
585
+ * `git:` spec and on disk. The URL is read here and nowhere else, so the two cannot drift apart.
586
+ */
587
+ function manifest(): { name?: string; repositoryUrl?: string; repoPath?: string } {
584
588
  try {
585
589
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8"));
586
590
  const url = String(pkg?.repository?.url ?? "").replace(/^git\+/, "").replace(/\.git$/, "");
587
- return { name: typeof pkg?.name === "string" ? pkg.name : undefined, repositoryUrl: url || undefined };
591
+ const at = /(?:^|@|\/\/)([^/@\s]+\.[a-z]{2,})[/:]([^/\s]+)\/([^/\s]+)$/.exec(url);
592
+ return {
593
+ name: typeof pkg?.name === "string" ? pkg.name : undefined,
594
+ repositoryUrl: url || undefined,
595
+ repoPath: at ? `${at[1]}/${at[2]}/${at[3]}` : undefined,
596
+ };
588
597
  } catch {
589
598
  return {}; // installed without its package.json: nothing to ask
590
599
  }
@@ -774,17 +783,41 @@ export async function installLauncherWithConfirm(args: string[], confirm: (title
774
783
  const target = launcherTarget(dir);
775
784
  if (!target) return installLauncher(undefined, out);
776
785
  const reach = isOnPath(target) ? ["Afterwards `pi-loops` starts a session from any directory."] : [`${target} is not on your PATH, so \`pi-loops\` will not be found until it is.`];
777
- const ok = await confirm("Put `pi-loops` on your PATH?", [`This writes a launcher into ${target}.`, "", "It is a two-line shell script that runs this package with this node.", ...reach].join("\n"));
786
+ const ok = await confirm("Put `pi-loops` on your PATH?", [`This writes a launcher into ${target}.`, "", "It is a small shell script that runs this package with this node.", ...reach].join("\n"));
778
787
  if (!ok) return undefined;
779
788
  // The absolute directory the question named, not the flag again: resolving a relative --dir a
780
789
  // second time, after the dialog, would follow a working directory that may have moved meanwhile.
781
790
  return installLauncher(target, out);
782
791
  }
783
792
 
793
+ /**
794
+ * What a launcher says when no copy of this package is at any of the paths it knows. One line and
795
+ * an exit code, because the alternative a person actually saw was Node's `Cannot find module` stack
796
+ * trace, which names a file that is gone and nothing they can do about it. It is written into the
797
+ * script inside single quotes, so it must contain none.
798
+ */
799
+ const LAUNCHER_LOST =
800
+ "pi-loops: the package this launcher was written for is not where it was — write the launcher again with `/pi-loops install-launcher` in pi, or `node <the directory pi installed it into>/src/cli-entry.mjs install-launcher`";
801
+
802
+ /**
803
+ * The other places this package can be, for a launcher whose recorded entry has gone: the two
804
+ * directories `pi install` puts it in, under the agent directory the launcher is written with. They
805
+ * are baked into the script as literal paths — the launcher looks, it does not work anything out.
806
+ */
807
+ function piInstallEntries(agentDir: string): string[] {
808
+ const { name, repoPath } = manifest();
809
+ const entries: string[] = [];
810
+ // `<agent dir>/npm/node_modules/<name>` and `<agent dir>/git/<host>/<owner>/<repo>`: the same two
811
+ // layouts `upgradeSpec` reads backwards to work out which route this copy came down.
812
+ if (name) entries.push(path.join(agentDir, "npm", "node_modules", ...name.split("/"), "src", "cli-entry.mjs"));
813
+ if (repoPath) entries.push(path.join(agentDir, "git", ...repoPath.split("/"), "src", "cli-entry.mjs"));
814
+ return entries;
815
+ }
816
+
784
817
  /**
785
818
  * `pi install` puts this package under pi's managed directory rather than on your PATH, so the
786
819
  * command that is supposed to start your sessions is reachable only by absolute path. This writes
787
- * a two-line launcher into a directory that is already on your PATH, which is the smallest thing
820
+ * a small launcher into a directory that is already on your PATH, which is the smallest thing
788
821
  * that fixes it without asking you to publish or install anything else.
789
822
  */
790
823
  export async function installLauncher(dir: string | undefined, out: (line: string) => void): Promise<number> {
@@ -797,9 +830,25 @@ export async function installLauncher(dir: string | undefined, out: (line: strin
797
830
  if (dir && !isOnPath(target)) out(`note: ${target} is not on your PATH, so the command will not be found there yet`);
798
831
  const entry = path.join(path.dirname(fileURLToPath(import.meta.url)), "cli-entry.mjs");
799
832
  const file = path.join(target, "pi-loops");
800
- // A launcher rather than a symlink: it survives the package moving, and it names the node that
801
- // is running now, which is the one known to be new enough for this code.
802
- const script = `#!/bin/sh\n# pi-loops launcher, written by \`pi-loops install-launcher\`.\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(entry)} "$@"\n`;
833
+ // A launcher rather than a symlink: it names the node that is running now, which is the one known
834
+ // to be new enough for this code. What it survives is this package being installed again by the
835
+ // other route — `pi install npm:` over a `git:` copy moves it, and a launcher that knew only where
836
+ // it used to be died in Node's `Cannot find module`. So the entry it recorded is the first place
837
+ // it looks and not the only one, and the recorded node is a path it checks rather than trusts.
838
+ const candidates = [entry, ...piInstallEntries(getAgentDir())].map((e) => JSON.stringify(e)).join(" ");
839
+ const script = [
840
+ "#!/bin/sh",
841
+ "# pi-loops launcher, written by `pi-loops install-launcher`.",
842
+ `node=${JSON.stringify(process.execPath)}`,
843
+ '[ -x "$node" ] || node=$(command -v node) || node=node',
844
+ `for entry in ${candidates}; do`,
845
+ '\t[ -f "$entry" ] || continue',
846
+ '\texec "$node" "$entry" "$@"',
847
+ "done",
848
+ `echo '${LAUNCHER_LOST}' >&2`,
849
+ "exit 1",
850
+ "",
851
+ ].join("\n");
803
852
  try {
804
853
  fs.mkdirSync(target, { recursive: true });
805
854
  fs.writeFileSync(file, script, { mode: 0o755 });