@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.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/AGENTS.md +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +64 -0
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
*
|
|
16
16
|
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, mkdirSync, openSync, closeSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, openSync, closeSync, readFileSync } from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import type { ChildProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
22
22
|
import type { SpawnStrategy } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
23
23
|
import { MANAGED_BIN } from "@blackbelt-technology/pi-dashboard-shared/managed-paths.js";
|
|
24
24
|
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
25
|
+
import { prependManagedNodeToPath } from "@blackbelt-technology/pi-dashboard-shared/platform/managed-node-path.js";
|
|
26
|
+
import { mintSpawnToken } from "./spawn-token.js";
|
|
25
27
|
import { execSync, spawnSync, buildSafeArgv } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
26
28
|
import {
|
|
27
29
|
spawnDetached,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
type SpawnMechanism,
|
|
35
37
|
type UserSpawnStrategy,
|
|
36
38
|
} from "@blackbelt-technology/pi-dashboard-shared/platform/spawn-mechanism.js";
|
|
39
|
+
import type { SpawnFailureCode } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
37
40
|
|
|
38
41
|
// ── Resolver seam (injectable for tests) ────────────────────────────────────
|
|
39
42
|
|
|
@@ -55,6 +58,15 @@ export interface SessionOptions {
|
|
|
55
58
|
sessionFile?: string;
|
|
56
59
|
mode?: "continue" | "fork";
|
|
57
60
|
strategy?: SpawnStrategy;
|
|
61
|
+
/**
|
|
62
|
+
* Server-minted spawn correlation token. When provided, injected into
|
|
63
|
+
* the spawned process env as `PI_DASHBOARD_SPAWN_TOKEN`. The bridge
|
|
64
|
+
* echoes it back in the first `session_register` so the server can
|
|
65
|
+
* resolve identity precisely (linkByToken). When omitted, callers
|
|
66
|
+
* fall through to pid-link or cwd-FIFO matching.
|
|
67
|
+
* See change: spawn-correlation-token.
|
|
68
|
+
*/
|
|
69
|
+
spawnToken?: string;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export interface SpawnResult {
|
|
@@ -64,11 +76,46 @@ export interface SpawnResult {
|
|
|
64
76
|
process?: ChildProcess;
|
|
65
77
|
/** True when spawned from the dashboard (for writing session meta) */
|
|
66
78
|
dashboardSpawned?: boolean;
|
|
79
|
+
/** Structured failure classifier. Set on every { success: false } path. See change: spawn-failure-diagnostics. */
|
|
80
|
+
code?: SpawnFailureCode;
|
|
81
|
+
/** Tail of pi's stderr log (Windows headless PI_CRASHED only). See change: spawn-failure-diagnostics. */
|
|
82
|
+
stderr?: string;
|
|
83
|
+
/** Path to the per-session stderr log (Windows headless). Forwarded to watchdog. See change: spawn-failure-diagnostics. */
|
|
84
|
+
logPath?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Token minted by `spawnPiSession` and injected into the spawned process's
|
|
87
|
+
* env as `PI_DASHBOARD_SPAWN_TOKEN`. Returned so callers can register it
|
|
88
|
+
* with the headless-pid registry, watchdog, and pending-* registries.
|
|
89
|
+
* See change: spawn-correlation-token.
|
|
90
|
+
*/
|
|
91
|
+
spawnToken?: string;
|
|
67
92
|
}
|
|
68
93
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Build env for pi-session spawns.
|
|
96
|
+
*
|
|
97
|
+
* Order of PATH prepends (highest priority first):
|
|
98
|
+
* 1. Managed Node runtime (`<managedDir>/node/{bin,}`) when installed.
|
|
99
|
+
* See change: embed-managed-node-runtime.
|
|
100
|
+
* 2. Managed bin (`<managedDir>/node_modules/.bin`).
|
|
101
|
+
* 3. Current Node binary dir, extra bin dirs, common user bin dirs.
|
|
102
|
+
*
|
|
103
|
+
* The managed-Node prepend happens AFTER the resolver's prepends so it
|
|
104
|
+
* lands at the very head of `PATH` — spawned children invoking plain
|
|
105
|
+
* `node` / `npm` resolve to the managed runtime first.
|
|
106
|
+
*/
|
|
107
|
+
export function buildSpawnEnv(
|
|
108
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
109
|
+
opts?: { spawnToken?: string },
|
|
110
|
+
): NodeJS.ProcessEnv {
|
|
111
|
+
const env = prependManagedNodeToPath(resolver.buildSpawnEnv(baseEnv));
|
|
112
|
+
if (opts?.spawnToken) {
|
|
113
|
+
// Inject the correlation token so the bridge inside the spawned pi
|
|
114
|
+
// process can read it and echo back in `session_register`.
|
|
115
|
+
// See change: spawn-correlation-token.
|
|
116
|
+
return { ...env, PI_DASHBOARD_SPAWN_TOKEN: opts.spawnToken };
|
|
117
|
+
}
|
|
118
|
+
return env;
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
/**
|
|
@@ -256,17 +303,28 @@ export async function spawnPiSession(
|
|
|
256
303
|
options?: SessionOptions & { electronMode?: boolean },
|
|
257
304
|
): Promise<SpawnResult> {
|
|
258
305
|
if (!existsSync(cwd)) {
|
|
259
|
-
return { success: false, message: `Directory does not exist: ${cwd}` };
|
|
306
|
+
return { success: false, code: "DIR_MISSING", message: `Directory does not exist: ${cwd}` };
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
|
|
309
|
+
// Mint a spawn token if the caller didn't provide one. Token is injected
|
|
310
|
+
// into the spawned process's env (via buildSpawnEnv) and surfaced on
|
|
311
|
+
// SpawnResult so callers can register it with the registries.
|
|
312
|
+
// See change: spawn-correlation-token.
|
|
313
|
+
const spawnToken = options?.spawnToken ?? mintSpawnToken();
|
|
314
|
+
const opts: SessionOptions & { electronMode?: boolean } = { ...(options ?? {}), spawnToken };
|
|
315
|
+
|
|
316
|
+
const mechanism = chooseMechanism(opts, opts?.electronMode ?? false);
|
|
263
317
|
|
|
318
|
+
let result: SpawnResult;
|
|
264
319
|
switch (mechanism) {
|
|
265
|
-
case "tmux":
|
|
266
|
-
case "wt":
|
|
267
|
-
case "wsl-tmux":
|
|
268
|
-
case "headless":
|
|
320
|
+
case "tmux": result = spawnTmux(cwd, opts); break;
|
|
321
|
+
case "wt": result = await spawnWt(cwd, opts); break;
|
|
322
|
+
case "wsl-tmux": result = spawnWslTmux(cwd, opts); break;
|
|
323
|
+
case "headless": result = await spawnHeadless(cwd, opts); break;
|
|
269
324
|
}
|
|
325
|
+
// Surface the token on every result (success or failure) so callers
|
|
326
|
+
// can clean up registries deterministically.
|
|
327
|
+
return { ...result, spawnToken };
|
|
270
328
|
}
|
|
271
329
|
|
|
272
330
|
// ── Per-mechanism spawn ────────────────────────────────────────────────────
|
|
@@ -274,36 +332,41 @@ export async function spawnPiSession(
|
|
|
274
332
|
function spawnTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
275
333
|
const exists = dashboardSessionExists();
|
|
276
334
|
const cmd = buildTmuxCommand(cwd, exists, options);
|
|
335
|
+
// Pass env explicitly so PI_DASHBOARD_SPAWN_TOKEN reaches the tmux pane's
|
|
336
|
+
// pi process (tmux inherits the caller's env into new windows/sessions).
|
|
337
|
+
// See change: spawn-correlation-token.
|
|
338
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
277
339
|
try {
|
|
278
|
-
execSync(cmd, { stdio: "ignore" });
|
|
340
|
+
execSync(cmd, { stdio: "ignore", env });
|
|
279
341
|
return {
|
|
280
342
|
success: true,
|
|
281
343
|
dashboardSpawned: true,
|
|
282
344
|
message: `Pi session spawned in tmux (${exists ? "new window" : "new session"})`,
|
|
283
345
|
};
|
|
284
346
|
} catch (err: any) {
|
|
285
|
-
return { success: false, message: `Failed to spawn session: ${err.message}` };
|
|
347
|
+
return { success: false, code: "TMUX_MISSING", message: `Failed to spawn session: ${err.message}` };
|
|
286
348
|
}
|
|
287
349
|
}
|
|
288
350
|
|
|
289
351
|
function spawnWslTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
290
352
|
try {
|
|
291
353
|
const cmd = `wsl ${buildTmuxCommand(cwd, false, options)}`;
|
|
292
|
-
|
|
354
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
355
|
+
execSync(cmd, { stdio: "ignore", env });
|
|
293
356
|
return { success: true, dashboardSpawned: true, message: "Pi session spawned via WSL tmux" };
|
|
294
357
|
} catch (err: any) {
|
|
295
|
-
return { success: false, message: `Failed to spawn via WSL tmux: ${err.message}` };
|
|
358
|
+
return { success: false, code: "TMUX_MISSING", message: `Failed to spawn via WSL tmux (wsl-tmux mechanism): ${err.message}` };
|
|
296
359
|
}
|
|
297
360
|
}
|
|
298
361
|
|
|
299
362
|
async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
|
|
300
363
|
const wt = resolver.which("wt");
|
|
301
364
|
if (!wt) {
|
|
302
|
-
return { success: false, message: "Windows Terminal (wt.exe) not found" };
|
|
365
|
+
return { success: false, code: "WT_MISSING", message: "Windows Terminal (wt.exe) not found" };
|
|
303
366
|
}
|
|
304
367
|
const piCmd = resolvePiCommand();
|
|
305
368
|
if (!piCmd) {
|
|
306
|
-
return { success: false, message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
369
|
+
return { success: false, code: "PI_NOT_FOUND", message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
307
370
|
}
|
|
308
371
|
|
|
309
372
|
const piArgv = [...piCmd, ...buildInteractivePiArgs(options)];
|
|
@@ -313,11 +376,11 @@ async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResu
|
|
|
313
376
|
cmd: wt,
|
|
314
377
|
args,
|
|
315
378
|
cwd,
|
|
316
|
-
env: buildSpawnEnv(),
|
|
379
|
+
env: buildSpawnEnv(process.env, { spawnToken: options?.spawnToken }),
|
|
317
380
|
});
|
|
318
381
|
|
|
319
382
|
if (!r.ok) {
|
|
320
|
-
return { success: false, message: `Failed to launch Windows Terminal: ${r.error}` };
|
|
383
|
+
return { success: false, code: "SPAWN_ERRNO", message: `Failed to launch Windows Terminal: ${r.error}` };
|
|
321
384
|
}
|
|
322
385
|
|
|
323
386
|
return {
|
|
@@ -331,10 +394,10 @@ async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResu
|
|
|
331
394
|
|
|
332
395
|
async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
|
|
333
396
|
const args = buildHeadlessArgs(options);
|
|
334
|
-
const env = buildSpawnEnv();
|
|
397
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
335
398
|
const piCmd = resolvePiCommand();
|
|
336
399
|
if (!piCmd) {
|
|
337
|
-
return { success: false, message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
400
|
+
return { success: false, code: "PI_NOT_FOUND", message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
338
401
|
}
|
|
339
402
|
const [bin, ...prefixArgs] = piCmd;
|
|
340
403
|
|
|
@@ -356,7 +419,7 @@ async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<Spa
|
|
|
356
419
|
env,
|
|
357
420
|
});
|
|
358
421
|
if (!r.ok) {
|
|
359
|
-
return { success: false, message: `Failed to spawn headless (Unix): ${r.error}` };
|
|
422
|
+
return { success: false, code: "SPAWN_ERRNO", message: `Failed to spawn headless (Unix): ${r.error}` };
|
|
360
423
|
}
|
|
361
424
|
return {
|
|
362
425
|
success: true,
|
|
@@ -397,6 +460,7 @@ async function spawnHeadlessDetached(
|
|
|
397
460
|
if (bin.toLowerCase().endsWith(".cmd") || bin.toLowerCase().endsWith(".bat")) {
|
|
398
461
|
return {
|
|
399
462
|
success: false,
|
|
463
|
+
code: "WIN_PI_CMD_ONLY",
|
|
400
464
|
message:
|
|
401
465
|
"Windows pi spawn requires node.exe + cli.js (managed install). " +
|
|
402
466
|
"Found only pi.cmd on PATH. Run the dashboard setup wizard or " +
|
|
@@ -465,6 +529,8 @@ async function spawnHeadlessDetached(
|
|
|
465
529
|
if (!r.ok || !r.process || !r.pid) {
|
|
466
530
|
return {
|
|
467
531
|
success: false,
|
|
532
|
+
code: "SPAWN_ERRNO",
|
|
533
|
+
logPath: logFd !== undefined ? logPath : undefined,
|
|
468
534
|
message: `Failed to spawn pi: ${r.error ?? "unknown error"}. Command: ${cmdForLog}`,
|
|
469
535
|
};
|
|
470
536
|
}
|
|
@@ -473,8 +539,16 @@ async function spawnHeadlessDetached(
|
|
|
473
539
|
// but still catch immediate crashes (missing modules, config errors).
|
|
474
540
|
const gate = await waitForNoCrash({ child: r.process, windowMs: 300 });
|
|
475
541
|
if (!gate.ok) {
|
|
542
|
+
// Read last 4 KB of stderr log for diagnostic forwarding. See change: spawn-failure-diagnostics.
|
|
543
|
+
let stderrTail: string | undefined;
|
|
544
|
+
if (logFd !== undefined) {
|
|
545
|
+
stderrTail = readLogTail(logPath);
|
|
546
|
+
}
|
|
476
547
|
return {
|
|
477
548
|
success: false,
|
|
549
|
+
code: "PI_CRASHED",
|
|
550
|
+
logPath: logFd !== undefined ? logPath : undefined,
|
|
551
|
+
stderr: stderrTail,
|
|
478
552
|
message:
|
|
479
553
|
`Pi process exited immediately (code ${gate.exitCode}). ` +
|
|
480
554
|
`See ${logPath} for details.\nCommand: ${cmdForLog}`,
|
|
@@ -487,5 +561,25 @@ async function spawnHeadlessDetached(
|
|
|
487
561
|
message: `Pi session spawned headless (pid ${r.pid})`,
|
|
488
562
|
pid: r.pid,
|
|
489
563
|
process: r.process,
|
|
564
|
+
logPath: logFd !== undefined ? logPath : undefined,
|
|
490
565
|
};
|
|
491
566
|
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Read last `maxBytes` bytes of `filePath`, stripping leading UTF-8 continuation bytes.
|
|
570
|
+
* Returns `undefined` on any error or if file is empty.
|
|
571
|
+
* See change: spawn-failure-diagnostics.
|
|
572
|
+
*/
|
|
573
|
+
function readLogTail(filePath: string, maxBytes = 4096): string | undefined {
|
|
574
|
+
try {
|
|
575
|
+
const buf = readFileSync(filePath);
|
|
576
|
+
if (!buf.length) return undefined;
|
|
577
|
+
const slice = buf.length <= maxBytes ? buf : buf.slice(buf.length - maxBytes);
|
|
578
|
+
// Strip leading UTF-8 continuation bytes (0x80..0xBF)
|
|
579
|
+
let start = 0;
|
|
580
|
+
while (start < slice.length && (slice[start]! & 0xC0) === 0x80) start++;
|
|
581
|
+
return slice.slice(start).toString("utf-8");
|
|
582
|
+
} catch {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
@@ -29,6 +29,8 @@ export function generateState(): string {
|
|
|
29
29
|
export interface AuthCodeHandler {
|
|
30
30
|
flowType: "auth_code";
|
|
31
31
|
providerId: string;
|
|
32
|
+
/** Human-readable name surfaced to the UI. */
|
|
33
|
+
displayName: string;
|
|
32
34
|
/** Port registered with the OAuth provider for the redirect URI */
|
|
33
35
|
callbackPort: number;
|
|
34
36
|
/** Path registered with the OAuth provider for the redirect URI */
|
|
@@ -40,6 +42,8 @@ export interface AuthCodeHandler {
|
|
|
40
42
|
export interface DeviceCodeHandler {
|
|
41
43
|
flowType: "device_code";
|
|
42
44
|
providerId: string;
|
|
45
|
+
/** Human-readable name surfaced to the UI. */
|
|
46
|
+
displayName: string;
|
|
43
47
|
requestDeviceCode(enterpriseDomain?: string): Promise<DeviceCodeData>;
|
|
44
48
|
pollForToken(deviceCode: string, interval: number, expiresIn: number, extra?: Record<string, unknown>): Promise<OAuthCredential>;
|
|
45
49
|
}
|
|
@@ -93,6 +97,7 @@ const ANTHROPIC_SCOPES = "org:create_api_key user:profile user:inference user:se
|
|
|
93
97
|
export const anthropicHandler: AuthCodeHandler = {
|
|
94
98
|
flowType: "auth_code",
|
|
95
99
|
providerId: "anthropic",
|
|
100
|
+
displayName: "Anthropic (Claude Pro/Max)",
|
|
96
101
|
callbackPort: 53692,
|
|
97
102
|
callbackPath: "/callback",
|
|
98
103
|
|
|
@@ -146,6 +151,7 @@ const CODEX_SCOPE = "openid profile email offline_access";
|
|
|
146
151
|
export const codexHandler: AuthCodeHandler = {
|
|
147
152
|
flowType: "auth_code",
|
|
148
153
|
providerId: "openai-codex",
|
|
154
|
+
displayName: "ChatGPT Plus/Pro (Codex)",
|
|
149
155
|
callbackPort: 1455,
|
|
150
156
|
callbackPath: "/auth/callback",
|
|
151
157
|
|
|
@@ -206,6 +212,7 @@ function githubUrls(domain: string) {
|
|
|
206
212
|
export const githubCopilotHandler: DeviceCodeHandler = {
|
|
207
213
|
flowType: "device_code",
|
|
208
214
|
providerId: "github-copilot",
|
|
215
|
+
displayName: "GitHub Copilot",
|
|
209
216
|
|
|
210
217
|
async requestDeviceCode(enterpriseDomain) {
|
|
211
218
|
const domain = enterpriseDomain || "github.com";
|
|
@@ -336,6 +343,7 @@ const GEMINI_SCOPES = [
|
|
|
336
343
|
export const geminiCliHandler: AuthCodeHandler = {
|
|
337
344
|
flowType: "auth_code",
|
|
338
345
|
providerId: "google-gemini-cli",
|
|
346
|
+
displayName: "Google Gemini CLI",
|
|
339
347
|
callbackPort: 8085,
|
|
340
348
|
callbackPath: "/oauth2callback",
|
|
341
349
|
|
|
@@ -384,6 +392,7 @@ const AG_SCOPES = [
|
|
|
384
392
|
export const antigravityHandler: AuthCodeHandler = {
|
|
385
393
|
flowType: "auth_code",
|
|
386
394
|
providerId: "google-antigravity",
|
|
395
|
+
displayName: "Antigravity",
|
|
387
396
|
callbackPort: 51121,
|
|
388
397
|
callbackPath: "/oauth-callback",
|
|
389
398
|
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read/write ~/.pi/agent/auth.json for pi provider credentials.
|
|
3
3
|
* Uses lockfile + atomic write to avoid race conditions with running pi sessions.
|
|
4
|
+
*
|
|
5
|
+
* The OAuth provider list derives from the local handler registry
|
|
6
|
+
* (`getAllHandlers()` in provider-auth-handlers.ts). The API-key list
|
|
7
|
+
* derives from the bridge-pushed catalogue (provider-catalogue-cache.ts).
|
|
8
|
+
* See change: replace-hardcoded-provider-lists.
|
|
4
9
|
*/
|
|
5
10
|
import fs from "node:fs";
|
|
6
11
|
import path from "node:path";
|
|
7
12
|
import os from "node:os";
|
|
8
13
|
import type { ProviderAuthStatus } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
14
|
+
import type { ProviderInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
15
|
+
import { getAllHandlers, type ProviderHandler } from "./provider-auth-handlers.js";
|
|
16
|
+
import { getLatestCatalogue } from "./provider-catalogue-cache.js";
|
|
9
17
|
|
|
10
18
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
11
19
|
|
|
@@ -21,33 +29,12 @@ export type OAuthCredential = { type: "oauth"; refresh: string; access: string;
|
|
|
21
29
|
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
22
30
|
export type AuthData = Record<string, AuthCredential>;
|
|
23
31
|
|
|
24
|
-
// ── OAuth provider metadata (for status display) ────────────────────────────
|
|
25
|
-
|
|
26
32
|
interface OAuthProviderMeta {
|
|
27
33
|
id: string;
|
|
28
34
|
name: string;
|
|
29
35
|
flowType: "auth_code" | "device_code";
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
const OAUTH_PROVIDERS: OAuthProviderMeta[] = [
|
|
33
|
-
{ id: "anthropic", name: "Anthropic (Claude Pro/Max)", flowType: "auth_code" },
|
|
34
|
-
{ id: "openai-codex", name: "ChatGPT Plus/Pro (Codex)", flowType: "auth_code" },
|
|
35
|
-
{ id: "github-copilot", name: "GitHub Copilot", flowType: "device_code" },
|
|
36
|
-
{ id: "google-gemini-cli", name: "Google Gemini CLI", flowType: "auth_code" },
|
|
37
|
-
{ id: "google-antigravity", name: "Antigravity", flowType: "auth_code" },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const API_KEY_PROVIDERS = [
|
|
41
|
-
{ id: "anthropic-api", authJsonKey: "anthropic", name: "Anthropic (API Key)" },
|
|
42
|
-
{ id: "openai", authJsonKey: "openai", name: "OpenAI" },
|
|
43
|
-
{ id: "google", authJsonKey: "google", name: "Google Gemini (API Key)" },
|
|
44
|
-
{ id: "mistral", authJsonKey: "mistral", name: "Mistral" },
|
|
45
|
-
{ id: "groq", authJsonKey: "groq", name: "Groq" },
|
|
46
|
-
{ id: "xai", authJsonKey: "xai", name: "xAI" },
|
|
47
|
-
{ id: "openrouter", authJsonKey: "openrouter", name: "OpenRouter" },
|
|
48
|
-
{ id: "zai", authJsonKey: "zai", name: "Z.ai" },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
38
|
// ── Lock helpers ─────────────────────────────────────────────────────────────
|
|
52
39
|
|
|
53
40
|
function acquireLock(): void {
|
|
@@ -114,7 +101,7 @@ function writeAuthJson(data: AuthData): void {
|
|
|
114
101
|
fs.renameSync(tmp, AUTH_PATH);
|
|
115
102
|
}
|
|
116
103
|
|
|
117
|
-
// ── Public API
|
|
104
|
+
// ── Public API: write/remove ─────────────────────────────────────────────────
|
|
118
105
|
|
|
119
106
|
export function writeCredential(provider: string, credential: AuthCredential): void {
|
|
120
107
|
acquireLock();
|
|
@@ -138,63 +125,108 @@ export function removeCredential(provider: string): void {
|
|
|
138
125
|
}
|
|
139
126
|
}
|
|
140
127
|
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
// ── Pure status builder (testable) ───────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Pure derivation of `ProviderAuthStatus[]` from auth.json data, the
|
|
132
|
+
* bridge-pushed provider catalogue, and the local OAuth handler set.
|
|
133
|
+
* No I/O. See change: replace-hardcoded-provider-lists.
|
|
134
|
+
*/
|
|
135
|
+
export function _buildAuthStatus(
|
|
136
|
+
catalogue: ProviderInfo[],
|
|
137
|
+
authData: AuthData,
|
|
138
|
+
oauthHandlers: ProviderHandler[],
|
|
139
|
+
): ProviderAuthStatus[] {
|
|
143
140
|
const statuses: ProviderAuthStatus[] = [];
|
|
141
|
+
const oauthIds = new Set(oauthHandlers.map((h) => h.providerId));
|
|
144
142
|
|
|
145
|
-
// OAuth
|
|
146
|
-
for (const
|
|
147
|
-
const cred =
|
|
143
|
+
// OAuth rows from local handler registry.
|
|
144
|
+
for (const h of oauthHandlers) {
|
|
145
|
+
const cred = authData[h.providerId];
|
|
148
146
|
if (cred && cred.type === "oauth") {
|
|
149
147
|
statuses.push({
|
|
150
|
-
id:
|
|
151
|
-
name:
|
|
152
|
-
flowType:
|
|
148
|
+
id: h.providerId,
|
|
149
|
+
name: h.displayName,
|
|
150
|
+
flowType: h.flowType,
|
|
153
151
|
authenticated: true,
|
|
154
152
|
expires: (cred as OAuthCredential).expires,
|
|
155
153
|
});
|
|
156
154
|
} else {
|
|
157
155
|
statuses.push({
|
|
158
|
-
id:
|
|
159
|
-
name:
|
|
160
|
-
flowType:
|
|
156
|
+
id: h.providerId,
|
|
157
|
+
name: h.displayName,
|
|
158
|
+
flowType: h.flowType,
|
|
161
159
|
authenticated: false,
|
|
162
160
|
});
|
|
163
161
|
}
|
|
164
162
|
}
|
|
165
163
|
|
|
166
|
-
// API
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
// API-key rows from bridge-pushed catalogue.
|
|
165
|
+
// Skip custom providers (registered via pi.registerProvider() from
|
|
166
|
+
// ~/.pi/agent/providers.json) — those are managed by the dedicated
|
|
167
|
+
// LLM Providers settings section. OAuth rows for custom providers
|
|
168
|
+
// were already emitted above when the OAuth handler registry has
|
|
169
|
+
// a matching id.
|
|
170
|
+
for (const entry of catalogue) {
|
|
171
|
+
if (entry.custom) continue;
|
|
172
|
+
const hasOAuthCollision = oauthIds.has(entry.id);
|
|
173
|
+
const uiId = hasOAuthCollision ? `${entry.id}-api` : entry.id;
|
|
174
|
+
const displayName = hasOAuthCollision
|
|
175
|
+
? `${entry.displayName} (API Key)`
|
|
176
|
+
: entry.displayName;
|
|
177
|
+
const authJsonKey = entry.id;
|
|
178
|
+
const cred = authData[authJsonKey];
|
|
179
|
+
const hasStoredKey = !!(cred && cred.type === "api_key" && (cred as ApiKeyCredential).key);
|
|
180
|
+
|
|
181
|
+
const row: ProviderAuthStatus = {
|
|
182
|
+
id: uiId,
|
|
183
|
+
name: displayName,
|
|
175
184
|
flowType: "api_key",
|
|
176
|
-
authenticated:
|
|
185
|
+
authenticated: hasStoredKey || !!entry.ambient,
|
|
177
186
|
};
|
|
178
|
-
if (
|
|
187
|
+
if (hasStoredKey) {
|
|
179
188
|
const key = (cred as ApiKeyCredential).key;
|
|
180
|
-
|
|
189
|
+
row.maskedKey = key.length >= 12 ? `${key.slice(0, 5)}...${key.slice(-3)}` : "****";
|
|
190
|
+
} else if (entry.ambient) {
|
|
191
|
+
row.maskedKey = "(ambient)";
|
|
181
192
|
}
|
|
182
|
-
|
|
193
|
+
if (entry.envVar) row.envVar = entry.envVar;
|
|
194
|
+
if (entry.ambient) row.ambient = true;
|
|
195
|
+
statuses.push(row);
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
return statuses;
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
// ── Public API: status / OAuth meta / id resolution ─────────────────────────
|
|
202
|
+
|
|
203
|
+
export function getAuthStatus(): ProviderAuthStatus[] {
|
|
204
|
+
return _buildAuthStatus(getLatestCatalogue(), readAuthJson(), getAllHandlers());
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
export function getOAuthProvidersMeta(): OAuthProviderMeta[] {
|
|
189
|
-
return
|
|
208
|
+
return getAllHandlers().map((h) => ({
|
|
209
|
+
id: h.providerId,
|
|
210
|
+
name: h.displayName,
|
|
211
|
+
flowType: h.flowType,
|
|
212
|
+
}));
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
/**
|
|
193
216
|
* Resolve a UI provider ID to the auth.json key.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
217
|
+
*
|
|
218
|
+
* The catalogue encodes API-key rows with `<id>-api` suffix when an
|
|
219
|
+
* OAuth handler exists for the same id. This unwraps the suffix back
|
|
220
|
+
* to the underlying auth.json key. OAuth ids pass through unchanged
|
|
221
|
+
* (their UI id == their auth.json key). Unknown ids pass through too,
|
|
222
|
+
* matching the previous behavior.
|
|
196
223
|
*/
|
|
197
224
|
export function resolveAuthJsonKey(providerId: string): string {
|
|
198
|
-
const
|
|
199
|
-
|
|
225
|
+
const oauthIds = new Set(getAllHandlers().map((h) => h.providerId));
|
|
226
|
+
// <id>-api suffix → strip suffix iff the bare id is an OAuth handler.
|
|
227
|
+
if (providerId.endsWith("-api")) {
|
|
228
|
+
const bare = providerId.slice(0, -"-api".length);
|
|
229
|
+
if (oauthIds.has(bare)) return bare;
|
|
230
|
+
}
|
|
231
|
+
return providerId;
|
|
200
232
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache of the most-recently-pushed provider catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Each pi process pushes a `providers_list` over WS, derived from its
|
|
5
|
+
* `ModelRegistry`. The server caches the latest snapshot. `GET
|
|
6
|
+
* /api/provider-auth/status` reads `getLatestCatalogue()`.
|
|
7
|
+
*
|
|
8
|
+
* The catalogue is a property of the machine's auth + provider config,
|
|
9
|
+
* not of individual sessions: every bridge in the same process tree
|
|
10
|
+
* derives an identical catalogue from `~/.pi/agent/auth.json` +
|
|
11
|
+
* `~/.pi/agent/providers.json` + pi-ai's MODELS table. We therefore
|
|
12
|
+
* keep ONE global snapshot — the last push wins. A previous version
|
|
13
|
+
* kept a per-session Map plus a `changed` deep-equality gate to avoid
|
|
14
|
+
* spurious `models_refreshed` broadcasts; that broadcast is gone
|
|
15
|
+
* (see change: simplify-model-selection-channels), so the gate is
|
|
16
|
+
* unnecessary and the per-session split was redundant.
|
|
17
|
+
*
|
|
18
|
+
* See changes: replace-hardcoded-provider-lists,
|
|
19
|
+
* fix-providers-list-spurious-models-refreshed,
|
|
20
|
+
* simplify-model-selection-channels.
|
|
21
|
+
*/
|
|
22
|
+
import type { ProviderInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
23
|
+
|
|
24
|
+
let latest: ProviderInfo[] | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Replace the cached catalogue. Called from event-wiring.ts on every
|
|
28
|
+
* `providers_list` arrival. No-op semantically beyond the assignment;
|
|
29
|
+
* no signal to callers because no caller needs one.
|
|
30
|
+
*/
|
|
31
|
+
export function setCatalogueForSession(_sessionId: string, providers: ProviderInfo[]): void {
|
|
32
|
+
latest = providers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Most recent catalogue across any session. Returns [] when no bridge
|
|
37
|
+
* has pushed yet — callers should treat that as "waiting for pi" and
|
|
38
|
+
* may issue a `request_providers` nudge to fetch one synchronously.
|
|
39
|
+
*/
|
|
40
|
+
export function getLatestCatalogue(): ProviderInfo[] {
|
|
41
|
+
return latest ?? [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Test-only: reset all cached state. */
|
|
45
|
+
export function _resetForTests(): void {
|
|
46
|
+
latest = null;
|
|
47
|
+
}
|