@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0
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 +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +108 -9
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +363 -26
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +59 -3
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +93 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +71 -49
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST routes for pi core version check and update.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/pi-core/versions[?refresh=true]
|
|
5
|
+
* POST /api/pi-core/update { packages?: string[] }
|
|
6
|
+
*
|
|
7
|
+
* Complements /api/packages/* (extension management): this endpoint covers
|
|
8
|
+
* globally-installed pi CLI packages like @mariozechner/pi-coding-agent,
|
|
9
|
+
* pi-dashboard itself, pi-model-proxy, etc.
|
|
10
|
+
*/
|
|
11
|
+
import type { FastifyInstance } from "fastify";
|
|
12
|
+
import type {
|
|
13
|
+
ApiResponse,
|
|
14
|
+
PiCoreStatus,
|
|
15
|
+
PiCoreUpdateRequest,
|
|
16
|
+
PiCoreUpdateResponse,
|
|
17
|
+
} from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
18
|
+
import type { PiCoreChecker } from "../pi-core-checker.js";
|
|
19
|
+
import type { PiCoreUpdater } from "../pi-core-updater.js";
|
|
20
|
+
import { PackageOperationBusyError } from "../package-manager-wrapper.js";
|
|
21
|
+
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
22
|
+
|
|
23
|
+
export interface PiCoreRouteDeps {
|
|
24
|
+
piCoreChecker: PiCoreChecker;
|
|
25
|
+
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
|
+
/**
|
|
32
|
+
* Called after the updater finishes a batch (success or per-package failure).
|
|
33
|
+
* The server wires this to broadcast a `pi_core_update_complete` WS message
|
|
34
|
+
* so listeners (PiUpdateBadge, PiCoreVersionsSection, usePiCoreVersions
|
|
35
|
+
* hook instances in other open tabs) refetch their state.
|
|
36
|
+
*/
|
|
37
|
+
onUpdateComplete?: (payload: {
|
|
38
|
+
results: Array<{ name: string; success: boolean; error?: string }>;
|
|
39
|
+
sessionsReloaded: number;
|
|
40
|
+
}) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerPiCoreRoutes(
|
|
44
|
+
fastify: FastifyInstance,
|
|
45
|
+
deps: PiCoreRouteDeps,
|
|
46
|
+
): void {
|
|
47
|
+
const { piCoreChecker, piCoreUpdater, bootstrapState } = deps;
|
|
48
|
+
|
|
49
|
+
/** Gate: 503 unless bootstrap is ready. Returns undefined when OK to proceed. */
|
|
50
|
+
const bootstrapGate = async (
|
|
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
|
+
};
|
|
63
|
+
|
|
64
|
+
// ── GET /api/pi-core/versions ──────────────────────────────────
|
|
65
|
+
|
|
66
|
+
fastify.get<{ Querystring: { refresh?: string } }>(
|
|
67
|
+
"/api/pi-core/versions",
|
|
68
|
+
{ preHandler: bootstrapGate as any },
|
|
69
|
+
async (request) => {
|
|
70
|
+
const refresh = request.query.refresh === "true";
|
|
71
|
+
try {
|
|
72
|
+
const status = await piCoreChecker.getStatus(refresh);
|
|
73
|
+
return { success: true, data: status } satisfies ApiResponse<PiCoreStatus>;
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
return { success: false, error: err?.message ?? String(err) } satisfies ApiResponse;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ── POST /api/pi-core/update ───────────────────────────────────
|
|
81
|
+
|
|
82
|
+
fastify.post<{ Body: PiCoreUpdateRequest }>(
|
|
83
|
+
"/api/pi-core/update",
|
|
84
|
+
{ preHandler: bootstrapGate as any },
|
|
85
|
+
async (request, reply) => {
|
|
86
|
+
const requested = request.body?.packages ?? [];
|
|
87
|
+
|
|
88
|
+
// Load current status to determine install source and eligibility.
|
|
89
|
+
const status = await piCoreChecker.getStatus();
|
|
90
|
+
const allByName = new Map(status.packages.map((p) => [p.name, p]));
|
|
91
|
+
|
|
92
|
+
const targetNames =
|
|
93
|
+
requested.length > 0
|
|
94
|
+
? requested
|
|
95
|
+
: status.packages.filter((p) => p.updateAvailable).map((p) => p.name);
|
|
96
|
+
|
|
97
|
+
const resolved = [];
|
|
98
|
+
const unknown: string[] = [];
|
|
99
|
+
for (const name of targetNames) {
|
|
100
|
+
const pkg = allByName.get(name);
|
|
101
|
+
if (!pkg) {
|
|
102
|
+
unknown.push(name);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
resolved.push(pkg);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (unknown.length > 0) {
|
|
109
|
+
reply.code(400);
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: `Unknown package(s): ${unknown.join(", ")}`,
|
|
113
|
+
} satisfies ApiResponse;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (resolved.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
data: { results: [], sessionsReloaded: 0 },
|
|
120
|
+
} satisfies ApiResponse<PiCoreUpdateResponse>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const out = await piCoreUpdater.update(resolved);
|
|
125
|
+
// Invalidate cache so next version check reflects new versions.
|
|
126
|
+
piCoreChecker.invalidate();
|
|
127
|
+
// Notify other browser tabs / the header badge hook instance so
|
|
128
|
+
// their independent usePiCoreVersions state refetches.
|
|
129
|
+
deps.onUpdateComplete?.(out);
|
|
130
|
+
return { success: true, data: out } satisfies ApiResponse<PiCoreUpdateResponse>;
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
if (err instanceof PackageOperationBusyError) {
|
|
133
|
+
reply.code(409);
|
|
134
|
+
return { success: false, error: err.message } satisfies ApiResponse;
|
|
135
|
+
}
|
|
136
|
+
return { success: false, error: err?.message ?? String(err) } satisfies ApiResponse;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* REST routes for browser-based pi provider authentication.
|
|
3
3
|
*/
|
|
4
|
-
import { exec } from "node:child_process";
|
|
5
4
|
import type { FastifyInstance } from "fastify";
|
|
6
5
|
import {
|
|
7
6
|
getProviderHandler,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
} from "../provider-auth-storage.js";
|
|
22
21
|
import { startCallbackServer } from "../oauth-callback-server.js";
|
|
23
22
|
import type { PiGateway } from "../pi-gateway.js";
|
|
23
|
+
import type { BrowserGateway } from "../browser-gateway.js";
|
|
24
24
|
|
|
25
25
|
// ── In-memory flow store (short-lived PKCE + device code state) ──────────────
|
|
26
26
|
|
|
@@ -65,15 +65,16 @@ function makeFlowId(): string {
|
|
|
65
65
|
|
|
66
66
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
|
+
// Delegate to the shared platform primitive. The cross-OS dispatch
|
|
69
|
+
// (open/start/xdg-open) and URL escaping live in
|
|
70
|
+
// `packages/shared/src/platform/commands.ts`.
|
|
71
|
+
// See change: consolidate-platform-handlers.
|
|
72
|
+
import { openBrowser as platformOpenBrowser } from "@blackbelt-technology/pi-dashboard-shared/platform/commands.js";
|
|
73
|
+
|
|
68
74
|
/** Open a URL in the system's default browser */
|
|
69
75
|
function openInBrowser(url: string): void {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
: process.platform === "win32"
|
|
73
|
-
? `start "" ${JSON.stringify(url)}`
|
|
74
|
-
: `xdg-open ${JSON.stringify(url)}`;
|
|
75
|
-
exec(cmd, (err) => {
|
|
76
|
-
if (err) console.error("[provider-auth] Failed to open browser:", err.message);
|
|
76
|
+
platformOpenBrowser(url, {
|
|
77
|
+
onError: (err) => console.error("[provider-auth] Failed to open browser:", err.message),
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -81,12 +82,13 @@ function openInBrowser(url: string): void {
|
|
|
81
82
|
|
|
82
83
|
export function registerProviderAuthRoutes(
|
|
83
84
|
fastify: FastifyInstance,
|
|
84
|
-
deps: { piGateway: PiGateway },
|
|
85
|
+
deps: { piGateway: PiGateway; browserGateway: BrowserGateway },
|
|
85
86
|
) {
|
|
86
|
-
const { piGateway } = deps;
|
|
87
|
+
const { piGateway, browserGateway } = deps;
|
|
87
88
|
|
|
88
89
|
function notifyBridges() {
|
|
89
90
|
piGateway.broadcast({ type: "credentials_updated" });
|
|
91
|
+
browserGateway.broadcastToAll({ type: "models_refreshed" });
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
// List OAuth providers
|
|
@@ -6,6 +6,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join, dirname } from "node:path";
|
|
8
8
|
import type { NetworkGuard } from "./route-deps.js";
|
|
9
|
+
import type { PiGateway } from "../pi-gateway.js";
|
|
10
|
+
import type { BrowserGateway } from "../browser-gateway.js";
|
|
11
|
+
import { probeProvider, resolveProbeApiKey, type ProbeApi } from "../provider-probe.js";
|
|
9
12
|
|
|
10
13
|
const REDACTED = "***";
|
|
11
14
|
const CONFIG_PATH = join(homedir(), ".pi", "agent", "providers.json");
|
|
@@ -44,8 +47,8 @@ function redactProviders(
|
|
|
44
47
|
return redacted;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function registerProviderRoutes(fastify: FastifyInstance, deps: { networkGuard: NetworkGuard }): void {
|
|
48
|
-
const { networkGuard } = deps;
|
|
50
|
+
export function registerProviderRoutes(fastify: FastifyInstance, deps: { networkGuard: NetworkGuard; piGateway?: PiGateway; browserGateway?: BrowserGateway }): void {
|
|
51
|
+
const { networkGuard, piGateway } = deps;
|
|
49
52
|
fastify.get(
|
|
50
53
|
"/api/providers",
|
|
51
54
|
{ preHandler: networkGuard },
|
|
@@ -95,7 +98,57 @@ export function registerProviderRoutes(fastify: FastifyInstance, deps: { network
|
|
|
95
98
|
mkdirSync(dir, { recursive: true });
|
|
96
99
|
writeFileSync(CONFIG_PATH, JSON.stringify(fileData, null, 2) + "\n", "utf-8");
|
|
97
100
|
|
|
101
|
+
// Broadcast credentials_updated so all sessions refresh their model registries
|
|
102
|
+
if (piGateway) {
|
|
103
|
+
piGateway.broadcast({ type: "credentials_updated" });
|
|
104
|
+
}
|
|
105
|
+
if (deps.browserGateway) {
|
|
106
|
+
deps.browserGateway.broadcastToAll({ type: "models_refreshed" });
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
return { success: true };
|
|
99
110
|
},
|
|
100
111
|
);
|
|
112
|
+
|
|
113
|
+
// Test a provider configuration without saving it. Accepts literal api keys,
|
|
114
|
+
// $ENV_VAR references, or the REDACTED sentinel (***) for already-saved entries.
|
|
115
|
+
fastify.post(
|
|
116
|
+
"/api/providers/test",
|
|
117
|
+
{ preHandler: networkGuard },
|
|
118
|
+
async (request, reply) => {
|
|
119
|
+
const body = request.body as Record<string, any> | null;
|
|
120
|
+
if (!body || typeof body !== "object") {
|
|
121
|
+
return reply.code(400).send({ ok: false, error: "Invalid body" });
|
|
122
|
+
}
|
|
123
|
+
const name = typeof body.name === "string" ? body.name : undefined;
|
|
124
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
125
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey : "";
|
|
126
|
+
const api = typeof body.api === "string" ? (body.api as ProbeApi) : undefined;
|
|
127
|
+
if (!baseUrl) {
|
|
128
|
+
return reply.code(400).send({ ok: false, error: "baseUrl is required" });
|
|
129
|
+
}
|
|
130
|
+
if (!apiKey) {
|
|
131
|
+
return reply.code(400).send({ ok: false, error: "apiKey is required" });
|
|
132
|
+
}
|
|
133
|
+
if (!api) {
|
|
134
|
+
return reply.code(400).send({ ok: false, error: "api type is required" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const resolved = resolveProbeApiKey({
|
|
138
|
+
apiKey,
|
|
139
|
+
name,
|
|
140
|
+
readProviders: readProvidersRaw,
|
|
141
|
+
});
|
|
142
|
+
if (!resolved.ok) {
|
|
143
|
+
return { ok: false, error: resolved.error };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = await probeProvider({
|
|
147
|
+
baseUrl,
|
|
148
|
+
apiKey: resolved.key,
|
|
149
|
+
api,
|
|
150
|
+
});
|
|
151
|
+
return result;
|
|
152
|
+
},
|
|
153
|
+
);
|
|
101
154
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST route for the dashboard's curated "recommended extensions" list.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/packages/recommended
|
|
5
|
+
*
|
|
6
|
+
* Returns the static RECOMMENDED_EXTENSIONS manifest enriched with:
|
|
7
|
+
* - live description + version from npm or GitHub (falls back to
|
|
8
|
+
* fallbackDescription on network failure)
|
|
9
|
+
* - installed.scope cross-reference via packageManagerWrapper
|
|
10
|
+
* - activeInPi flag from ~/.pi/agent/settings.json packages[]
|
|
11
|
+
* - updateAvailable flag
|
|
12
|
+
*
|
|
13
|
+
* Results are cached for 60 seconds. The cache is busted when any package
|
|
14
|
+
* install / remove / update operation completes successfully.
|
|
15
|
+
*/
|
|
16
|
+
import type { FastifyInstance } from "fastify";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
21
|
+
import type { EnrichedRecommendedExtension } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
22
|
+
import {
|
|
23
|
+
RECOMMENDED_EXTENSIONS,
|
|
24
|
+
type RecommendedExtension,
|
|
25
|
+
} from "@blackbelt-technology/pi-dashboard-shared/recommended-extensions.js";
|
|
26
|
+
import {
|
|
27
|
+
parseSourceKey,
|
|
28
|
+
sourcesMatch,
|
|
29
|
+
type SourceKey,
|
|
30
|
+
} from "@blackbelt-technology/pi-dashboard-shared/source-matching.js";
|
|
31
|
+
export { parseSourceKey, sourcesMatch, type SourceKey };
|
|
32
|
+
import {
|
|
33
|
+
fetchPackageMeta,
|
|
34
|
+
fetchGithubPackageJson,
|
|
35
|
+
type PackageMeta,
|
|
36
|
+
} from "../npm-search-proxy.js";
|
|
37
|
+
import type { PackageManagerWrapper } from "../package-manager-wrapper.js";
|
|
38
|
+
|
|
39
|
+
const CACHE_TTL_MS = 60 * 1000;
|
|
40
|
+
|
|
41
|
+
interface CacheEntry {
|
|
42
|
+
at: number;
|
|
43
|
+
data: EnrichedRecommendedExtension[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let cache: CacheEntry | null = null;
|
|
47
|
+
|
|
48
|
+
/** Invalidate the recommended-extensions cache. */
|
|
49
|
+
export function invalidateRecommendedCache(): void {
|
|
50
|
+
cache = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse a pi install source into a lookup key for matching against
|
|
55
|
+
* listInstalled() results.
|
|
56
|
+
*
|
|
57
|
+
* Supported forms (matches pi's DefaultPackageManager.parseSource):
|
|
58
|
+
* npm:<name>[@<version>]
|
|
59
|
+
* git@<host>:<owner>/<repo>.git
|
|
60
|
+
* git:<host>/<owner>/<repo>[#<ref>]
|
|
61
|
+
* https://<host>/<owner>/<repo>[.git][#<ref>]
|
|
62
|
+
*
|
|
63
|
+
* Returns:
|
|
64
|
+
* { kind: "npm", name } for npm sources
|
|
65
|
+
* { kind: "git", host, owner, repo } for git sources
|
|
66
|
+
* { kind: "raw", source } for anything else (local paths)
|
|
67
|
+
*
|
|
68
|
+
* Source-matching logic lives in
|
|
69
|
+
* `@blackbelt-technology/pi-dashboard-shared/source-matching.js` so the
|
|
70
|
+
* Electron wizard's bootstrap enricher can apply the same rules without
|
|
71
|
+
* depending on the server runtime. We re-export above so existing
|
|
72
|
+
* imports from this module keep working.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/** Read pi's project-local `.pi/settings.json` (if any) for the given cwd. */
|
|
76
|
+
function readLocalSources(cwd: string): string[] {
|
|
77
|
+
const settingsPath = path.join(cwd, ".pi", "settings.json");
|
|
78
|
+
try {
|
|
79
|
+
if (!fs.existsSync(settingsPath)) return [];
|
|
80
|
+
const raw = fs.readFileSync(settingsPath, "utf-8").trim();
|
|
81
|
+
if (!raw) return [];
|
|
82
|
+
const data = JSON.parse(raw);
|
|
83
|
+
const pkgs = Array.isArray(data?.packages) ? (data.packages as unknown[]) : [];
|
|
84
|
+
return pkgs.filter((p): p is string => typeof p === "string");
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Collect active package sources from both the user's global
|
|
91
|
+
* `~/.pi/agent/settings.json` and the project's `<cwd>/.pi/settings.json`.
|
|
92
|
+
* Mirrors pi's SettingsManager behavior: a package is "active" in pi if
|
|
93
|
+
* it appears in EITHER scope's packages[] list. */
|
|
94
|
+
function readActiveSources(cwd?: string): string[] {
|
|
95
|
+
const sources: string[] = [];
|
|
96
|
+
|
|
97
|
+
const globalPath = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(globalPath)) {
|
|
100
|
+
const raw = fs.readFileSync(globalPath, "utf-8").trim();
|
|
101
|
+
if (raw) {
|
|
102
|
+
const data = JSON.parse(raw);
|
|
103
|
+
const pkgs = Array.isArray(data?.packages) ? (data.packages as unknown[]) : [];
|
|
104
|
+
for (const p of pkgs) if (typeof p === "string") sources.push(p);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
/* ignore corrupt global settings */
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cwd) {
|
|
112
|
+
for (const p of readLocalSources(cwd)) sources.push(p);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return sources;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function semverOlder(installed: string | undefined, latest: string | undefined): boolean {
|
|
119
|
+
if (!installed || !latest) return false;
|
|
120
|
+
if (installed === latest) return false;
|
|
121
|
+
// Very conservative comparison: if they differ textually, assume an
|
|
122
|
+
// update may be available. The Packages tab's check-updates flow can
|
|
123
|
+
// resolve the definitive answer.
|
|
124
|
+
return installed !== latest;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function enrichEntry(
|
|
128
|
+
entry: RecommendedExtension,
|
|
129
|
+
installedGlobal: Array<{ source: string; installedPath?: string }>,
|
|
130
|
+
installedLocal: Array<{ source: string; installedPath?: string }>,
|
|
131
|
+
activeSources: string[],
|
|
132
|
+
): Promise<EnrichedRecommendedExtension> {
|
|
133
|
+
const key = parseSourceKey(entry.source);
|
|
134
|
+
let meta: PackageMeta | null = null;
|
|
135
|
+
|
|
136
|
+
if (key.kind === "npm") {
|
|
137
|
+
meta = await fetchPackageMeta(key.name);
|
|
138
|
+
} else if (key.kind === "git" && key.host.toLowerCase() === "github.com") {
|
|
139
|
+
meta = await fetchGithubPackageJson(key.owner, key.repo);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const description = meta?.description ?? entry.fallbackDescription;
|
|
143
|
+
const version = meta?.version;
|
|
144
|
+
|
|
145
|
+
const inGlobal = installedGlobal.some((p) => sourcesMatch(p.source, entry.source));
|
|
146
|
+
const inLocal = installedLocal.some((p) => sourcesMatch(p.source, entry.source));
|
|
147
|
+
const installedScope: "global" | "local" | null = inGlobal
|
|
148
|
+
? "global"
|
|
149
|
+
: inLocal
|
|
150
|
+
? "local"
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
const activeInPi = activeSources.some((s) => sourcesMatch(s, entry.source));
|
|
154
|
+
|
|
155
|
+
// Best-effort update indicator: for npm sources, try to read the installed
|
|
156
|
+
// package.json version and compare to the live registry version. For git
|
|
157
|
+
// sources we currently don't track ref pins, so updateAvailable defaults
|
|
158
|
+
// to false (the Packages-tab check-updates action handles this separately).
|
|
159
|
+
let updateAvailable = false;
|
|
160
|
+
if (version && key.kind === "npm" && installedScope) {
|
|
161
|
+
const installed = inGlobal ? installedGlobal : installedLocal;
|
|
162
|
+
const match = installed.find((p) => sourcesMatch(p.source, entry.source));
|
|
163
|
+
if (match?.installedPath) {
|
|
164
|
+
try {
|
|
165
|
+
const pj = path.join(match.installedPath, "package.json");
|
|
166
|
+
if (fs.existsSync(pj)) {
|
|
167
|
+
const parsed = JSON.parse(fs.readFileSync(pj, "utf-8"));
|
|
168
|
+
updateAvailable = semverOlder(parsed?.version, version);
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
/* ignore */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...entry,
|
|
178
|
+
description,
|
|
179
|
+
version,
|
|
180
|
+
installed: { scope: installedScope },
|
|
181
|
+
activeInPi,
|
|
182
|
+
updateAvailable,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function registerRecommendedRoutes(
|
|
187
|
+
fastify: FastifyInstance,
|
|
188
|
+
deps: { packageManagerWrapper: PackageManagerWrapper },
|
|
189
|
+
): void {
|
|
190
|
+
fastify.get("/api/packages/recommended", async () => {
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (cache && now - cache.at < CACHE_TTL_MS) {
|
|
193
|
+
return { success: true, data: { recommended: cache.data } } satisfies ApiResponse<{
|
|
194
|
+
recommended: EnrichedRecommendedExtension[];
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Run global + local listInstalled in parallel to halve cold-start
|
|
199
|
+
// latency. On Windows where each call instantiates pi's
|
|
200
|
+
// DefaultPackageManager (1-3s cold), sequential awaits were making
|
|
201
|
+
// the "Loading recommended extensions" spinner stick for ~15s.
|
|
202
|
+
const [installedGlobalRes, installedLocalRes] = await Promise.allSettled([
|
|
203
|
+
deps.packageManagerWrapper.listInstalled("global"),
|
|
204
|
+
deps.packageManagerWrapper.listInstalled("local"),
|
|
205
|
+
]);
|
|
206
|
+
const installedGlobal = (installedGlobalRes.status === "fulfilled" ? installedGlobalRes.value : []) as Array<{ source: string; installedPath?: string }>;
|
|
207
|
+
const installedLocal = (installedLocalRes.status === "fulfilled" ? installedLocalRes.value : []) as Array<{ source: string; installedPath?: string }>;
|
|
208
|
+
|
|
209
|
+
// Include both global + project-local settings.json `packages[]`.
|
|
210
|
+
// The server's CWD is a reasonable proxy for the active project.
|
|
211
|
+
const activeSources = readActiveSources(process.cwd());
|
|
212
|
+
|
|
213
|
+
const enriched = await Promise.all(
|
|
214
|
+
RECOMMENDED_EXTENSIONS.map((entry) =>
|
|
215
|
+
enrichEntry(entry, installedGlobal, installedLocal, activeSources),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
cache = { at: now, data: enriched };
|
|
220
|
+
|
|
221
|
+
return { success: true, data: { recommended: enriched } } satisfies ApiResponse<{
|
|
222
|
+
recommended: EnrichedRecommendedExtension[];
|
|
223
|
+
}>;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
|
|
|
5
5
|
import type { SessionManager } from "../memory-session-manager.js";
|
|
6
6
|
import type { PreferencesStore } from "../preferences-store.js";
|
|
7
7
|
import type { MetaPersistence } from "../meta-persistence.js";
|
|
8
|
+
import type { DirectoryService } from "../directory-service.js";
|
|
8
9
|
import type { ServerConfig } from "../server.js";
|
|
9
10
|
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
10
11
|
import type { NetworkGuard } from "./route-deps.js";
|
|
@@ -12,7 +13,8 @@ import { detectEditors, EDITORS } from "../editor-registry.js";
|
|
|
12
13
|
import { detectCodeServerBinary, resetDetectionCache } from "../editor-detection.js";
|
|
13
14
|
import { readConfigRedacted, writeConfigPartial } from "../config-api.js";
|
|
14
15
|
import { createTunnel, deleteTunnel, getTunnelStatus } from "../tunnel.js";
|
|
15
|
-
import {
|
|
16
|
+
import { spawnRestart } from "../restart-helper.js";
|
|
17
|
+
import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
16
18
|
import path from "node:path";
|
|
17
19
|
import os from "node:os";
|
|
18
20
|
import { localhostGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
|
|
@@ -27,9 +29,10 @@ export function registerSystemRoutes(
|
|
|
27
29
|
config: ServerConfig;
|
|
28
30
|
networkGuard: NetworkGuard;
|
|
29
31
|
version?: string;
|
|
32
|
+
directoryService?: DirectoryService;
|
|
30
33
|
},
|
|
31
34
|
) {
|
|
32
|
-
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version } = deps;
|
|
35
|
+
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService } = deps;
|
|
33
36
|
const serverStartTime = Date.now();
|
|
34
37
|
|
|
35
38
|
// Editor detection endpoint
|
|
@@ -127,6 +130,9 @@ export function registerSystemRoutes(
|
|
|
127
130
|
await (fastify as any)._reloadAuth(reloaded.auth);
|
|
128
131
|
}
|
|
129
132
|
}
|
|
133
|
+
if (partial.openspec !== undefined && directoryService) {
|
|
134
|
+
directoryService.reconfigurePolling(reloaded.openspec);
|
|
135
|
+
}
|
|
130
136
|
|
|
131
137
|
return { success: true, restartRequired: result.restartRequired };
|
|
132
138
|
},
|
|
@@ -147,7 +153,9 @@ export function registerSystemRoutes(
|
|
|
147
153
|
});
|
|
148
154
|
|
|
149
155
|
fastify.post("/api/tunnel-disconnect", async () => {
|
|
150
|
-
|
|
156
|
+
// Pass port so orphan zrok processes bound to this endpoint are also
|
|
157
|
+
// swept (not just the one we tracked via pid-file).
|
|
158
|
+
await deleteTunnel(config.port);
|
|
151
159
|
return { ok: true };
|
|
152
160
|
});
|
|
153
161
|
|
|
@@ -186,6 +194,9 @@ export function registerSystemRoutes(
|
|
|
186
194
|
async () => {
|
|
187
195
|
metaPersistence.flushAll();
|
|
188
196
|
preferencesStore.flush();
|
|
197
|
+
// Tear down the zrok tunnel (and sweep orphans on our port) so restarts
|
|
198
|
+
// don't leak reservations that leave stale URLs backed by nothing.
|
|
199
|
+
try { await deleteTunnel(config.port); } catch { /* best-effort */ }
|
|
189
200
|
setTimeout(() => process.exit(0), 100);
|
|
190
201
|
return { ok: true };
|
|
191
202
|
},
|
|
@@ -199,47 +210,32 @@ export function registerSystemRoutes(
|
|
|
199
210
|
metaPersistence.flushAll();
|
|
200
211
|
preferencesStore.flush();
|
|
201
212
|
|
|
213
|
+
// Tear down tunnel before spawning the replacement process so the new
|
|
214
|
+
// server doesn't race an orphan zrok agent on the same port.
|
|
215
|
+
try { await deleteTunnel(config.port); } catch { /* best-effort */ }
|
|
216
|
+
|
|
202
217
|
const cliPath = process.argv[1];
|
|
203
218
|
if (!cliPath) return { ok: false, error: "Cannot determine CLI path" };
|
|
204
219
|
|
|
205
220
|
// Find the TypeScript loader from process.execArgv (--import <loader>)
|
|
206
221
|
const importIdx = process.execArgv.indexOf("--import");
|
|
207
|
-
const
|
|
222
|
+
const loader = importIdx >= 0 ? (process.execArgv[importIdx + 1] ?? "") : "";
|
|
208
223
|
|
|
209
224
|
// Allow overriding dev mode via request body
|
|
210
225
|
const useDev = request.body?.dev ?? config.dev;
|
|
211
|
-
const
|
|
212
|
-
if (useDev)
|
|
213
|
-
|
|
214
|
-
// Spawn a shell script that:
|
|
215
|
-
// 1. Waits for the old server's port to be free (up to 10s)
|
|
216
|
-
// 2. Starts the new server
|
|
217
|
-
// 3. Verifies health (up to 10s)
|
|
218
|
-
// 4. If health check fails, logs error
|
|
219
|
-
const port = config.port;
|
|
220
|
-
const nodeCmd = `${JSON.stringify(process.execPath)} ${loaderArgs.map(a => JSON.stringify(a)).join(" ")} ${JSON.stringify(cliPath)} ${args.join(" ")}`;
|
|
221
|
-
const script = [
|
|
222
|
-
// Wait for port to be free
|
|
223
|
-
`for i in $(seq 1 20); do`,
|
|
224
|
-
` lsof -i :${port} -sTCP:LISTEN >/dev/null 2>&1 || break`,
|
|
225
|
-
` sleep 0.5`,
|
|
226
|
-
`done`,
|
|
227
|
-
// Start new server
|
|
228
|
-
nodeCmd,
|
|
229
|
-
// Verify health
|
|
230
|
-
`for i in $(seq 1 20); do`,
|
|
231
|
-
` curl -sf http://localhost:${port}/api/health >/dev/null 2>&1 && exit 0`,
|
|
232
|
-
` sleep 0.5`,
|
|
233
|
-
`done`,
|
|
234
|
-
`echo "[dashboard] Restart health check failed" >&2`,
|
|
235
|
-
].join("\n");
|
|
226
|
+
const extraArgs: string[] = [];
|
|
227
|
+
if (useDev) extraArgs.push("--dev");
|
|
236
228
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
229
|
+
// Cross-platform restart: spawns a detached Node orchestrator that
|
|
230
|
+
// polls the port via net, spawns the new server, polls /api/health
|
|
231
|
+
// via http. No dependency on sh/lsof/curl — works on Windows too.
|
|
232
|
+
// See change: fix-windows-server-parity.
|
|
233
|
+
spawnRestart({
|
|
234
|
+
cliPath,
|
|
235
|
+
loader,
|
|
236
|
+
port: config.port,
|
|
237
|
+
extraArgs,
|
|
241
238
|
});
|
|
242
|
-
child.unref();
|
|
243
239
|
|
|
244
240
|
setTimeout(() => process.exit(0), 200);
|
|
245
241
|
return { ok: true };
|