@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4
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 +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -14,10 +14,10 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
14
14
|
[
|
|
15
15
|
"pi-anthropic-messages",
|
|
16
16
|
"pi-agent-browser",
|
|
17
|
+
"pi-dashboard-subagents",
|
|
17
18
|
"pi-flows",
|
|
18
19
|
"pi-memory-honcho",
|
|
19
20
|
"pi-web-access",
|
|
20
|
-
"tintinweb-pi-subagents",
|
|
21
21
|
].sort(),
|
|
22
22
|
);
|
|
23
23
|
});
|
|
@@ -34,9 +34,21 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
34
34
|
expect(["required", "strongly-suggested", "optional"]).toContain(entry.status);
|
|
35
35
|
expect(Array.isArray(entry.unlocks)).toBe(true);
|
|
36
36
|
expect(entry.unlocks.length).toBeGreaterThan(0);
|
|
37
|
+
// dashboardPlugin is optional; when present, must be a non-empty string.
|
|
38
|
+
// See change: add-plugin-activation-ui.
|
|
39
|
+
if (entry.dashboardPlugin !== undefined) {
|
|
40
|
+
expect(typeof entry.dashboardPlugin).toBe("string");
|
|
41
|
+
expect(entry.dashboardPlugin.length).toBeGreaterThan(0);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
});
|
|
39
45
|
|
|
46
|
+
it("pi-memory-honcho declares its companion dashboard plugin id", () => {
|
|
47
|
+
// See change: add-plugin-activation-ui (Layer 1.5).
|
|
48
|
+
const entry = getRecommendedExtension("pi-memory-honcho");
|
|
49
|
+
expect(entry?.dashboardPlugin).toBe("honcho");
|
|
50
|
+
});
|
|
51
|
+
|
|
40
52
|
it("pi-anthropic-messages is marked required and uses HTTPS git URL", () => {
|
|
41
53
|
const entry = getRecommendedExtension("pi-anthropic-messages");
|
|
42
54
|
expect(entry).toBeDefined();
|
|
@@ -53,11 +65,16 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
53
65
|
expect(entry?.toolsRegistered).toContain("flow_write");
|
|
54
66
|
});
|
|
55
67
|
|
|
56
|
-
it("
|
|
57
|
-
|
|
68
|
+
it("pi-dashboard-subagents registers Agent and pairs with the subagents plugin", () => {
|
|
69
|
+
// See change: add-subagent-inspector.
|
|
70
|
+
const entry = getRecommendedExtension("pi-dashboard-subagents");
|
|
58
71
|
expect(entry).toBeDefined();
|
|
59
|
-
expect(entry?.source).toBe(
|
|
60
|
-
|
|
72
|
+
expect(entry?.source).toBe(
|
|
73
|
+
"https://github.com/BlackBeltTechnology/pi-dashboard-subagents.git",
|
|
74
|
+
);
|
|
75
|
+
expect(entry?.toolsRegistered).toEqual(["Agent"]);
|
|
76
|
+
expect(entry?.dashboardPlugin).toBe("subagents");
|
|
77
|
+
expect(entry?.autowired).toBe(true);
|
|
61
78
|
});
|
|
62
79
|
|
|
63
80
|
it("npm-sourced entries use the npm: prefix", () => {
|
|
@@ -67,7 +84,6 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
67
84
|
"pi-agent-browser",
|
|
68
85
|
"pi-memory-honcho",
|
|
69
86
|
"pi-web-access",
|
|
70
|
-
"tintinweb-pi-subagents",
|
|
71
87
|
].sort(),
|
|
72
88
|
);
|
|
73
89
|
});
|
|
@@ -80,7 +96,7 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
80
96
|
expect(entry.source).toMatch(/^https:\/\/github\.com\/[^/]+\/[^/]+\.git$/);
|
|
81
97
|
}
|
|
82
98
|
expect(gitEntries.map((e) => e.id).sort()).toEqual(
|
|
83
|
-
["pi-anthropic-messages", "pi-flows"].sort(),
|
|
99
|
+
["pi-anthropic-messages", "pi-dashboard-subagents", "pi-flows"].sort(),
|
|
84
100
|
);
|
|
85
101
|
});
|
|
86
102
|
});
|
|
@@ -105,14 +121,14 @@ describe("getRecommendedByStatus", () => {
|
|
|
105
121
|
it("filters by strongly-suggested", () => {
|
|
106
122
|
const suggested = getRecommendedByStatus("strongly-suggested");
|
|
107
123
|
expect(suggested.map((e) => e.id).sort()).toEqual(
|
|
108
|
-
["pi-flows", "pi-web-access"
|
|
124
|
+
["pi-flows", "pi-web-access"].sort(),
|
|
109
125
|
);
|
|
110
126
|
});
|
|
111
127
|
|
|
112
128
|
it("filters by optional", () => {
|
|
113
129
|
const optional = getRecommendedByStatus("optional");
|
|
114
130
|
expect(optional.map((e) => e.id).sort()).toEqual(
|
|
115
|
-
["pi-agent-browser", "pi-memory-honcho"].sort(),
|
|
131
|
+
["pi-agent-browser", "pi-dashboard-subagents", "pi-memory-honcho"].sort(),
|
|
116
132
|
);
|
|
117
133
|
});
|
|
118
134
|
});
|
|
@@ -139,8 +155,10 @@ describe("BUNDLED_EXTENSION_IDS manifest", () => {
|
|
|
139
155
|
// blocking the bundle-recommended-extensions.mjs license check.
|
|
140
156
|
// Re-add when https://github.com/BlackBeltTechnology/pi-flows has
|
|
141
157
|
// a license declared.
|
|
158
|
+
// pi-dashboard-subagents added in add-subagent-inspector §13.6
|
|
159
|
+
// (git source + MIT license, both gates pass).
|
|
142
160
|
expect([...BUNDLED_EXTENSION_IDS].sort()).toEqual(
|
|
143
|
-
["pi-anthropic-messages"].sort(),
|
|
161
|
+
["pi-anthropic-messages", "pi-dashboard-subagents"].sort(),
|
|
144
162
|
);
|
|
145
163
|
});
|
|
146
164
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-helper parity: `packages/shared/src/pi-package-resolver.ts`
|
|
3
|
+
* duplicates source-kind parsing + install-path computation from
|
|
4
|
+
* `packages/server/src/pi-resource-scanner.ts` (the older server-only
|
|
5
|
+
* helper). If a future maintainer updates one without the other,
|
|
6
|
+
* package resolution diverges silently between the dashboard plugin
|
|
7
|
+
* bridges (shared) and the server-side resources scanner.
|
|
8
|
+
*
|
|
9
|
+
* This test is a structural pin: it asserts the scanner source still
|
|
10
|
+
* contains the same source-kind prefixes the shared resolver handles
|
|
11
|
+
* AND the same install-path layout strings (`.pi/git`, `.pi/agent/git`,
|
|
12
|
+
* `node_modules`). The cross-package file is read via fs only — no
|
|
13
|
+
* import statement so the shared package's tsconfig rootDir is
|
|
14
|
+
* respected.
|
|
15
|
+
*
|
|
16
|
+
* If this fails, sync the two helpers by hand (the resolver here in
|
|
17
|
+
* shared, the scanner in server). A follow-up cleanup that has the
|
|
18
|
+
* scanner consume the shared helper would retire this test.
|
|
19
|
+
*
|
|
20
|
+
* See change: add-shared-pi-package-resolver (Decision D3).
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import url from "node:url";
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
28
|
+
const scannerPath = path.resolve(
|
|
29
|
+
__dirname,
|
|
30
|
+
"..",
|
|
31
|
+
"..",
|
|
32
|
+
"..",
|
|
33
|
+
"server",
|
|
34
|
+
"src",
|
|
35
|
+
"pi-resource-scanner.ts",
|
|
36
|
+
);
|
|
37
|
+
const resolverPath = path.resolve(__dirname, "..", "pi-package-resolver.ts");
|
|
38
|
+
|
|
39
|
+
describe("pi-package-resolver / pi-resource-scanner parity (structural)", () => {
|
|
40
|
+
it("scanner source recognizes all source-kind prefixes the resolver handles", () => {
|
|
41
|
+
const scannerSrc = fs.readFileSync(scannerPath, "utf-8");
|
|
42
|
+
// The shared resolver parses these prefixes; the scanner must too.
|
|
43
|
+
for (const prefix of ['"npm:"', '"git:"', '"https://"', '"http://"', '"ssh://"']) {
|
|
44
|
+
expect(scannerSrc.includes(`.startsWith(${prefix})`)).toBe(true);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("both helpers reference identical install-path layout markers", () => {
|
|
49
|
+
const scannerSrc = fs.readFileSync(scannerPath, "utf-8");
|
|
50
|
+
const resolverSrc = fs.readFileSync(resolverPath, "utf-8");
|
|
51
|
+
// Both helpers must layer git installs under a "git" subdir of either
|
|
52
|
+
// the agentDir (user scope) or `<cwd>/.pi/` (project scope). If one
|
|
53
|
+
// ever switches to e.g. `"repos"` while the other stays on `"git"`,
|
|
54
|
+
// resolutions diverge silently. Marker assertion accepts both spellings.
|
|
55
|
+
for (const [label, src] of [["scanner", scannerSrc], ["resolver", resolverSrc]] as const) {
|
|
56
|
+
const hasGitMarker =
|
|
57
|
+
src.includes('"agent", "git"') ||
|
|
58
|
+
src.includes('".pi", "agent", "git"') ||
|
|
59
|
+
src.includes('"git"');
|
|
60
|
+
expect(hasGitMarker, `${label} must reference a "git" subdir marker`).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
// project-scope <cwd>/.pi/<arm> marker is identical in both.
|
|
63
|
+
for (const [label, src] of [["scanner", scannerSrc], ["resolver", resolverSrc]] as const) {
|
|
64
|
+
expect(src.includes('".pi"'), `${label} must reference the ".pi" config dir`).toBe(true);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("resolver and scanner both consume npm.rootGlobalOr for npm: arm", () => {
|
|
69
|
+
const scannerSrc = fs.readFileSync(scannerPath, "utf-8");
|
|
70
|
+
const resolverSrc = fs.readFileSync(resolverPath, "utf-8");
|
|
71
|
+
// Both must obtain the npm global root the same way; if one stops
|
|
72
|
+
// using this helper the other will go stale.
|
|
73
|
+
expect(scannerSrc.includes("rootGlobalOr")).toBe(true);
|
|
74
|
+
expect(resolverSrc.includes("rootGlobalOr")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -70,4 +70,131 @@ describe("isDashboardRunning", () => {
|
|
|
70
70
|
expect.any(Object),
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
it("returns version from health response when present", async () => {
|
|
75
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: () => Promise.resolve({ ok: true, pid: 12345, version: "1.2.3" }),
|
|
78
|
+
});
|
|
79
|
+
const result = await isDashboardRunning(8000);
|
|
80
|
+
expect(result).toEqual({ running: true, pid: 12345, version: "1.2.3" });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("isDashboardRunning retry semantics (cherry-pick 2)", () => {
|
|
85
|
+
const originalFetch = globalThis.fetch;
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
globalThis.fetch = originalFetch;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("single-shot is the default (retries=0, 1 attempt)", async () => {
|
|
92
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
93
|
+
globalThis.fetch = vi.fn().mockRejectedValue(
|
|
94
|
+
Object.assign(new Error("timeout"), { name: "AbortError" }),
|
|
95
|
+
);
|
|
96
|
+
const result = await isDashboardRunning(8000, "localhost", { _sleep: sleep });
|
|
97
|
+
expect(result).toEqual({ running: false });
|
|
98
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(sleep).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("retries=2 with two transient failures then success returns running:true", async () => {
|
|
103
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
104
|
+
const abort = Object.assign(new Error("timeout"), { name: "AbortError" });
|
|
105
|
+
let call = 0;
|
|
106
|
+
globalThis.fetch = vi.fn().mockImplementation(() => {
|
|
107
|
+
call++;
|
|
108
|
+
if (call < 3) return Promise.reject(abort);
|
|
109
|
+
return Promise.resolve({
|
|
110
|
+
ok: true,
|
|
111
|
+
json: () => Promise.resolve({ ok: true, pid: 999 }),
|
|
112
|
+
} as Response);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await isDashboardRunning(8000, "localhost", {
|
|
116
|
+
retries: 2,
|
|
117
|
+
retryDelayMs: 100,
|
|
118
|
+
_sleep: sleep,
|
|
119
|
+
});
|
|
120
|
+
expect(result).toEqual({ running: true, pid: 999 });
|
|
121
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(3);
|
|
122
|
+
expect(sleep).toHaveBeenCalledTimes(2);
|
|
123
|
+
expect(sleep).toHaveBeenNthCalledWith(1, 100);
|
|
124
|
+
expect(sleep).toHaveBeenNthCalledWith(2, 100);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("portConflict:true short-circuits retries", async () => {
|
|
128
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
129
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
json: () => Promise.resolve({ status: "ok", service: "nginx" }),
|
|
132
|
+
} as Response);
|
|
133
|
+
|
|
134
|
+
const result = await isDashboardRunning(8000, "localhost", {
|
|
135
|
+
retries: 5,
|
|
136
|
+
_sleep: sleep,
|
|
137
|
+
});
|
|
138
|
+
expect(result).toEqual({ running: false, portConflict: true });
|
|
139
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(sleep).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("ECONNREFUSED with default retries=0 returns running:false without retry", async () => {
|
|
144
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
145
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
|
146
|
+
|
|
147
|
+
const result = await isDashboardRunning(8000, "localhost", { _sleep: sleep });
|
|
148
|
+
expect(result).toEqual({ running: false });
|
|
149
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(sleep).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("exhausted retries return the last non-success result", async () => {
|
|
154
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
155
|
+
globalThis.fetch = vi.fn().mockRejectedValue(
|
|
156
|
+
Object.assign(new Error("timeout"), { name: "AbortError" }),
|
|
157
|
+
);
|
|
158
|
+
const result = await isDashboardRunning(8000, "localhost", {
|
|
159
|
+
retries: 3,
|
|
160
|
+
_sleep: sleep,
|
|
161
|
+
});
|
|
162
|
+
expect(result).toEqual({ running: false });
|
|
163
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(4); // 1 + 3 retries
|
|
164
|
+
expect(sleep).toHaveBeenCalledTimes(3);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("custom timeoutMs is passed to AbortController", async () => {
|
|
168
|
+
// Indirect assertion: with timeoutMs=10 and a fetch that takes 50ms,
|
|
169
|
+
// the abort fires and we get running:false. With timeoutMs=200, the
|
|
170
|
+
// same fetch succeeds.
|
|
171
|
+
const makeSlowFetch = (delayMs: number) =>
|
|
172
|
+
vi.fn().mockImplementation(
|
|
173
|
+
(_url: string, init: RequestInit) =>
|
|
174
|
+
new Promise((resolve, reject) => {
|
|
175
|
+
const timer = setTimeout(
|
|
176
|
+
() =>
|
|
177
|
+
resolve({
|
|
178
|
+
ok: true,
|
|
179
|
+
json: () => Promise.resolve({ ok: true, pid: 1 }),
|
|
180
|
+
} as Response),
|
|
181
|
+
delayMs,
|
|
182
|
+
);
|
|
183
|
+
init.signal?.addEventListener("abort", () => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
const err = new Error("aborted");
|
|
186
|
+
err.name = "AbortError";
|
|
187
|
+
reject(err);
|
|
188
|
+
});
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
globalThis.fetch = makeSlowFetch(50);
|
|
193
|
+
const tight = await isDashboardRunning(8000, "localhost", { timeoutMs: 10 });
|
|
194
|
+
expect(tight).toEqual({ running: false });
|
|
195
|
+
|
|
196
|
+
globalThis.fetch = makeSlowFetch(10);
|
|
197
|
+
const loose = await isDashboardRunning(8000, "localhost", { timeoutMs: 200 });
|
|
198
|
+
expect(loose).toEqual({ running: true, pid: 1 });
|
|
199
|
+
});
|
|
73
200
|
});
|
|
@@ -202,6 +202,41 @@ describe("launchDashboardServer — env merge", () => {
|
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
describe("launchDashboardServer — onChildExit (cherry-pick 6a)", () => {
|
|
206
|
+
it("invokes onChildExit when child emits exit after readiness", async () => {
|
|
207
|
+
const child = makeFakeChild();
|
|
208
|
+
const onChildExit = vi.fn();
|
|
209
|
+
await launchDashboardServer(baseOpts({
|
|
210
|
+
_spawnNodeScript: spawnSpy(() => child),
|
|
211
|
+
onChildExit,
|
|
212
|
+
}));
|
|
213
|
+
// Simulate post-readiness crash
|
|
214
|
+
(child as unknown as EventEmitter).emit("exit", 1, null);
|
|
215
|
+
expect(onChildExit).toHaveBeenCalledOnce();
|
|
216
|
+
expect(onChildExit).toHaveBeenCalledWith(1, null);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("fires only once even if exit emitted twice", async () => {
|
|
220
|
+
const child = makeFakeChild();
|
|
221
|
+
const onChildExit = vi.fn();
|
|
222
|
+
await launchDashboardServer(baseOpts({
|
|
223
|
+
_spawnNodeScript: spawnSpy(() => child),
|
|
224
|
+
onChildExit,
|
|
225
|
+
}));
|
|
226
|
+
(child as unknown as EventEmitter).emit("exit", 0, null);
|
|
227
|
+
(child as unknown as EventEmitter).emit("exit", 0, null);
|
|
228
|
+
expect(onChildExit).toHaveBeenCalledOnce(); // child.once not child.on
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does NOT attach any listener when onChildExit omitted", async () => {
|
|
232
|
+
const child = makeFakeChild();
|
|
233
|
+
const listenersBefore = (child as unknown as EventEmitter).listenerCount("exit");
|
|
234
|
+
await launchDashboardServer(baseOpts({ _spawnNodeScript: spawnSpy(() => child) }));
|
|
235
|
+
const listenersAfter = (child as unknown as EventEmitter).listenerCount("exit");
|
|
236
|
+
expect(listenersAfter).toBe(listenersBefore);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
205
240
|
describe("launchDashboardServer — entry URL-wrapping", () => {
|
|
206
241
|
// The launcher delegates to spawnNodeScript, which uses
|
|
207
242
|
// `shouldUrlWrapEntry(loader, platform)`. We verify the launcher
|
|
@@ -14,16 +14,16 @@ describe("parseSourceKey", () => {
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("parses scoped npm name without version", () => {
|
|
17
|
-
expect(parseSourceKey("npm:@
|
|
17
|
+
expect(parseSourceKey("npm:@scope/example-pkg")).toEqual({
|
|
18
18
|
kind: "npm",
|
|
19
|
-
name: "@
|
|
19
|
+
name: "@scope/example-pkg",
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("parses scoped npm name with version", () => {
|
|
24
|
-
expect(parseSourceKey("npm:@
|
|
24
|
+
expect(parseSourceKey("npm:@scope/example-pkg@0.5.2")).toEqual({
|
|
25
25
|
kind: "npm",
|
|
26
|
-
name: "@
|
|
26
|
+
name: "@scope/example-pkg",
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -67,7 +67,7 @@ describe("sourcesMatch", () => {
|
|
|
67
67
|
|
|
68
68
|
it("matches scoped npm names", () => {
|
|
69
69
|
expect(
|
|
70
|
-
sourcesMatch("npm:@
|
|
70
|
+
sourcesMatch("npm:@scope/example-pkg@0.5.2", "npm:@scope/example-pkg"),
|
|
71
71
|
).toBe(true);
|
|
72
72
|
});
|
|
73
73
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `scripts/sync-versions-spec.js::isRewritableSemverSpec`.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `packages/shared/__tests__/` because the repo's vitest projects all
|
|
5
|
+
* scope to `packages/`; the helper itself stays in `scripts/` (pure JS, no
|
|
6
|
+
* runtime deps) so the release script can `import` it without crossing into
|
|
7
|
+
* a workspace package boundary.
|
|
8
|
+
*
|
|
9
|
+
* The classifier decides which dependency specifiers are eligible for the
|
|
10
|
+
* release bump's `^<version>` rewrite. False values are deliberate human
|
|
11
|
+
* overrides and MUST be preserved (the bug this guards: a future hotfix
|
|
12
|
+
* pin like `"*"` being silently rewritten on the next release).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from "vitest";
|
|
15
|
+
// @ts-expect-error — pure JS helper one level above the package; no .d.ts
|
|
16
|
+
import { isRewritableSemverSpec } from "../../../../scripts/sync-versions-spec.js";
|
|
17
|
+
|
|
18
|
+
describe("isRewritableSemverSpec", () => {
|
|
19
|
+
describe("rewritable forms (returns true)", () => {
|
|
20
|
+
it.each([
|
|
21
|
+
["plain", "0.5.0"],
|
|
22
|
+
["caret", "^0.5.0"],
|
|
23
|
+
["tilde", "~0.5.0"],
|
|
24
|
+
["caret + prerelease", "^0.5.0-alpha.1"],
|
|
25
|
+
["plain + prerelease", "0.5.0-rc.0"],
|
|
26
|
+
["caret + build", "^0.5.0+sha.abc"],
|
|
27
|
+
["caret + prerelease + build", "^0.5.0-alpha.1+sha.abc"],
|
|
28
|
+
["multi-digit", "10.20.30"],
|
|
29
|
+
])("returns true for %s (%s)", (_label, spec) => {
|
|
30
|
+
expect(isRewritableSemverSpec(spec)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("preserved forms (returns false)", () => {
|
|
35
|
+
it.each([
|
|
36
|
+
["wildcard", "*"],
|
|
37
|
+
["latest tag", "latest"],
|
|
38
|
+
["dist-tag", "next"],
|
|
39
|
+
["workspace protocol", "workspace:*"],
|
|
40
|
+
["workspace caret", "workspace:^0.5.0"],
|
|
41
|
+
["github URL", "github:owner/repo#sha"],
|
|
42
|
+
["github tarball URL", "https://github.com/o/r/tarball/main"],
|
|
43
|
+
["git+ssh URL", "git+ssh://git@github.com/o/r.git"],
|
|
44
|
+
["file path", "file:../foo"],
|
|
45
|
+
["plain http tarball", "http://example.com/x.tgz"],
|
|
46
|
+
["range gte", ">=1.0.0"],
|
|
47
|
+
["range or-union", "1.0.0 || 2.0.0"],
|
|
48
|
+
["range hyphen", "1.0.0 - 2.0.0"],
|
|
49
|
+
["range x", "1.x"],
|
|
50
|
+
["range x-dotted", "1.x.x"],
|
|
51
|
+
["empty", ""],
|
|
52
|
+
["whitespace only", " "],
|
|
53
|
+
["partial caret", "^"],
|
|
54
|
+
])("returns false for %s (%s)", (_label, spec) => {
|
|
55
|
+
expect(isRewritableSemverSpec(spec)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("non-string inputs", () => {
|
|
60
|
+
it("returns false for undefined", () => {
|
|
61
|
+
expect(isRewritableSemverSpec(undefined)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns false for null", () => {
|
|
65
|
+
expect(isRewritableSemverSpec(null)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns false for number", () => {
|
|
69
|
+
expect(isRewritableSemverSpec(0.5)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns false for object", () => {
|
|
73
|
+
expect(isRewritableSemverSpec({ version: "1.0.0" })).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -40,11 +40,24 @@ function freshRegistry(opts: {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
describe("pi binary definition", () => {
|
|
43
|
-
it("chain order: override → managed → where", () => {
|
|
44
|
-
|
|
43
|
+
it("chain order: override → bare-import ×2 → managed → where", () => {
|
|
44
|
+
// bare-import strategies probe both pi-coding-agent aliases
|
|
45
|
+
// (@earendil-works + @mariozechner) before falling through to
|
|
46
|
+
// managed-bin and PATH. They fail in this fixture because the
|
|
47
|
+
// injected `exists` returns false for all paths.
|
|
48
|
+
// See change: eliminate-electron-runtime-install F9.
|
|
49
|
+
const r = freshRegistry({
|
|
50
|
+
which: (n) => (n === "pi" ? "/usr/bin/pi" : null),
|
|
51
|
+
// No resolveModule injection — real resolver runs against the
|
|
52
|
+
// repo's node_modules. The bare-import strategy returns a
|
|
53
|
+
// path, but `exists: () => false` invalidates it, so the chain
|
|
54
|
+
// falls through to `where`.
|
|
55
|
+
});
|
|
45
56
|
const res = r.resolve("pi");
|
|
46
57
|
expect(res.tried.map((t) => t.strategy)).toEqual([
|
|
47
58
|
"override",
|
|
59
|
+
"bare-import",
|
|
60
|
+
"bare-import",
|
|
48
61
|
"managed",
|
|
49
62
|
"where",
|
|
50
63
|
]);
|
|
@@ -53,6 +66,41 @@ describe("pi binary definition", () => {
|
|
|
53
66
|
expect(res.source).toBe("system");
|
|
54
67
|
});
|
|
55
68
|
|
|
69
|
+
it("bare-import wins over PATH when bundled cli.js exists (F9)", () => {
|
|
70
|
+
// Simulates the Electron immutable-bundle architecture: a
|
|
71
|
+
// bundled @earendil-works/pi-coding-agent ships inside the
|
|
72
|
+
// server's own node_modules. With no PATH, no managed dir,
|
|
73
|
+
// bare-import must resolve the bundled cli.js — otherwise the
|
|
74
|
+
// server falls into bootstrapInstall() and writes to
|
|
75
|
+
// ~/.pi-dashboard/ (the failure mode F9 documents).
|
|
76
|
+
const bundledPkgJson =
|
|
77
|
+
"/Volumes/PI Dashboard/PI-Dashboard.app/Contents/Resources/server/node_modules/@earendil-works/pi-coding-agent/package.json";
|
|
78
|
+
const bundledCli =
|
|
79
|
+
"/Volumes/PI Dashboard/PI-Dashboard.app/Contents/Resources/server/node_modules/@earendil-works/pi-coding-agent/dist/cli.js";
|
|
80
|
+
const r = new ToolRegistry({
|
|
81
|
+
overrides: new OverridesStore({
|
|
82
|
+
filePath: path.join(os.tmpdir(), `f9-test-${Math.random()}.json`),
|
|
83
|
+
warn: () => {},
|
|
84
|
+
}),
|
|
85
|
+
platform: "linux",
|
|
86
|
+
});
|
|
87
|
+
registerDefaultTools(r, {
|
|
88
|
+
exists: (p) => p === bundledCli, // only the bundled cli.js exists
|
|
89
|
+
which: () => null, // no PATH
|
|
90
|
+
npmRootGlobal: () => "", // no npm-global
|
|
91
|
+
resolveModule: (id, _from) =>
|
|
92
|
+
id === "@earendil-works/pi-coding-agent/package.json"
|
|
93
|
+
? bundledPkgJson
|
|
94
|
+
: null,
|
|
95
|
+
});
|
|
96
|
+
const res = r.resolve("pi");
|
|
97
|
+
expect(res.ok).toBe(true);
|
|
98
|
+
expect(res.path).toBe(bundledCli);
|
|
99
|
+
expect(res.tried.find((t) => t.strategy === "bare-import")?.result).toBe(
|
|
100
|
+
"ok",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
56
104
|
it("managed wins over system when MANAGED_BIN/pi exists", () => {
|
|
57
105
|
const managed = path.join(os.homedir(), ".pi-dashboard", "node_modules", ".bin", "pi");
|
|
58
106
|
const r = freshRegistry({
|
|
@@ -43,6 +43,23 @@ export interface FindExtensionDeps {
|
|
|
43
43
|
resolvePackage?: () => string | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Read `name` from `<dir>/package.json`. Returns null on any error
|
|
48
|
+
* (missing file, unreadable, invalid JSON, missing name field).
|
|
49
|
+
* Used for identity-based dedup in `registerBridgeExtension`.
|
|
50
|
+
*/
|
|
51
|
+
function readPackageName(dir: string): string | null {
|
|
52
|
+
try {
|
|
53
|
+
const pkgPath = path.join(dir, "package.json");
|
|
54
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
55
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
return typeof parsed?.name === "string" ? parsed.name : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
function defaultResolvePackage(): string | null {
|
|
47
64
|
try {
|
|
48
65
|
const req = createRequire(import.meta.url);
|
|
@@ -133,12 +150,28 @@ export function registerBridgeExtension(
|
|
|
133
150
|
// Already registered?
|
|
134
151
|
if (packages.includes(extensionPath)) return;
|
|
135
152
|
|
|
136
|
-
//
|
|
153
|
+
// Compute the identity (package.json#name) of the new entry. We use it
|
|
154
|
+
// to dedupe across install layouts (dev / .app / npm-global / legacy
|
|
155
|
+
// managed dir) that all register the same extension under different
|
|
156
|
+
// absolute paths.
|
|
157
|
+
const newIdentity = readPackageName(extensionPath);
|
|
158
|
+
|
|
159
|
+
// Non-destructive cleanup: drop stale dashboard paths AND drop any
|
|
160
|
+
// local entry with the same package.json#name as the new one
|
|
161
|
+
// (most-recently-asserted path wins). npm:-scheme entries pass through
|
|
162
|
+
// untouched.
|
|
137
163
|
const cleaned = packages.filter((p) => {
|
|
138
164
|
if (typeof p !== "string") return true;
|
|
139
165
|
const isLocalPath = p.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(p);
|
|
140
166
|
if (!isLocalPath) return true;
|
|
141
|
-
|
|
167
|
+
|
|
168
|
+
// Identity dedup: same package name as the incoming entry?
|
|
169
|
+
if (newIdentity) {
|
|
170
|
+
const existingIdentity = readPackageName(p);
|
|
171
|
+
if (existingIdentity && existingIdentity === newIdentity) return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Only consider dashboard-related paths for path-based cleanup
|
|
142
175
|
// Normalize: lowercase + collapse spaces/hyphens so "PI Dashboard" matches "pi-dashboard"
|
|
143
176
|
const normalized = p.toLowerCase().replace(/[\s_-]/g, "");
|
|
144
177
|
if (!normalized.includes("pidashboard") && !normalized.includes("piagentdashboard")) return true;
|