@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1
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 +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- 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.test.ts +24 -1
- 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 +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- 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 +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -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 +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -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 +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- 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__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -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-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 +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- 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-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- 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 +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- 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 +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -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 +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -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 +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +64 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem helpers for locating an installed package's
|
|
3
|
+
* `CHANGELOG.md` and `package.json`, plus deriving a public GitHub
|
|
4
|
+
* URL from the `repository` field.
|
|
5
|
+
*
|
|
6
|
+
* Search order matches the ToolRegistry resolution chain for `pi`:
|
|
7
|
+
* 1. Managed install (`~/.pi-dashboard/node_modules/<pkg>/`)
|
|
8
|
+
* 2. Bare-import via `createRequire` (process resolves the package
|
|
9
|
+
* through its own node_modules — covers npm-global on Unix when
|
|
10
|
+
* the symlink lands inside this Node prefix, and dev-checkout
|
|
11
|
+
* paths during local builds).
|
|
12
|
+
*
|
|
13
|
+
* Both helpers return `null` rather than throwing on absence so route
|
|
14
|
+
* handlers can degrade to the empty-changelog response per spec
|
|
15
|
+
* `pi-changelog-display#Scenario: Package not installed returns empty`.
|
|
16
|
+
*
|
|
17
|
+
* See change: pi-update-whats-new-panel.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
|
|
24
|
+
/** Default managed install root. Test seam: caller may override. */
|
|
25
|
+
function defaultManagedDir(): string {
|
|
26
|
+
return path.join(os.homedir(), ".pi-dashboard");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Locate a package's CHANGELOG.md on disk.
|
|
31
|
+
*
|
|
32
|
+
* Returns `{ changelogPath, packageDir }` so callers can read the
|
|
33
|
+
* adjacent `package.json` without re-resolving.
|
|
34
|
+
*
|
|
35
|
+
* Strategy:
|
|
36
|
+
* 1. `<managedDir>/node_modules/<pkg>/CHANGELOG.md` (Electron + CLI bootstrap).
|
|
37
|
+
* 2. `createRequire(import.meta.url).resolve("<pkg>/package.json")` then
|
|
38
|
+
* look for `CHANGELOG.md` next to it (covers bare-import / npm-global
|
|
39
|
+
* via the standard Node resolution mechanism).
|
|
40
|
+
*
|
|
41
|
+
* Returns `null` when no readable CHANGELOG can be located.
|
|
42
|
+
*/
|
|
43
|
+
export interface ChangelogLocation {
|
|
44
|
+
changelogPath: string;
|
|
45
|
+
packageDir: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FindOptions {
|
|
49
|
+
/** Override managed dir for tests. */
|
|
50
|
+
managedDir?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Override the require-resolver used for bare-import lookup. Tests
|
|
53
|
+
* pass a stub that throws to force the managed path; production
|
|
54
|
+
* uses `createRequire(import.meta.url).resolve`.
|
|
55
|
+
*/
|
|
56
|
+
resolveBareImport?: (pkgJsonSpec: string) => string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findChangelogPath(
|
|
60
|
+
pkg: string,
|
|
61
|
+
opts: FindOptions = {},
|
|
62
|
+
): ChangelogLocation | null {
|
|
63
|
+
// Strategy 1: managed install.
|
|
64
|
+
const managedDir = opts.managedDir ?? defaultManagedDir();
|
|
65
|
+
const managedPkg = path.join(managedDir, "node_modules", pkg);
|
|
66
|
+
const managedCl = path.join(managedPkg, "CHANGELOG.md");
|
|
67
|
+
if (fs.existsSync(managedCl)) {
|
|
68
|
+
return { changelogPath: managedCl, packageDir: managedPkg };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Strategy 2: bare-import via require.resolve.
|
|
72
|
+
const resolver =
|
|
73
|
+
opts.resolveBareImport ??
|
|
74
|
+
((spec: string) => createRequire(import.meta.url).resolve(spec));
|
|
75
|
+
try {
|
|
76
|
+
const pkgJsonPath = resolver(`${pkg}/package.json`);
|
|
77
|
+
const dir = path.dirname(pkgJsonPath);
|
|
78
|
+
const cl = path.join(dir, "CHANGELOG.md");
|
|
79
|
+
if (fs.existsSync(cl)) {
|
|
80
|
+
return { changelogPath: cl, packageDir: dir };
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
/* not resolvable — fall through */
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read and parse `package.json` next to a previously-located
|
|
91
|
+
* CHANGELOG. Returns the parsed object or `null` on read/parse error.
|
|
92
|
+
*/
|
|
93
|
+
export function readPackageJson(packageDir: string): Record<string, unknown> | null {
|
|
94
|
+
const p = path.join(packageDir, "package.json");
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (parsed && typeof parsed === "object") {
|
|
99
|
+
return parsed as Record<string, unknown>;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Derive a public GitHub URL pointing at the CHANGELOG given a
|
|
109
|
+
* `package.json#repository` field. Returns `null` when the field is
|
|
110
|
+
* missing, not GitHub-hosted, or unparseable.
|
|
111
|
+
*
|
|
112
|
+
* Accepted shapes (per spec `pi-changelog-display#Requirement: Changelog URL derivation`):
|
|
113
|
+
* - `"github:org/repo"` shorthand
|
|
114
|
+
* - `"https://github.com/org/repo.git"` URL string
|
|
115
|
+
* - `{ "type": "git", "url": "git+https://github.com/org/repo.git" }`
|
|
116
|
+
* - same object form with optional `"directory": "packages/foo"` (monorepo)
|
|
117
|
+
*/
|
|
118
|
+
export function deriveChangelogUrl(repository: unknown): string | null {
|
|
119
|
+
if (!repository) return null;
|
|
120
|
+
|
|
121
|
+
let urlStr: string | null = null;
|
|
122
|
+
let directory: string | null = null;
|
|
123
|
+
|
|
124
|
+
if (typeof repository === "string") {
|
|
125
|
+
urlStr = repository;
|
|
126
|
+
} else if (typeof repository === "object" && repository !== null) {
|
|
127
|
+
const rec = repository as Record<string, unknown>;
|
|
128
|
+
if (typeof rec.url === "string") urlStr = rec.url;
|
|
129
|
+
if (typeof rec.directory === "string" && rec.directory.length > 0) {
|
|
130
|
+
directory = rec.directory.replace(/^\/+|\/+$/g, "");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!urlStr) return null;
|
|
134
|
+
|
|
135
|
+
const m = parseGitHubUrl(urlStr);
|
|
136
|
+
if (!m) return null;
|
|
137
|
+
|
|
138
|
+
const subPath = directory ? `${directory}/` : "";
|
|
139
|
+
return `https://github.com/${m.org}/${m.repo}/blob/main/${subPath}CHANGELOG.md`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse the various GitHub URL forms used in `package.json#repository`
|
|
144
|
+
* into `{ org, repo }`. Returns null for non-GitHub or unparseable
|
|
145
|
+
* inputs.
|
|
146
|
+
*/
|
|
147
|
+
function parseGitHubUrl(s: string): { org: string; repo: string } | null {
|
|
148
|
+
const trimmed = s.trim();
|
|
149
|
+
|
|
150
|
+
// github:org/repo shorthand
|
|
151
|
+
let m = trimmed.match(/^github:([^/]+)\/([^/#]+)/i);
|
|
152
|
+
if (m) return { org: m[1], repo: stripGitSuffix(m[2]) };
|
|
153
|
+
|
|
154
|
+
// git+https://github.com/org/repo.git
|
|
155
|
+
// https://github.com/org/repo
|
|
156
|
+
// git://github.com/org/repo.git
|
|
157
|
+
// ssh://git@github.com/org/repo.git
|
|
158
|
+
// git@github.com:org/repo.git
|
|
159
|
+
m = trimmed.match(/(?:^|[/@:])github\.com[/:]([^/]+)\/([^/#?]+)/i);
|
|
160
|
+
if (m) return { org: m[1], repo: stripGitSuffix(m[2]) };
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stripGitSuffix(repo: string): string {
|
|
166
|
+
return repo.replace(/\.git$/i, "");
|
|
167
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
import type { DashboardServer } from "./server.js";
|
|
54
54
|
import { updateBootstrapCompatibility } from "./pi-version-skew.js";
|
|
55
55
|
import type { BootstrapStateStore } from "./bootstrap-state.js";
|
|
56
|
+
import { parseDashboardStarter } from "@blackbelt-technology/pi-dashboard-shared/dashboard-starter.js";
|
|
57
|
+
import { bootstrapInstallFromList } from "./bootstrap-install-from-list.js";
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* Emit a stderr warning at CLI startup when the resolved pi version is
|
|
@@ -166,6 +168,12 @@ async function runForeground(config: ServerConfig): Promise<void> {
|
|
|
166
168
|
assertNodeVersionSupported();
|
|
167
169
|
const server = await createServer(config);
|
|
168
170
|
|
|
171
|
+
// Stamp the bootstrap state with who started this server process.
|
|
172
|
+
// parseDashboardStarter defaults to "Standalone" when DASHBOARD_STARTER is unset.
|
|
173
|
+
const starter = parseDashboardStarter(process.env);
|
|
174
|
+
server.bootstrapState.set({ starter });
|
|
175
|
+
console.log(`[bootstrap] starter=${starter}`);
|
|
176
|
+
|
|
169
177
|
let shuttingDown = false;
|
|
170
178
|
const shutdown = async () => {
|
|
171
179
|
if (shuttingDown) {
|
|
@@ -181,6 +189,19 @@ async function runForeground(config: ServerConfig): Promise<void> {
|
|
|
181
189
|
process.on("SIGINT", shutdown);
|
|
182
190
|
process.on("SIGTERM", shutdown);
|
|
183
191
|
|
|
192
|
+
// Reconcile installable.json before binding the port.
|
|
193
|
+
// Required-package failures throw and prevent server start.
|
|
194
|
+
// Optional failures are logged and continue.
|
|
195
|
+
// File-absent is a no-op (Bridge/Standalone starters don't seed installable.json).
|
|
196
|
+
// See change: simplify-electron-bootstrap-derived-state.
|
|
197
|
+
try {
|
|
198
|
+
await bootstrapInstallFromList(server.bootstrapState);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
201
|
+
console.error(`[bootstrap] installable reconcile failed (required package): ${message}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
184
205
|
await server.start();
|
|
185
206
|
|
|
186
207
|
// Kick off the degraded-mode first-run bootstrap if pi is unresolvable.
|
|
@@ -231,7 +252,7 @@ async function runDegradedModeBootstrap(server: DashboardServer): Promise<void>
|
|
|
231
252
|
return;
|
|
232
253
|
}
|
|
233
254
|
|
|
234
|
-
const installPackages = ["@
|
|
255
|
+
const installPackages = ["@earendil-works/pi-coding-agent", "@fission-ai/openspec", "tsx"];
|
|
235
256
|
server.bootstrapState.setLastInstallPackages(installPackages);
|
|
236
257
|
console.log("[bootstrap] installing (pi unresolved, running background install)");
|
|
237
258
|
server.bootstrapState.set({
|
|
@@ -577,7 +598,7 @@ async function cmdUpgradePi(config: ServerConfig): Promise<void> {
|
|
|
577
598
|
|
|
578
599
|
console.log("[upgrade-pi] no dashboard running — installing directly");
|
|
579
600
|
const res = await bootstrapInstall({
|
|
580
|
-
packages: ["@
|
|
601
|
+
packages: ["@earendil-works/pi-coding-agent"],
|
|
581
602
|
progress: (p) => {
|
|
582
603
|
const line = p.output
|
|
583
604
|
? `[upgrade-pi] ${p.step} ${p.status}: ${p.output}`
|