@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,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider probe — ping a custom LLM provider's base URL + API key to verify the
|
|
3
|
+
* combination is reachable and authenticated. Used by `POST /api/providers/test`
|
|
4
|
+
* (client Test button) and re-used by the bridge's startup discovery path via
|
|
5
|
+
* the same per-API request builders.
|
|
6
|
+
*
|
|
7
|
+
* Pure helpers first (`buildProbeRequest`, `resolveProbeApiKey`), then the
|
|
8
|
+
* I/O-bearing `probeProvider`. All responses are scrubbed to never echo the
|
|
9
|
+
* resolved api key.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "providers.json");
|
|
17
|
+
const REDACTED = "***";
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
19
|
+
const MAX_ERROR_BODY_CHARS = 500;
|
|
20
|
+
const SAMPLE_LIMIT = 5;
|
|
21
|
+
|
|
22
|
+
// -- Types ----------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type ProbeApi =
|
|
25
|
+
| "openai-completions"
|
|
26
|
+
| "openai-responses"
|
|
27
|
+
| "anthropic-messages"
|
|
28
|
+
| "google-generative-ai";
|
|
29
|
+
|
|
30
|
+
export interface ProbeInput {
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
apiKey: string;
|
|
33
|
+
api: ProbeApi;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProbeRequest {
|
|
38
|
+
url: string;
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ProbeResult =
|
|
43
|
+
| { ok: true; status: number; modelCount: number; sample: string[] }
|
|
44
|
+
| { ok: false; status?: number; error: string };
|
|
45
|
+
|
|
46
|
+
interface StoredProviderEntry {
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
apiKey: string;
|
|
49
|
+
api?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -- Pure: build per-API-type probe request --------------------------------
|
|
53
|
+
|
|
54
|
+
function stripTrailingSlash(url: string): string {
|
|
55
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildProbeRequest(input: {
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
apiKey: string;
|
|
61
|
+
api: ProbeApi;
|
|
62
|
+
}): ProbeRequest {
|
|
63
|
+
const base = stripTrailingSlash(input.baseUrl);
|
|
64
|
+
switch (input.api) {
|
|
65
|
+
case "openai-completions":
|
|
66
|
+
case "openai-responses":
|
|
67
|
+
return {
|
|
68
|
+
url: `${base}/models`,
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
case "anthropic-messages":
|
|
75
|
+
return {
|
|
76
|
+
url: `${base}/v1/models`,
|
|
77
|
+
headers: {
|
|
78
|
+
"x-api-key": input.apiKey,
|
|
79
|
+
"anthropic-version": "2023-06-01",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
case "google-generative-ai":
|
|
84
|
+
return {
|
|
85
|
+
url: `${base}/models?key=${encodeURIComponent(input.apiKey)}`,
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Unsupported api type: ${String(input.api)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// -- Pure: resolve an apiKey value (literal / $ENV / *** REDACTED) --------
|
|
96
|
+
|
|
97
|
+
export type ProvidersReader = () => Record<string, StoredProviderEntry>;
|
|
98
|
+
|
|
99
|
+
export function readProvidersFromDisk(): Record<string, StoredProviderEntry> {
|
|
100
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
103
|
+
return raw.providers ?? {};
|
|
104
|
+
} catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ResolveResult =
|
|
110
|
+
| { ok: true; key: string }
|
|
111
|
+
| { ok: false; error: string };
|
|
112
|
+
|
|
113
|
+
export function resolveProbeApiKey(args: {
|
|
114
|
+
apiKey: string;
|
|
115
|
+
name?: string;
|
|
116
|
+
readProviders: ProvidersReader;
|
|
117
|
+
}): ResolveResult {
|
|
118
|
+
let raw = args.apiKey;
|
|
119
|
+
|
|
120
|
+
if (!raw) {
|
|
121
|
+
return { ok: false, error: "apiKey is required" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// REDACTED sentinel: look up the real key in providers.json by name
|
|
125
|
+
if (raw === REDACTED) {
|
|
126
|
+
if (!args.name) {
|
|
127
|
+
return { ok: false, error: "No provider name given for saved API key lookup" };
|
|
128
|
+
}
|
|
129
|
+
const providers = args.readProviders();
|
|
130
|
+
const entry = providers[args.name];
|
|
131
|
+
if (!entry) {
|
|
132
|
+
return { ok: false, error: `No saved API key for provider "${args.name}"` };
|
|
133
|
+
}
|
|
134
|
+
raw = entry.apiKey;
|
|
135
|
+
if (!raw) {
|
|
136
|
+
return { ok: false, error: `Stored API key for "${args.name}" is empty` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// $ENV_VAR indirection
|
|
141
|
+
if (raw.startsWith("$")) {
|
|
142
|
+
const envName = raw.slice(1);
|
|
143
|
+
const value = process.env[envName];
|
|
144
|
+
if (!value) {
|
|
145
|
+
return { ok: false, error: `Environment variable ${envName} is not set` };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, key: value };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { ok: true, key: raw };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -- Helpers --------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function redactErrorText(text: string, apiKey: string): string {
|
|
156
|
+
// Belt-and-braces: never let the resolved api key leak back to the caller.
|
|
157
|
+
let out = text;
|
|
158
|
+
if (apiKey && out.includes(apiKey)) {
|
|
159
|
+
out = out.split(apiKey).join("[REDACTED]");
|
|
160
|
+
}
|
|
161
|
+
return out.length > MAX_ERROR_BODY_CHARS ? out.slice(0, MAX_ERROR_BODY_CHARS) : out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function extractModelIds(body: any): string[] {
|
|
165
|
+
// OpenAI-style { data: [{ id }, ...] }
|
|
166
|
+
if (body && Array.isArray(body.data)) {
|
|
167
|
+
return body.data
|
|
168
|
+
.filter((m: any) => m && typeof m.id === "string")
|
|
169
|
+
.map((m: any) => m.id as string);
|
|
170
|
+
}
|
|
171
|
+
// Google-style { models: [{ name: "models/gemini-..." }] }
|
|
172
|
+
if (body && Array.isArray(body.models)) {
|
|
173
|
+
return body.models
|
|
174
|
+
.filter((m: any) => m && typeof m.name === "string")
|
|
175
|
+
.map((m: any) => (m.name as string).replace(/^models\//, ""));
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// -- I/O: probe ----------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export async function probeProvider(input: ProbeInput): Promise<ProbeResult> {
|
|
183
|
+
let req: ProbeRequest;
|
|
184
|
+
try {
|
|
185
|
+
req = buildProbeRequest(input);
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(req.url, {
|
|
196
|
+
method: "GET",
|
|
197
|
+
headers: req.headers,
|
|
198
|
+
signal: controller.signal,
|
|
199
|
+
});
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
let bodyText = "";
|
|
204
|
+
try {
|
|
205
|
+
bodyText = await response.text();
|
|
206
|
+
} catch {
|
|
207
|
+
bodyText = "";
|
|
208
|
+
}
|
|
209
|
+
const excerpt = redactErrorText(
|
|
210
|
+
bodyText || response.statusText || `HTTP ${response.status}`,
|
|
211
|
+
input.apiKey,
|
|
212
|
+
);
|
|
213
|
+
return { ok: false, status: response.status, error: excerpt };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let body: any = null;
|
|
217
|
+
try {
|
|
218
|
+
body = await response.json();
|
|
219
|
+
} catch {
|
|
220
|
+
body = null;
|
|
221
|
+
}
|
|
222
|
+
const ids = extractModelIds(body);
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
status: response.status,
|
|
226
|
+
modelCount: ids.length,
|
|
227
|
+
sample: ids.slice(0, SAMPLE_LIMIT),
|
|
228
|
+
};
|
|
229
|
+
} catch (err: any) {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
const message = err?.message ?? String(err);
|
|
232
|
+
return { ok: false, error: redactErrorText(message, input.apiKey) };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform restart helper for POST /api/restart.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the previous `sh -c` script that depended on `lsof` and `curl` —
|
|
5
|
+
* neither of which exists on Windows. The new implementation spawns a
|
|
6
|
+
* detached plain-Node orchestrator (via `node -e`) that:
|
|
7
|
+
* 1. Polls the port via net.createConnection until free
|
|
8
|
+
* 2. Spawns the new server with the same loader + args as the current run
|
|
9
|
+
* 3. Polls /api/health via http.get until it returns ok
|
|
10
|
+
* 4. On failure, appends a line to ~/.pi/dashboard/restart.log
|
|
11
|
+
*
|
|
12
|
+
* See change: fix-windows-server-parity.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface RestartParams {
|
|
19
|
+
/** Absolute path to the server CLI (typically process.argv[1]) */
|
|
20
|
+
cliPath: string;
|
|
21
|
+
/** Loader value from --import (e.g. file:// URL). Empty string = none. */
|
|
22
|
+
loader: string;
|
|
23
|
+
/** Port the server listens on */
|
|
24
|
+
port: number;
|
|
25
|
+
/** Extra args to pass to `cli start` (e.g. ["--dev"]) */
|
|
26
|
+
extraArgs: string[];
|
|
27
|
+
/** Override Node binary (defaults to process.execPath) */
|
|
28
|
+
execPath?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the JS source (to run via `node -e`) that performs the restart
|
|
33
|
+
* orchestration. Exported for testing. Pure function — no I/O.
|
|
34
|
+
*/
|
|
35
|
+
export function buildOrchestratorScript(params: RestartParams): string {
|
|
36
|
+
const execPath = params.execPath ?? process.execPath;
|
|
37
|
+
const logPath = path.join(os.homedir(), ".pi", "dashboard", "restart.log");
|
|
38
|
+
const spawnArgs: string[] = [];
|
|
39
|
+
if (params.loader) {
|
|
40
|
+
spawnArgs.push("--import", params.loader);
|
|
41
|
+
}
|
|
42
|
+
spawnArgs.push(params.cliPath, "start", ...params.extraArgs);
|
|
43
|
+
|
|
44
|
+
// The script runs in a fresh Node process. Keep it self-contained and use
|
|
45
|
+
// only built-ins (net, http, fs, child_process). JSON.stringify is used to
|
|
46
|
+
// embed strings safely (handles quotes, backslashes, Windows paths).
|
|
47
|
+
return `
|
|
48
|
+
const net = require("node:net");
|
|
49
|
+
const http = require("node:http");
|
|
50
|
+
const { spawn } = require("node:child_process"); // ban:child_process-ok — runs in a detached 'node -e' process, not in-host
|
|
51
|
+
const fs = require("node:fs");
|
|
52
|
+
const path = require("node:path");
|
|
53
|
+
|
|
54
|
+
const PORT = ${params.port};
|
|
55
|
+
const EXEC = ${JSON.stringify(execPath)};
|
|
56
|
+
const ARGS = ${JSON.stringify(spawnArgs)};
|
|
57
|
+
const LOG_PATH = ${JSON.stringify(logPath)};
|
|
58
|
+
|
|
59
|
+
function log(msg) {
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
|
|
62
|
+
fs.appendFileSync(LOG_PATH, "[" + new Date().toISOString() + "] " + msg + "\\n");
|
|
63
|
+
} catch (_) { /* ignore */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function portFree(port) {
|
|
67
|
+
return new Promise(resolve => {
|
|
68
|
+
const sock = net.createConnection({ port, host: "127.0.0.1" });
|
|
69
|
+
let done = false;
|
|
70
|
+
const finish = (free) => { if (done) return; done = true; try { sock.destroy(); } catch(_){} resolve(free); };
|
|
71
|
+
sock.setTimeout(500);
|
|
72
|
+
sock.once("connect", () => finish(false));
|
|
73
|
+
sock.once("error", () => finish(true));
|
|
74
|
+
sock.once("timeout", () => finish(true));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function healthOk() {
|
|
79
|
+
return new Promise(resolve => {
|
|
80
|
+
const req = http.get({ host: "127.0.0.1", port: PORT, path: "/api/health", timeout: 1000 }, res => {
|
|
81
|
+
resolve(res.statusCode === 200);
|
|
82
|
+
res.resume();
|
|
83
|
+
});
|
|
84
|
+
req.once("error", () => resolve(false));
|
|
85
|
+
req.once("timeout", () => { req.destroy(); resolve(false); });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
90
|
+
|
|
91
|
+
(async () => {
|
|
92
|
+
// 1. Wait for port to be free (up to 10s)
|
|
93
|
+
for (let i = 0; i < 20; i++) {
|
|
94
|
+
if (await portFree(PORT)) break;
|
|
95
|
+
await sleep(500);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Spawn new server
|
|
99
|
+
const child = spawn(EXEC, ARGS, { detached: true, stdio: "ignore", env: process.env });
|
|
100
|
+
child.unref();
|
|
101
|
+
|
|
102
|
+
// 3. Poll health (up to 10s)
|
|
103
|
+
for (let i = 0; i < 20; i++) {
|
|
104
|
+
await sleep(500);
|
|
105
|
+
if (await healthOk()) {
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log("restart failed: new server did not respond to /api/health within 10s");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
})();
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Spawn a detached orchestrator child that restarts the server.
|
|
118
|
+
* Returns immediately (the caller is expected to exit shortly after).
|
|
119
|
+
*/
|
|
120
|
+
export function spawnRestart(params: RestartParams): void {
|
|
121
|
+
const script = buildOrchestratorScript(params);
|
|
122
|
+
const execPath = params.execPath ?? process.execPath;
|
|
123
|
+
const child = spawn(execPath, ["-e", script], {
|
|
124
|
+
detached: true,
|
|
125
|
+
stdio: "ignore",
|
|
126
|
+
env: { ...process.env },
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
});
|
|
129
|
+
child.unref();
|
|
130
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap REST API routes: `/api/bootstrap/status`, `/api/bootstrap/upgrade-pi`,
|
|
3
|
+
* `/api/bootstrap/retry`.
|
|
4
|
+
*
|
|
5
|
+
* The routes are thin — they read/write the injected `BootstrapStateStore`
|
|
6
|
+
* and delegate actual install work to the supplied `trigger` callbacks.
|
|
7
|
+
* Keeping triggers as callbacks lets the CLI wire them to `bootstrapInstall`
|
|
8
|
+
* while tests wire them to mocks.
|
|
9
|
+
*
|
|
10
|
+
* See change: unified-bootstrap-install.
|
|
11
|
+
*/
|
|
12
|
+
import type { FastifyInstance } from "fastify";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
15
|
+
import type { NetworkGuard } from "./route-deps.js";
|
|
16
|
+
|
|
17
|
+
export interface BootstrapRouteDeps {
|
|
18
|
+
bootstrapState: BootstrapStateStore;
|
|
19
|
+
networkGuard: NetworkGuard;
|
|
20
|
+
/**
|
|
21
|
+
* Trigger a pi upgrade. Called when `POST /api/bootstrap/upgrade-pi`
|
|
22
|
+
* succeeds the 409-gate. Implementation is responsible for setting
|
|
23
|
+
* state to "installing" before returning, and to "ready"/"failed"
|
|
24
|
+
* when complete. Must NOT throw synchronously.
|
|
25
|
+
*/
|
|
26
|
+
triggerUpgradePi: (ticketId: string) => Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Trigger a retry of the last bootstrap install. Called when
|
|
29
|
+
* `POST /api/bootstrap/retry` succeeds the 409-gate. Implementation
|
|
30
|
+
* should re-run the same install that failed and flip status back to
|
|
31
|
+
* "installing" before returning.
|
|
32
|
+
*/
|
|
33
|
+
triggerRetry: (ticketId: string) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerBootstrapRoutes(
|
|
37
|
+
fastify: FastifyInstance,
|
|
38
|
+
deps: BootstrapRouteDeps,
|
|
39
|
+
): void {
|
|
40
|
+
const { bootstrapState, networkGuard, triggerUpgradePi, triggerRetry } = deps;
|
|
41
|
+
|
|
42
|
+
fastify.get(
|
|
43
|
+
"/api/bootstrap/status",
|
|
44
|
+
{ preHandler: networkGuard },
|
|
45
|
+
async () => {
|
|
46
|
+
return bootstrapState.get();
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
fastify.post(
|
|
51
|
+
"/api/bootstrap/upgrade-pi",
|
|
52
|
+
{ preHandler: networkGuard },
|
|
53
|
+
async (_request, reply) => {
|
|
54
|
+
const current = bootstrapState.get();
|
|
55
|
+
if (current.status === "installing") {
|
|
56
|
+
return reply.code(409).send({
|
|
57
|
+
error: "bootstrap is currently installing; try again when status becomes ready or failed",
|
|
58
|
+
status: current.status,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const ticketId = randomUUID();
|
|
62
|
+
// Fire-and-forget. Errors flow through state.
|
|
63
|
+
void triggerUpgradePi(ticketId).catch((err) => {
|
|
64
|
+
console.error("[bootstrap-routes] upgrade-pi trigger failed:", err);
|
|
65
|
+
});
|
|
66
|
+
return reply.code(202).send({ ticketId, status: "accepted" });
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
fastify.post(
|
|
71
|
+
"/api/bootstrap/retry",
|
|
72
|
+
{ preHandler: networkGuard },
|
|
73
|
+
async (_request, reply) => {
|
|
74
|
+
const current = bootstrapState.get();
|
|
75
|
+
if (current.status !== "failed") {
|
|
76
|
+
return reply.code(409).send({
|
|
77
|
+
error: "retry is only valid when status is failed",
|
|
78
|
+
status: current.status,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const ticketId = randomUUID();
|
|
82
|
+
void triggerRetry(ticketId).catch((err) => {
|
|
83
|
+
console.error("[bootstrap-routes] retry trigger failed:", err);
|
|
84
|
+
});
|
|
85
|
+
return reply.code(202).send({ ticketId, status: "accepted" });
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { SessionManager } from "../memory-session-manager.js";
|
|
|
6
6
|
import type { PreferencesStore } from "../preferences-store.js";
|
|
7
7
|
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
8
8
|
import type { NetworkGuard } from "./route-deps.js";
|
|
9
|
-
import { listDirectories } from "../browse.js";
|
|
9
|
+
import { listDirectories, createDirectory } from "../browse.js";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import fs from "node:fs/promises";
|
|
12
12
|
|
|
@@ -21,12 +21,15 @@ export function registerFileRoutes(
|
|
|
21
21
|
const { sessionManager, preferencesStore, networkGuard } = deps;
|
|
22
22
|
|
|
23
23
|
// Directory browse endpoint
|
|
24
|
-
fastify.get<{ Querystring: { path?: string } }>(
|
|
24
|
+
fastify.get<{ Querystring: { path?: string; q?: string } }>(
|
|
25
25
|
"/api/browse",
|
|
26
26
|
{ preHandler: networkGuard },
|
|
27
27
|
async (request) => {
|
|
28
28
|
try {
|
|
29
|
-
const result = await listDirectories(
|
|
29
|
+
const result = await listDirectories(
|
|
30
|
+
request.query.path || undefined,
|
|
31
|
+
request.query.q || undefined,
|
|
32
|
+
);
|
|
30
33
|
return { success: true, data: result } satisfies ApiResponse;
|
|
31
34
|
} catch {
|
|
32
35
|
return { success: false, error: "directory not found" } satisfies ApiResponse;
|
|
@@ -34,6 +37,30 @@ export function registerFileRoutes(
|
|
|
34
37
|
},
|
|
35
38
|
);
|
|
36
39
|
|
|
40
|
+
// Directory create endpoint
|
|
41
|
+
fastify.post<{ Body: { parent?: unknown; name?: unknown } }>(
|
|
42
|
+
"/api/browse/mkdir",
|
|
43
|
+
{ preHandler: networkGuard },
|
|
44
|
+
async (request, reply) => {
|
|
45
|
+
const body = request.body ?? {};
|
|
46
|
+
const parent = typeof body.parent === "string" ? body.parent : "";
|
|
47
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
48
|
+
try {
|
|
49
|
+
const newPath = await createDirectory(parent, name);
|
|
50
|
+
return { success: true, data: { path: newPath } } satisfies ApiResponse;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : "mkdir failed";
|
|
53
|
+
// Map known errors to status codes; unknown → 500
|
|
54
|
+
if (msg === "invalid name") reply.code(400);
|
|
55
|
+
else if (msg === "parent not found") reply.code(404);
|
|
56
|
+
else if (msg === "parent is not a directory") reply.code(400);
|
|
57
|
+
else if (msg === "already exists") reply.code(409);
|
|
58
|
+
else reply.code(500);
|
|
59
|
+
return { success: false, error: msg } satisfies ApiResponse;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
37
64
|
// File read endpoint — read file content or list directory
|
|
38
65
|
fastify.get<{ Querystring: { cwd?: string; path?: string } }>(
|
|
39
66
|
"/api/file",
|
|
@@ -8,9 +8,19 @@ import type { DirectoryService } from "../directory-service.js";
|
|
|
8
8
|
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
9
9
|
import type { NetworkGuard } from "./route-deps.js";
|
|
10
10
|
import { scanOpenSpecArchive } from "../openspec-archive.js";
|
|
11
|
+
import {
|
|
12
|
+
readTasks,
|
|
13
|
+
toggleTask,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
LineMismatchError,
|
|
16
|
+
NotACheckboxError,
|
|
17
|
+
} from "../openspec-tasks.js";
|
|
11
18
|
import path from "node:path";
|
|
12
19
|
import fs from "node:fs/promises";
|
|
13
20
|
|
|
21
|
+
/** Callback to broadcast an openspec_update after a successful toggle. */
|
|
22
|
+
export type OpenSpecBroadcaster = (cwd: string) => void;
|
|
23
|
+
|
|
14
24
|
export function registerOpenSpecRoutes(
|
|
15
25
|
fastify: FastifyInstance,
|
|
16
26
|
deps: {
|
|
@@ -18,9 +28,18 @@ export function registerOpenSpecRoutes(
|
|
|
18
28
|
preferencesStore: PreferencesStore;
|
|
19
29
|
directoryService: DirectoryService;
|
|
20
30
|
networkGuard: NetworkGuard;
|
|
31
|
+
/** Optional — called after a successful toggle to trigger openspec_update. */
|
|
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;
|
|
21
40
|
},
|
|
22
41
|
) {
|
|
23
|
-
const { sessionManager, preferencesStore, directoryService, networkGuard } = deps;
|
|
42
|
+
const { sessionManager, preferencesStore, directoryService, networkGuard, onOpenSpecChanged, bootstrapState } = deps;
|
|
24
43
|
|
|
25
44
|
// OpenSpec archive listing endpoint
|
|
26
45
|
fastify.get<{ Querystring: { cwd?: string } }>(
|
|
@@ -47,6 +66,23 @@ export function registerOpenSpecRoutes(
|
|
|
47
66
|
reply.code(400);
|
|
48
67
|
return { success: false, error: "cwd parameter required" } satisfies ApiResponse;
|
|
49
68
|
}
|
|
69
|
+
// Bootstrap gate: during degraded-mode install, return empty result
|
|
70
|
+
// with a `bootstrap` field so the UI can render the "pi not yet
|
|
71
|
+
// installed" state. See change: unified-bootstrap-install §5.4.
|
|
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
|
+
}
|
|
50
86
|
const forceRefresh = request.query.refresh === "true" || request.query.refresh === "1";
|
|
51
87
|
let data = forceRefresh ? undefined : directoryService.getPiResources(cwd);
|
|
52
88
|
if (!data) {
|
|
@@ -96,4 +132,74 @@ export function registerOpenSpecRoutes(
|
|
|
96
132
|
}
|
|
97
133
|
},
|
|
98
134
|
);
|
|
135
|
+
|
|
136
|
+
// --- Tasks.md list + toggle ---
|
|
137
|
+
|
|
138
|
+
fastify.get<{ Querystring: { cwd?: string; change?: string } }>(
|
|
139
|
+
"/api/openspec/tasks",
|
|
140
|
+
{ preHandler: networkGuard },
|
|
141
|
+
async (request, reply) => {
|
|
142
|
+
const { cwd, change } = request.query;
|
|
143
|
+
if (!cwd || !change) {
|
|
144
|
+
reply.code(400);
|
|
145
|
+
return { success: false, error: "cwd and change query params required" } satisfies ApiResponse;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const tasks = await readTasks(cwd, change);
|
|
149
|
+
const groups = Array.from(new Set(tasks.map((t) => t.group).filter((g) => g.length > 0)));
|
|
150
|
+
return { success: true, data: { tasks, groups } } satisfies ApiResponse;
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
if (err instanceof NotFoundError) {
|
|
153
|
+
reply.code(404);
|
|
154
|
+
return { success: false, error: "tasks.md not found" } satisfies ApiResponse;
|
|
155
|
+
}
|
|
156
|
+
reply.code(500);
|
|
157
|
+
return { success: false, error: err?.message ?? "read error" } satisfies ApiResponse;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
fastify.post<{
|
|
163
|
+
Body: { cwd?: string; change?: string; id?: string; done?: boolean; line?: number };
|
|
164
|
+
}>(
|
|
165
|
+
"/api/openspec/tasks/toggle",
|
|
166
|
+
{ preHandler: networkGuard },
|
|
167
|
+
async (request, reply) => {
|
|
168
|
+
const body = request.body ?? {};
|
|
169
|
+
const { cwd, change, id, done, line } = body;
|
|
170
|
+
if (
|
|
171
|
+
typeof cwd !== "string" ||
|
|
172
|
+
typeof change !== "string" ||
|
|
173
|
+
typeof id !== "string" ||
|
|
174
|
+
typeof done !== "boolean" ||
|
|
175
|
+
typeof line !== "number"
|
|
176
|
+
) {
|
|
177
|
+
reply.code(400);
|
|
178
|
+
return { success: false, error: "invalid body" } satisfies ApiResponse;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const task = await toggleTask(cwd, change, id, done, line);
|
|
182
|
+
// Fire-and-forget: refresh cache + broadcast openspec_update.
|
|
183
|
+
directoryService.refreshOpenSpec(cwd).then(() => {
|
|
184
|
+
onOpenSpecChanged?.(cwd);
|
|
185
|
+
}).catch(() => {});
|
|
186
|
+
return { success: true, data: { task } } satisfies ApiResponse;
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
if (err instanceof NotFoundError) {
|
|
189
|
+
reply.code(404);
|
|
190
|
+
return { success: false, error: "tasks.md not found" } satisfies ApiResponse;
|
|
191
|
+
}
|
|
192
|
+
if (err instanceof LineMismatchError) {
|
|
193
|
+
reply.code(409);
|
|
194
|
+
return { success: false, error: "line mismatch" } satisfies ApiResponse;
|
|
195
|
+
}
|
|
196
|
+
if (err instanceof NotACheckboxError) {
|
|
197
|
+
reply.code(400);
|
|
198
|
+
return { success: false, error: "target line is not a checkbox" } satisfies ApiResponse;
|
|
199
|
+
}
|
|
200
|
+
reply.code(500);
|
|
201
|
+
return { success: false, error: err?.message ?? "toggle error" } satisfies ApiResponse;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
);
|
|
99
205
|
}
|