@fitchmultz/pi-subagents 0.52.10 → 0.52.12
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 +26 -0
- package/dist/build-info.js +1 -1
- package/docs/code-quality.md +44 -0
- package/package.json +1 -1
- package/scripts/archive-retired-history.mjs +19 -12
- package/scripts/build.mjs +54 -27
- package/scripts/check-compat.mjs +18 -9
- package/scripts/compat-host.mjs +22 -10
- package/scripts/intercom-bridge-credentials.mjs +69 -25
- package/scripts/intercom-bridge-wake-preflight.mjs +9 -8
- package/scripts/intercom-remote.mjs +68 -31
- package/scripts/local-install-smoke.mjs +10 -5
- package/scripts/measure-size.mjs +5 -1
- package/scripts/native-package-smoke-assertions.mjs +84 -0
- package/scripts/native-package-smoke.mjs +122 -120
- package/scripts/package-smoke.mjs +44 -14
- package/scripts/prepare.mjs +20 -12
- package/scripts/real-pi-live.mjs +259 -69
- package/scripts/real-pi-processes.mjs +117 -54
- package/scripts/real-pi-smoke.mjs +56 -22
- package/scripts/run-tests.mjs +45 -17
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.12 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
<!-- npm-release:unreleased:end -->
|
|
9
|
+
|
|
10
|
+
- Merge pull request #326 from fitchmultz/chore/oxlint-v512-records ([e8ff5c5](https://github.com/fitchmultz/pi-subagents/commit/e8ff5c54a0efc481875949e98f96223c4d46ffc4))
|
|
11
|
+
|
|
12
|
+
### What's Changed
|
|
13
|
+
|
|
14
|
+
- Adopt the strict standard records and policy integrity keeper (5/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/326
|
|
15
|
+
|
|
16
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.11...v0.52.12
|
|
17
|
+
|
|
18
|
+
## 0.52.11 - 2026-11-02
|
|
19
|
+
|
|
20
|
+
<!-- npm-release:unreleased:start -->
|
|
21
|
+
<!-- npm-release:unreleased:end -->
|
|
22
|
+
|
|
23
|
+
- Merge pull request #325 from fitchmultz/chore/oxlint-v512-checkjs ([f55599c](https://github.com/fitchmultz/pi-subagents/commit/f55599cbf8c23c6ee1f9b5e55e7b963bc7140d58))
|
|
24
|
+
|
|
25
|
+
### What's Changed
|
|
26
|
+
|
|
27
|
+
- Enable checkJs and clear every strict-lint error in scripts and fixtures (4/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/325
|
|
28
|
+
|
|
29
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.10...v0.52.11
|
|
30
|
+
|
|
5
31
|
## 0.52.10 - 2026-11-02
|
|
6
32
|
|
|
7
33
|
<!-- npm-release:unreleased:start -->
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.12","sha256":"afeb6d4b7e13dfe796c199d39eddf3836e8f7ee7185f302254f4182659a0388c"});
|
package/docs/code-quality.md
CHANGED
|
@@ -39,3 +39,47 @@ substitutes the selected host graph. The retired workflow schema generator and
|
|
|
39
39
|
its patching dependency are not part of the new runtime. `npm run check:size`
|
|
40
40
|
still reports source and production line counts for visibility; no line count
|
|
41
41
|
is mandated. The 1 MiB byte ceiling and all lint limits stay unchanged.
|
|
42
|
+
|
|
43
|
+
## Adoption record
|
|
44
|
+
|
|
45
|
+
Repository: `fitchmultz/pi-subagents`. Adopted standard: strict Oxlint/Oxfmt
|
|
46
|
+
engineering standard v5.1.2, kept as the repository copy at
|
|
47
|
+
`docs/strict-oxlint-oxfmt-standard.md`. Exception records live in
|
|
48
|
+
[Quality exceptions](quality-exceptions.md) and follow the standard's Appendix B.
|
|
49
|
+
|
|
50
|
+
Supported runtime and package manager: Node `>=24.21.0` with npm and the
|
|
51
|
+
committed lockfile. Installed tool versions (devDependencies): oxlint 1.90.0,
|
|
52
|
+
oxlint-tsgolint 7.0.2005, oxfmt 0.75.0, `@types/node` 24.19.2, typebox 1.4.3.
|
|
53
|
+
|
|
54
|
+
Configuration and acceptance paths: lint `.oxlintrc.json`, formatting
|
|
55
|
+
`.oxfmtrc.json`, compiler `tsconfig.json`. Local acceptance commands are
|
|
56
|
+
`npm run lint`, `npm run format:check` and `npm run typecheck`; the aggregate
|
|
57
|
+
acceptance command is hosted `npm run ci` (lint, formatting, TypeScript, byte
|
|
58
|
+
ceiling, package/install smokes and the complete test suites, plus separate
|
|
59
|
+
official-Pi and fork qualification jobs).
|
|
60
|
+
|
|
61
|
+
Compiler projects and source profiles: one `tsc --noEmit` project covers all
|
|
62
|
+
maintained TypeScript and checked-JavaScript (`src`, `scripts`, `test`,
|
|
63
|
+
`.github`) with `strict`, `checkJs`, `noUncheckedIndexedAccess`,
|
|
64
|
+
`exactOptionalPropertyTypes` and the full strict flag set. Lint profiles:
|
|
65
|
+
production limits (complexity 10, depth 3, parameters 4, statements 40,
|
|
66
|
+
function lines 80, file lines 500), test/fixture limits (complexity 15, depth
|
|
67
|
+
4, parameters 6, no size limits) and declaration files with size limits off.
|
|
68
|
+
|
|
69
|
+
Formatter and generator ownership: oxfmt formats all maintained source at
|
|
70
|
+
width 100; npm owns lockfile formatting; build outputs keep their own bytes.
|
|
71
|
+
|
|
72
|
+
Migration status: complete. The toolchain, baseline configuration, strict
|
|
73
|
+
TypeScript compiler options, checked-JavaScript coverage and this records
|
|
74
|
+
policy landed as PRs #319, #320, #321, #325 and the records PR; the policy
|
|
75
|
+
integrity keeper `test/unit/strict-oxlint-policy.test.ts` pins the effective
|
|
76
|
+
configuration, the four-entry overrides inventory and the directive rule-name
|
|
77
|
+
inventory against drift.
|
|
78
|
+
|
|
79
|
+
Integrated verification: hosted `npm run ci` on the merge of each part, with
|
|
80
|
+
the single-agent-runtime, agent-receipts, pi-intercom-native-replay and
|
|
81
|
+
retired-history-archive keepers exercising the native contracts. Locally
|
|
82
|
+
verified state for the checked-JavaScript part is recorded in PR #325; the
|
|
83
|
+
retired-history-archive suite requires passwordless `sudo` and
|
|
84
|
+
`scripts/package-smoke.mjs` requires a short Unix socket directory, so hosted
|
|
85
|
+
CI is the authoritative gate for both.
|
package/package.json
CHANGED
|
@@ -389,6 +389,21 @@ function prepare(values, uid) {
|
|
|
389
389
|
syncDirectory(path.dirname(archive));
|
|
390
390
|
return inspect(record, uid);
|
|
391
391
|
}
|
|
392
|
+
function ownerUid(/** @type {{ "owner-uid"?: string }} */ values) {
|
|
393
|
+
assert.match(values["owner-uid"] ?? "", /^(?:0|[1-9][0-9]*)$/, "E_OWNER_UID");
|
|
394
|
+
const uid = Number(values["owner-uid"]);
|
|
395
|
+
assert.ok(Number.isSafeInteger(uid) && uid <= 4_294_967_294, "E_OWNER_UID");
|
|
396
|
+
return uid;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function requireLinuxCapability() {
|
|
400
|
+
const capability = /^CapEff:\s*([a-f0-9]+)$/im.exec(fs.readFileSync("/proc/self/status", "utf8"));
|
|
401
|
+
assert.ok(capability !== null, "E_PROCESS_VISIBILITY");
|
|
402
|
+
const capEff = capability[1];
|
|
403
|
+
assert.ok(capEff !== undefined, "E_PROCESS_VISIBILITY");
|
|
404
|
+
assert.ok((BigInt(`0x${capEff}`) & (1n << 19n)) !== 0n, "E_PROCESS_VISIBILITY");
|
|
405
|
+
}
|
|
406
|
+
|
|
392
407
|
function main() {
|
|
393
408
|
const { values, positionals } = parseArgs({
|
|
394
409
|
options: {
|
|
@@ -408,23 +423,15 @@ function main() {
|
|
|
408
423
|
return;
|
|
409
424
|
}
|
|
410
425
|
assert.ok(positionals.length === 1, "E_ACTION");
|
|
411
|
-
const action = positionals[0];
|
|
426
|
+
const action = positionals[0] ?? "";
|
|
412
427
|
assert.ok(["prepare", "inspect", "archive"].includes(action), "E_ACTION");
|
|
413
428
|
assert.ok(["linux", "darwin"].includes(process.platform), "E_PLATFORM");
|
|
414
429
|
assert.ok(process.geteuid !== undefined, "E_PLATFORM");
|
|
415
430
|
assert.ok(process.geteuid() === 0, "E_PRIVILEGE");
|
|
416
431
|
if (process.platform === "linux") {
|
|
417
|
-
|
|
418
|
-
fs.readFileSync("/proc/self/status", "utf8"),
|
|
419
|
-
);
|
|
420
|
-
assert.ok(capability !== null, "E_PROCESS_VISIBILITY");
|
|
421
|
-
const capEff = capability[1];
|
|
422
|
-
assert.ok(capEff !== undefined, "E_PROCESS_VISIBILITY");
|
|
423
|
-
assert.ok((BigInt(`0x${capEff}`) & (1n << 19n)) !== 0n, "E_PROCESS_VISIBILITY");
|
|
432
|
+
requireLinuxCapability();
|
|
424
433
|
}
|
|
425
|
-
|
|
426
|
-
const uid = Number(values["owner-uid"]);
|
|
427
|
-
assert.ok(Number.isSafeInteger(uid) && uid <= 4_294_967_294, "E_OWNER_UID");
|
|
434
|
+
const uid = ownerUid(values);
|
|
428
435
|
if (action !== "inspect") {
|
|
429
436
|
assert.ok(values.quiesced === true, "E_QUIESCENCE_ACK");
|
|
430
437
|
}
|
|
@@ -454,7 +461,7 @@ try {
|
|
|
454
461
|
} catch (error) {
|
|
455
462
|
let code = "E_INSPECTION_FAILED";
|
|
456
463
|
if (error instanceof assert.AssertionError && !error.generatedMessage) {
|
|
457
|
-
const firstLine = error.message.split("\n")[0];
|
|
464
|
+
const firstLine = error.message.split("\n")[0] ?? "";
|
|
458
465
|
if (/^E_[A-Z_]+$/.test(firstLine)) {
|
|
459
466
|
code = firstLine;
|
|
460
467
|
}
|
package/scripts/build.mjs
CHANGED
|
@@ -20,8 +20,14 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
20
20
|
import { promisify } from "node:util";
|
|
21
21
|
import { compilerInvocation } from "./typescript.mjs";
|
|
22
22
|
|
|
23
|
+
/** @type {(file: string, args: readonly string[], options: import("node:child_process").ExecFileOptions) => Promise<{ stdout: string, stderr: string }>} */
|
|
24
|
+
// oxlint-disable-next-line typescript/strict-void-return
|
|
23
25
|
const execFile = promisify(execFileCallback);
|
|
24
26
|
const RM_OPTIONS = { force: true, maxRetries: 5, recursive: true, retryDelay: 100 };
|
|
27
|
+
|
|
28
|
+
function isErrno(/** @type {unknown} */ error, /** @type {string} */ code) {
|
|
29
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
30
|
+
}
|
|
25
31
|
const RENAME_RETRY_LIMIT = 50;
|
|
26
32
|
const RENAME_RETRY_MS = 50;
|
|
27
33
|
|
|
@@ -32,26 +38,28 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
|
32
38
|
process.exit(0);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
async function discardStaging(path) {
|
|
41
|
+
async function discardStaging(/** @type {string} */ path) {
|
|
36
42
|
try {
|
|
37
43
|
await rm(path, RM_OPTIONS);
|
|
38
44
|
} catch (error) {
|
|
39
45
|
// Cleanup is always best-effort: it must not turn a successful race loss
|
|
40
46
|
// into failure or replace the compiler/publish error that matters.
|
|
41
|
-
console.warn(
|
|
47
|
+
console.warn(
|
|
48
|
+
`could not remove staging ${path}: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
49
|
+
);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function isOwnerGone(pid) {
|
|
53
|
+
function isOwnerGone(/** @type {number} */ pid) {
|
|
46
54
|
try {
|
|
47
55
|
process.kill(pid, 0);
|
|
48
56
|
return false;
|
|
49
57
|
} catch (error) {
|
|
50
|
-
return error
|
|
58
|
+
return isErrno(error, "ESRCH");
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
async function hasPublishedDist(path) {
|
|
62
|
+
async function hasPublishedDist(/** @type {string} */ path) {
|
|
55
63
|
try {
|
|
56
64
|
return (await readdir(path)).length > 0;
|
|
57
65
|
} catch {
|
|
@@ -59,7 +67,7 @@ async function hasPublishedDist(path) {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
async function reapStrandedStaging(cwd) {
|
|
70
|
+
async function reapStrandedStaging(/** @type {string} */ cwd) {
|
|
63
71
|
// Signal 0 proves only that the owner was gone at probe time. Pid reuse
|
|
64
72
|
// between this probe and removal is inherent to pid-based reaping.
|
|
65
73
|
for (const entry of await readdir(cwd, { withFileTypes: true })) {
|
|
@@ -77,38 +85,49 @@ async function reapStrandedStaging(cwd) {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
async function compileProject(
|
|
88
|
+
async function compileProject(
|
|
89
|
+
/** @type {string} */ cwd,
|
|
90
|
+
/** @type {string} */ stagingDir,
|
|
91
|
+
/** @type {{ command: string, args: string[] }} */ compiler,
|
|
92
|
+
/** @type {string} */ project,
|
|
93
|
+
) {
|
|
81
94
|
const { stderr, stdout } = await execFile(
|
|
82
95
|
compiler.command,
|
|
83
96
|
[...compiler.args, "-p", project, "--outDir", stagingDir],
|
|
84
97
|
{ cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
85
98
|
);
|
|
86
|
-
if (stdout) {
|
|
99
|
+
if (stdout !== "") {
|
|
87
100
|
process.stdout.write(stdout);
|
|
88
101
|
}
|
|
89
|
-
if (stderr) {
|
|
102
|
+
if (stderr !== "") {
|
|
90
103
|
process.stderr.write(stderr);
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
async function compileToStaging(
|
|
107
|
+
async function compileToStaging(
|
|
108
|
+
/** @type {string} */ cwd,
|
|
109
|
+
/** @type {string} */ stagingDir,
|
|
110
|
+
/** @type {{ command: string, args: string[] }} */ compiler,
|
|
111
|
+
) {
|
|
95
112
|
try {
|
|
96
113
|
await compileProject(cwd, stagingDir, compiler, "tsconfig.build.json");
|
|
97
114
|
await compileProject(cwd, stagingDir, compiler, "tsconfig.client.json");
|
|
98
115
|
await stampBuild(cwd, stagingDir);
|
|
99
116
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- exec rejections carry partial output
|
|
118
|
+
const output = /** @type {{ stdout?: string, stderr?: string }} */ (error);
|
|
119
|
+
if (output.stdout !== undefined && output.stdout !== "") {
|
|
120
|
+
process.stdout.write(output.stdout);
|
|
102
121
|
}
|
|
103
|
-
if (
|
|
104
|
-
process.stderr.write(
|
|
122
|
+
if (output.stderr !== undefined && output.stderr !== "") {
|
|
123
|
+
process.stderr.write(output.stderr);
|
|
105
124
|
}
|
|
106
125
|
await discardStaging(stagingDir);
|
|
107
126
|
throw error;
|
|
108
127
|
}
|
|
109
128
|
}
|
|
110
129
|
|
|
111
|
-
async function stampBuild(cwd, stagingDir) {
|
|
130
|
+
async function stampBuild(/** @type {string} */ cwd, /** @type {string} */ stagingDir) {
|
|
112
131
|
const stamp = "build-info.js";
|
|
113
132
|
const hash = createHash("sha256");
|
|
114
133
|
const files = (await readdir(stagingDir, { recursive: true }))
|
|
@@ -120,14 +139,21 @@ async function stampBuild(cwd, stagingDir) {
|
|
|
120
139
|
const content = await readFile(join(stagingDir, file));
|
|
121
140
|
hash.update(file).update("\0").update(content).update("\0");
|
|
122
141
|
}
|
|
123
|
-
|
|
142
|
+
/** @type {{ version: string }} */
|
|
143
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
144
|
+
const pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
|
|
145
|
+
const { version } = pkg;
|
|
124
146
|
await writeFile(
|
|
125
147
|
join(stagingDir, stamp),
|
|
126
148
|
`export const EXTENSION_BUILD = Object.freeze(${JSON.stringify({ version, sha256: hash.digest("hex") })});\n`,
|
|
127
149
|
);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
|
-
async function tryPublish(
|
|
152
|
+
async function tryPublish(
|
|
153
|
+
/** @type {string} */ stagingDir,
|
|
154
|
+
/** @type {string} */ distDir,
|
|
155
|
+
/** @type {number} */ retry,
|
|
156
|
+
) {
|
|
131
157
|
try {
|
|
132
158
|
await rename(stagingDir, distDir);
|
|
133
159
|
return true;
|
|
@@ -147,12 +173,12 @@ async function tryPublish(stagingDir, distDir, retry) {
|
|
|
147
173
|
}
|
|
148
174
|
}
|
|
149
175
|
|
|
150
|
-
async function publishStaging(stagingDir, distDir) {
|
|
176
|
+
async function publishStaging(/** @type {string} */ stagingDir, /** @type {string} */ distDir) {
|
|
151
177
|
const previousDir = `${stagingDir}.previous`;
|
|
152
178
|
try {
|
|
153
179
|
// Retire dist exactly once, outside the retry loop, so retries never retire a concurrent winner.
|
|
154
|
-
await rename(distDir, previousDir).catch((error) => {
|
|
155
|
-
if (error
|
|
180
|
+
await rename(distDir, previousDir).catch((/** @type {unknown} */ error) => {
|
|
181
|
+
if (!isErrno(error, "ENOENT")) {
|
|
156
182
|
throw error;
|
|
157
183
|
}
|
|
158
184
|
});
|
|
@@ -166,12 +192,13 @@ async function publishStaging(stagingDir, distDir) {
|
|
|
166
192
|
await discardStaging(previousDir);
|
|
167
193
|
} catch (error) {
|
|
168
194
|
// ENOENT: nothing was retired. Otherwise a concurrent winner may already own dist.
|
|
169
|
-
await rename(previousDir, distDir).catch((restoreError) => {
|
|
170
|
-
if (restoreError
|
|
171
|
-
|
|
172
|
-
`could not restore ${previousDir} to dist: ${restoreError?.message ?? restoreError}`,
|
|
173
|
-
);
|
|
195
|
+
await rename(previousDir, distDir).catch((/** @type {unknown} */ restoreError) => {
|
|
196
|
+
if (isErrno(restoreError, "ENOENT")) {
|
|
197
|
+
return;
|
|
174
198
|
}
|
|
199
|
+
const message =
|
|
200
|
+
restoreError instanceof Error ? restoreError.message : JSON.stringify(restoreError);
|
|
201
|
+
console.warn(`could not restore ${previousDir} to dist: ${message}`);
|
|
175
202
|
});
|
|
176
203
|
throw error;
|
|
177
204
|
} finally {
|
|
@@ -192,7 +219,7 @@ async function main() {
|
|
|
192
219
|
await publishStaging(stagingDir, join(cwd, "dist"));
|
|
193
220
|
}
|
|
194
221
|
|
|
195
|
-
main().catch((error) => {
|
|
196
|
-
console.error(error instanceof Error ? error.message :
|
|
222
|
+
main().catch((/** @type {unknown} */ error) => {
|
|
223
|
+
console.error(error instanceof Error ? error.message : JSON.stringify(error));
|
|
197
224
|
process.exitCode = 1;
|
|
198
225
|
});
|
package/scripts/check-compat.mjs
CHANGED
|
@@ -13,7 +13,11 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
|
13
13
|
}
|
|
14
14
|
assert.equal(process.argv.length, 2, "Expected no arguments; use --help");
|
|
15
15
|
await import("./compat-host.mjs");
|
|
16
|
-
const run = (
|
|
16
|
+
const run = (
|
|
17
|
+
/** @type {string} */ command,
|
|
18
|
+
/** @type {string[]} */ args,
|
|
19
|
+
/** @type {import("node:child_process").SpawnSyncOptions} */ options = {},
|
|
20
|
+
) => {
|
|
17
21
|
// Outlasts the 1_200_000 ms integration watchdog in scripts/run-tests.mjs.
|
|
18
22
|
const result = spawnSync(command, args, { stdio: "inherit", timeout: 1_500_000, ...options });
|
|
19
23
|
if (result.error) {
|
|
@@ -22,18 +26,19 @@ const run = (command, args, options = {}) => {
|
|
|
22
26
|
assert.equal(
|
|
23
27
|
result.status,
|
|
24
28
|
0,
|
|
25
|
-
`${command} ${args.join(" ")} failed (${result.status ?? result.signal})`,
|
|
29
|
+
`${command} ${args.join(" ")} failed (${result.status ?? result.signal ?? "unknown"})`,
|
|
26
30
|
);
|
|
27
|
-
|
|
31
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spawnSync stdout shape depends on caller options
|
|
32
|
+
return /** @type {string | undefined} */ (/** @type {unknown} */ (result.stdout))?.trim();
|
|
28
33
|
};
|
|
29
|
-
const git = (cwd, args) =>
|
|
34
|
+
const git = (/** @type {string} */ cwd, /** @type {string[]} */ args) =>
|
|
30
35
|
run("git", args, { cwd, stdio: ["ignore", "pipe", "inherit"], encoding: "utf8" });
|
|
31
36
|
const root = mkdtempSync(join(tmpdir(), "ps-editor-"));
|
|
32
37
|
const editor = join(root, "source");
|
|
33
38
|
try {
|
|
34
39
|
const supplied = process.env.PI_EDITOR_RECEIPT_TEST_ROOT;
|
|
35
40
|
let expectedRef;
|
|
36
|
-
if (supplied) {
|
|
41
|
+
if (supplied !== undefined) {
|
|
37
42
|
const source = resolve(supplied);
|
|
38
43
|
expectedRef = git(source, ["rev-parse", "HEAD"]);
|
|
39
44
|
assert.equal(
|
|
@@ -42,7 +47,7 @@ try {
|
|
|
42
47
|
"Supplied editor source must be clean",
|
|
43
48
|
);
|
|
44
49
|
run("git", ["clone", "--no-hardlinks", "--quiet", source, editor]);
|
|
45
|
-
git(editor, ["checkout", "--quiet", "--detach", expectedRef]);
|
|
50
|
+
git(editor, ["checkout", "--quiet", "--detach", expectedRef ?? ""]);
|
|
46
51
|
} else {
|
|
47
52
|
run("git", [
|
|
48
53
|
"clone",
|
|
@@ -57,8 +62,8 @@ try {
|
|
|
57
62
|
]);
|
|
58
63
|
}
|
|
59
64
|
const ref = git(editor, ["rev-parse", "HEAD"]);
|
|
60
|
-
assert.match(ref, /^[a-f0-9]{40}$/);
|
|
61
|
-
if (expectedRef) {
|
|
65
|
+
assert.match(ref ?? "", /^[a-f0-9]{40}$/);
|
|
66
|
+
if (expectedRef !== undefined) {
|
|
62
67
|
assert.equal(ref, expectedRef);
|
|
63
68
|
}
|
|
64
69
|
assert.ok(
|
|
@@ -74,7 +79,11 @@ try {
|
|
|
74
79
|
git(editor, ["diff", "--exit-code"]);
|
|
75
80
|
console.log(
|
|
76
81
|
JSON.stringify({
|
|
77
|
-
nativeEditor: {
|
|
82
|
+
nativeEditor: {
|
|
83
|
+
ref,
|
|
84
|
+
source: editor,
|
|
85
|
+
supplied: supplied !== undefined ? resolve(supplied) : null,
|
|
86
|
+
},
|
|
78
87
|
}),
|
|
79
88
|
);
|
|
80
89
|
const env = { ...process.env, PI_EDITOR_RECEIPT_TEST_ROOT: editor };
|
package/scripts/compat-host.mjs
CHANGED
|
@@ -6,33 +6,45 @@ import { findPackageJSON } from "node:module";
|
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
9
|
+
const hostPackageJson = findPackageJSON("@earendil-works/pi-coding-agent", import.meta.url);
|
|
10
|
+
if (hostPackageJson === undefined) {
|
|
11
|
+
throw new Error("Could not locate the installed pi-coding-agent package");
|
|
12
|
+
}
|
|
13
|
+
export const hostRoot = realpathSync(dirname(hostPackageJson));
|
|
12
14
|
export const hostIndex = join(hostRoot, "dist/index.js");
|
|
15
|
+
/** @type {{ version: string, bin: { pi: string } }} */
|
|
16
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
13
17
|
const manifest = JSON.parse(readFileSync(join(hostRoot, "package.json"), "utf8"));
|
|
14
18
|
export const hostCli = realpathSync(join(hostRoot, manifest.bin.pi));
|
|
15
|
-
|
|
19
|
+
/** @type {[string, string][]} */
|
|
20
|
+
const expectedHostPaths = [
|
|
16
21
|
["PI_COMPAT_EXPECTED_PACKAGE_DIR", hostRoot],
|
|
17
22
|
["PI_HOST_INDEX", hostIndex],
|
|
18
23
|
["PI_HOST_CLI", hostCli],
|
|
19
|
-
]
|
|
20
|
-
|
|
24
|
+
];
|
|
25
|
+
for (const [name, actual] of expectedHostPaths) {
|
|
26
|
+
const expected = process.env[name];
|
|
27
|
+
if (expected !== undefined) {
|
|
21
28
|
assert.equal(
|
|
22
|
-
|
|
29
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
30
|
+
realpathSync(expected),
|
|
23
31
|
realpathSync(actual),
|
|
24
32
|
`${name} must select the installed graph`,
|
|
25
33
|
);
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
const expectedVersion = process.env.PI_COMPAT_EXPECTED_VERSION;
|
|
37
|
+
if (expectedVersion !== undefined) {
|
|
38
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
39
|
+
assert.equal(manifest.version, expectedVersion);
|
|
30
40
|
}
|
|
31
41
|
assert.equal(
|
|
42
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
32
43
|
realpathSync(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))),
|
|
33
44
|
realpathSync(hostIndex),
|
|
34
45
|
);
|
|
35
|
-
const hash = (path) =>
|
|
46
|
+
const hash = (/** @type {string} */ path) =>
|
|
47
|
+
createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
36
48
|
console.log(
|
|
37
49
|
JSON.stringify({
|
|
38
50
|
host: process.env.PI_COMPAT_HOST ?? "local",
|
|
@@ -13,12 +13,34 @@ import {
|
|
|
13
13
|
import { resolve, join } from "node:path";
|
|
14
14
|
import { parseArgs } from "node:util";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {unknown} value
|
|
18
|
+
* @returns {value is Record<string, unknown>}
|
|
19
|
+
*/
|
|
16
20
|
function isRecord(value) {
|
|
17
21
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @param {string | undefined} value
|
|
26
|
+
* @returns {value is string}
|
|
27
|
+
*/
|
|
28
|
+
function hasValue(value) {
|
|
29
|
+
return value !== undefined && value !== "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {(string | undefined)[]} values */
|
|
33
|
+
function firstValue(values) {
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
if (hasValue(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function errorMessage(/** @type {unknown} */ error) {
|
|
43
|
+
return error instanceof Error ? error.message : JSON.stringify(error);
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
function help() {
|
|
@@ -50,19 +72,21 @@ Exit: 0 success, 1 filesystem/OpenSSL failure, 2 invalid arguments.
|
|
|
50
72
|
Never transfer ca.key or server.key to the remote host.`);
|
|
51
73
|
}
|
|
52
74
|
|
|
53
|
-
function privatePath(path, directory = false) {
|
|
75
|
+
function privatePath(/** @type {string} */ path, /** @type {boolean} */ directory = false) {
|
|
54
76
|
const st = lstatSync(path);
|
|
77
|
+
const uid = process.getuid?.();
|
|
55
78
|
if (
|
|
79
|
+
uid === undefined ||
|
|
56
80
|
st.isSymbolicLink() ||
|
|
57
81
|
!(directory ? st.isDirectory() : st.isFile()) ||
|
|
58
|
-
st.uid !==
|
|
59
|
-
st.mode & 0o077
|
|
82
|
+
st.uid !== uid ||
|
|
83
|
+
(st.mode & 0o077) !== 0
|
|
60
84
|
) {
|
|
61
85
|
throw new Error(`Expected an owned, private ${directory ? "directory" : "file"}: ${path}`);
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
function atomicConfig(path, config) {
|
|
89
|
+
function atomicConfig(/** @type {string} */ path, /** @type {unknown} */ config) {
|
|
66
90
|
const temp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
67
91
|
try {
|
|
68
92
|
writeFileSync(temp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
@@ -72,19 +96,26 @@ function atomicConfig(path, config) {
|
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
|
|
99
|
+
/** @param {string[]} args */
|
|
75
100
|
function openssl(...args) {
|
|
76
|
-
|
|
101
|
+
const opensslBin = process.env.OPENSSL;
|
|
102
|
+
execFileSync(opensslBin !== undefined && opensslBin !== "" ? opensslBin : "openssl", args, {
|
|
77
103
|
stdio: ["ignore", "pipe", "pipe"],
|
|
78
104
|
timeout: 30000,
|
|
79
105
|
});
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
function key(path) {
|
|
108
|
+
function key(/** @type {string} */ path) {
|
|
83
109
|
openssl("ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", path);
|
|
84
110
|
privatePath(path);
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
function issueCertificate(
|
|
113
|
+
function issueCertificate(
|
|
114
|
+
/** @type {string} */ dir,
|
|
115
|
+
/** @type {string} */ stem,
|
|
116
|
+
/** @type {string} */ name,
|
|
117
|
+
/** @type {{ server?: boolean, days?: number }} */ { server = false, days = 7 } = {},
|
|
118
|
+
) {
|
|
88
119
|
const keyPath = join(dir, `${stem}.key`);
|
|
89
120
|
const certPath = join(dir, `${stem}.crt`);
|
|
90
121
|
const csrPath = join(dir, `${stem}.csr`);
|
|
@@ -129,7 +160,9 @@ function issueCertificate(dir, stem, name, { server = false, days = 7 } = {}) {
|
|
|
129
160
|
};
|
|
130
161
|
}
|
|
131
162
|
|
|
132
|
-
function readConfig(path) {
|
|
163
|
+
function readConfig(/** @type {string} */ path) {
|
|
164
|
+
/** @type {{ ca?: string, clients?: unknown[], cert?: string, key?: string }} */
|
|
165
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
133
166
|
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
134
167
|
if (!isRecord(config) || !Array.isArray(config.clients) || typeof config.ca !== "string") {
|
|
135
168
|
throw new Error("Invalid clients allowlist.");
|
|
@@ -161,22 +194,24 @@ try {
|
|
|
161
194
|
days: { type: "string" },
|
|
162
195
|
},
|
|
163
196
|
});
|
|
164
|
-
if (args.values.help) {
|
|
197
|
+
if (args.values.help === true) {
|
|
165
198
|
help();
|
|
166
199
|
process.exit(0);
|
|
167
200
|
}
|
|
168
|
-
const [action, dir, name] = args.positionals;
|
|
201
|
+
const [action = "", dir = "", name] = args.positionals;
|
|
202
|
+
const cwdArg = args.values.cwd;
|
|
203
|
+
const daysArg = args.values.days;
|
|
169
204
|
port = args.values.port === undefined ? 9443 : Number(args.values.port);
|
|
170
205
|
const defaultDays = action === "renew-server" ? 365 : 7;
|
|
171
206
|
certificateDays = args.values.days === undefined ? defaultDays : Number(args.values.days);
|
|
172
207
|
if (
|
|
173
208
|
!["init", "issue", "revoke", "renew-server"].includes(action) ||
|
|
174
|
-
|
|
209
|
+
dir === "" ||
|
|
175
210
|
args.positionals.length > 3 ||
|
|
176
|
-
(["issue", "revoke"].includes(action) && !name) ||
|
|
177
|
-
(action === "renew-server" && (name ||
|
|
178
|
-
(name && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) ||
|
|
179
|
-
(action === "revoke" && (
|
|
211
|
+
(["issue", "revoke"].includes(action) && !hasValue(name)) ||
|
|
212
|
+
(action === "renew-server" && (hasValue(name) || hasValue(cwdArg))) ||
|
|
213
|
+
(hasValue(name) && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) ||
|
|
214
|
+
(action === "revoke" && (hasValue(cwdArg) || hasValue(daysArg))) ||
|
|
180
215
|
(action !== "init" && args.values.port !== undefined) ||
|
|
181
216
|
!Number.isInteger(port) ||
|
|
182
217
|
port < 0 ||
|
|
@@ -192,12 +227,15 @@ try {
|
|
|
192
227
|
process.exit(2);
|
|
193
228
|
}
|
|
194
229
|
|
|
195
|
-
let lock;
|
|
230
|
+
/** @type {string | undefined} */ let lock;
|
|
196
231
|
try {
|
|
197
232
|
process.umask(0o077);
|
|
198
233
|
const [action, rawDir, suppliedName] = args.positionals;
|
|
234
|
+
if (rawDir === undefined) {
|
|
235
|
+
throw new Error("A directory argument is required.");
|
|
236
|
+
}
|
|
199
237
|
const dir = resolve(rawDir);
|
|
200
|
-
const name = suppliedName
|
|
238
|
+
const name = hasValue(suppliedName) ? suppliedName : "grok-bot";
|
|
201
239
|
const configPath = join(dir, "config.json");
|
|
202
240
|
let config;
|
|
203
241
|
if (action === "init") {
|
|
@@ -253,10 +291,12 @@ try {
|
|
|
253
291
|
config = readConfig(configPath);
|
|
254
292
|
}
|
|
255
293
|
if (action === "revoke") {
|
|
256
|
-
if (!config.clients.some((client) => client.name === name)) {
|
|
294
|
+
if (!config.clients.some((/** @type {{ name: string }} */ client) => client.name === name)) {
|
|
257
295
|
throw new Error(`No authorized client named ${name}.`);
|
|
258
296
|
}
|
|
259
|
-
config.clients = config.clients.filter(
|
|
297
|
+
config.clients = config.clients.filter(
|
|
298
|
+
(/** @type {{ name: string }} */ client) => client.name !== name,
|
|
299
|
+
);
|
|
260
300
|
atomicConfig(configPath, config);
|
|
261
301
|
console.log(
|
|
262
302
|
JSON.stringify({ revoked: name, config: configPath, next: "Send SIGHUP to the helper PID." }),
|
|
@@ -284,13 +324,17 @@ try {
|
|
|
284
324
|
const credential = issueCertificate(dir, `${name}-${randomBytes(8).toString("hex")}`, name, {
|
|
285
325
|
days: certificateDays,
|
|
286
326
|
});
|
|
287
|
-
const previous = config.clients.find(
|
|
288
|
-
|
|
327
|
+
const previous = config.clients.find(
|
|
328
|
+
(/** @type {{ name: string }} */ client) => client.name === name,
|
|
329
|
+
);
|
|
330
|
+
config.clients = config.clients.filter(
|
|
331
|
+
(/** @type {{ name: string }} */ client) => client.name !== name,
|
|
332
|
+
);
|
|
289
333
|
config.clients.push({
|
|
290
334
|
...previous,
|
|
291
335
|
fingerprint256: credential.fingerprint256,
|
|
292
336
|
name,
|
|
293
|
-
cwd: resolve(args.values.cwd
|
|
337
|
+
cwd: resolve(firstValue([args.values.cwd, previous?.cwd]) ?? process.cwd()),
|
|
294
338
|
});
|
|
295
339
|
atomicConfig(configPath, config);
|
|
296
340
|
console.log(
|
|
@@ -307,7 +351,7 @@ try {
|
|
|
307
351
|
console.error(errorMessage(error));
|
|
308
352
|
process.exitCode = 1;
|
|
309
353
|
} finally {
|
|
310
|
-
if (lock) {
|
|
354
|
+
if (lock !== undefined) {
|
|
311
355
|
rmSync(join(lock, "owner"), { force: true });
|
|
312
356
|
rmdirSync(lock);
|
|
313
357
|
}
|