@fitchmultz/pi-subagents 0.52.9 → 0.52.11

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
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## 0.52.11 - 2026-11-02
6
+
7
+ <!-- npm-release:unreleased:start -->
8
+ <!-- npm-release:unreleased:end -->
9
+
10
+ - Merge pull request #325 from fitchmultz/chore/oxlint-v512-checkjs ([f55599c](https://github.com/fitchmultz/pi-subagents/commit/f55599cbf8c23c6ee1f9b5e55e7b963bc7140d58))
11
+
12
+ ### What's Changed
13
+
14
+ - 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
15
+
16
+ **Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.10...v0.52.11
17
+
18
+ ## 0.52.10 - 2026-11-02
19
+
20
+ <!-- npm-release:unreleased:start -->
21
+ <!-- npm-release:unreleased:end -->
22
+
23
+ Release metadata only: no source changes since v0.52.9. That release had already shipped
24
+ #319, #320 and #321, so 0.52.10 updates only the package version, lockfile and these
25
+ release notes.
26
+
27
+ **Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.9...v0.52.10
28
+
5
29
  ## 0.52.9 - 2026-11-02
6
30
 
7
31
  <!-- npm-release:unreleased:start -->
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.9","sha256":"afeb6d4b7e13dfe796c199d39eddf3836e8f7ee7185f302254f4182659a0388c"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.11","sha256":"afeb6d4b7e13dfe796c199d39eddf3836e8f7ee7185f302254f4182659a0388c"});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.52.9",
3
+ "version": "0.52.11",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- const capability = /^CapEff:\s*([a-f0-9]+)$/im.exec(
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
- assert.match(values["owner-uid"] ?? "", /^(?:0|[1-9][0-9]*)$/, "E_OWNER_UID");
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(`could not remove staging ${path}: ${error?.message ?? error}`);
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?.code === "ESRCH";
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(cwd, stagingDir, compiler, project) {
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(cwd, stagingDir, compiler) {
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
- if (error?.stdout) {
101
- process.stdout.write(error.stdout);
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 (error?.stderr) {
104
- process.stderr.write(error.stderr);
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
- const { version } = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
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(stagingDir, distDir, retry) {
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?.code !== "ENOENT") {
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?.code !== "ENOENT") {
171
- console.warn(
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 : String(error));
222
+ main().catch((/** @type {unknown} */ error) => {
223
+ console.error(error instanceof Error ? error.message : JSON.stringify(error));
197
224
  process.exitCode = 1;
198
225
  });
@@ -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 = (command, args, options = {}) => {
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
- return result.stdout?.trim();
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: { ref, source: editor, supplied: supplied ? resolve(supplied) : null },
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 };
@@ -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
- export const hostRoot = realpathSync(
10
- dirname(findPackageJSON("@earendil-works/pi-coding-agent", import.meta.url)),
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
- for (const [name, actual] of [
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
- if (process.env[name]) {
24
+ ];
25
+ for (const [name, actual] of expectedHostPaths) {
26
+ const expected = process.env[name];
27
+ if (expected !== undefined) {
21
28
  assert.equal(
22
- realpathSync(process.env[name]),
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
- if (process.env.PI_COMPAT_EXPECTED_VERSION) {
29
- assert.equal(manifest.version, process.env.PI_COMPAT_EXPECTED_VERSION);
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) => createHash("sha256").update(readFileSync(path)).digest("hex");
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
- function errorMessage(error) {
21
- return error instanceof Error ? error.message : String(error);
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 !== process.getuid() ||
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
- execFileSync(process.env.OPENSSL || "openssl", args, {
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(dir, stem, name, { server = false, days = 7 } = {}) {
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
- !dir ||
209
+ dir === "" ||
175
210
  args.positionals.length > 3 ||
176
- (["issue", "revoke"].includes(action) && !name) ||
177
- (action === "renew-server" && (name || args.values.cwd)) ||
178
- (name && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) ||
179
- (action === "revoke" && (args.values.cwd || args.values.days)) ||
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 || "grok-bot";
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((client) => client.name !== name);
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((client) => client.name === name);
288
- config.clients = config.clients.filter((client) => client.name !== name);
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 || previous?.cwd || process.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
  }
@@ -46,7 +46,9 @@ try {
46
46
  process.kill(pid, 0);
47
47
  throw new Error("Legacy helper is still running; stop it before preflight.");
48
48
  } catch (error) {
49
- if (error.code !== "ESRCH") {
49
+ if (
50
+ !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH")
51
+ ) {
50
52
  throw error;
51
53
  }
52
54
  }
@@ -68,23 +70,22 @@ try {
68
70
  if (!isRecord(raw) || typeof raw.timestamp !== "string") {
69
71
  throw new Error("Invalid audit record.");
70
72
  }
71
- const entry = raw;
72
- const timestamp = Date.parse(entry.timestamp);
73
+ const timestamp = Date.parse(raw.timestamp);
73
74
  if (!Number.isSafeInteger(timestamp)) {
74
75
  throw new Error("Audit record has no valid timestamp.");
75
76
  }
76
77
  if (timestamp < coveredSince || timestamp > stoppedAt) {
77
78
  continue;
78
79
  }
79
- if (entry.event === "log_overflow") {
80
+ if (raw.event === "log_overflow") {
80
81
  throw new Error(
81
82
  "Audit overflow prevents complete-history migration; use conservative startup.",
82
83
  );
83
84
  }
84
85
  if (
85
- entry.event === "wake_delivery" &&
86
- typeof entry.result === "string" &&
87
- outcomes.has(entry.result)
86
+ raw.event === "wake_delivery" &&
87
+ typeof raw.result === "string" &&
88
+ outcomes.has(raw.result)
88
89
  ) {
89
90
  attempts.push(timestamp);
90
91
  }
@@ -107,6 +108,6 @@ try {
107
108
  }),
108
109
  );
109
110
  } catch (error) {
110
- console.error(error.message);
111
+ console.error(error instanceof Error ? error.message : JSON.stringify(error));
111
112
  process.exitCode = 1;
112
113
  }