@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/doctor — server-side diagnostic endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Calls `runSharedChecks(...)` with server-appropriate `deps`, post-stamps
|
|
5
|
+
* `section` + `suggestion`, returns `{ checks, summary, generatedAt }`.
|
|
6
|
+
*
|
|
7
|
+
* Auth-gated identically to `/api/config`; unauthenticated requests yield
|
|
8
|
+
* the same status code as `/api/config` (the auth plugin's onRequest hook
|
|
9
|
+
* intercepts before this handler runs).
|
|
10
|
+
*
|
|
11
|
+
* On a thrown error from `runSharedChecks`, returns a 200 with a single
|
|
12
|
+
* fallback `error` row rather than a 500 — the web client always has
|
|
13
|
+
* something to render. See change: doctor-rich-output (tasks 4.1–4.5).
|
|
14
|
+
*/
|
|
15
|
+
import type { FastifyInstance } from "fastify";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import {
|
|
20
|
+
runSharedChecks,
|
|
21
|
+
stampSectionsAndSuggestions,
|
|
22
|
+
safeExec,
|
|
23
|
+
type DoctorCheck,
|
|
24
|
+
type DoctorReport,
|
|
25
|
+
type SharedChecksDeps,
|
|
26
|
+
} from "@blackbelt-technology/pi-dashboard-shared/doctor-core.js";
|
|
27
|
+
|
|
28
|
+
function getManagedDir(): string {
|
|
29
|
+
return process.env.MANAGED_DIR || path.join(os.homedir(), ".pi-dashboard");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectSystemNode(): { found: boolean; path?: string } {
|
|
33
|
+
const cmd = process.platform === "win32" ? "where node" : "which node"; // platform-branch-ok: localised PATH-lookup primitive
|
|
34
|
+
const r = safeExec(cmd, { timeoutMs: 3000 });
|
|
35
|
+
if (!r.ok) return { found: false };
|
|
36
|
+
const first = r.stdout.trim().split("\n")[0];
|
|
37
|
+
return first ? { found: true, path: first } : { found: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectOnPath(name: string): { found: boolean; path?: string; source?: string } {
|
|
41
|
+
const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`; // platform-branch-ok: localised PATH-lookup primitive
|
|
42
|
+
const r = safeExec(cmd, { timeoutMs: 3000 });
|
|
43
|
+
if (!r.ok) return { found: false };
|
|
44
|
+
const first = r.stdout.trim().split("\n")[0];
|
|
45
|
+
return first ? { found: true, path: first, source: "system" } : { found: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isApiKeyConfigured(): boolean {
|
|
49
|
+
try {
|
|
50
|
+
const settings = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
51
|
+
if (!existsSync(settings)) return false;
|
|
52
|
+
const data = JSON.parse(readFileSync(settings, "utf-8"));
|
|
53
|
+
if (data?.anthropicApiKey || data?.openaiApiKey || data?.apiKey) return true;
|
|
54
|
+
if (data?.providers && typeof data.providers === "object") {
|
|
55
|
+
for (const v of Object.values(data.providers as Record<string, unknown>)) {
|
|
56
|
+
if (v && typeof v === "object" && "apiKey" in (v as object)) return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildDefaultDeps(): SharedChecksDeps {
|
|
66
|
+
return {
|
|
67
|
+
managedDir: getManagedDir(),
|
|
68
|
+
detectSystemNode,
|
|
69
|
+
detectPi: () => detectOnPath("pi"),
|
|
70
|
+
detectOpenSpec: () => detectOnPath("openspec"),
|
|
71
|
+
isApiKeyConfigured,
|
|
72
|
+
probeServer: async () => {
|
|
73
|
+
const r = safeExec("curl -sf http://localhost:8000/api/health", { timeoutMs: 3000 });
|
|
74
|
+
if (!r.ok || !r.stdout.trim()) return { running: false };
|
|
75
|
+
try {
|
|
76
|
+
const h = JSON.parse(r.stdout);
|
|
77
|
+
return {
|
|
78
|
+
running: true,
|
|
79
|
+
version: typeof h.version === "string" ? h.version : undefined,
|
|
80
|
+
mode: typeof h.mode === "string" ? h.mode : undefined,
|
|
81
|
+
starter: typeof h.starter === "string" ? h.starter : null,
|
|
82
|
+
installable:
|
|
83
|
+
h.installable && typeof h.installable === "object"
|
|
84
|
+
? {
|
|
85
|
+
total: h.installable.total ?? 0,
|
|
86
|
+
installed: h.installable.installed ?? 0,
|
|
87
|
+
failed: Array.isArray(h.installable.failed) ? h.installable.failed : [],
|
|
88
|
+
}
|
|
89
|
+
: null,
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return { running: true };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function summarize(checks: DoctorCheck[]): DoctorReport["summary"] {
|
|
99
|
+
return {
|
|
100
|
+
ok: checks.filter((c) => c.status === "ok").length,
|
|
101
|
+
warnings: checks.filter((c) => c.status === "warning").length,
|
|
102
|
+
errors: checks.filter((c) => c.status === "error").length,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DoctorRouteDeps {
|
|
107
|
+
/** Override for tests — substitutes a different `runSharedChecks` deps shape (or throws to exercise fault tolerance). */
|
|
108
|
+
buildDeps?: () => SharedChecksDeps;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function registerDoctorRoutes(fastify: FastifyInstance, deps: DoctorRouteDeps = {}): void {
|
|
112
|
+
fastify.get("/api/doctor", async (_request, _reply): Promise<DoctorReport> => {
|
|
113
|
+
try {
|
|
114
|
+
const sharedDeps = deps.buildDeps ? deps.buildDeps() : buildDefaultDeps();
|
|
115
|
+
const checks = await runSharedChecks(sharedDeps);
|
|
116
|
+
stampSectionsAndSuggestions(checks);
|
|
117
|
+
return {
|
|
118
|
+
checks,
|
|
119
|
+
summary: summarize(checks),
|
|
120
|
+
generatedAt: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
124
|
+
const fallback: DoctorCheck = {
|
|
125
|
+
name: "Doctor failed to produce a report",
|
|
126
|
+
section: "diagnostics",
|
|
127
|
+
status: "error",
|
|
128
|
+
message: "Unexpected internal failure",
|
|
129
|
+
detail: `${e.message}\n${(e.stack || "").split("\n").slice(0, 4).join("\n")}`,
|
|
130
|
+
suggestion:
|
|
131
|
+
"Check `~/.pi-dashboard/doctor.log` on the server, then file an issue with the captured error.",
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
checks: [fallback],
|
|
135
|
+
summary: { ok: 0, warnings: 0, errors: 1 },
|
|
136
|
+
generatedAt: Date.now(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST route for `GET /api/pi-core/changelog`.
|
|
3
|
+
*
|
|
4
|
+
* Returns the parsed `CHANGELOG.md` for a core package filtered to
|
|
5
|
+
* a `(from, to]` half-open version range, plus a derived `hasBreaking`
|
|
6
|
+
* flag and a public GitHub URL for the full changelog.
|
|
7
|
+
*
|
|
8
|
+
* Gated identically to other `/api/pi-core/*` routes via the shared
|
|
9
|
+
* bootstrap-status gate.
|
|
10
|
+
*
|
|
11
|
+
* See change: pi-update-whats-new-panel.
|
|
12
|
+
*/
|
|
13
|
+
import type { FastifyInstance } from "fastify";
|
|
14
|
+
import type {
|
|
15
|
+
ChangelogResponse,
|
|
16
|
+
} from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
|
|
17
|
+
import { CORE_PACKAGE_NAMES } from "../pi-core-checker.js";
|
|
18
|
+
import { parseVersion, compareVersions } from "../pi-version-skew.js";
|
|
19
|
+
import {
|
|
20
|
+
findChangelogPath,
|
|
21
|
+
readPackageJson,
|
|
22
|
+
deriveChangelogUrl,
|
|
23
|
+
} from "../changelog-fs.js";
|
|
24
|
+
import {
|
|
25
|
+
parseChangelog,
|
|
26
|
+
readAndParseChangelog,
|
|
27
|
+
getCachedRemoteChangelog,
|
|
28
|
+
setRemoteChangelog,
|
|
29
|
+
refreshRemoteChangelogTtl,
|
|
30
|
+
} from "../changelog-parser.js";
|
|
31
|
+
import {
|
|
32
|
+
deriveChangelogRawUrl,
|
|
33
|
+
fetchRemoteChangelog,
|
|
34
|
+
} from "../changelog-remote.js";
|
|
35
|
+
import type { ChangelogRelease } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
|
|
36
|
+
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
37
|
+
|
|
38
|
+
export interface PiChangelogRouteDeps {
|
|
39
|
+
/** When provided, route returns 503 unless bootstrap status is "ready". */
|
|
40
|
+
bootstrapState?: BootstrapStateStore;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface QueryShape {
|
|
44
|
+
pkg?: string;
|
|
45
|
+
from?: string;
|
|
46
|
+
to?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerPiChangelogRoutes(
|
|
50
|
+
fastify: FastifyInstance,
|
|
51
|
+
deps: PiChangelogRouteDeps,
|
|
52
|
+
): void {
|
|
53
|
+
const { bootstrapState } = deps;
|
|
54
|
+
|
|
55
|
+
const bootstrapGate = async (
|
|
56
|
+
_req: unknown,
|
|
57
|
+
reply: { code: (n: number) => { send: (body: unknown) => unknown } },
|
|
58
|
+
): Promise<unknown> => {
|
|
59
|
+
if (!bootstrapState) return undefined;
|
|
60
|
+
const status = bootstrapState.get().status;
|
|
61
|
+
if (status === "ready") return undefined;
|
|
62
|
+
return reply.code(503).send({
|
|
63
|
+
success: false,
|
|
64
|
+
error: `pi not yet installed (bootstrap status: ${status})`,
|
|
65
|
+
bootstrap: status,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
fastify.get<{ Querystring: QueryShape }>(
|
|
70
|
+
"/api/pi-core/changelog",
|
|
71
|
+
{ preHandler: bootstrapGate as any },
|
|
72
|
+
async (request, reply) => {
|
|
73
|
+
const pkg = (request.query.pkg ?? "").trim();
|
|
74
|
+
const from = (request.query.from ?? "").trim();
|
|
75
|
+
const to = (request.query.to ?? "").trim();
|
|
76
|
+
|
|
77
|
+
// Validate `pkg` against the core whitelist BEFORE touching
|
|
78
|
+
// the filesystem — prevents arbitrary path reads via crafted
|
|
79
|
+
// input.
|
|
80
|
+
if (!pkg || !CORE_PACKAGE_NAMES.includes(pkg)) {
|
|
81
|
+
return reply.code(400).send({
|
|
82
|
+
success: false,
|
|
83
|
+
error: `pkg must be one of: ${CORE_PACKAGE_NAMES.join(", ")}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate version range using the existing parseVersion helper
|
|
88
|
+
// from pi-version-skew. Both endpoints are required.
|
|
89
|
+
if (!from || !to) {
|
|
90
|
+
return reply.code(400).send({
|
|
91
|
+
success: false,
|
|
92
|
+
error: "from and to query params are required",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!parseVersion(from) || !parseVersion(to)) {
|
|
96
|
+
return reply.code(400).send({
|
|
97
|
+
success: false,
|
|
98
|
+
error: "from and to must be parseable semver versions",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const located = findChangelogPath(pkg);
|
|
103
|
+
|
|
104
|
+
// Spec: package not installed / CHANGELOG missing → 200 with empty body.
|
|
105
|
+
if (!located) {
|
|
106
|
+
const body: ChangelogResponse = {
|
|
107
|
+
pkg,
|
|
108
|
+
from,
|
|
109
|
+
to,
|
|
110
|
+
releases: [],
|
|
111
|
+
hasBreaking: false,
|
|
112
|
+
changelogUrl: null,
|
|
113
|
+
parsedAt: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
return body;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Read package.json once for both URLs (raw for the parser,
|
|
119
|
+
// human for the dialog footer link).
|
|
120
|
+
const pkgJson = readPackageJson(located.packageDir);
|
|
121
|
+
const rawUrl = pkgJson ? deriveChangelogRawUrl(pkgJson.repository) : null;
|
|
122
|
+
const changelogUrl = pkgJson ? deriveChangelogUrl(pkgJson.repository) : null;
|
|
123
|
+
|
|
124
|
+
// Try remote first — the upstream CHANGELOG describes versions
|
|
125
|
+
// newer than the locally-installed tarball. Fall back to local
|
|
126
|
+
// on failure / offline / non-GitHub repo. See change:
|
|
127
|
+
// read-changelog-from-github.
|
|
128
|
+
let allReleases: ChangelogRelease[] | undefined;
|
|
129
|
+
let usedRemote = false;
|
|
130
|
+
|
|
131
|
+
if (rawUrl) {
|
|
132
|
+
const cached = getCachedRemoteChangelog(pkg);
|
|
133
|
+
if (cached && !cached.expired) {
|
|
134
|
+
// Within TTL — reuse cached result, no fetch.
|
|
135
|
+
allReleases = cached.releases;
|
|
136
|
+
usedRemote = true;
|
|
137
|
+
} else {
|
|
138
|
+
const fetchResult = await fetchRemoteChangelog(rawUrl, {
|
|
139
|
+
etag: cached?.etag ?? null,
|
|
140
|
+
});
|
|
141
|
+
if (fetchResult?.status === "ok") {
|
|
142
|
+
const parsed = parseChangelog(fetchResult.text);
|
|
143
|
+
setRemoteChangelog(pkg, parsed, fetchResult.etag);
|
|
144
|
+
allReleases = parsed;
|
|
145
|
+
usedRemote = true;
|
|
146
|
+
} else if (fetchResult?.status === "not-modified" && cached) {
|
|
147
|
+
refreshRemoteChangelogTtl(pkg);
|
|
148
|
+
allReleases = cached.releases;
|
|
149
|
+
usedRemote = true;
|
|
150
|
+
}
|
|
151
|
+
// null result (network error / offline / non-2xx): fall through
|
|
152
|
+
// to local read below.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!usedRemote) {
|
|
157
|
+
try {
|
|
158
|
+
allReleases = readAndParseChangelog(pkg, located.changelogPath);
|
|
159
|
+
} catch (err: any) {
|
|
160
|
+
request.log.warn(
|
|
161
|
+
{ err: err?.message },
|
|
162
|
+
"[pi-changelog-routes] local read/parse failed; returning empty",
|
|
163
|
+
);
|
|
164
|
+
allReleases = [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Filter to (from, to]. Unparseable release versions are
|
|
169
|
+
// dropped — conservative.
|
|
170
|
+
const filtered = (allReleases ?? []).filter((r) => {
|
|
171
|
+
const rv = parseVersion(r.version);
|
|
172
|
+
if (!rv) return false;
|
|
173
|
+
return compareVersions(r.version, from) > 0 &&
|
|
174
|
+
compareVersions(r.version, to) <= 0;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const hasBreaking = filtered.some((r) => r.breaking.length > 0);
|
|
178
|
+
|
|
179
|
+
const body: ChangelogResponse = {
|
|
180
|
+
pkg,
|
|
181
|
+
from,
|
|
182
|
+
to,
|
|
183
|
+
releases: filtered,
|
|
184
|
+
hasBreaking,
|
|
185
|
+
changelogUrl,
|
|
186
|
+
parsedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
return body;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Re-export for tests that need to bypass the cache.
|
|
194
|
+
export { parseChangelog };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* POST /api/pi-core/update { packages?: string[] }
|
|
6
6
|
*
|
|
7
7
|
* Complements /api/packages/* (extension management): this endpoint covers
|
|
8
|
-
* globally-installed pi CLI packages like @
|
|
8
|
+
* globally-installed pi CLI packages like @earendil-works/pi-coding-agent,
|
|
9
9
|
* pi-dashboard itself, pi-model-proxy, etc.
|
|
10
10
|
*/
|
|
11
11
|
import type { FastifyInstance } from "fastify";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveAuthJsonKey,
|
|
19
19
|
type ApiKeyCredential,
|
|
20
20
|
} from "../provider-auth-storage.js";
|
|
21
|
+
import { getLatestCatalogue } from "../provider-catalogue-cache.js";
|
|
21
22
|
import { startCallbackServer } from "../oauth-callback-server.js";
|
|
22
23
|
import type { PiGateway } from "../pi-gateway.js";
|
|
23
24
|
import type { BrowserGateway } from "../browser-gateway.js";
|
|
@@ -87,8 +88,12 @@ export function registerProviderAuthRoutes(
|
|
|
87
88
|
const { piGateway, browserGateway } = deps;
|
|
88
89
|
|
|
89
90
|
function notifyBridges() {
|
|
91
|
+
// Tell every bridge to reload auth.json + refresh its model registry.
|
|
92
|
+
// Each bridge will then push a fresh per-session models_list (and
|
|
93
|
+
// providers_list); browsers pick those up via the existing per-session
|
|
94
|
+
// broadcast and update modelsMap / catalogue cache without needing a
|
|
95
|
+
// global wipe. See change: simplify-model-selection-channels.
|
|
90
96
|
piGateway.broadcast({ type: "credentials_updated" });
|
|
91
|
-
browserGateway.broadcastToAll({ type: "models_refreshed" });
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
// List OAuth providers
|
|
@@ -98,6 +103,14 @@ export function registerProviderAuthRoutes(
|
|
|
98
103
|
|
|
99
104
|
// Full status (OAuth + API key)
|
|
100
105
|
fastify.get("/api/provider-auth/status", async () => {
|
|
106
|
+
// Cold-cache nudge: if no bridge has pushed a catalogue yet, ask
|
|
107
|
+
// every connected pi to send one. Best-effort, doesn't block this
|
|
108
|
+
// response. See change: replace-hardcoded-provider-lists.
|
|
109
|
+
if (getLatestCatalogue().length === 0) {
|
|
110
|
+
for (const sid of piGateway.getConnectedSessionIds()) {
|
|
111
|
+
piGateway.sendToSession(sid, { type: "request_providers", sessionId: sid });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
101
114
|
return getAuthStatus();
|
|
102
115
|
});
|
|
103
116
|
|
|
@@ -98,13 +98,13 @@ export function registerProviderRoutes(fastify: FastifyInstance, deps: { network
|
|
|
98
98
|
mkdirSync(dir, { recursive: true });
|
|
99
99
|
writeFileSync(CONFIG_PATH, JSON.stringify(fileData, null, 2) + "\n", "utf-8");
|
|
100
100
|
|
|
101
|
-
// Broadcast credentials_updated so
|
|
101
|
+
// Broadcast credentials_updated so each bridge re-reads providers.json
|
|
102
|
+
// and pushes a fresh per-session models_list. Browsers receive those
|
|
103
|
+
// pushes via the existing per-session broadcast — no global wipe.
|
|
104
|
+
// See change: simplify-model-selection-channels.
|
|
102
105
|
if (piGateway) {
|
|
103
106
|
piGateway.broadcast({ type: "credentials_updated" });
|
|
104
107
|
}
|
|
105
|
-
if (deps.browserGateway) {
|
|
106
|
-
deps.browserGateway.broadcastToAll({ type: "models_refreshed" });
|
|
107
|
-
}
|
|
108
108
|
|
|
109
109
|
return { success: true };
|
|
110
110
|
},
|
|
@@ -19,8 +19,10 @@ import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.j
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import { localhostGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
|
|
22
|
+
import { readSpawnFailures } from "../spawn-failure-log.js";
|
|
22
23
|
import { getPluginStatusStore } from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
23
24
|
import type { NetworkInterface } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
25
|
+
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
24
26
|
|
|
25
27
|
export function registerSystemRoutes(
|
|
26
28
|
fastify: FastifyInstance,
|
|
@@ -33,9 +35,10 @@ export function registerSystemRoutes(
|
|
|
33
35
|
version?: string;
|
|
34
36
|
directoryService?: DirectoryService;
|
|
35
37
|
piGateway?: PiGateway;
|
|
38
|
+
bootstrapState?: BootstrapStateStore;
|
|
36
39
|
},
|
|
37
40
|
) {
|
|
38
|
-
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway } = deps;
|
|
41
|
+
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway, bootstrapState } = deps;
|
|
39
42
|
|
|
40
43
|
// Quiesce windows for the bridge `server_restarting` broadcast. See change
|
|
41
44
|
// `fix-restart-bridge-auto-start-race`. Bridges that receive this message
|
|
@@ -189,6 +192,8 @@ export function registerSystemRoutes(
|
|
|
189
192
|
return {
|
|
190
193
|
ok: true,
|
|
191
194
|
pid: process.pid,
|
|
195
|
+
starter: bootstrapState?.get().starter ?? "Standalone",
|
|
196
|
+
installable: bootstrapState?.get().installable,
|
|
192
197
|
version: version ?? "unknown",
|
|
193
198
|
uptime: Math.floor((Date.now() - serverStartTime) / 1000),
|
|
194
199
|
mode: config.dev ? "dev" : "production",
|
|
@@ -223,6 +228,26 @@ export function registerSystemRoutes(
|
|
|
223
228
|
},
|
|
224
229
|
);
|
|
225
230
|
|
|
231
|
+
// Re-extract endpoint — Electron-only; 403 for Bridge/Standalone, 202 for Electron.
|
|
232
|
+
// See change: simplify-electron-bootstrap-derived-state (task 6.4).
|
|
233
|
+
fastify.post(
|
|
234
|
+
"/api/electron/reextract",
|
|
235
|
+
{ preHandler: networkGuard },
|
|
236
|
+
async (_request, reply) => {
|
|
237
|
+
const starter = bootstrapState?.get().starter ?? "Standalone";
|
|
238
|
+
if (starter !== "Electron") {
|
|
239
|
+
reply.status(403);
|
|
240
|
+
return {
|
|
241
|
+
error: "reextract_not_allowed",
|
|
242
|
+
message: `Re-extract is only available when the server was started by Electron (current starter: ${starter})`,
|
|
243
|
+
starter,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
reply.status(202);
|
|
247
|
+
return { ok: true, message: "Re-extraction scheduled. Electron will restart the server." };
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
226
251
|
// Restart endpoint — flush state, spawn new server, then exit
|
|
227
252
|
fastify.post<{ Body: { dev?: boolean } }>(
|
|
228
253
|
"/api/restart",
|
|
@@ -269,6 +294,18 @@ export function registerSystemRoutes(
|
|
|
269
294
|
);
|
|
270
295
|
|
|
271
296
|
// Network interfaces for trusted networks UI (localhost-only for security)
|
|
297
|
+
// GET /api/spawn-failures — rolling log of failed spawn attempts. See change: spawn-failure-diagnostics.
|
|
298
|
+
fastify.get<{ Querystring: { limit?: string } }>(
|
|
299
|
+
"/api/spawn-failures",
|
|
300
|
+
async (request) => {
|
|
301
|
+
const rawLimit = request.query.limit;
|
|
302
|
+
const parsed = rawLimit !== undefined ? parseInt(rawLimit, 10) : NaN;
|
|
303
|
+
const limit = Number.isNaN(parsed) ? 50 : parsed;
|
|
304
|
+
const entries = readSpawnFailures(limit);
|
|
305
|
+
return { entries };
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
272
309
|
fastify.get(
|
|
273
310
|
"/api/network-interfaces",
|
|
274
311
|
{ preHandler: localhostGuard },
|