@blackbelt-technology/pi-agent-dashboard 0.4.1 → 0.4.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 +79 -32
- package/README.md +7 -3
- package/docs/architecture.md +361 -12
- package/package.json +7 -7
- package/packages/extension/package.json +7 -2
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +51 -7
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +165 -57
- package/packages/extension/src/bridge.ts +97 -4
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-polyfill.ts +38 -8
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +9 -3
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +5 -6
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +56 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +11 -1
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -16,6 +16,7 @@ import { writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/sess
|
|
|
16
16
|
import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
17
17
|
import { detectOpenSpecActivity } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
|
|
18
18
|
import { extractTurnStats } from "@blackbelt-technology/pi-dashboard-shared/stats-extractor.js";
|
|
19
|
+
import { attachRenameTarget, isNameAutoSetFromAttachment } from "./proposal-attach-naming.js";
|
|
19
20
|
|
|
20
21
|
export interface EventWiringDeps {
|
|
21
22
|
sessionManager: SessionManager;
|
|
@@ -27,6 +28,12 @@ export interface EventWiringDeps {
|
|
|
27
28
|
directoryService: DirectoryService;
|
|
28
29
|
knownSessionIds: Set<string>;
|
|
29
30
|
pendingDashboardSpawns: Map<string, number>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional pending-attach registry. When provided, the wiring consumes a
|
|
33
|
+
* pending intent on each `session_register` and applies the attach +
|
|
34
|
+
* auto-rename. See change: add-folder-task-checker-and-spawn-attach.
|
|
35
|
+
*/
|
|
36
|
+
pendingAttachRegistry?: import("./pending-attach-registry.js").PendingAttachRegistry;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/**
|
|
@@ -44,6 +51,7 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
44
51
|
directoryService,
|
|
45
52
|
knownSessionIds,
|
|
46
53
|
pendingDashboardSpawns,
|
|
54
|
+
pendingAttachRegistry,
|
|
47
55
|
} = deps;
|
|
48
56
|
|
|
49
57
|
// Broadcast placeholder session to browsers when auto-created from early events
|
|
@@ -54,6 +62,28 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
54
62
|
}
|
|
55
63
|
};
|
|
56
64
|
|
|
65
|
+
// Consume any pending spawn-with-attach intent for the registering session.
|
|
66
|
+
// See change: add-folder-task-checker-and-spawn-attach.
|
|
67
|
+
piGateway.onSessionRegistered = (sessionId, cwd) => {
|
|
68
|
+
if (!pendingAttachRegistry) return;
|
|
69
|
+
const changeName = pendingAttachRegistry.consume(cwd);
|
|
70
|
+
if (!changeName) return;
|
|
71
|
+
// Lazy import to avoid a circular type dep at module load.
|
|
72
|
+
void import("./browser-handlers/session-meta-handler.js").then(({ applyAttachProposal }) => {
|
|
73
|
+
applyAttachProposal(sessionId, changeName, {
|
|
74
|
+
sessionManager,
|
|
75
|
+
piGateway,
|
|
76
|
+
broadcast: (msg) => {
|
|
77
|
+
// applyAttachProposal only emits `session_updated`; route via the
|
|
78
|
+
// browser gateway's typed helper to match the rest of this file.
|
|
79
|
+
if (msg.type === "session_updated") {
|
|
80
|
+
browserGateway.broadcastSessionUpdated(msg.sessionId, msg.updates);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
57
87
|
// Broadcast session ended to browsers when sessions are unregistered
|
|
58
88
|
sessionManager.onUnregister = (sessionId) => {
|
|
59
89
|
const session = sessionManager.get(sessionId);
|
|
@@ -66,6 +96,11 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
66
96
|
}
|
|
67
97
|
};
|
|
68
98
|
|
|
99
|
+
// Per-event cap for `Session.uiDataMap[event]`. Phase-1 spec contract:
|
|
100
|
+
// last-write-wins on overflow; oldest items are discarded.
|
|
101
|
+
// See change: add-extension-ui-modal, design.md §5.
|
|
102
|
+
const UI_DATA_PER_EVENT_CAP = 1000;
|
|
103
|
+
|
|
69
104
|
// Track sessions replaying history — suppress status broadcasts to avoid card flicker
|
|
70
105
|
const replayingSessions = new Set<string>();
|
|
71
106
|
// Sessions whose replay should be discarded (canSkipWipe was true — events already in store)
|
|
@@ -139,17 +174,30 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
139
174
|
// (write/CLI). Reads are passive (browsing/analysis) and don't trigger attach.
|
|
140
175
|
// Phase is optional — skills loaded via prompt templates don't emit a SKILL.md read event.
|
|
141
176
|
const attachUpdates: Partial<DashboardSession> = {};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
// Auto-detect parallel path — see change: fix-mobile-attach-proposal-display
|
|
178
|
+
// (design.md §"Auto-detect parallel path"). Mirrors the witness rule in
|
|
179
|
+
// session-meta-handler.ts: re-attach when the previous attachment was
|
|
180
|
+
// auto-tracked (name === attachedProposal) AND a different changeName
|
|
181
|
+
// is now detected. Inner rename guard reuses attachRenameTarget.
|
|
182
|
+
if (updatedSession?.openspecChange && detected.isActive) {
|
|
183
|
+
const attachmentWasAutoTracked =
|
|
184
|
+
!updatedSession.attachedProposal ||
|
|
185
|
+
isNameAutoSetFromAttachment(updatedSession);
|
|
186
|
+
const differentChangeDetected =
|
|
187
|
+
updatedSession.attachedProposal !== updatedSession.openspecChange;
|
|
188
|
+
if (attachmentWasAutoTracked && differentChangeDetected) {
|
|
189
|
+
attachUpdates.attachedProposal = updatedSession.openspecChange;
|
|
190
|
+
const newName = attachRenameTarget(updatedSession, updatedSession.openspecChange);
|
|
191
|
+
if (newName !== undefined) {
|
|
192
|
+
attachUpdates.name = newName;
|
|
193
|
+
piGateway.sendToSession(sessionId, {
|
|
194
|
+
type: "rename_session",
|
|
195
|
+
sessionId,
|
|
196
|
+
name: newName,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
sessionManager.update(sessionId, attachUpdates);
|
|
151
200
|
}
|
|
152
|
-
sessionManager.update(sessionId, attachUpdates);
|
|
153
201
|
}
|
|
154
202
|
if (!replayingSessions.has(sessionId)) {
|
|
155
203
|
browserGateway.broadcastSessionUpdated(sessionId, {
|
|
@@ -523,6 +571,59 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
523
571
|
browserGateway.sendToSubscribers(sessionId, msg as any);
|
|
524
572
|
}
|
|
525
573
|
|
|
574
|
+
// ── Extension UI System (Phase 1): cache + broadcast ──
|
|
575
|
+
// See change: add-extension-ui-modal.
|
|
576
|
+
if (msg.type === "ui_modules_list") {
|
|
577
|
+
sessionManager.update(sessionId, { uiModules: msg.modules });
|
|
578
|
+
browserGateway.sendToSubscribers(sessionId, {
|
|
579
|
+
type: "ui_modules_list",
|
|
580
|
+
sessionId,
|
|
581
|
+
modules: msg.modules,
|
|
582
|
+
} as any);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (msg.type === "ui_data_list") {
|
|
586
|
+
const session = sessionManager.get(sessionId);
|
|
587
|
+
const dataMap = { ...(session?.uiDataMap ?? {}) };
|
|
588
|
+
// Per-event item cap (default N = 1000). Last-write-wins on overflow.
|
|
589
|
+
const items = Array.isArray(msg.items) ? msg.items : [];
|
|
590
|
+
const capped = items.length > UI_DATA_PER_EVENT_CAP
|
|
591
|
+
? items.slice(items.length - UI_DATA_PER_EVENT_CAP)
|
|
592
|
+
: items;
|
|
593
|
+
dataMap[msg.event] = capped;
|
|
594
|
+
sessionManager.update(sessionId, { uiDataMap: dataMap });
|
|
595
|
+
browserGateway.sendToSubscribers(sessionId, {
|
|
596
|
+
type: "ui_data_list",
|
|
597
|
+
sessionId,
|
|
598
|
+
event: msg.event,
|
|
599
|
+
items: capped,
|
|
600
|
+
} as any);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── Extension UI System (Phase 2): live decorator cache + broadcast ──
|
|
604
|
+
// See change: add-extension-ui-decorations.
|
|
605
|
+
if (msg.type === "ext_ui_decorator") {
|
|
606
|
+
const session = sessionManager.get(sessionId);
|
|
607
|
+
if (session) {
|
|
608
|
+
const descriptor = msg.descriptor;
|
|
609
|
+
if (descriptor && typeof descriptor.kind === "string" && typeof descriptor.namespace === "string" && typeof descriptor.id === "string") {
|
|
610
|
+
const key = `${descriptor.kind}:${descriptor.namespace}:${descriptor.id}`;
|
|
611
|
+
const next = { ...(session.uiDecorators ?? {}) };
|
|
612
|
+
if (msg.removed === true) delete next[key];
|
|
613
|
+
else next[key] = descriptor;
|
|
614
|
+
sessionManager.update(sessionId, { uiDecorators: next });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Broadcast verbatim regardless of whether the session is known — mirrors
|
|
618
|
+
// the Phase-1 contract for `ui_modules_list` / `ui_data_list`.
|
|
619
|
+
browserGateway.sendToSubscribers(sessionId, {
|
|
620
|
+
type: "ext_ui_decorator",
|
|
621
|
+
sessionId,
|
|
622
|
+
descriptor: msg.descriptor,
|
|
623
|
+
...(msg.removed === true ? { removed: true } : {}),
|
|
624
|
+
} as any);
|
|
625
|
+
}
|
|
626
|
+
|
|
526
627
|
if (msg.type === "session_name_update") {
|
|
527
628
|
const nameUpdates = { name: msg.name || undefined };
|
|
528
629
|
sessionManager.update(sessionId, nameUpdates);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich rows returned by `packageManagerWrapper.listInstalled()` with
|
|
3
|
+
* version, description, displayName, isRecommended, and isBundled fields.
|
|
4
|
+
*
|
|
5
|
+
* The raw rows from pi's `DefaultPackageManager.listConfiguredPackages()`
|
|
6
|
+
* carry only `{ source, scope, filtered, installedPath }`. The Settings
|
|
7
|
+
* Packages tab needs more to render a friendly identity and badges
|
|
8
|
+
* without a second fetch.
|
|
9
|
+
*
|
|
10
|
+
* Pure helpers (`extractBasenameFromSource`, `computeIsBundled`) are
|
|
11
|
+
* exported for unit tests; the I/O-bearing enricher (`enrichInstalled`)
|
|
12
|
+
* reads the on-disk `package.json` for each row.
|
|
13
|
+
*
|
|
14
|
+
* See change: consolidate-packages-settings-ui.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import {
|
|
19
|
+
RECOMMENDED_EXTENSIONS,
|
|
20
|
+
BUNDLED_EXTENSION_IDS,
|
|
21
|
+
type RecommendedExtension,
|
|
22
|
+
} from "@blackbelt-technology/pi-dashboard-shared/recommended-extensions.js";
|
|
23
|
+
import { sourcesMatch } from "@blackbelt-technology/pi-dashboard-shared/source-matching.js";
|
|
24
|
+
import type { InstalledPackage } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
25
|
+
|
|
26
|
+
/** Raw row shape returned by pi's listConfiguredPackages(). */
|
|
27
|
+
export interface RawInstalledRow {
|
|
28
|
+
source: string;
|
|
29
|
+
scope: "user" | "project";
|
|
30
|
+
filtered: boolean;
|
|
31
|
+
installedPath?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read package.json#version and #description from a directory.
|
|
35
|
+
* Swallows all errors and returns `{}` on any failure. Exported for tests. */
|
|
36
|
+
export function readPackageJsonMeta(installedPath: string | undefined): {
|
|
37
|
+
version?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
} {
|
|
40
|
+
if (!installedPath) return {};
|
|
41
|
+
try {
|
|
42
|
+
const pj = path.join(installedPath, "package.json");
|
|
43
|
+
if (!fs.existsSync(pj)) return {};
|
|
44
|
+
const raw = fs.readFileSync(pj, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
const version = typeof parsed?.version === "string" ? parsed.version : undefined;
|
|
47
|
+
const description = typeof parsed?.description === "string" ? parsed.description : undefined;
|
|
48
|
+
return { version, description };
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Pull a friendly basename out of a raw source string when no
|
|
55
|
+
* RECOMMENDED_EXTENSIONS match is available. Pure. */
|
|
56
|
+
export function extractBasenameFromSource(source: string): string {
|
|
57
|
+
// npm:<name>[@<ver>]
|
|
58
|
+
const npmMatch = source.match(/^npm:(@?[^@]+)(?:@.*)?$/);
|
|
59
|
+
if (npmMatch) return npmMatch[1];
|
|
60
|
+
|
|
61
|
+
// git: strip .git suffix and trailing slash, take last path segment
|
|
62
|
+
const gitMatch = source.match(/[/:]([^/:]+?)(?:\.git)?\/?$/);
|
|
63
|
+
if (gitMatch) return gitMatch[1];
|
|
64
|
+
|
|
65
|
+
// local file:// or path: take last path segment
|
|
66
|
+
const localMatch = source.match(/[/\\]([^/\\]+)\/?$/);
|
|
67
|
+
if (localMatch) return localMatch[1];
|
|
68
|
+
|
|
69
|
+
return source;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Find the recommended manifest entry whose source matches the row.
|
|
73
|
+
* Pure. */
|
|
74
|
+
export function matchRecommendedEntry(
|
|
75
|
+
source: string,
|
|
76
|
+
manifest: readonly RecommendedExtension[] = RECOMMENDED_EXTENSIONS,
|
|
77
|
+
): RecommendedExtension | undefined {
|
|
78
|
+
return manifest.find((entry) => sourcesMatch(entry.source, source));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Compute isBundled. Pure (takes injected resourcesPath + existsSync).
|
|
82
|
+
* Outside Electron (no resourcesPath), always false. */
|
|
83
|
+
export function computeIsBundled(
|
|
84
|
+
id: string,
|
|
85
|
+
resourcesPath: string | undefined,
|
|
86
|
+
existsFn: (p: string) => boolean = fs.existsSync,
|
|
87
|
+
bundledIds: readonly string[] = BUNDLED_EXTENSION_IDS,
|
|
88
|
+
): boolean {
|
|
89
|
+
if (!resourcesPath) return false;
|
|
90
|
+
if (!bundledIds.includes(id)) return false;
|
|
91
|
+
const dir = path.join(resourcesPath, "bundled-extensions", id);
|
|
92
|
+
return existsFn(dir);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Enrich a single raw row. Pure dependency injection for tests. */
|
|
96
|
+
export function enrichInstalledRow(
|
|
97
|
+
row: RawInstalledRow,
|
|
98
|
+
opts: {
|
|
99
|
+
resourcesPath?: string;
|
|
100
|
+
manifest?: readonly RecommendedExtension[];
|
|
101
|
+
bundledIds?: readonly string[];
|
|
102
|
+
readMeta?: (p: string | undefined) => { version?: string; description?: string };
|
|
103
|
+
existsFn?: (p: string) => boolean;
|
|
104
|
+
} = {},
|
|
105
|
+
): InstalledPackage {
|
|
106
|
+
const readMeta = opts.readMeta ?? readPackageJsonMeta;
|
|
107
|
+
const existsFn = opts.existsFn ?? fs.existsSync;
|
|
108
|
+
const manifest = opts.manifest ?? RECOMMENDED_EXTENSIONS;
|
|
109
|
+
const bundledIds = opts.bundledIds ?? BUNDLED_EXTENSION_IDS;
|
|
110
|
+
|
|
111
|
+
const meta = readMeta(row.installedPath);
|
|
112
|
+
const recommended = matchRecommendedEntry(row.source, manifest);
|
|
113
|
+
|
|
114
|
+
const displayName =
|
|
115
|
+
recommended?.displayName ?? extractBasenameFromSource(row.source);
|
|
116
|
+
const description = recommended?.fallbackDescription ?? meta.description;
|
|
117
|
+
const isRecommended = !!recommended;
|
|
118
|
+
const isBundled = recommended
|
|
119
|
+
? computeIsBundled(recommended.id, opts.resourcesPath, existsFn, bundledIds)
|
|
120
|
+
: false;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
source: row.source,
|
|
124
|
+
scope: row.scope,
|
|
125
|
+
filtered: row.filtered,
|
|
126
|
+
installedPath: row.installedPath,
|
|
127
|
+
version: meta.version,
|
|
128
|
+
description,
|
|
129
|
+
displayName,
|
|
130
|
+
isRecommended,
|
|
131
|
+
isBundled,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Enrich a list of raw rows. Reads the Electron resourcesPath at
|
|
136
|
+
* runtime (or undefined in CLI mode). */
|
|
137
|
+
export function enrichInstalledRows(
|
|
138
|
+
rows: RawInstalledRow[],
|
|
139
|
+
resourcesPath: string | undefined = (process as { resourcesPath?: string })
|
|
140
|
+
.resourcesPath,
|
|
141
|
+
): InstalledPackage[] {
|
|
142
|
+
return rows.map((row) => enrichInstalledRow(row, { resourcesPath }));
|
|
143
|
+
}
|