@blackbelt-technology/pi-agent-dashboard 0.4.0 → 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 +104 -35
- package/README.md +390 -494
- package/docs/architecture.md +423 -20
- package/package.json +11 -8
- package/packages/extension/package.json +11 -4
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -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 +170 -61
- package/packages/extension/src/bridge.ts +199 -19
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +73 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +11 -5
- 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__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -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__/pi-version-skew.test.ts +72 -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__/restart-helper.test.ts +34 -6
- 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 +61 -15
- 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/pi-version-skew.ts +12 -1
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/restart-helper.ts +13 -2
- 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-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -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__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -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/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +79 -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 +20 -1
- package/packages/shared/src/tool-registry/definitions.ts +92 -0
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -23,12 +23,47 @@
|
|
|
23
23
|
import { listOr, statusOr, OPENSPEC_LIST, OPENSPEC_STATUS } from "./platform/openspec.js";
|
|
24
24
|
import { runAsync, unwrap } from "./platform/runner.js";
|
|
25
25
|
import type { OpenSpecData, OpenSpecChange, OpenSpecArtifact } from "./types.js";
|
|
26
|
+
import {
|
|
27
|
+
evaluateLocalDesignSatisfaction,
|
|
28
|
+
createFsDesignEvidenceProbe,
|
|
29
|
+
type DesignEvidenceProbe,
|
|
30
|
+
} from "./openspec-design-evidence.js";
|
|
31
|
+
import {
|
|
32
|
+
evaluateLocalSpecsSatisfaction,
|
|
33
|
+
createFsSpecsEvidenceProbe,
|
|
34
|
+
type SpecsEvidenceProbe,
|
|
35
|
+
} from "./openspec-specs-evidence.js";
|
|
36
|
+
import path from "node:path";
|
|
26
37
|
|
|
27
38
|
const EMPTY_DATA: OpenSpecData = { initialized: false, changes: [] };
|
|
28
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Factory that returns a probe for a given change name. Production callers
|
|
42
|
+
* pass a closure rooted at `<cwd>/openspec/changes/<name>`. Tests pass an
|
|
43
|
+
* in-memory factory. When omitted, the design override does NOT fire and
|
|
44
|
+
* `buildOpenSpecData` matches today's behavior verbatim.
|
|
45
|
+
*
|
|
46
|
+
* See change: fix-openspec-design-detection.
|
|
47
|
+
*/
|
|
48
|
+
export type DesignProbeFactory = (changeName: string) => DesignEvidenceProbe;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Factory that returns a specs-evidence probe for a given change name.
|
|
52
|
+
* Parallel to `DesignProbeFactory` — production callers pass a closure
|
|
53
|
+
* rooted at `<cwd>/openspec/changes/<name>`; tests pass an in-memory
|
|
54
|
+
* factory. When omitted, the specs override does NOT fire and
|
|
55
|
+
* `buildOpenSpecData` matches today's behavior verbatim for the specs
|
|
56
|
+
* artifact.
|
|
57
|
+
*
|
|
58
|
+
* See change: fix-openspec-specs-mtime-gate-blind-spot.
|
|
59
|
+
*/
|
|
60
|
+
export type SpecsProbeFactory = (changeName: string) => SpecsEvidenceProbe;
|
|
61
|
+
|
|
29
62
|
export function buildOpenSpecData(
|
|
30
63
|
listResult: { changes?: Array<{ name: string; status: string; completedTasks: number; totalTasks: number }> } | null,
|
|
31
64
|
statusResults: Map<string, { artifacts?: Array<{ id: string; status: string }>; isComplete?: boolean } | null>,
|
|
65
|
+
probeFactory?: DesignProbeFactory,
|
|
66
|
+
specsProbeFactory?: SpecsProbeFactory,
|
|
32
67
|
): OpenSpecData {
|
|
33
68
|
if (!listResult || !Array.isArray(listResult.changes)) {
|
|
34
69
|
return EMPTY_DATA;
|
|
@@ -41,9 +76,40 @@ export function buildOpenSpecData(
|
|
|
41
76
|
status: (a.status === "done" ? "done" : a.status === "ready" ? "ready" : "blocked") as OpenSpecArtifact["status"],
|
|
42
77
|
}));
|
|
43
78
|
|
|
44
|
-
|
|
79
|
+
// Design-artifact override: promote-only, design-only. See change:
|
|
80
|
+
// fix-openspec-design-detection.
|
|
81
|
+
if (probeFactory) {
|
|
82
|
+
const designIdx = artifacts.findIndex((a) => a.id === "design");
|
|
83
|
+
if (designIdx !== -1 && artifacts[designIdx].status === "ready") {
|
|
84
|
+
const probe = probeFactory(c.name);
|
|
85
|
+
if (evaluateLocalDesignSatisfaction("", probe)) {
|
|
86
|
+
artifacts[designIdx] = { ...artifacts[designIdx], status: "done" };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Specs-artifact override: promote-only, specs-only. See change:
|
|
92
|
+
// fix-openspec-specs-mtime-gate-blind-spot.
|
|
93
|
+
if (specsProbeFactory) {
|
|
94
|
+
const specsIdx = artifacts.findIndex((a) => a.id === "specs");
|
|
95
|
+
if (specsIdx !== -1 && artifacts[specsIdx].status === "ready") {
|
|
96
|
+
const probe = specsProbeFactory(c.name);
|
|
97
|
+
if (evaluateLocalSpecsSatisfaction("", probe)) {
|
|
98
|
+
artifacts[specsIdx] = { ...artifacts[specsIdx], status: "done" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cliIsComplete =
|
|
45
104
|
typeof statusResult?.isComplete === "boolean" ? statusResult.isComplete : undefined;
|
|
46
105
|
|
|
106
|
+
// Re-derive isComplete from post-override artifacts. Promote false→true
|
|
107
|
+
// only when every artifact is done; never demote CLI true.
|
|
108
|
+
let isComplete = cliIsComplete;
|
|
109
|
+
if (artifacts.length > 0 && artifacts.every((a) => a.status === "done")) {
|
|
110
|
+
isComplete = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
47
113
|
const change: OpenSpecChange = {
|
|
48
114
|
name: c.name,
|
|
49
115
|
status: (c.status === "complete" ? "complete" : c.status === "in-progress" ? "in-progress" : "no-tasks") as OpenSpecChange["status"],
|
|
@@ -58,6 +124,44 @@ export function buildOpenSpecData(
|
|
|
58
124
|
return { initialized: true, changes };
|
|
59
125
|
}
|
|
60
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Build a real-fs probe factory rooted at `<cwd>/openspec/changes/<name>`.
|
|
129
|
+
* Production callers (`pollOpenSpec`, `pollOpenSpecAsync`,
|
|
130
|
+
* `directory-service.ts`) use this to wire the override. Tests inject
|
|
131
|
+
* their own factory.
|
|
132
|
+
*/
|
|
133
|
+
export function createFsProbeFactory(cwd: string): DesignProbeFactory {
|
|
134
|
+
const probe = createFsDesignEvidenceProbe();
|
|
135
|
+
const changesRoot = path.join(cwd, "openspec", "changes");
|
|
136
|
+
return (changeName) => {
|
|
137
|
+
const changeDir = path.join(changesRoot, changeName);
|
|
138
|
+
return {
|
|
139
|
+
hasDesignFile: () => probe.hasDesignFile(changeDir),
|
|
140
|
+
hasDesignDirWithMd: () => probe.hasDesignDirWithMd(changeDir),
|
|
141
|
+
tasksHasCheckboxes: () => probe.tasksHasCheckboxes(changeDir),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build a real-fs specs-probe factory rooted at `<cwd>/openspec/changes/<name>`.
|
|
148
|
+
* Parallel to `createFsProbeFactory` — production callers (`pollOpenSpec`,
|
|
149
|
+
* `pollOpenSpecAsync`, `directory-service.ts`) use this to wire the specs
|
|
150
|
+
* override. Tests inject their own factory.
|
|
151
|
+
*
|
|
152
|
+
* See change: fix-openspec-specs-mtime-gate-blind-spot.
|
|
153
|
+
*/
|
|
154
|
+
export function createFsSpecsProbeFactory(cwd: string): SpecsProbeFactory {
|
|
155
|
+
const probe = createFsSpecsEvidenceProbe();
|
|
156
|
+
const changesRoot = path.join(cwd, "openspec", "changes");
|
|
157
|
+
return (changeName) => {
|
|
158
|
+
const changeDir = path.join(changesRoot, changeName);
|
|
159
|
+
return {
|
|
160
|
+
hasAnySpecFile: () => probe.hasAnySpecFile(changeDir),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
61
165
|
/**
|
|
62
166
|
* Synchronous poll — blocks the event loop. Used by the bridge extension
|
|
63
167
|
* where async isn't practical (some pi extension hooks are sync).
|
|
@@ -70,7 +174,12 @@ export function pollOpenSpec(cwd: string): OpenSpecData {
|
|
|
70
174
|
for (const c of listResult.changes) {
|
|
71
175
|
statusResults.set(c.name, statusOr({ cwd, change: c.name }));
|
|
72
176
|
}
|
|
73
|
-
return buildOpenSpecData(
|
|
177
|
+
return buildOpenSpecData(
|
|
178
|
+
listResult,
|
|
179
|
+
statusResults,
|
|
180
|
+
createFsProbeFactory(cwd),
|
|
181
|
+
createFsSpecsProbeFactory(cwd),
|
|
182
|
+
);
|
|
74
183
|
}
|
|
75
184
|
|
|
76
185
|
/**
|
|
@@ -114,5 +223,10 @@ export async function pollOpenSpecAsync(cwd: string): Promise<OpenSpecData> {
|
|
|
114
223
|
}),
|
|
115
224
|
);
|
|
116
225
|
const statusResults = new Map<string, any>(statusEntries);
|
|
117
|
-
return buildOpenSpecData(
|
|
226
|
+
return buildOpenSpecData(
|
|
227
|
+
listResult,
|
|
228
|
+
statusResults,
|
|
229
|
+
createFsProbeFactory(cwd),
|
|
230
|
+
createFsSpecsProbeFactory(cwd),
|
|
231
|
+
);
|
|
118
232
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-evidence override for the OpenSpec `specs` artifact.
|
|
3
|
+
*
|
|
4
|
+
* The `spec-driven` schema declares `specs/**\/*.md` as the `generates`
|
|
5
|
+
* pattern for the `specs` artifact, and the openspec CLI marks the
|
|
6
|
+
* artifact `done` whenever that glob matches anything. The dashboard's
|
|
7
|
+
* mtime-gated cache, however, can momentarily stale on `specs: ready`
|
|
8
|
+
* for multi-spec changes (see change:
|
|
9
|
+
* fix-openspec-specs-mtime-gate-blind-spot — the watch set is now
|
|
10
|
+
* extended to cover `specs/**`, but this override is the second line
|
|
11
|
+
* of defence).
|
|
12
|
+
*
|
|
13
|
+
* This module computes a boolean "is specs satisfied locally?" from
|
|
14
|
+
* file-system evidence the dashboard's cache might miss between polls.
|
|
15
|
+
* It is consumed by:
|
|
16
|
+
*
|
|
17
|
+
* 1. `buildOpenSpecData` in `openspec-poller.ts` — promotes
|
|
18
|
+
* `artifacts[specs].status` from "ready" to "done" when at least
|
|
19
|
+
* one `specs/**\/*.md` file exists. Promote-only; specs-only;
|
|
20
|
+
* never demotes; never touches other artifacts.
|
|
21
|
+
*
|
|
22
|
+
* One rule:
|
|
23
|
+
*
|
|
24
|
+
* any file matching `specs/**\/*.md` exists in the change folder
|
|
25
|
+
*
|
|
26
|
+
* The probe walks the `specs/` subtree once and short-circuits on the
|
|
27
|
+
* first `*.md` it finds. Defensive: every fs call is wrapped in
|
|
28
|
+
* try/catch and treated as "no match" on error.
|
|
29
|
+
*
|
|
30
|
+
* See change: fix-openspec-specs-mtime-gate-blind-spot.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readdirSync } from "node:fs";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
|
|
36
|
+
/** Probe surface — kept tiny so unit tests can pass an in-memory stub. */
|
|
37
|
+
export interface SpecsEvidenceProbe {
|
|
38
|
+
/** Returns true iff at least one `*.md` file exists under `<changeDir>/specs/`. */
|
|
39
|
+
hasAnySpecFile(changeDir: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Pure rule evaluator. Single rule; short-circuits on first match. */
|
|
43
|
+
export function evaluateLocalSpecsSatisfaction(
|
|
44
|
+
changeDir: string,
|
|
45
|
+
probe: SpecsEvidenceProbe,
|
|
46
|
+
): boolean {
|
|
47
|
+
return probe.hasAnySpecFile(changeDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Production probe — backed by the real filesystem. Walks `<changeDir>/specs/`
|
|
52
|
+
* iteratively, short-circuits on the first `*.md` file encountered. Every
|
|
53
|
+
* `readdirSync` is wrapped in try/catch (handles ENOENT, permission errors,
|
|
54
|
+
* symlink loops, and any unexpected fs error) and treated as "no match".
|
|
55
|
+
*/
|
|
56
|
+
export function createFsSpecsEvidenceProbe(): SpecsEvidenceProbe {
|
|
57
|
+
return {
|
|
58
|
+
hasAnySpecFile(changeDir: string): boolean {
|
|
59
|
+
const root = path.join(changeDir, "specs");
|
|
60
|
+
// Iterative DFS — no recursion to avoid stack overflow on pathological trees.
|
|
61
|
+
const stack: string[] = [root];
|
|
62
|
+
while (stack.length > 0) {
|
|
63
|
+
const dir = stack.pop()!;
|
|
64
|
+
let entries: import("node:fs").Dirent[];
|
|
65
|
+
try {
|
|
66
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
} catch {
|
|
68
|
+
// Missing dir, permission denied, or any other fs error — skip.
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (e.isFile() && e.name.endsWith(".md")) return true;
|
|
73
|
+
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -4,11 +4,106 @@
|
|
|
4
4
|
* with a single configurable resolver.
|
|
5
5
|
*/
|
|
6
6
|
import { execSync, spawnSync, buildSafeArgv } from "./exec.js";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
7
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import { MANAGED_BIN, MANAGED_DIR } from "../managed-paths.js";
|
|
11
11
|
|
|
12
|
+
// ── AppImage self-hit guard (Linux power-user mode safety) ────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optional environment overrides for {@link isAppImageSelfHit}. Tests
|
|
16
|
+
* inject explicit values so the helper can exercise both branches
|
|
17
|
+
* without mutating `process.env` or `process.execPath`. Production
|
|
18
|
+
* callers omit `opts` and the helper reads from the live process.
|
|
19
|
+
*
|
|
20
|
+
* See change: fix-electron-appimage-cli-self-detection (D1).
|
|
21
|
+
*/
|
|
22
|
+
export interface AppImageSelfHitOpts {
|
|
23
|
+
/** Override `process.execPath`. Default: `process.execPath`. */
|
|
24
|
+
execPath?: string;
|
|
25
|
+
/** Override `process.env.APPDIR`. Default: `process.env.APPDIR`. */
|
|
26
|
+
appDir?: string | undefined;
|
|
27
|
+
/** Override `process.env.APPIMAGE`. Default: `process.env.APPIMAGE`. */
|
|
28
|
+
appImage?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Defensive realpath — returns the input on any error (broken symlink / ENOENT). */
|
|
32
|
+
function safeRealpath(p: string): string {
|
|
33
|
+
try {
|
|
34
|
+
return realpathSync(p);
|
|
35
|
+
} catch {
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns `true` when `candidatePath` is the running process's own
|
|
42
|
+
* Electron launcher binary — the bug class that motivates this helper:
|
|
43
|
+
* AppImage's runtime prepends its squashfs mount (`/tmp/.mount_*`) to
|
|
44
|
+
* `PATH` of the Electron child, and `packagerConfig.executableName =
|
|
45
|
+
* "pi-dashboard"` makes the launcher a name-collision with the dashboard
|
|
46
|
+
* CLI. Trusting the first `which pi-dashboard` hit therefore spawns the
|
|
47
|
+
* Electron app recursively as if it were the CLI.
|
|
48
|
+
*
|
|
49
|
+
* A path is considered a self-hit when ANY of the following is true:
|
|
50
|
+
* - `realpath(candidatePath) === realpath(execPath)`, OR
|
|
51
|
+
* - `candidatePath` lives under the directory named by `appDir`, OR
|
|
52
|
+
* - `realpath(candidatePath) === realpath(appImage)`.
|
|
53
|
+
*
|
|
54
|
+
* `realpath` calls are wrapped in try/catch so broken symlinks / ENOENT
|
|
55
|
+
* fall back to literal string comparisons. The helper never throws.
|
|
56
|
+
*
|
|
57
|
+
* Production callers (`whereStrategy`, `detectPiDashboardCli`,
|
|
58
|
+
* `detectPi`, `detectSystemNode`) omit `opts`. Tests pass explicit
|
|
59
|
+
* overrides via `opts`.
|
|
60
|
+
*
|
|
61
|
+
* See change: fix-electron-appimage-cli-self-detection (D1).
|
|
62
|
+
*/
|
|
63
|
+
export function isAppImageSelfHit(
|
|
64
|
+
candidatePath: string,
|
|
65
|
+
opts?: AppImageSelfHitOpts,
|
|
66
|
+
): boolean {
|
|
67
|
+
if (!candidatePath) return false;
|
|
68
|
+
|
|
69
|
+
const execPath = opts && "execPath" in opts ? opts.execPath : process.execPath;
|
|
70
|
+
const appDir = opts && "appDir" in opts ? opts.appDir : process.env.APPDIR;
|
|
71
|
+
const appImage = opts && "appImage" in opts ? opts.appImage : process.env.APPIMAGE;
|
|
72
|
+
|
|
73
|
+
const realCandidate = safeRealpath(candidatePath);
|
|
74
|
+
|
|
75
|
+
// Rule 1: realpath equals process.execPath
|
|
76
|
+
if (execPath) {
|
|
77
|
+
const realExec = safeRealpath(execPath);
|
|
78
|
+
if (realCandidate === realExec) return true;
|
|
79
|
+
if (candidatePath === execPath) return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Rule 2: candidate lives under APPDIR (the AppImage squashfs mount).
|
|
83
|
+
// We compare the candidate's realpath against APPDIR's realpath so a
|
|
84
|
+
// symlink under the mount is still recognized as a self-hit.
|
|
85
|
+
if (appDir) {
|
|
86
|
+
const realAppDir = safeRealpath(appDir);
|
|
87
|
+
const sep = path.sep;
|
|
88
|
+
// Append separator so /tmp/.mount_PI doesn't accidentally match
|
|
89
|
+
// /tmp/.mount_PIxx-elsewhere via prefix.
|
|
90
|
+
const prefix = realAppDir.endsWith(sep) ? realAppDir : realAppDir + sep;
|
|
91
|
+
if (realCandidate === realAppDir || realCandidate.startsWith(prefix)) return true;
|
|
92
|
+
// Literal fallback (broken symlinks / ENOENT keep a useful answer).
|
|
93
|
+
const litPrefix = appDir.endsWith(sep) ? appDir : appDir + sep;
|
|
94
|
+
if (candidatePath === appDir || candidatePath.startsWith(litPrefix)) return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rule 3: realpath equals APPIMAGE (the .AppImage file the user clicked).
|
|
98
|
+
if (appImage) {
|
|
99
|
+
const realAppImage = safeRealpath(appImage);
|
|
100
|
+
if (realCandidate === realAppImage) return true;
|
|
101
|
+
if (candidatePath === appImage) return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
12
107
|
/**
|
|
13
108
|
* Well-known globalThis symbol for the default `ToolRegistry`.
|
|
14
109
|
*
|
|
@@ -9,6 +9,7 @@ export * from "./detached-spawn.js";
|
|
|
9
9
|
export * from "./spawn-mechanism.js";
|
|
10
10
|
export * from "./process-identify.js";
|
|
11
11
|
export * from "./subprocess-adapter.js";
|
|
12
|
+
export * from "./node-spawn.js";
|
|
12
13
|
export * as git from "./git.js";
|
|
13
14
|
export * as openspec from "./openspec.js";
|
|
14
15
|
export * as npm from "./npm.js";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical helper for spawning `node --import <loader> <entry>` argv.
|
|
3
|
+
*
|
|
4
|
+
* Node ≥ 20's ESM loader parses BOTH the `--import` loader position AND
|
|
5
|
+
* the entry-script position as URLs. Raw Windows paths like
|
|
6
|
+
* `B:\Dev\foo.ts` URL-parse to scheme `b:`, which is not in the ESM
|
|
7
|
+
* loader's allowlist (file, data, node) → the process crashes with
|
|
8
|
+
* `ERR_UNSUPPORTED_ESM_URL_SCHEME` before any filesystem access.
|
|
9
|
+
*
|
|
10
|
+
* Node's internal drive-letter heuristic catches the common cases
|
|
11
|
+
* (`C:\`, `D:\`) but has known gaps for `A:`, `B:`, and other letters.
|
|
12
|
+
* Rather than relying on the heuristic, we wrap the loader position
|
|
13
|
+
* with `file://` unconditionally.
|
|
14
|
+
*
|
|
15
|
+
* The entry-script position needs a more nuanced rule. Node's default
|
|
16
|
+
* resolver AND jiti's ESM hook both accept `file://` URL entries. But
|
|
17
|
+
* **tsx's ESM hook rejects `file://` URLs as entries** — tsx's resolver
|
|
18
|
+
* treats the entry as a user-typed specifier and attempts bare-import
|
|
19
|
+
* / relative-path resolution, producing `<cwd>/file:/...` errors.
|
|
20
|
+
* Since tsx is used as the jiti fallback on dev machines without pi
|
|
21
|
+
* installed (the most common Linux dev path), we must NOT URL-wrap
|
|
22
|
+
* the entry when the loader is tsx. Detection: the loader path
|
|
23
|
+
* contains `/tsx/` (every tsx install ships its hook under a `tsx/`
|
|
24
|
+
* directory; jiti's hook is under `jiti/`).
|
|
25
|
+
*
|
|
26
|
+
* This module is the canonical chokepoint. The repo-level lint test
|
|
27
|
+
* `no-raw-node-import.test.ts` refuses any other call site that
|
|
28
|
+
* passes a raw path to `--import` / `--loader`.
|
|
29
|
+
*
|
|
30
|
+
* See change: fix-windows-entry-script-url.
|
|
31
|
+
*/
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { pathToFileURL } from "node:url";
|
|
34
|
+
import type { SpawnOptions, ChildProcess } from "node:child_process"; // ban:child_process-ok — types only
|
|
35
|
+
import { spawn as execSpawn } from "./exec.js";
|
|
36
|
+
|
|
37
|
+
export interface SpawnNodeScriptOptions {
|
|
38
|
+
/** Path to node.exe / node (raw OS path — binary, not ESM-loaded). */
|
|
39
|
+
nodeBin?: string;
|
|
40
|
+
|
|
41
|
+
/** Path to the script Node will run. Raw path OR file:// URL. */
|
|
42
|
+
entry: string;
|
|
43
|
+
|
|
44
|
+
/** Optional ESM loader for --import. Raw path OR file:// URL. */
|
|
45
|
+
loader?: string;
|
|
46
|
+
|
|
47
|
+
/** Arguments passed to the script (after entry). */
|
|
48
|
+
args?: string[];
|
|
49
|
+
|
|
50
|
+
/** Standard spawn options (cwd, env, stdio, detached, etc.). */
|
|
51
|
+
spawnOptions?: SpawnOptions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect whether a loader (file:// URL or raw path) is tsx.
|
|
56
|
+
*
|
|
57
|
+
* tsx's ESM hook rejects `file://` URLs at the entry-script position,
|
|
58
|
+
* so the caller must pass a raw OS path for the entry when this
|
|
59
|
+
* returns true. jiti and Node's default resolver both accept URL
|
|
60
|
+
* entries.
|
|
61
|
+
*
|
|
62
|
+
* Heuristic: every tsx install places its hook under a `tsx/` package
|
|
63
|
+
* directory (e.g. `.../node_modules/tsx/dist/esm/index.mjs`). The
|
|
64
|
+
* check is tolerant of `file://` URLs, raw POSIX paths, and raw
|
|
65
|
+
* Windows paths with either slash direction.
|
|
66
|
+
*/
|
|
67
|
+
export function isTsxLoader(loader: string | null | undefined): boolean {
|
|
68
|
+
if (!loader) return false;
|
|
69
|
+
// Normalize backslashes so the `/tsx/` probe works on Windows paths.
|
|
70
|
+
const normalized = loader.replace(/\\/g, "/");
|
|
71
|
+
return /\/tsx\//i.test(normalized);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert a path-or-url string to a file:// URL.
|
|
76
|
+
*
|
|
77
|
+
* Pure and idempotent. Safe to call on strings that are already
|
|
78
|
+
* file:// URLs — returns them unchanged.
|
|
79
|
+
*
|
|
80
|
+
* Handles Windows-style input (drive letter + backslash) regardless of
|
|
81
|
+
* host OS, so unit tests on Linux/macOS can exercise the Windows path
|
|
82
|
+
* contract. Mirrors the pattern in
|
|
83
|
+
* `packages/shared/src/resolve-jiti.ts::buildJitiRegisterUrl`.
|
|
84
|
+
*/
|
|
85
|
+
export function toFileUrl(pathOrUrl: string): string {
|
|
86
|
+
if (pathOrUrl.startsWith("file:")) return pathOrUrl;
|
|
87
|
+
|
|
88
|
+
const isWindowsStyle = /^[A-Za-z]:[\\/]/.test(pathOrUrl);
|
|
89
|
+
if (isWindowsStyle) {
|
|
90
|
+
// pathToFileURL on POSIX hosts URL-encodes backslashes rather than
|
|
91
|
+
// treating them as separators. Build the URL manually so tests on
|
|
92
|
+
// Linux produce the same result a Windows host would.
|
|
93
|
+
return `file:///${pathOrUrl.replace(/\\/g, "/")}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Use path.resolve to ensure absolute path on the host OS, then
|
|
97
|
+
// let Node's pathToFileURL handle any host-specific quirks.
|
|
98
|
+
const absolute = path.isAbsolute(pathOrUrl) ? pathOrUrl : path.resolve(pathOrUrl);
|
|
99
|
+
return pathToFileURL(absolute).href;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Decide whether the entry-script position needs `file://` URL wrapping.
|
|
104
|
+
*
|
|
105
|
+
* Rule:
|
|
106
|
+
* - tsx loader: always raw path (tsx rejects file:// entries on every OS)
|
|
107
|
+
* - non-tsx (jiti / Node default) on POSIX: raw path
|
|
108
|
+
* (POSIX has no drive-letter / URL-scheme collision; jiti's resolver
|
|
109
|
+
* actively MISBEHAVES when handed `file://` URL entries — it
|
|
110
|
+
* normalises away the triple-slash and then treats `file:/...` as
|
|
111
|
+
* a relative specifier, producing `<cwd>/file:/...` ENOENT errors.)
|
|
112
|
+
* - non-tsx on Windows: file:// URL
|
|
113
|
+
* (Node parses drive letters like `B:` / `A:` as URL schemes in argv
|
|
114
|
+
* before loaders run, throwing ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
115
|
+
* Wrapping with `file://` sidesteps the parse.)
|
|
116
|
+
*
|
|
117
|
+
* Keeps a `platform` parameter for testability so unit tests on a POSIX
|
|
118
|
+
* host can exercise the Windows branch without mutating `process.platform`.
|
|
119
|
+
*/
|
|
120
|
+
export function shouldUrlWrapEntry(
|
|
121
|
+
loader: string | null | undefined,
|
|
122
|
+
platform: NodeJS.Platform = process.platform,
|
|
123
|
+
): boolean {
|
|
124
|
+
if (isTsxLoader(loader)) return false;
|
|
125
|
+
return platform === "win32";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Spawn `node` with an optional `--import` loader and a script entry.
|
|
130
|
+
*
|
|
131
|
+
* The loader position is always URL-wrapped (Node's ESM loader
|
|
132
|
+
* requires `file://` on Windows drive letters outside the heuristic).
|
|
133
|
+
*
|
|
134
|
+
* The entry position follows `shouldUrlWrapEntry(loader, platform)` —
|
|
135
|
+
* URL on Windows + non-tsx, raw everywhere else.
|
|
136
|
+
*
|
|
137
|
+
* Delegates actual spawning to `platform/exec.ts::spawn` so the
|
|
138
|
+
* `windowsHide: true` default and other safe-spawn invariants are
|
|
139
|
+
* preserved. Does not import `node:child_process` directly (the type
|
|
140
|
+
* imports above are annotated with the opt-out marker).
|
|
141
|
+
*/
|
|
142
|
+
export function spawnNodeScript(opts: SpawnNodeScriptOptions): ChildProcess {
|
|
143
|
+
const nodeBin = opts.nodeBin ?? process.execPath;
|
|
144
|
+
const wrapEntry = shouldUrlWrapEntry(opts.loader);
|
|
145
|
+
|
|
146
|
+
const argv: string[] = [];
|
|
147
|
+
if (opts.loader) {
|
|
148
|
+
argv.push("--import", toFileUrl(opts.loader));
|
|
149
|
+
}
|
|
150
|
+
argv.push(wrapEntry ? toFileUrl(opts.entry) : opts.entry);
|
|
151
|
+
if (opts.args) argv.push(...opts.args);
|
|
152
|
+
|
|
153
|
+
return execSpawn(nodeBin, argv, opts.spawnOptions ?? {});
|
|
154
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin bridge entry management in pi's settings.json.
|
|
3
|
+
*
|
|
4
|
+
* Manages `dashboard-<plugin-id>` keys in a dedicated
|
|
5
|
+
* `dashboardPluginBridges` object inside settings.json.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Only touches entries under the `dashboardPluginBridges` key.
|
|
9
|
+
* - NEVER modifies user-owned `packages[]` entries.
|
|
10
|
+
* - Uses atomic write (tmp + rename) for all updates.
|
|
11
|
+
* - Detects path conflicts (existing entry with mismatched path).
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
|
|
17
|
+
export interface PluginBridgeRegisterOptions {
|
|
18
|
+
homedir?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PluginBridgeConflict =
|
|
22
|
+
| { type: "ok" }
|
|
23
|
+
| { type: "conflict"; existingPath: string; newPath: string };
|
|
24
|
+
|
|
25
|
+
function getSettingsPath(homedir?: string): string {
|
|
26
|
+
const home = homedir ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
27
|
+
return path.join(home, ".pi", "agent", "settings.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readSettings(settingsPath: string): Record<string, unknown> {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(settingsPath)) return {};
|
|
33
|
+
const raw = fs.readFileSync(settingsPath, "utf-8").trim();
|
|
34
|
+
if (!raw) return {};
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeSettings(settingsPath: string, settings: Record<string, unknown>): void {
|
|
42
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
43
|
+
const tmp = settingsPath + ".tmp." + process.pid;
|
|
44
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
45
|
+
fs.renameSync(tmp, settingsPath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getManagedBridges(
|
|
49
|
+
settings: Record<string, unknown>,
|
|
50
|
+
): Record<string, string> {
|
|
51
|
+
const val = settings.dashboardPluginBridges;
|
|
52
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
53
|
+
return val as Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MANAGED_PREFIX = "dashboard-";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register a plugin's bridge entry in pi's settings.json.
|
|
62
|
+
*
|
|
63
|
+
* Returns { type: "conflict", existingPath, newPath } if a
|
|
64
|
+
* `dashboard-<pluginId>` key already exists but points to a different path.
|
|
65
|
+
* In that case the settings.json is NOT modified.
|
|
66
|
+
*
|
|
67
|
+
* Returns { type: "ok" } on success (including when the entry already matches).
|
|
68
|
+
*/
|
|
69
|
+
export function registerPluginBridge(
|
|
70
|
+
pluginId: string,
|
|
71
|
+
bridgePath: string,
|
|
72
|
+
opts: PluginBridgeRegisterOptions = {},
|
|
73
|
+
): PluginBridgeConflict {
|
|
74
|
+
const settingsPath = getSettingsPath(opts.homedir);
|
|
75
|
+
const settings = readSettings(settingsPath);
|
|
76
|
+
const managed = getManagedBridges(settings);
|
|
77
|
+
const key = MANAGED_PREFIX + pluginId;
|
|
78
|
+
|
|
79
|
+
const existing = managed[key];
|
|
80
|
+
if (existing) {
|
|
81
|
+
if (existing === bridgePath) return { type: "ok" }; // already registered
|
|
82
|
+
return { type: "conflict", existingPath: existing, newPath: bridgePath };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
managed[key] = bridgePath;
|
|
86
|
+
settings.dashboardPluginBridges = managed;
|
|
87
|
+
writeSettings(settingsPath, settings);
|
|
88
|
+
console.info(`[plugin-bridge] Registered bridge for plugin "${pluginId}": ${bridgePath}`);
|
|
89
|
+
return { type: "ok" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove a plugin's bridge entry from pi's settings.json.
|
|
94
|
+
* No-op if the entry does not exist.
|
|
95
|
+
* NEVER touches entries without the `dashboard-` prefix.
|
|
96
|
+
*/
|
|
97
|
+
export function deregisterPluginBridge(
|
|
98
|
+
pluginId: string,
|
|
99
|
+
opts: PluginBridgeRegisterOptions = {},
|
|
100
|
+
): void {
|
|
101
|
+
const settingsPath = getSettingsPath(opts.homedir);
|
|
102
|
+
const settings = readSettings(settingsPath);
|
|
103
|
+
const managed = getManagedBridges(settings);
|
|
104
|
+
const key = MANAGED_PREFIX + pluginId;
|
|
105
|
+
|
|
106
|
+
if (!(key in managed)) return; // nothing to remove
|
|
107
|
+
|
|
108
|
+
delete managed[key];
|
|
109
|
+
settings.dashboardPluginBridges = managed;
|
|
110
|
+
writeSettings(settingsPath, settings);
|
|
111
|
+
console.info(`[plugin-bridge] Deregistered bridge for plugin "${pluginId}"`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register all plugins with bridge entries from the discovery list.
|
|
116
|
+
* Returns a map of pluginId → conflict/ok result.
|
|
117
|
+
* Plugins with conflicts are NOT registered; caller should surface via /api/health.
|
|
118
|
+
*/
|
|
119
|
+
export function registerAllPluginBridges(
|
|
120
|
+
plugins: Array<{ pluginId: string; bridgePath: string }>,
|
|
121
|
+
opts: PluginBridgeRegisterOptions = {},
|
|
122
|
+
): Record<string, PluginBridgeConflict> {
|
|
123
|
+
const results: Record<string, PluginBridgeConflict> = {};
|
|
124
|
+
for (const { pluginId, bridgePath } of plugins) {
|
|
125
|
+
results[pluginId] = registerPluginBridge(pluginId, bridgePath, opts);
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* List all currently managed plugin bridge entries.
|
|
132
|
+
*/
|
|
133
|
+
export function listManagedBridges(
|
|
134
|
+
opts: PluginBridgeRegisterOptions = {},
|
|
135
|
+
): Record<string, string> {
|
|
136
|
+
const settingsPath = getSettingsPath(opts.homedir);
|
|
137
|
+
const settings = readSettings(settingsPath);
|
|
138
|
+
return getManagedBridges(settings);
|
|
139
|
+
}
|