@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4
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 +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic /manifest.json route.
|
|
3
|
+
*
|
|
4
|
+
* Serves a PWA web-app manifest whose `name` and `short_name` vary by
|
|
5
|
+
* server identity, so the same dashboard installed as a PWA from multiple
|
|
6
|
+
* origins (LAN host, tunnel, loopback) shows distinct labels on the
|
|
7
|
+
* launcher. See change: add-dynamic-pwa-manifest-naming.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order for the name `<source>`:
|
|
10
|
+
* 1. `config.dashboardName` (user override; trimmed)
|
|
11
|
+
* 2. Request `Host` header with port stripped (IPv6-safe)
|
|
12
|
+
* 3. `os.hostname()`
|
|
13
|
+
* 4. Literal `"Pi-Dash"`
|
|
14
|
+
*
|
|
15
|
+
* Final manifest fields:
|
|
16
|
+
* name = `Pi-Dash \u00b7 ${source}`
|
|
17
|
+
* short_name = source.slice(0, 12)
|
|
18
|
+
* id = "/"
|
|
19
|
+
*
|
|
20
|
+
* All other fields (icons, theme/background color, display, start_url)
|
|
21
|
+
* are spread from the static `manifest.json` shipped in the client bundle.
|
|
22
|
+
*
|
|
23
|
+
* This route is registered BEFORE `@fastify/static` so explicit Fastify
|
|
24
|
+
* route matching wins over the on-disk static asset.
|
|
25
|
+
*/
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import type { FastifyInstance } from "fastify";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Strip the trailing port from a Host header.
|
|
33
|
+
*
|
|
34
|
+
* Handles:
|
|
35
|
+
* - bare hostnames (`mybox.local`)
|
|
36
|
+
* - host:port (`mybox.local:8000`)
|
|
37
|
+
* - bracketed IPv6 with port (`[::1]:8000`)
|
|
38
|
+
* - bracketed IPv6 without port (`[::1]`)
|
|
39
|
+
* - empty / undefined input → empty string
|
|
40
|
+
*
|
|
41
|
+
* Lower-cases the result so casing differences across requests don't
|
|
42
|
+
* produce ostensibly distinct labels.
|
|
43
|
+
*/
|
|
44
|
+
export function stripPort(host: string | undefined | null): string {
|
|
45
|
+
if (!host) return "";
|
|
46
|
+
const trimmed = host.trim();
|
|
47
|
+
if (!trimmed) return "";
|
|
48
|
+
|
|
49
|
+
// Bracketed IPv6: "[::1]" or "[::1]:8000"
|
|
50
|
+
if (trimmed.startsWith("[")) {
|
|
51
|
+
const close = trimmed.indexOf("]");
|
|
52
|
+
if (close > 0) return trimmed.slice(1, close).toLowerCase();
|
|
53
|
+
// Malformed — drop bracket, return as-is
|
|
54
|
+
return trimmed.slice(1).toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// host:port — last colon, but only if there's exactly one colon
|
|
58
|
+
// (bare IPv6 like "::1" has multiple colons; we leave it untouched
|
|
59
|
+
// since unbracketed IPv6 in a Host header is non-conformant anyway).
|
|
60
|
+
const firstColon = trimmed.indexOf(":");
|
|
61
|
+
const lastColon = trimmed.lastIndexOf(":");
|
|
62
|
+
if (firstColon === lastColon && firstColon > 0) {
|
|
63
|
+
return trimmed.slice(0, firstColon).toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
return trimmed.toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the `<source>` string used to build manifest `name` / `short_name`.
|
|
70
|
+
*
|
|
71
|
+
* Pure — no fs or process access. Pass `hostname` explicitly so tests can
|
|
72
|
+
* control it.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveManifestSource(
|
|
75
|
+
hostHeader: string | undefined | null,
|
|
76
|
+
configDashboardName: string | undefined | null,
|
|
77
|
+
hostname: string,
|
|
78
|
+
): string {
|
|
79
|
+
const override = (configDashboardName ?? "").trim();
|
|
80
|
+
if (override) return override;
|
|
81
|
+
|
|
82
|
+
const fromHost = stripPort(hostHeader);
|
|
83
|
+
if (fromHost) return fromHost;
|
|
84
|
+
|
|
85
|
+
const fromHostname = (hostname ?? "").trim();
|
|
86
|
+
if (fromHostname) return fromHostname;
|
|
87
|
+
|
|
88
|
+
return "Pi-Dash";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the dynamic manifest body. Spreads the static base, overrides
|
|
93
|
+
* `name`/`short_name`/`id`. Pure given a `staticBase`.
|
|
94
|
+
*/
|
|
95
|
+
export function buildManifestBody(
|
|
96
|
+
staticBase: Record<string, unknown>,
|
|
97
|
+
source: string,
|
|
98
|
+
): Record<string, unknown> {
|
|
99
|
+
return {
|
|
100
|
+
...staticBase,
|
|
101
|
+
id: "/",
|
|
102
|
+
name: `Pi-Dash \u00b7 ${source}`,
|
|
103
|
+
short_name: source.slice(0, 12) || "Pi-Dash",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load the static manifest JSON shipped in the client bundle. Returns an
|
|
109
|
+
* empty object if missing or unparseable (route still serves a valid
|
|
110
|
+
* minimal manifest in that case).
|
|
111
|
+
*
|
|
112
|
+
* Synchronous + cached — manifest content is immutable per server build.
|
|
113
|
+
*/
|
|
114
|
+
export function loadStaticManifest(clientDir: string): Record<string, unknown> {
|
|
115
|
+
if (!clientDir) return {};
|
|
116
|
+
try {
|
|
117
|
+
const manifestPath = path.join(clientDir, "manifest.json");
|
|
118
|
+
if (!fs.existsSync(manifestPath)) return {};
|
|
119
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
122
|
+
return parsed as Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ManifestRouteDeps {
|
|
131
|
+
/** Resolved client-dist directory (where the static manifest.json lives). */
|
|
132
|
+
clientDir: string;
|
|
133
|
+
/** Lazy accessor for the *latest* dashboard config — re-read per request
|
|
134
|
+
* so Settings panel changes propagate without a server restart. */
|
|
135
|
+
getDashboardName: () => string | undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register `GET /manifest.json` on the given Fastify instance.
|
|
140
|
+
*
|
|
141
|
+
* MUST be called BEFORE `fastify.register(fastifyStatic, ...)`. Explicit
|
|
142
|
+
* routes win over the static plugin's fallback handler.
|
|
143
|
+
*/
|
|
144
|
+
export function registerManifestRoute(
|
|
145
|
+
fastify: FastifyInstance,
|
|
146
|
+
deps: ManifestRouteDeps,
|
|
147
|
+
): void {
|
|
148
|
+
const staticBase = loadStaticManifest(deps.clientDir);
|
|
149
|
+
const hostname = os.hostname();
|
|
150
|
+
|
|
151
|
+
fastify.get("/manifest.json", async (request, reply) => {
|
|
152
|
+
const source = resolveManifestSource(
|
|
153
|
+
typeof request.headers.host === "string" ? request.headers.host : "",
|
|
154
|
+
deps.getDashboardName(),
|
|
155
|
+
hostname,
|
|
156
|
+
);
|
|
157
|
+
const body = buildManifestBody(staticBase, source);
|
|
158
|
+
reply.header("Content-Type", "application/manifest+json; charset=utf-8");
|
|
159
|
+
reply.header("Cache-Control", "no-cache, must-revalidate");
|
|
160
|
+
return body;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -30,16 +30,9 @@ export function registerOpenSpecRoutes(
|
|
|
30
30
|
networkGuard: NetworkGuard;
|
|
31
31
|
/** Optional — called after a successful toggle to trigger openspec_update. */
|
|
32
32
|
onOpenSpecChanged?: OpenSpecBroadcaster;
|
|
33
|
-
/**
|
|
34
|
-
* Optional bootstrap state. When provided AND status !== "ready", the
|
|
35
|
-
* `/api/pi-resources` endpoint returns an empty result set with a
|
|
36
|
-
* `bootstrap` passthrough so the UI can render "pi not yet installed".
|
|
37
|
-
* See change: unified-bootstrap-install §5.4.
|
|
38
|
-
*/
|
|
39
|
-
bootstrapState?: import("../bootstrap-state.js").BootstrapStateStore;
|
|
40
33
|
},
|
|
41
34
|
) {
|
|
42
|
-
const { sessionManager, preferencesStore, directoryService, networkGuard, onOpenSpecChanged
|
|
35
|
+
const { sessionManager, preferencesStore, directoryService, networkGuard, onOpenSpecChanged } = deps;
|
|
43
36
|
|
|
44
37
|
// OpenSpec archive listing endpoint
|
|
45
38
|
fastify.get<{ Querystring: { cwd?: string } }>(
|
|
@@ -66,23 +59,9 @@ export function registerOpenSpecRoutes(
|
|
|
66
59
|
reply.code(400);
|
|
67
60
|
return { success: false, error: "cwd parameter required" } satisfies ApiResponse;
|
|
68
61
|
}
|
|
69
|
-
// Bootstrap gate:
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
if (bootstrapState) {
|
|
73
|
-
const bs = bootstrapState.get();
|
|
74
|
-
if (bs.status !== "ready") {
|
|
75
|
-
return {
|
|
76
|
-
success: true,
|
|
77
|
-
data: {
|
|
78
|
-
local: { extensions: [], skills: [], prompts: [] },
|
|
79
|
-
global: { extensions: [], skills: [], prompts: [] },
|
|
80
|
-
packages: [],
|
|
81
|
-
bootstrap: bs,
|
|
82
|
-
},
|
|
83
|
-
} satisfies ApiResponse;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
62
|
+
// Bootstrap gate removed under change: eliminate-electron-runtime-install
|
|
63
|
+
// (task 3.5). pi/openspec/tsx ship as regular npm deps; pi-resources
|
|
64
|
+
// endpoint is unconditionally available.
|
|
86
65
|
const forceRefresh = request.query.refresh === "true" || request.query.refresh === "1";
|
|
87
66
|
let data = forceRefresh ? undefined : directoryService.getPiResources(cwd);
|
|
88
67
|
if (!data) {
|
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
* a `(from, to]` half-open version range, plus a derived `hasBreaking`
|
|
6
6
|
* flag and a public GitHub URL for the full changelog.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* See change: pi-update-whats-new-panel.
|
|
8
|
+
* See change: pi-update-whats-new-panel. Bootstrap gate removed under
|
|
9
|
+
* change: eliminate-electron-runtime-install (task 3.5).
|
|
12
10
|
*/
|
|
13
11
|
import type { FastifyInstance } from "fastify";
|
|
14
12
|
import type {
|
|
@@ -33,11 +31,9 @@ import {
|
|
|
33
31
|
fetchRemoteChangelog,
|
|
34
32
|
} from "../changelog-remote.js";
|
|
35
33
|
import type { ChangelogRelease } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
|
|
36
|
-
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
37
34
|
|
|
38
35
|
export interface PiChangelogRouteDeps {
|
|
39
|
-
|
|
40
|
-
bootstrapState?: BootstrapStateStore;
|
|
36
|
+
// Bootstrap gate field removed; route is unconditionally available.
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
interface QueryShape {
|
|
@@ -48,27 +44,12 @@ interface QueryShape {
|
|
|
48
44
|
|
|
49
45
|
export function registerPiChangelogRoutes(
|
|
50
46
|
fastify: FastifyInstance,
|
|
51
|
-
|
|
47
|
+
_deps: PiChangelogRouteDeps,
|
|
52
48
|
): void {
|
|
53
|
-
|
|
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
|
-
};
|
|
49
|
+
|
|
68
50
|
|
|
69
51
|
fastify.get<{ Querystring: QueryShape }>(
|
|
70
52
|
"/api/pi-core/changelog",
|
|
71
|
-
{ preHandler: bootstrapGate as any },
|
|
72
53
|
async (request, reply) => {
|
|
73
54
|
const pkg = (request.query.pkg ?? "").trim();
|
|
74
55
|
const from = (request.query.from ?? "").trim();
|
|
@@ -18,16 +18,10 @@ import type {
|
|
|
18
18
|
import type { PiCoreChecker } from "../pi-core-checker.js";
|
|
19
19
|
import type { PiCoreUpdater } from "../pi-core-updater.js";
|
|
20
20
|
import { PackageOperationBusyError } from "../package-manager-wrapper.js";
|
|
21
|
-
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
22
21
|
|
|
23
22
|
export interface PiCoreRouteDeps {
|
|
24
23
|
piCoreChecker: PiCoreChecker;
|
|
25
24
|
piCoreUpdater: PiCoreUpdater;
|
|
26
|
-
/**
|
|
27
|
-
* When provided, pi-core endpoints return 503 unless bootstrap
|
|
28
|
-
* status is "ready". See change: unified-bootstrap-install §5.5.
|
|
29
|
-
*/
|
|
30
|
-
bootstrapState?: BootstrapStateStore;
|
|
31
25
|
/**
|
|
32
26
|
* Called after the updater finishes a batch (success or per-package failure).
|
|
33
27
|
* The server wires this to broadcast a `pi_core_update_complete` WS message
|
|
@@ -44,28 +38,15 @@ export function registerPiCoreRoutes(
|
|
|
44
38
|
fastify: FastifyInstance,
|
|
45
39
|
deps: PiCoreRouteDeps,
|
|
46
40
|
): void {
|
|
47
|
-
const { piCoreChecker, piCoreUpdater
|
|
41
|
+
const { piCoreChecker, piCoreUpdater } = deps;
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
_req: unknown,
|
|
52
|
-
reply: { code: (n: number) => { send: (body: unknown) => unknown } },
|
|
53
|
-
): Promise<unknown> => {
|
|
54
|
-
if (!bootstrapState) return undefined;
|
|
55
|
-
const status = bootstrapState.get().status;
|
|
56
|
-
if (status === "ready") return undefined;
|
|
57
|
-
return reply.code(503).send({
|
|
58
|
-
success: false,
|
|
59
|
-
error: `pi not yet installed (bootstrap status: ${status})`,
|
|
60
|
-
bootstrap: status,
|
|
61
|
-
});
|
|
62
|
-
};
|
|
43
|
+
// bootstrapGate removed under change: eliminate-electron-runtime-install (task 3.5).
|
|
44
|
+
// pi-core endpoints are unconditionally available; pi ships as regular npm dep.
|
|
63
45
|
|
|
64
46
|
// ── GET /api/pi-core/versions ──────────────────────────────────
|
|
65
47
|
|
|
66
48
|
fastify.get<{ Querystring: { refresh?: string } }>(
|
|
67
49
|
"/api/pi-core/versions",
|
|
68
|
-
{ preHandler: bootstrapGate as any },
|
|
69
50
|
async (request) => {
|
|
70
51
|
const refresh = request.query.refresh === "true";
|
|
71
52
|
try {
|
|
@@ -81,7 +62,6 @@ export function registerPiCoreRoutes(
|
|
|
81
62
|
|
|
82
63
|
fastify.post<{ Body: PiCoreUpdateRequest }>(
|
|
83
64
|
"/api/pi-core/update",
|
|
84
|
-
{ preHandler: bootstrapGate as any },
|
|
85
65
|
async (request, reply) => {
|
|
86
66
|
const requested = request.body?.packages ?? [];
|
|
87
67
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin activation REST routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/plugins — list every discovered plugin (manifest + status)
|
|
5
|
+
* POST /api/plugins/:id/toggle — body { enabled: boolean }; writes
|
|
6
|
+
* config.plugins.<id>.enabled, broadcasts
|
|
7
|
+
* plugin_config_update, returns
|
|
8
|
+
* { restartRequired: true } or 404.
|
|
9
|
+
*
|
|
10
|
+
* See change: add-plugin-activation-ui.
|
|
11
|
+
*/
|
|
12
|
+
import type { FastifyInstance } from "fastify";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import {
|
|
17
|
+
discoverPlugins,
|
|
18
|
+
getPluginStatusStore,
|
|
19
|
+
buildGraph,
|
|
20
|
+
computeToggleImpact,
|
|
21
|
+
transitiveDependents,
|
|
22
|
+
} from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
23
|
+
import type { NetworkGuard } from "./route-deps.js";
|
|
24
|
+
import type { ServerToBrowserMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
25
|
+
|
|
26
|
+
// Resolved lazily so tests that override $HOME after import still work.
|
|
27
|
+
function configPaths() {
|
|
28
|
+
const dir = path.join(os.homedir(), ".pi", "dashboard");
|
|
29
|
+
return { dir, file: path.join(dir, "config.json") };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readRawConfig(): Record<string, unknown> {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(configPaths().file, "utf-8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeRawConfig(merged: Record<string, unknown>): void {
|
|
41
|
+
const { dir, file } = configPaths();
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
const tmp = file + ".tmp." + process.pid;
|
|
44
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + "\n");
|
|
45
|
+
fs.renameSync(tmp, file);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerPluginActivationRoutes(
|
|
49
|
+
fastify: FastifyInstance,
|
|
50
|
+
deps: {
|
|
51
|
+
networkGuard: NetworkGuard;
|
|
52
|
+
broadcast: (msg: ServerToBrowserMessage) => void;
|
|
53
|
+
repoRoot?: string;
|
|
54
|
+
},
|
|
55
|
+
) {
|
|
56
|
+
const { networkGuard, broadcast, repoRoot } = deps;
|
|
57
|
+
|
|
58
|
+
// GET /api/plugins — every discovered plugin's manifest summary + status.
|
|
59
|
+
fastify.get(
|
|
60
|
+
"/api/plugins",
|
|
61
|
+
{ preHandler: networkGuard },
|
|
62
|
+
async (_request, reply) => {
|
|
63
|
+
const plugins = discoverPlugins(repoRoot);
|
|
64
|
+
const store = getPluginStatusStore();
|
|
65
|
+
const all = store.listAll();
|
|
66
|
+
const statusById = new Map(all.map((s) => [s.id, s] as const));
|
|
67
|
+
|
|
68
|
+
// Compute dependents per plugin for the cascade-impact preview UX.
|
|
69
|
+
// See change: add-plugin-activation-ui (Layer 2 — dependency graph).
|
|
70
|
+
const graph = buildGraph(
|
|
71
|
+
plugins.map((p) => ({
|
|
72
|
+
id: p.manifest.id,
|
|
73
|
+
dependsOn: p.manifest.dependsOn ?? [],
|
|
74
|
+
})),
|
|
75
|
+
() => true,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const rows = plugins.map((p) => {
|
|
79
|
+
const m = p.manifest;
|
|
80
|
+
const status = statusById.get(m.id);
|
|
81
|
+
const dependents = Array.from(transitiveDependents(graph, m.id)).sort();
|
|
82
|
+
return {
|
|
83
|
+
id: m.id,
|
|
84
|
+
displayName: m.displayName,
|
|
85
|
+
priority: m.priority ?? 1000,
|
|
86
|
+
hasServer: Boolean(p.serverEntryPath),
|
|
87
|
+
hasBridge: Boolean(p.bridgeEntryPath),
|
|
88
|
+
hasClient: Boolean(p.clientEntryPath),
|
|
89
|
+
claims: m.claims.map((c) => ({
|
|
90
|
+
slot: c.slot,
|
|
91
|
+
component: c.component,
|
|
92
|
+
tab: c.tab,
|
|
93
|
+
command: c.command,
|
|
94
|
+
toolName: c.toolName,
|
|
95
|
+
})),
|
|
96
|
+
requires: m.requires ?? null,
|
|
97
|
+
dependsOn: m.dependsOn ?? [],
|
|
98
|
+
dependents,
|
|
99
|
+
status: status ?? null,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return reply.status(200).send({ success: true, plugins: rows });
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// POST /api/plugins/:id/toggle — write config.plugins.<id>.enabled.
|
|
108
|
+
//
|
|
109
|
+
// Honors dependency-graph cascade per Robert's add-plugin-activation-ui
|
|
110
|
+
// Layer 2: enabling cascades deps; disabling cascades dependents; enabling
|
|
111
|
+
// with a missing dep returns 409 with the blocker list.
|
|
112
|
+
fastify.post<{ Params: { id: string }; Body: { enabled?: boolean } }>(
|
|
113
|
+
"/api/plugins/:id/toggle",
|
|
114
|
+
{ preHandler: networkGuard },
|
|
115
|
+
async (request, reply) => {
|
|
116
|
+
const { id } = request.params;
|
|
117
|
+
const body = request.body ?? {};
|
|
118
|
+
|
|
119
|
+
if (typeof body.enabled !== "boolean") {
|
|
120
|
+
return reply
|
|
121
|
+
.status(400)
|
|
122
|
+
.send({ success: false, error: "body.enabled must be boolean" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const plugins = discoverPlugins(repoRoot);
|
|
126
|
+
const found = plugins.find((p) => p.manifest.id === id);
|
|
127
|
+
if (!found) {
|
|
128
|
+
return reply
|
|
129
|
+
.status(404)
|
|
130
|
+
.send({ success: false, error: `Plugin "${id}" not found` });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const existing = readRawConfig();
|
|
134
|
+
const existingPlugins =
|
|
135
|
+
(existing.plugins as Record<string, unknown> | undefined) ?? {};
|
|
136
|
+
|
|
137
|
+
function isEnabled(pid: string): boolean {
|
|
138
|
+
const cfg = existingPlugins[pid] as Record<string, unknown> | undefined;
|
|
139
|
+
return cfg?.enabled !== false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const graph = buildGraph(
|
|
143
|
+
plugins.map((p) => ({
|
|
144
|
+
id: p.manifest.id,
|
|
145
|
+
dependsOn: p.manifest.dependsOn ?? [],
|
|
146
|
+
})),
|
|
147
|
+
isEnabled,
|
|
148
|
+
);
|
|
149
|
+
const impact = computeToggleImpact(graph, id, body.enabled);
|
|
150
|
+
|
|
151
|
+
if (body.enabled && impact.blockers.length > 0) {
|
|
152
|
+
return reply
|
|
153
|
+
.status(409)
|
|
154
|
+
.send({ success: false, reason: "blockers", blockers: impact.blockers });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Atomic cascade write: collect every id whose `enabled` flips, write
|
|
158
|
+
// them all in a single config write, then emit one plugin_config_update
|
|
159
|
+
// per affected id.
|
|
160
|
+
const flips: Array<{ id: string; enabled: boolean }> = [
|
|
161
|
+
{ id, enabled: body.enabled },
|
|
162
|
+
];
|
|
163
|
+
if (body.enabled) {
|
|
164
|
+
for (const dep of impact.cascadeEnable) flips.push({ id: dep, enabled: true });
|
|
165
|
+
} else {
|
|
166
|
+
for (const dep of impact.cascadeDisable) flips.push({ id: dep, enabled: false });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const nextPlugins: Record<string, unknown> = { ...existingPlugins };
|
|
170
|
+
const mergedPerId = new Map<string, Record<string, unknown>>();
|
|
171
|
+
for (const flip of flips) {
|
|
172
|
+
const prev = (nextPlugins[flip.id] as Record<string, unknown> | undefined) ?? {};
|
|
173
|
+
const merged = { ...prev, enabled: flip.enabled };
|
|
174
|
+
nextPlugins[flip.id] = merged;
|
|
175
|
+
mergedPerId.set(flip.id, merged);
|
|
176
|
+
}
|
|
177
|
+
writeRawConfig({ ...existing, plugins: nextPlugins });
|
|
178
|
+
|
|
179
|
+
for (const [flipId, merged] of mergedPerId) {
|
|
180
|
+
broadcast({ type: "plugin_config_update", id: flipId, config: merged });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return reply.status(200).send({
|
|
184
|
+
success: true,
|
|
185
|
+
restartRequired: true,
|
|
186
|
+
cascade: {
|
|
187
|
+
...(body.enabled ? { enable: impact.cascadeEnable } : {}),
|
|
188
|
+
...(!body.enabled ? { disable: impact.cascadeDisable } : {}),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -173,6 +173,24 @@ async function enrichEntry(
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// Cross-reference companion dashboard plugin (Layer 1.5 of
|
|
177
|
+
// add-plugin-activation-ui). When the entry names a `dashboardPlugin`,
|
|
178
|
+
// look it up in the plugin status store so the install browser can show
|
|
179
|
+
// a "+plugin" badge that knows whether the plugin is currently present.
|
|
180
|
+
let dashboardPluginInstalled: boolean | undefined;
|
|
181
|
+
if (entry.dashboardPlugin) {
|
|
182
|
+
try {
|
|
183
|
+
const { getPluginStatusStore } = await import(
|
|
184
|
+
"@blackbelt-technology/dashboard-plugin-runtime/server"
|
|
185
|
+
);
|
|
186
|
+
dashboardPluginInstalled = Boolean(
|
|
187
|
+
getPluginStatusStore().getStatus(entry.dashboardPlugin),
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
dashboardPluginInstalled = false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
return {
|
|
177
195
|
...entry,
|
|
178
196
|
description,
|
|
@@ -180,6 +198,9 @@ async function enrichEntry(
|
|
|
180
198
|
installed: { scope: installedScope },
|
|
181
199
|
activeInPi,
|
|
182
200
|
updateAvailable,
|
|
201
|
+
...(entry.dashboardPlugin
|
|
202
|
+
? { dashboardPluginInstalled: dashboardPluginInstalled ?? false }
|
|
203
|
+
: {}),
|
|
183
204
|
};
|
|
184
205
|
}
|
|
185
206
|
|
|
@@ -22,9 +22,53 @@ import path from "node:path";
|
|
|
22
22
|
import os from "node:os";
|
|
23
23
|
import { localhostGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
|
|
24
24
|
import { readSpawnFailures } from "../spawn-failure-log.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getPluginStatusStore,
|
|
27
|
+
discoverPlugins,
|
|
28
|
+
pluginRegistryHash,
|
|
29
|
+
} from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
30
|
+
import { classifyBridgeSource } from "@blackbelt-technology/pi-dashboard-shared/plugin-bridge-register.js";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import type { BridgeLoadSource, PluginStatus } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/plugin-status.js";
|
|
26
33
|
import type { NetworkInterface } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
27
|
-
import
|
|
34
|
+
import { parseLaunchSource } from "@blackbelt-technology/pi-dashboard-shared/dashboard-starter.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enrich each plugin status with `bridgeLoadedFrom` by classifying the
|
|
38
|
+
* plugin's resolved bridge path against the live pi settings.json.
|
|
39
|
+
*
|
|
40
|
+
* Reads settings.json once per health call; cached `discoverPlugins()`
|
|
41
|
+
* result keeps the bridge path lookup O(1).
|
|
42
|
+
*
|
|
43
|
+
* See change: fix-pi-flows-end-to-end (Group 2, task 2.4).
|
|
44
|
+
*/
|
|
45
|
+
function enrichWithBridgeSource(statuses: PluginStatus[]): PluginStatus[] {
|
|
46
|
+
let settings: unknown = null;
|
|
47
|
+
try {
|
|
48
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
49
|
+
const p = path.join(home, ".pi", "agent", "settings.json");
|
|
50
|
+
if (fs.existsSync(p)) {
|
|
51
|
+
const raw = fs.readFileSync(p, "utf-8").trim();
|
|
52
|
+
if (raw) settings = JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
settings = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const plugins = discoverPlugins();
|
|
59
|
+
const bridgePaths = new Map<string, string>();
|
|
60
|
+
for (const p of plugins) {
|
|
61
|
+
if (p.bridgeEntryPath) bridgePaths.set(p.manifest.id, p.bridgeEntryPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return statuses.map((s) => {
|
|
65
|
+
const bp = bridgePaths.get(s.id);
|
|
66
|
+
const bridgeLoadedFrom: BridgeLoadSource = bp
|
|
67
|
+
? classifyBridgeSource(settings, bp)
|
|
68
|
+
: "none";
|
|
69
|
+
return { ...s, bridgeLoadedFrom };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
28
72
|
|
|
29
73
|
export function registerSystemRoutes(
|
|
30
74
|
fastify: FastifyInstance,
|
|
@@ -37,10 +81,9 @@ export function registerSystemRoutes(
|
|
|
37
81
|
version?: string;
|
|
38
82
|
directoryService?: DirectoryService;
|
|
39
83
|
piGateway?: PiGateway;
|
|
40
|
-
bootstrapState?: BootstrapStateStore;
|
|
41
84
|
},
|
|
42
85
|
) {
|
|
43
|
-
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway
|
|
86
|
+
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway } = deps;
|
|
44
87
|
|
|
45
88
|
// Quiesce windows for the bridge `server_restarting` broadcast. See change
|
|
46
89
|
// `fix-restart-bridge-auto-start-race`. Bridges that receive this message
|
|
@@ -233,10 +276,17 @@ export function registerSystemRoutes(
|
|
|
233
276
|
return {
|
|
234
277
|
ok: true,
|
|
235
278
|
pid: process.pid,
|
|
236
|
-
|
|
237
|
-
|
|
279
|
+
// launchSource: single source of truth for arm-aware client gating
|
|
280
|
+
// (e.g. hide pi-core update UI under Electron, since bundled
|
|
281
|
+
// node_modules/ is read-only). See change:
|
|
282
|
+
// eliminate-electron-runtime-install task 3.2.
|
|
283
|
+
launchSource: parseLaunchSource(process.env),
|
|
238
284
|
version: version ?? "unknown",
|
|
239
285
|
uptime: Math.floor((Date.now() - serverStartTime) / 1000),
|
|
286
|
+
// ISO timestamp of process start. Used by the Plugins tab to detect
|
|
287
|
+
// server restarts and clear the Restart-required banner.
|
|
288
|
+
// See change: add-plugin-activation-ui.
|
|
289
|
+
startedAt: new Date(serverStartTime).toISOString(),
|
|
240
290
|
mode: config.dev ? "dev" : "production",
|
|
241
291
|
server: {
|
|
242
292
|
rss: mem.rss,
|
|
@@ -246,7 +296,19 @@ export function registerSystemRoutes(
|
|
|
246
296
|
totalSessions: sessionManager.listAll().length,
|
|
247
297
|
},
|
|
248
298
|
agents: agentMetrics,
|
|
249
|
-
plugins: getPluginStatusStore().listAll(),
|
|
299
|
+
plugins: enrichWithBridgeSource(getPluginStatusStore().listAll()),
|
|
300
|
+
// Build-time-vs-runtime plugin-bundle hash. Clients compare it to
|
|
301
|
+
// the embedded `PLUGIN_REGISTRY_HASH` to detect stale bundles.
|
|
302
|
+
// See change: fix-pi-flows-end-to-end (Group 6).
|
|
303
|
+
// Must hash over the SAME plugin set the vite-plugin used at build
|
|
304
|
+
// time — production builds exclude `fixture: true` plugins (e.g. demo).
|
|
305
|
+
// Without this filter, the runtime hash would differ from the embedded
|
|
306
|
+
// PLUGIN_REGISTRY_HASH and the staleness banner would always show.
|
|
307
|
+
bundleHash: pluginRegistryHash(
|
|
308
|
+
discoverPlugins().filter((p) =>
|
|
309
|
+
config.dev ? true : p.manifest.fixture !== true,
|
|
310
|
+
),
|
|
311
|
+
),
|
|
250
312
|
proxy: getModelProxyStatus(),
|
|
251
313
|
};
|
|
252
314
|
});
|
|
@@ -276,13 +338,13 @@ export function registerSystemRoutes(
|
|
|
276
338
|
"/api/electron/reextract",
|
|
277
339
|
{ preHandler: networkGuard },
|
|
278
340
|
async (_request, reply) => {
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
341
|
+
const launchSource = parseLaunchSource(process.env);
|
|
342
|
+
if (launchSource !== "electron") {
|
|
281
343
|
reply.status(403);
|
|
282
344
|
return {
|
|
283
345
|
error: "reextract_not_allowed",
|
|
284
|
-
message: `Re-extract is only available when the server was started by Electron (current
|
|
285
|
-
|
|
346
|
+
message: `Re-extract is only available when the server was started by Electron (current launchSource: ${launchSource})`,
|
|
347
|
+
launchSource,
|
|
286
348
|
};
|
|
287
349
|
}
|
|
288
350
|
reply.status(202);
|