@vzlentin/harness 0.2.1 → 0.2.3

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
@@ -57,6 +57,6 @@ XDG_STATE_HOME=/tmp/harness-dev node src/cli.ts list
57
57
  `node src/cli.ts` runs the CLI of this checkout. Give it its own `XDG_STATE_HOME`, so its hosts do not share sockets and
58
58
  stores with the hosts of the installed `harness`.
59
59
 
60
- The checkout runs from its TypeScript source. `npm publish` builds `dist/` first, and the package contains `dist/`,
61
- the Pi extension in `pi/` and its `postinstall` script. `npm run check` builds `dist/` too, before the type check. The extension imports `../dist/`, so to try the checkout's
60
+ The checkout runs from its TypeScript source. The package contains `dist/`, the Pi extension in `pi/` and its
61
+ `postinstall` script. Change `version` in `package.json` and push to `main`: CI publishes it. `npm run check` builds `dist/` too, before the type check. The extension imports `../dist/`, so to try the checkout's
62
62
  extension in Pi, run `npm run build`, then `pi install ./pi`, and build again after each change in `src/`.
package/dist/setup.js CHANGED
@@ -6,10 +6,16 @@ import { spawnSync } from "node:child_process";
6
6
  import { createHash } from "node:crypto";
7
7
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
- import { basename, delimiter, dirname, join, sep } from "node:path";
9
+ import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
10
+ import { fileURLToPath } from "node:url";
10
11
  import { Type } from "typebox";
11
12
  import { Value } from "typebox/value";
12
13
  const PackageJson = Type.Object({ name: Type.String(), version: Type.String() });
14
+ const PiSettings = Type.Object({
15
+ packages: Type.Optional(Type.Array(Type.Union([Type.String(), Type.Object({ source: Type.String() })]))),
16
+ });
17
+ /** The prefixes of the Pi package sources that are not local paths, as Pi reads them. */
18
+ const REMOTE = ["npm:", "git:", "github:", "http:", "https:", "ssh:", "builtin:"];
13
19
  /**
14
20
  * Registers `<packageDir>/pi` in Pi when `packageDir` is a top-level global npm install. It writes its messages to
15
21
  * stderr, and it does not throw for a Pi failure: it writes a warning with the command to run. It writes the stamp
@@ -68,10 +74,15 @@ function writeStamp(installed) {
68
74
  mkdirSync(dirname(path), { recursive: true });
69
75
  writeFileSync(path, `${installed.dir}\n${installed.json.version}\n`);
70
76
  }
71
- /** False when `pi install` failed. */
77
+ /**
78
+ * False when `pi install` failed. It runs no `pi` when Pi already lists the extension, as `pi install` would write the
79
+ * entry again in its own form, relative to the agent directory.
80
+ */
72
81
  function registerExtension({ dir, json }) {
73
82
  const label = basename(json.name);
74
83
  const extension = join(dir, "pi");
84
+ if (piLists(extension))
85
+ return true;
75
86
  return runPi(label, ["install", extension], `To add the Pi extension of ${label}, run: pi install ${extension}`);
76
87
  }
77
88
  /** Runs `pi`, with a warning when it is missing or fails. False when it failed; true when it worked or is missing. */
@@ -92,6 +103,39 @@ function runPi(label, args, later) {
92
103
  }
93
104
  return true;
94
105
  }
106
+ /**
107
+ * Whether the `packages` of Pi's global settings list the folder `extension` as a local path, absolute or relative to
108
+ * the agent directory, with `~` or as a `file:` URL, as Pi resolves them.
109
+ */
110
+ function piLists(extension) {
111
+ const env = process.env.PI_CODING_AGENT_DIR;
112
+ const agentDir = env ? expandHome(env) : join(homedir(), ".pi", "agent");
113
+ let settings;
114
+ try {
115
+ settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8"));
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ if (!Value.Check(PiSettings, settings))
121
+ return false;
122
+ return (settings.packages ?? []).some((item) => {
123
+ const source = (Value.Check(Type.String(), item) ? item : item.source).trim();
124
+ if (REMOTE.some((prefix) => source.startsWith(prefix)))
125
+ return false;
126
+ const path = source.startsWith("file://") ? fileURLToPath(source) : resolve(agentDir, expandHome(source));
127
+ try {
128
+ return realpathSync(path) === extension;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ });
134
+ }
135
+ /** `path` with a leading `~` replaced by the home directory. */
136
+ function expandHome(path) {
137
+ return path === "~" || path.startsWith("~/") ? join(homedir(), path.slice(1)) : path;
138
+ }
95
139
  /** The `PATH` without the `node_modules/.bin` folders that npm adds: pi-coding-agent there has its own `pi`. */
96
140
  function userPath() {
97
141
  return (process.env.PATH ?? "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzlentin/harness",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A durable agent harness on Pi, starting with observable, persistent subagents",
5
5
  "repository": {
6
6
  "type": "git",