@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
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 +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser for Keep-a-Changelog-style markdown files.
|
|
3
|
+
*
|
|
4
|
+
* Pi (`@mariozechner/pi-coding-agent`) ships a `CHANGELOG.md` whose
|
|
5
|
+
* format is mechanically reliable:
|
|
6
|
+
* - H2 release headers: `## [<version>] - <date>`
|
|
7
|
+
* - H3 sub-section headers: `### Breaking Changes`, `### New Features`,
|
|
8
|
+
* `### Added`, `### Changed`, `### Fixed`
|
|
9
|
+
* - Bullets at column 0 starting `- `
|
|
10
|
+
* - Issue/PR links as `([#NNN](URL))` at end of bullet
|
|
11
|
+
*
|
|
12
|
+
* The parser is regex-based on purpose — a full markdown AST would
|
|
13
|
+
* add hundreds of LOC of dependency surface for marginal gain. When
|
|
14
|
+
* the file deviates from convention the parser degrades gracefully:
|
|
15
|
+
* unrecognized H3 headings are dropped from typed slots but the
|
|
16
|
+
* release's `raw` field still contains the verbatim section.
|
|
17
|
+
*
|
|
18
|
+
* Plus a 60-second mtime-keyed in-memory cache so the REST route
|
|
19
|
+
* doesn't re-parse 150 KB of markdown on every dialog open.
|
|
20
|
+
*
|
|
21
|
+
* See change: pi-update-whats-new-panel.
|
|
22
|
+
*/
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import type { ChangelogBullet, ChangelogRelease } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
|
|
25
|
+
|
|
26
|
+
/** Single line, anchors at line start: `## [<version>] - <date>` (date optional). */
|
|
27
|
+
const RELEASE_HEADER_RE = /^## \[([^\]]+)\](?:\s*-\s*(.+?))?\s*$/gm;
|
|
28
|
+
|
|
29
|
+
/** Single line, anchors at line start: `### <heading>`. */
|
|
30
|
+
const SUBSECTION_HEADER_RE = /^### (.+?)\s*$/gm;
|
|
31
|
+
|
|
32
|
+
/** End-of-bullet issue ref: `([#NNN](URL))`. */
|
|
33
|
+
const ISSUE_LINK_RE = /\(\[#(\d+)\]\((https?:\/\/[^)]+)\)\)/g;
|
|
34
|
+
|
|
35
|
+
/** Subset of YYYY-MM-DD-ish date strings we treat as "valid enough". */
|
|
36
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
37
|
+
|
|
38
|
+
/** Recognized H3 sub-section names → release-field key. */
|
|
39
|
+
const KNOWN_SUBSECTIONS: Record<string, "breaking" | "features" | "changed" | "fixed"> = {
|
|
40
|
+
"Breaking Changes": "breaking",
|
|
41
|
+
"New Features": "features",
|
|
42
|
+
"Added": "features",
|
|
43
|
+
"Changed": "changed",
|
|
44
|
+
"Fixed": "fixed",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a CHANGELOG.md text into a list of release entries, latest
|
|
49
|
+
* first. Pure function — no I/O. Returns `[]` when the input contains
|
|
50
|
+
* no recognizable H2 release headers.
|
|
51
|
+
*/
|
|
52
|
+
export function parseChangelog(markdown: string): ChangelogRelease[] {
|
|
53
|
+
if (!markdown || typeof markdown !== "string") return [];
|
|
54
|
+
|
|
55
|
+
// Locate every H2 release header and split the document by them.
|
|
56
|
+
// The regex's `lastIndex` walks the string giving us each header's
|
|
57
|
+
// (start offset, captured groups) in one pass.
|
|
58
|
+
const headers: { version: string; date: string | null; start: number; bodyStart: number }[] = [];
|
|
59
|
+
|
|
60
|
+
// Reset the global regex lastIndex to be safe across re-entrant calls.
|
|
61
|
+
RELEASE_HEADER_RE.lastIndex = 0;
|
|
62
|
+
let m: RegExpExecArray | null;
|
|
63
|
+
while ((m = RELEASE_HEADER_RE.exec(markdown)) !== null) {
|
|
64
|
+
const version = m[1].trim();
|
|
65
|
+
const dateRaw = (m[2] ?? "").trim();
|
|
66
|
+
const date = ISO_DATE_RE.test(dateRaw) ? dateRaw : null;
|
|
67
|
+
headers.push({
|
|
68
|
+
version,
|
|
69
|
+
date,
|
|
70
|
+
start: m.index,
|
|
71
|
+
bodyStart: m.index + m[0].length,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (headers.length === 0) return [];
|
|
76
|
+
|
|
77
|
+
// Sort by occurrence order (already in order since regex walks
|
|
78
|
+
// left→right). Assemble each release as the slice from this
|
|
79
|
+
// header's start to (the next header's start, or EOF).
|
|
80
|
+
const releases: ChangelogRelease[] = [];
|
|
81
|
+
for (let i = 0; i < headers.length; i++) {
|
|
82
|
+
const h = headers[i];
|
|
83
|
+
const nextStart = i + 1 < headers.length ? headers[i + 1].start : markdown.length;
|
|
84
|
+
const raw = markdown.slice(h.start, nextStart).replace(/\s+$/, "");
|
|
85
|
+
const body = markdown.slice(h.bodyStart, nextStart);
|
|
86
|
+
const sections = splitSubsections(body);
|
|
87
|
+
|
|
88
|
+
const release: ChangelogRelease = {
|
|
89
|
+
version: h.version,
|
|
90
|
+
date: h.date,
|
|
91
|
+
breaking: [],
|
|
92
|
+
features: [],
|
|
93
|
+
changed: [],
|
|
94
|
+
fixed: [],
|
|
95
|
+
raw,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
for (const sec of sections) {
|
|
99
|
+
const slot = KNOWN_SUBSECTIONS[sec.heading];
|
|
100
|
+
if (!slot) continue;
|
|
101
|
+
const bullets = extractBullets(sec.body);
|
|
102
|
+
// `features` is the merge of New Features + Added; both map to
|
|
103
|
+
// the same slot, so just push in source order.
|
|
104
|
+
release[slot].push(...bullets);
|
|
105
|
+
}
|
|
106
|
+
releases.push(release);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return releases;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Walk a release's body (text between its H2 and the next H2) and
|
|
114
|
+
* return one entry per H3 sub-heading. Text before the first H3 is
|
|
115
|
+
* dropped — pi never puts content there.
|
|
116
|
+
*/
|
|
117
|
+
function splitSubsections(body: string): { heading: string; body: string }[] {
|
|
118
|
+
const out: { heading: string; body: string }[] = [];
|
|
119
|
+
const positions: { heading: string; bodyStart: number; headerStart: number }[] = [];
|
|
120
|
+
|
|
121
|
+
SUBSECTION_HEADER_RE.lastIndex = 0;
|
|
122
|
+
let m: RegExpExecArray | null;
|
|
123
|
+
while ((m = SUBSECTION_HEADER_RE.exec(body)) !== null) {
|
|
124
|
+
positions.push({
|
|
125
|
+
heading: m[1].trim(),
|
|
126
|
+
headerStart: m.index,
|
|
127
|
+
bodyStart: m.index + m[0].length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (let i = 0; i < positions.length; i++) {
|
|
131
|
+
const p = positions[i];
|
|
132
|
+
const nextStart = i + 1 < positions.length ? positions[i + 1].headerStart : body.length;
|
|
133
|
+
out.push({ heading: p.heading, body: body.slice(p.bodyStart, nextStart) });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract bullets from a sub-section body. A bullet starts with `- `
|
|
140
|
+
* at column 0 and continues until the next column-0 `- ` line, the
|
|
141
|
+
* next H-heading line, or end-of-section.
|
|
142
|
+
*/
|
|
143
|
+
function extractBullets(body: string): ChangelogBullet[] {
|
|
144
|
+
const lines = body.split("\n");
|
|
145
|
+
const bullets: string[] = [];
|
|
146
|
+
let current: string | null = null;
|
|
147
|
+
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (/^- /.test(line)) {
|
|
150
|
+
if (current !== null) bullets.push(current);
|
|
151
|
+
current = line.slice(2);
|
|
152
|
+
} else if (current !== null) {
|
|
153
|
+
// Continuation of the current bullet only when the line is
|
|
154
|
+
// either blank-and-followed-by-content (we drop blanks) or
|
|
155
|
+
// indented. Stop on an undented non-bullet line that looks
|
|
156
|
+
// like new content.
|
|
157
|
+
if (line.trim() === "") {
|
|
158
|
+
// Skip blank lines inside a bullet block; they may separate
|
|
159
|
+
// paragraphs but don't terminate the bullet.
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (/^\s/.test(line)) {
|
|
163
|
+
current += "\n" + line.replace(/^\s+/, "");
|
|
164
|
+
} else {
|
|
165
|
+
// Undented non-bullet line: treat as end of bullet block.
|
|
166
|
+
bullets.push(current);
|
|
167
|
+
current = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (current !== null) bullets.push(current);
|
|
172
|
+
|
|
173
|
+
return bullets.map((text) => ({
|
|
174
|
+
text: text.trim(),
|
|
175
|
+
issues: extractIssues(text),
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Collect all `([#NNN](URL))` matches in a bullet's prose. */
|
|
180
|
+
function extractIssues(text: string): { num: number; url: string }[] {
|
|
181
|
+
const out: { num: number; url: string }[] = [];
|
|
182
|
+
ISSUE_LINK_RE.lastIndex = 0;
|
|
183
|
+
let m: RegExpExecArray | null;
|
|
184
|
+
while ((m = ISSUE_LINK_RE.exec(text)) !== null) {
|
|
185
|
+
const num = parseInt(m[1], 10);
|
|
186
|
+
if (!Number.isNaN(num)) out.push({ num, url: m[2] });
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Cache ──────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const CACHE_TTL_MS = 60_000;
|
|
194
|
+
|
|
195
|
+
export type ChangelogSource = "local" | "remote";
|
|
196
|
+
|
|
197
|
+
interface CacheEntry {
|
|
198
|
+
/** Result of parsing the file/text. */
|
|
199
|
+
releases: ChangelogRelease[];
|
|
200
|
+
/**
|
|
201
|
+
* mtime of the source file when this entry was created (local source
|
|
202
|
+
* only). 0 for remote entries (mtime irrelevant; ETag drives remote
|
|
203
|
+
* freshness).
|
|
204
|
+
*/
|
|
205
|
+
mtimeMs: number;
|
|
206
|
+
/** Wall-clock expiry. */
|
|
207
|
+
expiresAt: number;
|
|
208
|
+
/** ETag from a remote response, if any. Used for conditional GET. */
|
|
209
|
+
etag: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Cache key combines pkg + source so remote and local don't collide. */
|
|
213
|
+
function cacheKey(pkg: string, source: ChangelogSource): string {
|
|
214
|
+
return `${source}:${pkg}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const cache = new Map<string, CacheEntry>();
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Read + parse a CHANGELOG.md file with a 60-second mtime-keyed
|
|
221
|
+
* cache. The cache is keyed by `pkg`; the entry is invalidated when
|
|
222
|
+
* the file's mtime changes (e.g. after a fresh `npm install`) or
|
|
223
|
+
* when 60 seconds have elapsed.
|
|
224
|
+
*
|
|
225
|
+
* Returns `[]` (not throwing) for ENOENT — callers treat "not found"
|
|
226
|
+
* the same as "no releases". Other I/O errors propagate.
|
|
227
|
+
*/
|
|
228
|
+
export function readAndParseChangelog(
|
|
229
|
+
pkg: string,
|
|
230
|
+
filePath: string,
|
|
231
|
+
now: () => number = Date.now,
|
|
232
|
+
): ChangelogRelease[] {
|
|
233
|
+
let stat: fs.Stats;
|
|
234
|
+
try {
|
|
235
|
+
stat = fs.statSync(filePath);
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
if (err?.code === "ENOENT") return [];
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
const mtimeMs = stat.mtimeMs;
|
|
241
|
+
const t = now();
|
|
242
|
+
|
|
243
|
+
const key = cacheKey(pkg, "local");
|
|
244
|
+
const hit = cache.get(key);
|
|
245
|
+
if (hit && hit.mtimeMs === mtimeMs && t < hit.expiresAt) {
|
|
246
|
+
return hit.releases;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
250
|
+
const releases = parseChangelog(content);
|
|
251
|
+
cache.set(key, {
|
|
252
|
+
releases,
|
|
253
|
+
mtimeMs,
|
|
254
|
+
expiresAt: t + CACHE_TTL_MS,
|
|
255
|
+
etag: null,
|
|
256
|
+
});
|
|
257
|
+
return releases;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Cache-hit accessor for the remote-source entry. Returns the cached
|
|
262
|
+
* releases + last-known ETag. Caller decides what to do with `expired`
|
|
263
|
+
* (typically: try a conditional GET, reuse on 304).
|
|
264
|
+
*/
|
|
265
|
+
export function getCachedRemoteChangelog(
|
|
266
|
+
pkg: string,
|
|
267
|
+
now: () => number = Date.now,
|
|
268
|
+
): { releases: ChangelogRelease[]; etag: string | null; expired: boolean } | undefined {
|
|
269
|
+
const hit = cache.get(cacheKey(pkg, "remote"));
|
|
270
|
+
if (!hit) return undefined;
|
|
271
|
+
return {
|
|
272
|
+
releases: hit.releases,
|
|
273
|
+
etag: hit.etag,
|
|
274
|
+
expired: now() >= hit.expiresAt,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Store a remote fetch result. */
|
|
279
|
+
export function setRemoteChangelog(
|
|
280
|
+
pkg: string,
|
|
281
|
+
releases: ChangelogRelease[],
|
|
282
|
+
etag: string | null,
|
|
283
|
+
now: () => number = Date.now,
|
|
284
|
+
): void {
|
|
285
|
+
cache.set(cacheKey(pkg, "remote"), {
|
|
286
|
+
releases,
|
|
287
|
+
mtimeMs: 0,
|
|
288
|
+
expiresAt: now() + CACHE_TTL_MS,
|
|
289
|
+
etag,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Extend a remote cache entry's TTL without re-parsing. Used after a
|
|
295
|
+
* 304 Not Modified response.
|
|
296
|
+
*/
|
|
297
|
+
export function refreshRemoteChangelogTtl(
|
|
298
|
+
pkg: string,
|
|
299
|
+
now: () => number = Date.now,
|
|
300
|
+
): void {
|
|
301
|
+
const entry = cache.get(cacheKey(pkg, "remote"));
|
|
302
|
+
if (entry) entry.expiresAt = now() + CACHE_TTL_MS;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Clear all cache entries. Test seam + invalidation hook. */
|
|
306
|
+
export function _resetChangelogCache(): void {
|
|
307
|
+
cache.clear();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clear a single package's cache entries (both local and remote
|
|
312
|
+
* sources). Wired into PiCoreChecker.invalidate.
|
|
313
|
+
*/
|
|
314
|
+
export function invalidateChangelogCache(pkg?: string): void {
|
|
315
|
+
if (pkg === undefined) {
|
|
316
|
+
cache.clear();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
cache.delete(cacheKey(pkg, "local"));
|
|
320
|
+
cache.delete(cacheKey(pkg, "remote"));
|
|
321
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches the upstream CHANGELOG.md from GitHub raw, so the dashboard
|
|
3
|
+
* can show release notes for versions newer than the locally-installed
|
|
4
|
+
* tarball describes.
|
|
5
|
+
*
|
|
6
|
+
* Trust model identical to `pi-dev-version-check`: HTTPS, default Node
|
|
7
|
+
* trust store, 10-second timeout, env-skippable via `PI_OFFLINE`.
|
|
8
|
+
*
|
|
9
|
+
* See change: read-changelog-from-github.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
/** Discriminated result of a remote fetch attempt. */
|
|
15
|
+
export type RemoteChangelogResult =
|
|
16
|
+
| { status: "ok"; text: string; etag: string | null }
|
|
17
|
+
| { status: "not-modified" }
|
|
18
|
+
| null; // hard failure (network, parse, env-skipped) → caller falls back to local
|
|
19
|
+
|
|
20
|
+
export interface FetchRemoteChangelogOptions {
|
|
21
|
+
/** Optional ETag from a previous response. When set, sent as `If-None-Match`. */
|
|
22
|
+
etag?: string | null;
|
|
23
|
+
/** Override fetch timeout. Default 10 s. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Test seam. */
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sibling of `deriveChangelogUrl` that returns the *raw* form suitable
|
|
31
|
+
* for parsing (vs the `/blob/main/` URL which is for human viewing).
|
|
32
|
+
*
|
|
33
|
+
* Accepts the same `repository` field shapes:
|
|
34
|
+
* - `"github:org/repo"` shorthand
|
|
35
|
+
* - URL strings (`"https://github.com/org/repo.git"`, `"git@github.com:org/repo.git"`, etc.)
|
|
36
|
+
* - object form `{ url, directory? }` (monorepos)
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` for non-GitHub or unparseable input.
|
|
39
|
+
*/
|
|
40
|
+
export function deriveChangelogRawUrl(repository: unknown): string | null {
|
|
41
|
+
if (!repository) return null;
|
|
42
|
+
|
|
43
|
+
let urlStr: string | null = null;
|
|
44
|
+
let directory: string | null = null;
|
|
45
|
+
|
|
46
|
+
if (typeof repository === "string") {
|
|
47
|
+
urlStr = repository;
|
|
48
|
+
} else if (typeof repository === "object" && repository !== null) {
|
|
49
|
+
const rec = repository as Record<string, unknown>;
|
|
50
|
+
if (typeof rec.url === "string") urlStr = rec.url;
|
|
51
|
+
if (typeof rec.directory === "string" && rec.directory.length > 0) {
|
|
52
|
+
directory = rec.directory.replace(/^\/+|\/+$/g, "");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!urlStr) return null;
|
|
56
|
+
|
|
57
|
+
const m = parseGitHubUrl(urlStr);
|
|
58
|
+
if (!m) return null;
|
|
59
|
+
|
|
60
|
+
const subPath = directory ? `${directory}/` : "";
|
|
61
|
+
return `https://raw.githubusercontent.com/${m.org}/${m.repo}/main/${subPath}CHANGELOG.md`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the various GitHub URL forms used in `package.json#repository`
|
|
66
|
+
* into `{ org, repo }`. Internal helper duplicating the same logic in
|
|
67
|
+
* `changelog-fs.ts::parseGitHubUrl` — kept inline to avoid cross-module
|
|
68
|
+
* coupling for a 10-line regex.
|
|
69
|
+
*/
|
|
70
|
+
function parseGitHubUrl(s: string): { org: string; repo: string } | null {
|
|
71
|
+
const trimmed = s.trim();
|
|
72
|
+
|
|
73
|
+
// github:org/repo shorthand
|
|
74
|
+
let m = trimmed.match(/^github:([^/]+)\/([^/#]+)/i);
|
|
75
|
+
if (m) return { org: m[1], repo: stripGitSuffix(m[2]) };
|
|
76
|
+
|
|
77
|
+
// git+https://github.com/org/repo.git
|
|
78
|
+
// https://github.com/org/repo
|
|
79
|
+
// ssh://git@github.com/org/repo.git
|
|
80
|
+
// git@github.com:org/repo.git
|
|
81
|
+
m = trimmed.match(/(?:^|[/@:])github\.com[/:]([^/]+)\/([^/#?]+)/i);
|
|
82
|
+
if (m) return { org: m[1], repo: stripGitSuffix(m[2]) };
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stripGitSuffix(repo: string): string {
|
|
88
|
+
return repo.replace(/\.git$/i, "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fetch the markdown text at `rawUrl`. Returns:
|
|
93
|
+
* - `{ status: "ok", text, etag }` on 2xx
|
|
94
|
+
* - `{ status: "not-modified" }` on 304 (when If-None-Match was sent and
|
|
95
|
+
* server confirmed the cached body is current)
|
|
96
|
+
* - `null` for: PI_OFFLINE / env-skipped, non-2xx (other than 304),
|
|
97
|
+
* network error, abort, malformed response.
|
|
98
|
+
*
|
|
99
|
+
* Caller is responsible for falling back to the local CHANGELOG on
|
|
100
|
+
* `null` return.
|
|
101
|
+
*/
|
|
102
|
+
export async function fetchRemoteChangelog(
|
|
103
|
+
rawUrl: string,
|
|
104
|
+
opts: FetchRemoteChangelogOptions = {},
|
|
105
|
+
): Promise<RemoteChangelogResult> {
|
|
106
|
+
if (process.env.PI_OFFLINE) return null;
|
|
107
|
+
|
|
108
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
109
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
110
|
+
const headers: Record<string, string> = { accept: "text/plain, */*" };
|
|
111
|
+
if (opts.etag) headers["If-None-Match"] = opts.etag;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetchFn(rawUrl, {
|
|
115
|
+
headers,
|
|
116
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
117
|
+
redirect: "follow",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 304: server confirms cache is current. Caller reuses cached body.
|
|
121
|
+
if (response.status === 304) {
|
|
122
|
+
return { status: "not-modified" };
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) return null;
|
|
125
|
+
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
if (typeof text !== "string" || text.length === 0) return null;
|
|
128
|
+
|
|
129
|
+
const etag = response.headers.get("etag");
|
|
130
|
+
return { status: "ok", text, etag };
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* PI Dashboard Server CLI
|
|
4
4
|
*
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createServer, type ServerConfig } from "./server.js";
|
|
19
19
|
import { loadConfig, ensureConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
import {
|
|
21
|
+
launchDashboardServer,
|
|
22
|
+
JitiNotFoundError,
|
|
23
|
+
PortConflictError,
|
|
24
|
+
EarlyExitError,
|
|
25
|
+
} from "@blackbelt-technology/pi-dashboard-shared/server-launcher.js";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
25
27
|
import os from "node:os";
|
|
26
28
|
import path from "node:path";
|
|
27
29
|
import { readPid, removePid, isServerRunning } from "./server-pid.js";
|
|
@@ -42,7 +44,7 @@ export function findPortHolders(
|
|
|
42
44
|
}
|
|
43
45
|
import { isDashboardRunning } from "@blackbelt-technology/pi-dashboard-shared/server-identity.js";
|
|
44
46
|
import { discoverDashboard } from "@blackbelt-technology/pi-dashboard-shared/mdns-discovery.js";
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
import { assertNodeVersionSupported } from "./node-guard.js";
|
|
47
49
|
import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
48
50
|
import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
|
|
@@ -141,6 +143,7 @@ export function buildConfig(flags: Partial<ServerConfig>): ServerConfig {
|
|
|
141
143
|
shutdownIdleSeconds: fileConfig.shutdownIdleSeconds,
|
|
142
144
|
tunnel: flags.tunnel ?? fileConfig.tunnel.enabled,
|
|
143
145
|
tunnelReservedToken: fileConfig.tunnel.reservedToken,
|
|
146
|
+
tunnelWatchdog: fileConfig.tunnel.watchdog,
|
|
144
147
|
authConfig: fileConfig.auth,
|
|
145
148
|
maxEventsPerSession: fileConfig.memoryLimits.maxEventsPerSession,
|
|
146
149
|
maxStringFieldSize: fileConfig.memoryLimits.maxStringFieldSize,
|
|
@@ -252,7 +255,7 @@ async function runDegradedModeBootstrap(server: DashboardServer): Promise<void>
|
|
|
252
255
|
return;
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
const installPackages = ["@
|
|
258
|
+
const installPackages = ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"];
|
|
256
259
|
server.bootstrapState.setLastInstallPackages(installPackages);
|
|
257
260
|
console.log("[bootstrap] installing (pi unresolved, running background install)");
|
|
258
261
|
server.bootstrapState.set({
|
|
@@ -351,7 +354,10 @@ async function cmdStart(config: ServerConfig): Promise<void> {
|
|
|
351
354
|
process.exit(1);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
|
-
// Spawn ourselves in foreground mode (no subcommand) as a detached process
|
|
357
|
+
// Spawn ourselves in foreground mode (no subcommand) as a detached process.
|
|
358
|
+
// All concerns below — jiti loader resolution, --import argv URL-wrapping,
|
|
359
|
+
// env merge, log-file header, readiness polling, port-conflict / early-exit
|
|
360
|
+
// detection — are owned by the shared `launchDashboardServer` primitive.
|
|
355
361
|
const cliPath = fileURLToPath(import.meta.url);
|
|
356
362
|
const args: string[] = [];
|
|
357
363
|
if (config.port !== 8000) args.push("--port", String(config.port));
|
|
@@ -359,83 +365,39 @@ async function cmdStart(config: ServerConfig): Promise<void> {
|
|
|
359
365
|
if (config.dev) args.push("--dev");
|
|
360
366
|
if (!config.tunnel) args.push("--no-tunnel");
|
|
361
367
|
|
|
362
|
-
let tsLoader: string;
|
|
363
|
-
try {
|
|
364
|
-
tsLoader = resolveJitiImport();
|
|
365
|
-
} catch {
|
|
366
|
-
// Fallback to tsx when jiti is not available (e.g. running outside pi).
|
|
367
|
-
// The loader is passed to `node --import`; on Windows, Node >= 20 rejects
|
|
368
|
-
// raw absolute paths with a drive letter (parsed as URL scheme), so we
|
|
369
|
-
// return a file:// URL. See change: fix-windows-server-parity.
|
|
370
|
-
try {
|
|
371
|
-
const tsxMain = createRequire(cliPath).resolve("tsx");
|
|
372
|
-
const tsxLoaderPath = path.join(path.dirname(tsxMain), "esm", "index.mjs");
|
|
373
|
-
tsLoader = pathToFileURL(tsxLoaderPath).href;
|
|
374
|
-
} catch {
|
|
375
|
-
console.error(
|
|
376
|
-
"[pi-dashboard] Cannot find TypeScript loader. " +
|
|
377
|
-
"Install tsx (`npm install`) or run inside a pi session."
|
|
378
|
-
);
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Redirect daemon stdout/stderr to a log file for crash diagnosis.
|
|
384
|
-
// Log is opened in append mode ("a") so output from prior start attempts
|
|
385
|
-
// is preserved across retries — critical for diagnosing intermittent or
|
|
386
|
-
// silent launch failures. A timestamped header line distinguishes runs.
|
|
387
|
-
// See change: fix-windows-server-parity.
|
|
388
368
|
const logDir = path.join(os.homedir(), ".pi", "dashboard");
|
|
389
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
390
369
|
const logPath = path.join(logDir, "server.log");
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
loader: tsLoader,
|
|
401
|
-
entry: cliPath,
|
|
402
|
-
args,
|
|
403
|
-
spawnOptions: {
|
|
404
|
-
detached: true,
|
|
405
|
-
stdio: ["ignore", logFd, logFd],
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const result = await launchDashboardServer({
|
|
373
|
+
cliPath,
|
|
374
|
+
extraArgs: args,
|
|
375
|
+
stdio: { logFile: logPath },
|
|
376
|
+
starter: "Standalone",
|
|
377
|
+
healthTimeoutMs: 30_000,
|
|
378
|
+
port: config.port,
|
|
406
379
|
env: { ...process.env },
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// take 10s+ (TS compile on first boot, native module loads). 30s is the
|
|
415
|
-
// outer bound — if the server isn't up by then, something's genuinely wrong.
|
|
416
|
-
const READINESS_TIMEOUT_MS = 30_000;
|
|
417
|
-
const deadline = Date.now() + READINESS_TIMEOUT_MS;
|
|
418
|
-
let started = false;
|
|
419
|
-
while (Date.now() < deadline) {
|
|
420
|
-
// Also bail if the child has already exited (fast-path crash detection).
|
|
421
|
-
if (child.exitCode !== null) break;
|
|
422
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
423
|
-
const status = await isDashboardRunning(config.port);
|
|
424
|
-
if (status.running) {
|
|
425
|
-
started = true;
|
|
426
|
-
break;
|
|
380
|
+
});
|
|
381
|
+
const reportedPid = result.reportedPid ?? readPid() ?? result.childPid;
|
|
382
|
+
console.log(`Dashboard server started (pid ${reportedPid}) at http://localhost:${config.port}`);
|
|
383
|
+
} catch (err: unknown) {
|
|
384
|
+
if (err instanceof JitiNotFoundError) {
|
|
385
|
+
console.error(`[pi-dashboard] ${err.message}`);
|
|
386
|
+
process.exit(1);
|
|
427
387
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
388
|
+
if (err instanceof PortConflictError) {
|
|
389
|
+
console.error(`Port ${err.port} is occupied by another service (not the dashboard).`);
|
|
390
|
+
console.error(`Change the port in ~/.pi/dashboard/config.json or use --port <n>`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
if (err instanceof EarlyExitError) {
|
|
394
|
+
console.error(`Failed to start dashboard server (child process exited with code ${err.code})`);
|
|
395
|
+
console.error(`Check logs at ${logPath}`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
437
399
|
console.error(`Failed to start dashboard server (${reason})`);
|
|
438
|
-
console.error(`Check logs at ${
|
|
400
|
+
console.error(`Check logs at ${logPath}`);
|
|
439
401
|
process.exit(1);
|
|
440
402
|
}
|
|
441
403
|
}
|
|
@@ -598,7 +560,7 @@ async function cmdUpgradePi(config: ServerConfig): Promise<void> {
|
|
|
598
560
|
|
|
599
561
|
console.log("[upgrade-pi] no dashboard running — installing directly");
|
|
600
562
|
const res = await bootstrapInstall({
|
|
601
|
-
packages: ["@
|
|
563
|
+
packages: ["@earendil-works/pi-coding-agent"],
|
|
602
564
|
progress: (p) => {
|
|
603
565
|
const line = p.output
|
|
604
566
|
? `[upgrade-pi] ${p.step} ${p.status}: ${p.output}`
|
|
@@ -666,7 +628,25 @@ async function cmdStatus(port: number): Promise<void> {
|
|
|
666
628
|
console.log(`Dashboard server is running (pid ${pid}) on port ${port}`);
|
|
667
629
|
}
|
|
668
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Install process-level safety net so a single misbehaving plugin or
|
|
633
|
+
* library cannot kill the whole dashboard. Logs the offending error and
|
|
634
|
+
* keeps the event loop running. We do NOT exit; the surrounding daemon
|
|
635
|
+
* harness already restarts on real crashes (signal/exit-code), and
|
|
636
|
+
* silently swallowing recoverable async faults is the lesser evil here.
|
|
637
|
+
*/
|
|
638
|
+
function installCrashSafetyNet(): void {
|
|
639
|
+
process.on("unhandledRejection", (reason: unknown) => {
|
|
640
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
641
|
+
console.error("[crash-safety] unhandledRejection (suppressed):", err.stack || err.message);
|
|
642
|
+
});
|
|
643
|
+
process.on("uncaughtException", (err: Error) => {
|
|
644
|
+
console.error("[crash-safety] uncaughtException (suppressed):", err.stack || err.message);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
669
648
|
async function main() {
|
|
649
|
+
installCrashSafetyNet();
|
|
670
650
|
ensureConfig();
|
|
671
651
|
|
|
672
652
|
const { subcommand, flags } = parseArgs(process.argv.slice(2));
|