@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,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Spun up by `cli.ts` when the main server can't start because a top-level
|
|
5
|
+
* runtime dependency is missing (`fastify`, `toad-cache`, etc.). Binds to
|
|
6
|
+
* the same port the real server would have used, so a user pointing their
|
|
7
|
+
* browser at http://localhost:8000 sees a status page instead of a refused
|
|
8
|
+
* connection.
|
|
9
|
+
*
|
|
10
|
+
* STRICT CONSTRAINT: this module imports ONLY node built-ins. If it
|
|
11
|
+
* imported a third-party module, that module could be the one that's
|
|
12
|
+
* missing — and the recovery server itself would fail to load. Keep it
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
import http from "node:http";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
|
|
22
|
+
export interface RecoveryInfo {
|
|
23
|
+
/** Port to bind. */
|
|
24
|
+
port: number;
|
|
25
|
+
/** The original error that prevented startup. */
|
|
26
|
+
error: Error;
|
|
27
|
+
/** Optional: extracted missing module identifier. */
|
|
28
|
+
missingModule?: string | null;
|
|
29
|
+
/** Optional: suggested reinstall command. */
|
|
30
|
+
suggestedFix?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract the missing-module identifier from an `ERR_MODULE_NOT_FOUND` or
|
|
35
|
+
* legacy `MODULE_NOT_FOUND` error. Returns null if the error isn't of that
|
|
36
|
+
* shape.
|
|
37
|
+
*
|
|
38
|
+
* Examples it handles:
|
|
39
|
+
* "Cannot find module 'fastify'"
|
|
40
|
+
* "Cannot find module '/abs/path/foo.cjs'"
|
|
41
|
+
* "Cannot find package 'toad-cache' imported from /..."
|
|
42
|
+
* "Cannot find module 'file:///.../server.js' imported from /.../cli.ts"
|
|
43
|
+
*/
|
|
44
|
+
export function parseModuleNotFoundError(err: unknown): string | null {
|
|
45
|
+
if (!err || typeof err !== "object") return null;
|
|
46
|
+
const e = err as { code?: string; message?: string };
|
|
47
|
+
const code = e.code;
|
|
48
|
+
const msg = e.message ?? "";
|
|
49
|
+
const isModuleErr =
|
|
50
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
51
|
+
code === "MODULE_NOT_FOUND" ||
|
|
52
|
+
/Cannot find (module|package)/.test(msg);
|
|
53
|
+
if (!isModuleErr) return null;
|
|
54
|
+
|
|
55
|
+
// Try "Cannot find module 'X'" / "Cannot find package 'X'"
|
|
56
|
+
const m1 = msg.match(/Cannot find (?:module|package) ['"]([^'"]+)['"]/);
|
|
57
|
+
if (m1) return m1[1];
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Return true iff `err` looks like a top-level module-resolution failure
|
|
63
|
+
* (the class of error this recovery server exists to handle).
|
|
64
|
+
*/
|
|
65
|
+
export function isModuleNotFoundError(err: unknown): boolean {
|
|
66
|
+
if (!err || typeof err !== "object") return false;
|
|
67
|
+
const e = err as { code?: string; message?: string };
|
|
68
|
+
return (
|
|
69
|
+
e.code === "ERR_MODULE_NOT_FOUND" ||
|
|
70
|
+
e.code === "MODULE_NOT_FOUND" ||
|
|
71
|
+
(typeof e.message === "string" && /Cannot find (module|package)/.test(e.message))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Best-effort install-layout detection used to suggest the right reinstall
|
|
77
|
+
* command. Returns "electron" when running inside the packaged Electron
|
|
78
|
+
* resources tree, "npm-global" when running from a global npm install of
|
|
79
|
+
* @blackbelt-technology/pi-agent-dashboard, or "monorepo" / "unknown".
|
|
80
|
+
*/
|
|
81
|
+
export function detectInstallLayout(scriptPath?: string): "electron" | "npm-global" | "monorepo" | "unknown" {
|
|
82
|
+
const p = scriptPath ?? (process.argv[1] ?? "");
|
|
83
|
+
if (/[/\\]Contents[/\\]Resources[/\\]/.test(p)) return "electron"; // macOS app bundle
|
|
84
|
+
if (/[/\\]resources[/\\]/.test(p) && /Electron/i.test(p)) return "electron";
|
|
85
|
+
if (/[/\\]node_modules[/\\]@blackbelt-technology[/\\]pi-agent-dashboard[/\\]/.test(p)) return "npm-global";
|
|
86
|
+
if (/[/\\]packages[/\\]server[/\\]src[/\\]cli\.ts$/.test(p)) return "monorepo";
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Suggested reinstall command for the detected layout.
|
|
92
|
+
*/
|
|
93
|
+
export function suggestedReinstallCommand(layout: ReturnType<typeof detectInstallLayout>): string {
|
|
94
|
+
switch (layout) {
|
|
95
|
+
case "npm-global":
|
|
96
|
+
return "npm install -g @blackbelt-technology/pi-agent-dashboard";
|
|
97
|
+
case "electron":
|
|
98
|
+
return "Reinstall the Pi Dashboard application from your installer.";
|
|
99
|
+
case "monorepo":
|
|
100
|
+
return "npm install (from the repo root)";
|
|
101
|
+
default:
|
|
102
|
+
return "npm install -g @blackbelt-technology/pi-agent-dashboard";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the HTML page served at `/`. Pure function — exported for testing.
|
|
108
|
+
*/
|
|
109
|
+
export function buildRecoveryHtml(info: RecoveryInfo): string {
|
|
110
|
+
const escape = (s: string) =>
|
|
111
|
+
s
|
|
112
|
+
.replace(/&/g, "&")
|
|
113
|
+
.replace(/</g, "<")
|
|
114
|
+
.replace(/>/g, ">")
|
|
115
|
+
.replace(/"/g, """);
|
|
116
|
+
|
|
117
|
+
const missing = info.missingModule ? escape(info.missingModule) : "(unknown)";
|
|
118
|
+
const fix = escape(info.suggestedFix ?? "");
|
|
119
|
+
const stack = escape(info.error.stack ?? info.error.message ?? String(info.error));
|
|
120
|
+
|
|
121
|
+
return `<!doctype html>
|
|
122
|
+
<html lang="en">
|
|
123
|
+
<head>
|
|
124
|
+
<meta charset="utf-8" />
|
|
125
|
+
<title>Pi Dashboard — Recovery Mode</title>
|
|
126
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
127
|
+
<style>
|
|
128
|
+
body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
129
|
+
background: #fef2f2; color: #1f2937; margin: 0; padding: 2rem; }
|
|
130
|
+
main { max-width: 720px; margin: 0 auto; background: white; border-radius: 12px;
|
|
131
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.08); padding: 2rem; }
|
|
132
|
+
h1 { color: #b91c1c; margin: 0 0 .5rem; font-size: 1.5rem; }
|
|
133
|
+
.badge { display: inline-block; background: #fecaca; color: #991b1b;
|
|
134
|
+
padding: .15rem .5rem; border-radius: 4px; font-size: .75rem;
|
|
135
|
+
font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }
|
|
136
|
+
code { background: #f3f4f6; padding: .1rem .35rem; border-radius: 3px;
|
|
137
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; }
|
|
138
|
+
pre { background: #1f2937; color: #f9fafb; padding: 1rem; border-radius: 6px;
|
|
139
|
+
overflow-x: auto; font-size: .8em; line-height: 1.4; max-height: 280px; }
|
|
140
|
+
button { background: #2563eb; color: white; border: 0; border-radius: 6px;
|
|
141
|
+
padding: .55rem 1rem; font-size: .9em; cursor: pointer; margin-right: .5rem; }
|
|
142
|
+
button:hover { background: #1d4ed8; }
|
|
143
|
+
button.secondary { background: #6b7280; }
|
|
144
|
+
button.secondary:hover { background: #4b5563; }
|
|
145
|
+
button:disabled { background: #9ca3af; cursor: not-allowed; }
|
|
146
|
+
#status { margin-top: 1rem; font-size: .9em; color: #4b5563; }
|
|
147
|
+
</style>
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<main>
|
|
151
|
+
<span class="badge">Recovery Mode</span>
|
|
152
|
+
<h1>Dashboard failed to start</h1>
|
|
153
|
+
<p>The server could not load a required dependency:
|
|
154
|
+
<code>${missing}</code></p>
|
|
155
|
+
<p><strong>Suggested fix:</strong> <code>${fix}</code></p>
|
|
156
|
+
<div>
|
|
157
|
+
<button id="retry">Retry start</button>
|
|
158
|
+
<button id="reinstall" class="secondary">Reinstall dependencies</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div id="status"></div>
|
|
161
|
+
<h3 style="margin-top: 1.5rem;">Error details</h3>
|
|
162
|
+
<pre>${stack}</pre>
|
|
163
|
+
</main>
|
|
164
|
+
<script>
|
|
165
|
+
const $ = (id) => document.getElementById(id);
|
|
166
|
+
const status = $("status");
|
|
167
|
+
function setBusy(b, msg) {
|
|
168
|
+
$("retry").disabled = b; $("reinstall").disabled = b;
|
|
169
|
+
status.textContent = msg;
|
|
170
|
+
}
|
|
171
|
+
async function post(path) {
|
|
172
|
+
const res = await fetch(path, { method: "POST" });
|
|
173
|
+
const text = await res.text();
|
|
174
|
+
return { ok: res.ok, text };
|
|
175
|
+
}
|
|
176
|
+
$("retry").addEventListener("click", async () => {
|
|
177
|
+
setBusy(true, "Retrying…");
|
|
178
|
+
const r = await post("/api/recovery/retry");
|
|
179
|
+
status.textContent = r.text;
|
|
180
|
+
if (r.ok) setTimeout(() => location.reload(), 1500);
|
|
181
|
+
else setBusy(false, "Retry failed: " + r.text);
|
|
182
|
+
});
|
|
183
|
+
$("reinstall").addEventListener("click", async () => {
|
|
184
|
+
setBusy(true, "Reinstalling… this may take a minute.");
|
|
185
|
+
const r = await post("/api/recovery/reinstall");
|
|
186
|
+
status.textContent = r.text;
|
|
187
|
+
setBusy(false, r.text);
|
|
188
|
+
});
|
|
189
|
+
</script>
|
|
190
|
+
</body>
|
|
191
|
+
</html>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run `npm install -g ...` (or the per-layout equivalent) and stream
|
|
196
|
+
* progress lines to a callback. Resolves with the exit code.
|
|
197
|
+
*/
|
|
198
|
+
function runReinstall(
|
|
199
|
+
layout: ReturnType<typeof detectInstallLayout>,
|
|
200
|
+
onLine: (s: string) => void,
|
|
201
|
+
): Promise<number> {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
let cmd: string;
|
|
204
|
+
let args: string[];
|
|
205
|
+
if (layout === "monorepo") {
|
|
206
|
+
cmd = "npm";
|
|
207
|
+
args = ["install"];
|
|
208
|
+
} else {
|
|
209
|
+
cmd = "npm";
|
|
210
|
+
args = ["install", "-g", "@blackbelt-technology/pi-agent-dashboard"];
|
|
211
|
+
}
|
|
212
|
+
onLine(`> ${cmd} ${args.join(" ")}`);
|
|
213
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
214
|
+
child.stdout?.on("data", (b: Buffer) => onLine(b.toString("utf8").trimEnd()));
|
|
215
|
+
child.stderr?.on("data", (b: Buffer) => onLine(b.toString("utf8").trimEnd()));
|
|
216
|
+
child.on("error", (e: Error) => {
|
|
217
|
+
onLine(`spawn error: ${e.message}`);
|
|
218
|
+
resolve(1);
|
|
219
|
+
});
|
|
220
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Start the recovery HTTP server. Does not return — the server stays bound
|
|
226
|
+
* to the port until the process exits (typically after `/api/recovery/retry`
|
|
227
|
+
* respawns the CLI and `process.exit`s).
|
|
228
|
+
*
|
|
229
|
+
* If the port is already bound (something else listening), this will log
|
|
230
|
+
* and exit with code 2 — better than silent infinite-recovery loops.
|
|
231
|
+
*/
|
|
232
|
+
export async function startRecoveryServer(info: RecoveryInfo): Promise<void> {
|
|
233
|
+
const scriptPath = process.argv[1] ?? "";
|
|
234
|
+
const layout = detectInstallLayout(scriptPath);
|
|
235
|
+
const enrichedInfo: RecoveryInfo = {
|
|
236
|
+
...info,
|
|
237
|
+
suggestedFix: info.suggestedFix ?? suggestedReinstallCommand(layout),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Log a clear banner so log-tailers see what's happening.
|
|
241
|
+
console.error("");
|
|
242
|
+
console.error("══════════════════════════════════════════════════════════════");
|
|
243
|
+
console.error(" Pi Dashboard — entering RECOVERY MODE");
|
|
244
|
+
console.error(` reason: ${enrichedInfo.error.message}`);
|
|
245
|
+
console.error(` missing: ${enrichedInfo.missingModule ?? "(unknown)"}`);
|
|
246
|
+
console.error(` suggested: ${enrichedInfo.suggestedFix}`);
|
|
247
|
+
console.error(` serving recovery UI at http://localhost:${info.port}/`);
|
|
248
|
+
console.error("══════════════════════════════════════════════════════════════");
|
|
249
|
+
console.error("");
|
|
250
|
+
|
|
251
|
+
// Persist a snapshot of the failure under ~/.pi/dashboard/last-recovery.json
|
|
252
|
+
// so tooling/diagnostics can see why the server is in recovery mode.
|
|
253
|
+
try {
|
|
254
|
+
const dir = path.join(os.homedir(), ".pi", "dashboard");
|
|
255
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
256
|
+
fs.writeFileSync(
|
|
257
|
+
path.join(dir, "last-recovery.json"),
|
|
258
|
+
JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
at: new Date().toISOString(),
|
|
261
|
+
port: info.port,
|
|
262
|
+
missingModule: enrichedInfo.missingModule ?? null,
|
|
263
|
+
error: enrichedInfo.error.message,
|
|
264
|
+
stack: enrichedInfo.error.stack ?? null,
|
|
265
|
+
layout,
|
|
266
|
+
scriptPath,
|
|
267
|
+
},
|
|
268
|
+
null,
|
|
269
|
+
2,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
// Non-fatal — recovery still works without the snapshot.
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const server = http.createServer((req, res) => {
|
|
277
|
+
const url = req.url ?? "/";
|
|
278
|
+
if (req.method === "GET" && (url === "/" || url === "/index.html")) {
|
|
279
|
+
res.writeHead(200, {
|
|
280
|
+
"content-type": "text/html; charset=utf-8",
|
|
281
|
+
"cache-control": "no-cache, no-store, must-revalidate",
|
|
282
|
+
});
|
|
283
|
+
res.end(buildRecoveryHtml(enrichedInfo));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (req.method === "GET" && url === "/api/health") {
|
|
287
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
288
|
+
res.end(
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
ok: false,
|
|
291
|
+
mode: "recovery",
|
|
292
|
+
missingModule: enrichedInfo.missingModule ?? null,
|
|
293
|
+
error: enrichedInfo.error.message,
|
|
294
|
+
suggestedFix: enrichedInfo.suggestedFix,
|
|
295
|
+
layout,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (req.method === "POST" && url === "/api/recovery/retry") {
|
|
301
|
+
// Respawn ourselves detached, then exit.
|
|
302
|
+
try {
|
|
303
|
+
const cliPath = scriptPath || fileURLToPath(import.meta.url);
|
|
304
|
+
const child = spawn(process.execPath, [cliPath, ...process.argv.slice(2)], {
|
|
305
|
+
detached: true,
|
|
306
|
+
stdio: "ignore",
|
|
307
|
+
env: process.env,
|
|
308
|
+
});
|
|
309
|
+
child.unref();
|
|
310
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
311
|
+
res.end("Respawning… give it a few seconds, then reload.");
|
|
312
|
+
// Defer exit so the response actually flushes.
|
|
313
|
+
setTimeout(() => process.exit(0), 250);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
316
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
317
|
+
res.end("Failed to respawn: " + msg);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (req.method === "POST" && url === "/api/recovery/reinstall") {
|
|
322
|
+
// Stream isn't easy via simple text response; just buffer and return.
|
|
323
|
+
const lines: string[] = [];
|
|
324
|
+
runReinstall(layout, (s) => {
|
|
325
|
+
lines.push(s);
|
|
326
|
+
console.log("[recovery-install] " + s);
|
|
327
|
+
}).then((code) => {
|
|
328
|
+
if (res.writableEnded) return;
|
|
329
|
+
if (code === 0) {
|
|
330
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
331
|
+
res.end("Reinstall complete. Click Retry start.");
|
|
332
|
+
} else {
|
|
333
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
334
|
+
res.end("Reinstall failed (exit " + code + ").\n\n" + lines.slice(-30).join("\n"));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Everything else: serve the same HTML so SPA-style links still work.
|
|
341
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
342
|
+
res.end(buildRecoveryHtml(enrichedInfo));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return new Promise<void>((resolve, reject) => {
|
|
346
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
347
|
+
if (err.code === "EADDRINUSE") {
|
|
348
|
+
console.error(
|
|
349
|
+
`[recovery] port ${info.port} already in use — cannot bind recovery server. ` +
|
|
350
|
+
`Another process (possibly an older dashboard) is holding the port. ` +
|
|
351
|
+
`Run \`pi-dashboard stop\` or kill the holder, then retry.`,
|
|
352
|
+
);
|
|
353
|
+
process.exit(2);
|
|
354
|
+
}
|
|
355
|
+
reject(err);
|
|
356
|
+
});
|
|
357
|
+
server.listen(info.port, () => {
|
|
358
|
+
console.error(`[recovery] listening on http://localhost:${info.port}`);
|
|
359
|
+
// Never resolve — recovery server runs until the process exits.
|
|
360
|
+
// This promise stays pending so the caller `await`s forever.
|
|
361
|
+
// The caller may also choose to short-circuit with `return` after
|
|
362
|
+
// invoking us; either way is safe.
|
|
363
|
+
resolve();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for pure helpers in `manifest-route.ts`.
|
|
3
|
+
* See change: add-dynamic-pwa-manifest-naming.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
stripPort,
|
|
8
|
+
resolveManifestSource,
|
|
9
|
+
buildManifestBody,
|
|
10
|
+
} from "../manifest-route.js";
|
|
11
|
+
|
|
12
|
+
describe("stripPort", () => {
|
|
13
|
+
it("returns empty for null/undefined/empty/whitespace", () => {
|
|
14
|
+
expect(stripPort(undefined)).toBe("");
|
|
15
|
+
expect(stripPort(null)).toBe("");
|
|
16
|
+
expect(stripPort("")).toBe("");
|
|
17
|
+
expect(stripPort(" ")).toBe("");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns bare hostname unchanged (lower-cased)", () => {
|
|
21
|
+
expect(stripPort("mybox.local")).toBe("mybox.local");
|
|
22
|
+
expect(stripPort("MyBox.Local")).toBe("mybox.local");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("strips port from host:port", () => {
|
|
26
|
+
expect(stripPort("mybox.local:8000")).toBe("mybox.local");
|
|
27
|
+
expect(stripPort("example.com:443")).toBe("example.com");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("strips port from bracketed IPv6 with port", () => {
|
|
31
|
+
expect(stripPort("[::1]:8000")).toBe("::1");
|
|
32
|
+
expect(stripPort("[fe80::1]:443")).toBe("fe80::1");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles bracketed IPv6 without port", () => {
|
|
36
|
+
expect(stripPort("[::1]")).toBe("::1");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("leaves unbracketed IPv6 untouched (multiple colons)", () => {
|
|
40
|
+
// Non-conformant Host header — Node parses this as "::1" with no port.
|
|
41
|
+
// We don't try to be clever; pass through verbatim.
|
|
42
|
+
expect(stripPort("::1")).toBe("::1");
|
|
43
|
+
expect(stripPort("fe80::1")).toBe("fe80::1");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("trims leading/trailing whitespace before parsing", () => {
|
|
47
|
+
expect(stripPort(" mybox:8000 ")).toBe("mybox");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("resolveManifestSource", () => {
|
|
52
|
+
const HOSTNAME = "macbook-pro";
|
|
53
|
+
|
|
54
|
+
it("returns config override when set", () => {
|
|
55
|
+
expect(resolveManifestSource("foo:8000", "Home NAS", HOSTNAME)).toBe("Home NAS");
|
|
56
|
+
expect(resolveManifestSource(undefined, "Home NAS", HOSTNAME)).toBe("Home NAS");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("trims config override and treats whitespace-only as unset", () => {
|
|
60
|
+
expect(resolveManifestSource("foo:8000", " ", HOSTNAME)).toBe("foo");
|
|
61
|
+
expect(resolveManifestSource("foo:8000", "", HOSTNAME)).toBe("foo");
|
|
62
|
+
expect(resolveManifestSource("foo:8000", " Home NAS ", HOSTNAME)).toBe("Home NAS");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to Host header (port stripped) when no override", () => {
|
|
66
|
+
expect(resolveManifestSource("mybox.local:8000", undefined, HOSTNAME)).toBe(
|
|
67
|
+
"mybox.local",
|
|
68
|
+
);
|
|
69
|
+
expect(resolveManifestSource("[::1]:8000", null, HOSTNAME)).toBe("::1");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back to os.hostname() when override and Host header are empty", () => {
|
|
73
|
+
expect(resolveManifestSource(undefined, undefined, HOSTNAME)).toBe(HOSTNAME);
|
|
74
|
+
expect(resolveManifestSource("", "", HOSTNAME)).toBe(HOSTNAME);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to 'Pi-Dash' when everything is empty", () => {
|
|
78
|
+
expect(resolveManifestSource(undefined, undefined, "")).toBe("Pi-Dash");
|
|
79
|
+
expect(resolveManifestSource("", "", " ")).toBe("Pi-Dash");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("override wins over Host header even when Host is non-empty", () => {
|
|
83
|
+
expect(resolveManifestSource("anything.local:8000", "Override", HOSTNAME)).toBe(
|
|
84
|
+
"Override",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("buildManifestBody", () => {
|
|
90
|
+
const BASE = {
|
|
91
|
+
icons: [{ src: "/icon-192.png", sizes: "192x192", type: "image/png" }],
|
|
92
|
+
theme_color: "#3b82f6",
|
|
93
|
+
background_color: "#0f172a",
|
|
94
|
+
display: "standalone",
|
|
95
|
+
start_url: "/",
|
|
96
|
+
name: "should-be-overridden",
|
|
97
|
+
short_name: "should-be-overridden",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
it("spreads static base then overrides name/short_name/id", () => {
|
|
101
|
+
const body = buildManifestBody(BASE, "mybox.local");
|
|
102
|
+
expect(body.icons).toEqual(BASE.icons);
|
|
103
|
+
expect(body.theme_color).toBe("#3b82f6");
|
|
104
|
+
expect(body.background_color).toBe("#0f172a");
|
|
105
|
+
expect(body.display).toBe("standalone");
|
|
106
|
+
expect(body.start_url).toBe("/");
|
|
107
|
+
expect(body.id).toBe("/");
|
|
108
|
+
expect(body.name).toBe("Pi-Dash \u00b7 mybox.local");
|
|
109
|
+
expect(body.short_name).toBe("mybox.local");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("truncates short_name to 12 characters", () => {
|
|
113
|
+
const body = buildManifestBody(BASE, "abc123.share.zrok.io");
|
|
114
|
+
expect(body.short_name).toBe("abc123.share");
|
|
115
|
+
expect((body.short_name as string).length).toBe(12);
|
|
116
|
+
// Full name keeps the untruncated source
|
|
117
|
+
expect(body.name).toBe("Pi-Dash \u00b7 abc123.share.zrok.io");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("falls back to 'Pi-Dash' for short_name when source is empty", () => {
|
|
121
|
+
const body = buildManifestBody(BASE, "");
|
|
122
|
+
expect(body.short_name).toBe("Pi-Dash");
|
|
123
|
+
expect(body.name).toBe("Pi-Dash \u00b7 ");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not mutate the input base object", () => {
|
|
127
|
+
const base = { ...BASE };
|
|
128
|
+
const frozen = JSON.stringify(base);
|
|
129
|
+
buildManifestBody(base, "foo");
|
|
130
|
+
expect(JSON.stringify(base)).toBe(frozen);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("preserves arbitrary extra fields from the static base", () => {
|
|
134
|
+
const body = buildManifestBody({ ...BASE, scope: "/", lang: "en" }, "x");
|
|
135
|
+
expect(body.scope).toBe("/");
|
|
136
|
+
expect(body.lang).toBe("en");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -70,27 +70,32 @@ function buildDefaultDeps(): SharedChecksDeps {
|
|
|
70
70
|
detectOpenSpec: () => detectOnPath("openspec"),
|
|
71
71
|
isApiKeyConfigured,
|
|
72
72
|
probeServer: async () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
// CRITICAL: do NOT shell out to `curl http://localhost:8000/api/health`
|
|
74
|
+
// here. `safeExec` uses synchronous `execSync`, which blocks the Node
|
|
75
|
+
// event loop until the child exits. The child is curl, talking back
|
|
76
|
+
// to *this same Node process* — a self-deadlock. curl waits for the
|
|
77
|
+
// server to respond, server is blocked in execSync, after 3 s the
|
|
78
|
+
// timeout kills curl and the probe falsely reports "Not running".
|
|
79
|
+
//
|
|
80
|
+
// Since we are currently handling an HTTP request, by definition the
|
|
81
|
+
// server IS running. Read process-resident health data directly
|
|
82
|
+
// instead of round-tripping through HTTP.
|
|
83
|
+
//
|
|
84
|
+
// See change: harvest-bootstrap-survivor-fixes (cherry-pick 5).
|
|
85
|
+
const installable =
|
|
86
|
+
process.env.DASHBOARD_INSTALLABLE_TOTAL !== undefined
|
|
87
|
+
? {
|
|
88
|
+
total: Number(process.env.DASHBOARD_INSTALLABLE_TOTAL ?? 0),
|
|
89
|
+
installed: Number(process.env.DASHBOARD_INSTALLABLE_INSTALLED ?? 0),
|
|
90
|
+
failed: [] as string[],
|
|
91
|
+
}
|
|
92
|
+
: null;
|
|
93
|
+
return {
|
|
94
|
+
running: true,
|
|
95
|
+
starter: process.env.DASHBOARD_STARTER ?? null,
|
|
96
|
+
mode: process.env.NODE_ENV === "development" ? "dev" : "production",
|
|
97
|
+
installable,
|
|
98
|
+
};
|
|
94
99
|
},
|
|
95
100
|
};
|
|
96
101
|
}
|