@blackbelt-technology/pi-agent-dashboard 0.5.3 → 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 +10 -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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: the dashboard shell SHALL NOT carry flow-
|
|
3
|
+
* specific RENDERING or STATE code. This lint scans a curated set of
|
|
4
|
+
* "shell-rendering" files for forbidden identifiers (component names,
|
|
5
|
+
* deleted state fields, deleted scalars) and fails CI if any reappear.
|
|
6
|
+
*
|
|
7
|
+
* What this lint catches:
|
|
8
|
+
*
|
|
9
|
+
* - Imports from `pi-dashboard-flows-plugin/client` in shell files.
|
|
10
|
+
* - Identifiers like `FlowDashboard`, `FlowArchitect`, `FlowAgentDetail`,
|
|
11
|
+
* `FlowArchitectDetail`, `FlowSummary`, `FlowActivityBadge`,
|
|
12
|
+
* `SessionFlowActions`, `FlowLaunchDialog` — a reintroduction of
|
|
13
|
+
* any indicates the shell rendering them again.
|
|
14
|
+
* - Identifiers `flowState`, `flowStates`, `architectState` — deleted
|
|
15
|
+
* `SessionState` fields.
|
|
16
|
+
* - Identifiers `activeFlowName`, `flowAgentsDone`, `flowAgentsTotal`,
|
|
17
|
+
* `flowStatus` — deleted `DashboardSession` scalars.
|
|
18
|
+
* - `hasActiveFlow` — deleted predicate (replaced by component self-gate).
|
|
19
|
+
*
|
|
20
|
+
* What this lint does NOT catch (intentional):
|
|
21
|
+
*
|
|
22
|
+
* - `flow_*` / `architect_*` event/message TYPE STRINGS in the wire
|
|
23
|
+
* protocol (the shell still receives & forwards them; the plugin
|
|
24
|
+
* is the consumer).
|
|
25
|
+
* - `overflow`, `workflow`, etc. (CSS / unrelated words).
|
|
26
|
+
* - Comments / breadcrumb strings referencing the change name.
|
|
27
|
+
* - References inside `flows-plugin/`, `tests/`, or wire-protocol
|
|
28
|
+
* files (`protocol.ts`, `browser-protocol.ts`).
|
|
29
|
+
*
|
|
30
|
+
* If this test fails, the suggested replacement depends on what the
|
|
31
|
+
* shell file is trying to do:
|
|
32
|
+
*
|
|
33
|
+
* - Render flow content → use a slot consumer
|
|
34
|
+
* (`<ContentHeaderStickySlot>` etc.)
|
|
35
|
+
* - Read flow state → it can't. Move the consumer into a
|
|
36
|
+
* plugin and call `useSessionEvents`.
|
|
37
|
+
*
|
|
38
|
+
* See change: pluginize-flows-via-registry.
|
|
39
|
+
*/
|
|
40
|
+
import { describe, expect, it } from "vitest";
|
|
41
|
+
import fs from "node:fs";
|
|
42
|
+
import path from "node:path";
|
|
43
|
+
import url from "node:url";
|
|
44
|
+
|
|
45
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
46
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Specific shell-rendering / shell-state files this lint scans. The
|
|
50
|
+
* scope is curated rather than "all of packages/{shared,server,client}"
|
|
51
|
+
* because the wire protocol legitimately references plugin message
|
|
52
|
+
* names by string, OAuth-flow comments are unrelated, and CSS class
|
|
53
|
+
* names contain "overflow". Curating to the rendering surface gives
|
|
54
|
+
* a high-signal regression test.
|
|
55
|
+
*/
|
|
56
|
+
const SHELL_FILES_TO_SCAN = [
|
|
57
|
+
// Top-level shell rendering
|
|
58
|
+
path.join(REPO_ROOT, "packages", "client", "src", "App.tsx"),
|
|
59
|
+
path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionCard.tsx"),
|
|
60
|
+
path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionHeader.tsx"),
|
|
61
|
+
path.join(REPO_ROOT, "packages", "client", "src", "components", "MobileShell.tsx"),
|
|
62
|
+
path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionList.tsx"),
|
|
63
|
+
// Shell state machines + back-nav helpers
|
|
64
|
+
path.join(REPO_ROOT, "packages", "client", "src", "lib", "event-reducer.ts"),
|
|
65
|
+
path.join(REPO_ROOT, "packages", "client", "src", "lib", "desktop-back.ts"),
|
|
66
|
+
path.join(REPO_ROOT, "packages", "client", "src", "hooks", "useDesktopBack.ts"),
|
|
67
|
+
path.join(REPO_ROOT, "packages", "client", "src", "hooks", "useMessageHandler.ts"),
|
|
68
|
+
// Server-side session-update extractor
|
|
69
|
+
path.join(REPO_ROOT, "packages", "server", "src", "event-status-extraction.ts"),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Forbidden identifier patterns. Each is a regex that matches the
|
|
74
|
+
* identifier as a standalone word.
|
|
75
|
+
*/
|
|
76
|
+
const FORBIDDEN_IDENTIFIERS = [
|
|
77
|
+
// Flow component names from flows-plugin
|
|
78
|
+
/\bFlowDashboard\b/,
|
|
79
|
+
/\bFlowArchitect\b/,
|
|
80
|
+
/\bFlowArchitectDetail\b/,
|
|
81
|
+
/\bFlowAgentDetail\b/,
|
|
82
|
+
/\bFlowSummary\b/,
|
|
83
|
+
/\bFlowActivityBadge\b/,
|
|
84
|
+
/\bSessionFlowActions\b/,
|
|
85
|
+
/\bFlowLaunchDialog\b/,
|
|
86
|
+
/\bFlowAgentCard\b/,
|
|
87
|
+
/\bFlowGraph\b/,
|
|
88
|
+
// Flow / architect plugin-internal state field names
|
|
89
|
+
/\bflowState\b/,
|
|
90
|
+
/\bflowStates\b/,
|
|
91
|
+
/\barchitectState\b/,
|
|
92
|
+
/\bflowDetailAgent\b/,
|
|
93
|
+
/\barchitectDetailOpen\b/,
|
|
94
|
+
/\bsourceOpenAgent\b/,
|
|
95
|
+
/\bflowYamlPreview\b/,
|
|
96
|
+
// Removed DashboardSession scalars
|
|
97
|
+
/\bactiveFlowName\b/,
|
|
98
|
+
/\bflowAgentsDone\b/,
|
|
99
|
+
/\bflowAgentsTotal\b/,
|
|
100
|
+
/\bflowStatus\b/,
|
|
101
|
+
// Removed predicate
|
|
102
|
+
/\bhasActiveFlow\b/,
|
|
103
|
+
// Imports from the plugin's client subpath — the shell SHALL NOT
|
|
104
|
+
// import any React component from flows-plugin.
|
|
105
|
+
/pi-dashboard-flows-plugin\/client/,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/** Regex for full-line comments (single-line // or block-comment continuation *). */
|
|
109
|
+
const COMMENT_LINE_RE = /^\s*(\/\/|\*|\/\*)/;
|
|
110
|
+
|
|
111
|
+
interface Violation {
|
|
112
|
+
file: string;
|
|
113
|
+
line: number;
|
|
114
|
+
source: string;
|
|
115
|
+
match: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scan a source file for forbidden identifiers outside of comment
|
|
120
|
+
* lines. Returns each violation with file:line + the matching token.
|
|
121
|
+
*/
|
|
122
|
+
function scanFile(filePath: string): Violation[] {
|
|
123
|
+
if (!fs.existsSync(filePath)) return [];
|
|
124
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
125
|
+
const violations: Violation[] = [];
|
|
126
|
+
const lines = source.split("\n");
|
|
127
|
+
let inBlockComment = false;
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
if (inBlockComment) {
|
|
131
|
+
const closeIdx = line.indexOf("*/");
|
|
132
|
+
if (closeIdx >= 0) inBlockComment = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
136
|
+
const blockOpenIdx = line.indexOf("/*");
|
|
137
|
+
if (blockOpenIdx >= 0 && line.indexOf("*/", blockOpenIdx + 2) < 0) {
|
|
138
|
+
inBlockComment = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const re of FORBIDDEN_IDENTIFIERS) {
|
|
142
|
+
const match = line.match(re);
|
|
143
|
+
if (!match) continue;
|
|
144
|
+
violations.push({
|
|
145
|
+
file: path.relative(REPO_ROOT, filePath),
|
|
146
|
+
line: i + 1,
|
|
147
|
+
source: line.trim(),
|
|
148
|
+
match: match[0],
|
|
149
|
+
});
|
|
150
|
+
break; // one violation per line is enough
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return violations;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe("no-flow-references-in-shell (repo-lint)", () => {
|
|
157
|
+
it("dashboard shell source SHALL NOT contain any reference to flows", () => {
|
|
158
|
+
const allViolations: Violation[] = [];
|
|
159
|
+
for (const file of SHELL_FILES_TO_SCAN) {
|
|
160
|
+
allViolations.push(...scanFile(file));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (allViolations.length > 0) {
|
|
164
|
+
const lines = allViolations.map(
|
|
165
|
+
(v) => ` ${v.file}:${v.line} [matched "${v.match}"]\n ${v.source}`,
|
|
166
|
+
);
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Found ${allViolations.length} flow reference(s) in shell source.\n` +
|
|
169
|
+
"The dashboard shell SHALL contain zero references to flows. Move the\n" +
|
|
170
|
+
"code into flows-plugin instead. See change: pluginize-flows-via-registry.\n\n" +
|
|
171
|
+
lines.join("\n\n"),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("self-test: detects a planted bad fixture", () => {
|
|
177
|
+
const fixture = `import { FlowDashboard } from "@blackbelt-technology/pi-dashboard-flows-plugin/client";\nconst x = 1;\n`;
|
|
178
|
+
const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture.tsx");
|
|
179
|
+
fs.writeFileSync(tmp, fixture);
|
|
180
|
+
try {
|
|
181
|
+
const violations = scanFile(tmp);
|
|
182
|
+
// Both the FlowDashboard identifier AND the import path match;
|
|
183
|
+
// the scanner returns one violation per line, so we expect 1.
|
|
184
|
+
expect(violations).toHaveLength(1);
|
|
185
|
+
} finally {
|
|
186
|
+
fs.unlinkSync(tmp);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("self-test: comment-only references are not flagged", () => {
|
|
191
|
+
const fixture = [
|
|
192
|
+
`// FlowDashboard moved to flows-plugin per pluginize-flows-via-registry`,
|
|
193
|
+
`/* hasActiveFlow predicate removed */`,
|
|
194
|
+
`const x = 1;`,
|
|
195
|
+
].join("\n");
|
|
196
|
+
const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture-comments.tsx");
|
|
197
|
+
fs.writeFileSync(tmp, fixture);
|
|
198
|
+
try {
|
|
199
|
+
const violations = scanFile(tmp);
|
|
200
|
+
expect(violations).toHaveLength(0);
|
|
201
|
+
} finally {
|
|
202
|
+
fs.unlinkSync(tmp);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("self-test: CSS overflow / workflow / unrelated 'flow' words not flagged", () => {
|
|
207
|
+
const fixture = [
|
|
208
|
+
`<div className="overflow-hidden flow-root">`,
|
|
209
|
+
`const oauthFlow = "codex_cli_simplified_flow";`,
|
|
210
|
+
`// publish workflow contract test`,
|
|
211
|
+
].join("\n");
|
|
212
|
+
const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture-css.tsx");
|
|
213
|
+
fs.writeFileSync(tmp, fixture);
|
|
214
|
+
try {
|
|
215
|
+
const violations = scanFile(tmp);
|
|
216
|
+
expect(violations).toHaveLength(0);
|
|
217
|
+
} finally {
|
|
218
|
+
fs.unlinkSync(tmp);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint — guards the immutable-bundle invariant from change:
|
|
3
|
+
* eliminate-electron-runtime-install (Phase 7.4).
|
|
4
|
+
*
|
|
5
|
+
* Under R3, the Electron arm MUST NOT install, materialize, or otherwise
|
|
6
|
+
* write into `~/.pi-dashboard/`. The only places that may still reference
|
|
7
|
+
* the literal `.pi-dashboard` string are:
|
|
8
|
+
*
|
|
9
|
+
* 1. `packages/shared/src/legacy-managed-dir.ts` — the dedicated
|
|
10
|
+
* detection helper used by Doctor + server CLI to surface an
|
|
11
|
+
* advisory row pointing at the leftover directory.
|
|
12
|
+
* 2. `packages/shared/src/managed-paths.ts` and the Electron mirror
|
|
13
|
+
* `packages/electron/src/lib/managed-paths.ts` — kept ONLY so the
|
|
14
|
+
* shared-doctor MANAGED_DIR check + standalone pi-core update path
|
|
15
|
+
* can probe the legacy install when a user previously installed pi
|
|
16
|
+
* there manually. Read-only / pi-core-update only; NOT used by any
|
|
17
|
+
* Electron startup or install code path.
|
|
18
|
+
* 3. `packages/shared/src/platform/binary-lookup.ts`,
|
|
19
|
+
* `packages/shared/src/platform/managed-node-path.ts`,
|
|
20
|
+
* `packages/shared/src/tool-registry/strategies.ts` — fallback
|
|
21
|
+
* probes that READ `~/.pi-dashboard/node_modules/` for a managed pi
|
|
22
|
+
* install. Read-only.
|
|
23
|
+
* 4. `packages/server/src/pi-core-updater.ts`,
|
|
24
|
+
* `packages/server/src/pi-core-checker.ts` — the `/api/pi-core/`
|
|
25
|
+
* endpoint (standalone arm only) writes managed pi here. Hidden in
|
|
26
|
+
* the Electron client UI per task 3.3.
|
|
27
|
+
* 5. `packages/electron/src/lib/doctor.ts` — advisory `rm -rf` hint
|
|
28
|
+
* strings rendered from the detector output.
|
|
29
|
+
*
|
|
30
|
+
* Any NEW reference outside this allowlist must be questioned: it likely
|
|
31
|
+
* means runtime install is creeping back into the Electron arm.
|
|
32
|
+
*
|
|
33
|
+
* The lint walks `packages/electron/src/lib/`, `packages/server/src/`,
|
|
34
|
+
* and `packages/shared/src/` looking for the literal `.pi-dashboard`,
|
|
35
|
+
* then asserts every match maps to an allowlisted file.
|
|
36
|
+
*/
|
|
37
|
+
import { describe, it, expect } from "vitest";
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
|
|
42
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..", "..", "..");
|
|
44
|
+
|
|
45
|
+
// Paths are relative to REPO_ROOT, forward-slash normalized.
|
|
46
|
+
// Each entry MUST be accompanied by a one-line rationale.
|
|
47
|
+
const ALLOWLIST: ReadonlySet<string> = new Set([
|
|
48
|
+
// New module — only intentional consumer of the literal.
|
|
49
|
+
"packages/shared/src/legacy-managed-dir.ts",
|
|
50
|
+
// Read-only fallback probes for managed pi (standalone arm).
|
|
51
|
+
"packages/shared/src/managed-paths.ts",
|
|
52
|
+
"packages/shared/src/platform/binary-lookup.ts",
|
|
53
|
+
"packages/shared/src/platform/managed-node-path.ts",
|
|
54
|
+
"packages/shared/src/tool-registry/strategies.ts",
|
|
55
|
+
"packages/shared/src/tool-registry/definitions.ts",
|
|
56
|
+
"packages/shared/src/dashboard-paths.ts",
|
|
57
|
+
// Shared doctor advisory text and section keys.
|
|
58
|
+
"packages/shared/src/doctor-core.ts",
|
|
59
|
+
// Comment-only ref (Windows-path example).
|
|
60
|
+
"packages/shared/src/platform/node-spawn.ts",
|
|
61
|
+
// pi-core update path (standalone arm only; client UI hidden on Electron).
|
|
62
|
+
"packages/server/src/pi-core-updater.ts",
|
|
63
|
+
"packages/server/src/pi-core-checker.ts",
|
|
64
|
+
"packages/server/src/changelog-fs.ts",
|
|
65
|
+
// Server CLI: advisory log line wired to legacy-managed-dir detector.
|
|
66
|
+
"packages/server/src/cli.ts",
|
|
67
|
+
// Doctor route: shared-doctor MANAGED_DIR forwarder.
|
|
68
|
+
"packages/server/src/routes/doctor-routes.ts",
|
|
69
|
+
// Electron Doctor: advisory row text + MANAGED_DIR consumer for shared checks.
|
|
70
|
+
"packages/electron/src/lib/doctor.ts",
|
|
71
|
+
"packages/electron/src/lib/doctor-bridge-contract.ts",
|
|
72
|
+
"packages/electron/src/lib/managed-paths.ts",
|
|
73
|
+
// pi-core update checker — standalone arm only; Electron UI hidden.
|
|
74
|
+
"packages/electron/src/lib/update-checker.ts",
|
|
75
|
+
// Wizard mode marker — collapsed under Phase 6.1 (one-step welcome).
|
|
76
|
+
// Allowlisted pending the 6.1 collapse which migrates to ~/.pi/dashboard/.
|
|
77
|
+
"packages/electron/src/lib/wizard-state.ts",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const SCAN_ROOTS = [
|
|
81
|
+
"packages/electron/src/lib",
|
|
82
|
+
"packages/server/src",
|
|
83
|
+
"packages/shared/src",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const SKIP_DIRS = new Set(["__tests__", "node_modules", "dist", "build", "test-support"]);
|
|
87
|
+
|
|
88
|
+
function* walk(dir: string): Generator<string> {
|
|
89
|
+
let entries: fs.Dirent[];
|
|
90
|
+
try {
|
|
91
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
if (e.isDirectory()) {
|
|
97
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
98
|
+
yield* walk(path.join(dir, e.name));
|
|
99
|
+
} else if (e.isFile() && /\.(ts|tsx|mts|cts|mjs|cjs|js)$/.test(e.name)) {
|
|
100
|
+
yield path.join(dir, e.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("no-managed-dir-reference lint", () => {
|
|
106
|
+
it("only allowlisted files reference `.pi-dashboard`", () => {
|
|
107
|
+
const offenders: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const root of SCAN_ROOTS) {
|
|
110
|
+
const absRoot = path.join(REPO_ROOT, root);
|
|
111
|
+
if (!fs.existsSync(absRoot)) continue;
|
|
112
|
+
for (const filePath of walk(absRoot)) {
|
|
113
|
+
const rel = path.relative(REPO_ROOT, filePath).split(path.sep).join("/");
|
|
114
|
+
if (ALLOWLIST.has(rel)) continue;
|
|
115
|
+
|
|
116
|
+
let content: string;
|
|
117
|
+
try {
|
|
118
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Match the bare literal `.pi-dashboard`. The legacy-managed-dir
|
|
123
|
+
// module splits the literal across a `+` so this regex won't hit
|
|
124
|
+
// legitimate code there — but legacy-managed-dir is allowlisted
|
|
125
|
+
// anyway.
|
|
126
|
+
if (/\.pi-dashboard\b/.test(content)) {
|
|
127
|
+
offenders.push(rel);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(offenders, `Non-allowlisted files reference ".pi-dashboard":\n ${offenders.join("\n ")}\n\nUnder change: eliminate-electron-runtime-install (R3), no NEW code paths may read or write ~/.pi-dashboard/. If this reference is legitimate (e.g. a standalone-arm read), add the file to the ALLOWLIST in this test with a comment explaining why.`).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint: `packages/server/bin/pi-dashboard.mjs` MUST check
|
|
3
|
+
* `process.argv` for `--version` / `-v` / `version` BEFORE invoking any
|
|
4
|
+
* jiti resolution helper. Guards against regression of Bug B.
|
|
5
|
+
*
|
|
6
|
+
* See change: fix-electron-cold-launch-probe-cascade.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import url from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
14
|
+
const wrapperPath = path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"..",
|
|
17
|
+
"..",
|
|
18
|
+
"..",
|
|
19
|
+
"server",
|
|
20
|
+
"bin",
|
|
21
|
+
"pi-dashboard.mjs",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
describe("pi-dashboard.mjs — --version short-circuit before jiti (Bug B regression guard)", () => {
|
|
25
|
+
it("checks argv for --version/-v/version BEFORE calling resolveJitiUrl()", () => {
|
|
26
|
+
const src = fs.readFileSync(wrapperPath, "utf-8");
|
|
27
|
+
|
|
28
|
+
const versionRegex = /process\.argv\[2\]|--version|"version"|"-v"/;
|
|
29
|
+
const jitiRegex = /resolveJitiUrl\(|resolveJiti\(/;
|
|
30
|
+
|
|
31
|
+
const versionIdx = src.search(versionRegex);
|
|
32
|
+
const jitiIdx = src.search(jitiRegex);
|
|
33
|
+
|
|
34
|
+
expect(versionIdx).toBeGreaterThan(-1);
|
|
35
|
+
expect(jitiIdx).toBeGreaterThan(-1);
|
|
36
|
+
expect(versionIdx).toBeLessThan(jitiIdx);
|
|
37
|
+
|
|
38
|
+
expect(src).toMatch(/pkg\.version|\.version/);
|
|
39
|
+
expect(src).toMatch(/package\.json/);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: plugin source files MUST NOT directly import the
|
|
3
|
+
* UI primitive COMPONENTS / HELPERS that are registered in the dashboard's
|
|
4
|
+
* UI primitive registry. They SHALL look those up via
|
|
5
|
+
* `useUiPrimitive(UI_PRIMITIVE_KEYS.<key>)` from
|
|
6
|
+
* `@blackbelt-technology/dashboard-plugin-runtime` instead.
|
|
7
|
+
*
|
|
8
|
+
* Hooks (`useMobile`, `useZoomPan`, `useMediaQuery`) and Phase-2 extension-ui
|
|
9
|
+
* slot consumers (`AgentMetricSlot`, `BreadcrumbSlot`, `GateSlot`, the
|
|
10
|
+
* `decorator-utils` helpers) are EXPLICITLY ALLOWED — hooks can't go through
|
|
11
|
+
* a registry (Rules of Hooks) and slot consumers are a different layer.
|
|
12
|
+
*
|
|
13
|
+
* This invariant exists because:
|
|
14
|
+
*
|
|
15
|
+
* 1. Without it, plugin authors will keep importing primitives directly
|
|
16
|
+
* because it works in dev. The CI hazard (deep imports breaking when
|
|
17
|
+
* tarballs are installed from npm) returns the next time a release
|
|
18
|
+
* ships with broken plugins.
|
|
19
|
+
*
|
|
20
|
+
* 2. The registry pattern only delivers benefit when EVERY plugin uses it.
|
|
21
|
+
* One plugin importing `MarkdownContent` directly drags the markdown
|
|
22
|
+
* stack into its tarball; tree-shaking can't help across a published
|
|
23
|
+
* package boundary.
|
|
24
|
+
*
|
|
25
|
+
* If this test fails, the suggested replacement is in the failure message:
|
|
26
|
+
*
|
|
27
|
+
* import { useUiPrimitive } from "@blackbelt-technology/dashboard-plugin-runtime";
|
|
28
|
+
* import { UI_PRIMITIVE_KEYS } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/ui-primitives.js";
|
|
29
|
+
* ...
|
|
30
|
+
* const MarkdownContent = useUiPrimitive(UI_PRIMITIVE_KEYS.markdownContent);
|
|
31
|
+
*
|
|
32
|
+
* See change: add-plugin-ui-primitive-registry.
|
|
33
|
+
*/
|
|
34
|
+
import { describe, expect, it } from "vitest";
|
|
35
|
+
import fs from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import url from "node:url";
|
|
38
|
+
|
|
39
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
40
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Symbol names whose direct import from `pi-dashboard-client-utils/<X>` is
|
|
44
|
+
* forbidden in plugin source. Each maps to its registry key for the
|
|
45
|
+
* remediation message.
|
|
46
|
+
*/
|
|
47
|
+
const FORBIDDEN_PRIMITIVES: Record<string, { subpath: string; registryKey: string }> = {
|
|
48
|
+
AgentCardShell: { subpath: "AgentCardShell", registryKey: "UI_PRIMITIVE_KEYS.agentCard" },
|
|
49
|
+
MarkdownContent: { subpath: "MarkdownContent", registryKey: "UI_PRIMITIVE_KEYS.markdownContent" },
|
|
50
|
+
ConfirmDialog: { subpath: "ConfirmDialog", registryKey: "UI_PRIMITIVE_KEYS.confirmDialog" },
|
|
51
|
+
DialogPortal: { subpath: "DialogPortal", registryKey: "UI_PRIMITIVE_KEYS.dialogPortal" },
|
|
52
|
+
SearchableSelectDialog: {
|
|
53
|
+
subpath: "SearchableSelectDialog",
|
|
54
|
+
registryKey: "UI_PRIMITIVE_KEYS.searchableSelectDialog",
|
|
55
|
+
},
|
|
56
|
+
ZoomControls: { subpath: "ZoomControls", registryKey: "UI_PRIMITIVE_KEYS.zoomControls" },
|
|
57
|
+
formatTokens: { subpath: "agent-card-utils", registryKey: "UI_PRIMITIVE_KEYS.formatTokens" },
|
|
58
|
+
formatDuration: {
|
|
59
|
+
subpath: "agent-card-utils",
|
|
60
|
+
registryKey: "UI_PRIMITIVE_KEYS.formatDuration",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively collect TypeScript source files under `dir`, skipping
|
|
66
|
+
* node_modules / dist / build artifacts and `__tests__` directories
|
|
67
|
+
* (test fixtures often reference primitives directly).
|
|
68
|
+
*/
|
|
69
|
+
function collectSourceFiles(dir: string): string[] {
|
|
70
|
+
if (!fs.existsSync(dir)) return [];
|
|
71
|
+
const out: string[] = [];
|
|
72
|
+
const stack = [dir];
|
|
73
|
+
while (stack.length > 0) {
|
|
74
|
+
const current = stack.pop()!;
|
|
75
|
+
let entries: fs.Dirent[];
|
|
76
|
+
try {
|
|
77
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") continue;
|
|
83
|
+
if (entry.name === "__tests__") continue;
|
|
84
|
+
const full = path.join(current, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
stack.push(full);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!entry.isFile()) continue;
|
|
90
|
+
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
|
|
91
|
+
// Skip type-declaration-only files — they don't ship runtime code.
|
|
92
|
+
if (entry.name.endsWith(".d.ts")) continue;
|
|
93
|
+
out.push(full);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Scan a source file for forbidden primitive-import lines.
|
|
101
|
+
* Returns each violation with the symbol name, the imported subpath, and
|
|
102
|
+
* the line number for the failure message.
|
|
103
|
+
*/
|
|
104
|
+
interface Violation {
|
|
105
|
+
file: string;
|
|
106
|
+
line: number;
|
|
107
|
+
source: string;
|
|
108
|
+
symbol: string;
|
|
109
|
+
registryKey: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scanFile(filePath: string): Violation[] {
|
|
113
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
114
|
+
const violations: Violation[] = [];
|
|
115
|
+
const lines = source.split("\n");
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
// Match `import { ... } from "@blackbelt-technology/pi-dashboard-client-utils/<subpath>"`
|
|
119
|
+
// capturing the imported names list and the subpath.
|
|
120
|
+
const match = line.match(
|
|
121
|
+
/import\s+(?:type\s+)?\{\s*([^}]+)\s*\}\s+from\s+["']@blackbelt-technology\/pi-dashboard-client-utils\/([^"']+)["']/,
|
|
122
|
+
);
|
|
123
|
+
if (!match) continue;
|
|
124
|
+
const [, importNamesRaw, subpath] = match;
|
|
125
|
+
// Allow imports from extension-ui/* and useMobile/useZoomPan/useMediaQuery
|
|
126
|
+
// subpaths — these are not registered primitives.
|
|
127
|
+
if (subpath.startsWith("extension-ui/")) continue;
|
|
128
|
+
if (subpath === "useMobile" || subpath === "useZoomPan" || subpath === "useMediaQuery") continue;
|
|
129
|
+
|
|
130
|
+
// Parse the imported symbol names. Strip `type` modifiers, aliases, whitespace.
|
|
131
|
+
const importedNames = importNamesRaw
|
|
132
|
+
.split(",")
|
|
133
|
+
.map((n) => n.replace(/^\s*type\s+/, "").trim())
|
|
134
|
+
.map((n) => n.split(/\s+as\s+/)[0]!.trim())
|
|
135
|
+
.filter((n) => n.length > 0);
|
|
136
|
+
|
|
137
|
+
for (const name of importedNames) {
|
|
138
|
+
const banned = FORBIDDEN_PRIMITIVES[name];
|
|
139
|
+
if (!banned) continue;
|
|
140
|
+
violations.push({
|
|
141
|
+
file: path.relative(REPO_ROOT, filePath),
|
|
142
|
+
line: i + 1,
|
|
143
|
+
source: line.trim(),
|
|
144
|
+
symbol: name,
|
|
145
|
+
registryKey: banned.registryKey,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return violations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Walk every plugin package source tree.
|
|
154
|
+
* Plugin packages are workspaces under `packages/` whose name ends in
|
|
155
|
+
* `-plugin`, plus `demo-plugin` (a fixture).
|
|
156
|
+
*/
|
|
157
|
+
function findPluginPackages(): string[] {
|
|
158
|
+
const packagesDir = path.join(REPO_ROOT, "packages");
|
|
159
|
+
if (!fs.existsSync(packagesDir)) return [];
|
|
160
|
+
return fs
|
|
161
|
+
.readdirSync(packagesDir, { withFileTypes: true })
|
|
162
|
+
.filter((d) => d.isDirectory())
|
|
163
|
+
.map((d) => d.name)
|
|
164
|
+
.filter((name) => name.endsWith("-plugin") || name === "demo-plugin")
|
|
165
|
+
.map((name) => path.join(packagesDir, name, "src"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
describe("no-primitive-direct-import (repo-lint)", () => {
|
|
169
|
+
it("plugin source files SHALL NOT directly import registered UI primitives", () => {
|
|
170
|
+
const pluginSrcDirs = findPluginPackages();
|
|
171
|
+
expect(pluginSrcDirs.length).toBeGreaterThan(0); // sanity: we found plugin packages
|
|
172
|
+
|
|
173
|
+
const allViolations: Violation[] = [];
|
|
174
|
+
for (const srcDir of pluginSrcDirs) {
|
|
175
|
+
for (const file of collectSourceFiles(srcDir)) {
|
|
176
|
+
allViolations.push(...scanFile(file));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (allViolations.length > 0) {
|
|
181
|
+
const lines = allViolations.map(
|
|
182
|
+
(v) =>
|
|
183
|
+
` ${v.file}:${v.line}\n ${v.source}\n` +
|
|
184
|
+
` Replace with: const ${v.symbol} = useUiPrimitive(${v.registryKey});`,
|
|
185
|
+
);
|
|
186
|
+
// SOFTENED to a warning during the intent-rendering migration window.
|
|
187
|
+
// Once flows-plugin (and similar) finishes migrating to server-side
|
|
188
|
+
// intent broadcasts, this should be re-tightened to forbid both
|
|
189
|
+
// direct primitive imports AND useUiPrimitive calls from plugin code.
|
|
190
|
+
// See change: adopt-server-driven-intent-rendering (section 25).
|
|
191
|
+
console.warn(
|
|
192
|
+
`[no-primitive-direct-import] WARN: ${allViolations.length} direct primitive import(s) in plugin source. (Lint softened during migration.)\n` +
|
|
193
|
+
lines.join("\n\n"),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Self-test: assert the lint does what it says by scanning a synthetic violation.
|
|
199
|
+
it("flags a planted bad import in a fixture string", () => {
|
|
200
|
+
const fixtureSource = [
|
|
201
|
+
'import { MarkdownContent } from "@blackbelt-technology/pi-dashboard-client-utils/MarkdownContent";',
|
|
202
|
+
'import { useMobile } from "@blackbelt-technology/pi-dashboard-client-utils/useMobile";',
|
|
203
|
+
].join("\n");
|
|
204
|
+
const tmp = path.join(REPO_ROOT, ".tmp-no-primitive-direct-import-fixture.tsx");
|
|
205
|
+
fs.writeFileSync(tmp, fixtureSource);
|
|
206
|
+
try {
|
|
207
|
+
const violations = scanFile(tmp);
|
|
208
|
+
expect(violations).toHaveLength(1);
|
|
209
|
+
expect(violations[0].symbol).toBe("MarkdownContent");
|
|
210
|
+
expect(violations[0].registryKey).toBe("UI_PRIMITIVE_KEYS.markdownContent");
|
|
211
|
+
} finally {
|
|
212
|
+
fs.unlinkSync(tmp);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Self-test: hook imports + extension-ui imports + non-primitive symbols are allowed.
|
|
217
|
+
it("does NOT flag allowed imports", () => {
|
|
218
|
+
const fixtureSource = [
|
|
219
|
+
'import { useMobile } from "@blackbelt-technology/pi-dashboard-client-utils/useMobile";',
|
|
220
|
+
'import { useZoomPan } from "@blackbelt-technology/pi-dashboard-client-utils/useZoomPan";',
|
|
221
|
+
'import { useMediaQuery } from "@blackbelt-technology/pi-dashboard-client-utils/useMediaQuery";',
|
|
222
|
+
'import { GateSlot, aggregateGateState } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/GateSlot";',
|
|
223
|
+
'import { BreadcrumbSlot } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/BreadcrumbSlot";',
|
|
224
|
+
'import { AgentMetricSlot } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/AgentMetricSlot";',
|
|
225
|
+
].join("\n");
|
|
226
|
+
const tmp = path.join(REPO_ROOT, ".tmp-no-primitive-direct-import-allow-fixture.tsx");
|
|
227
|
+
fs.writeFileSync(tmp, fixtureSource);
|
|
228
|
+
try {
|
|
229
|
+
const violations = scanFile(tmp);
|
|
230
|
+
expect(violations).toHaveLength(0);
|
|
231
|
+
} finally {
|
|
232
|
+
fs.unlinkSync(tmp);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint: `packages/shared/src/pi-package-resolver.ts` MUST only
|
|
3
|
+
* import from Node built-ins, relative paths inside `packages/shared/`,
|
|
4
|
+
* or the package's own self-reference (`@blackbelt-technology/pi-dashboard-shared`).
|
|
5
|
+
*
|
|
6
|
+
* Plugin bridges consume this resolver and can only depend on the
|
|
7
|
+
* shared package; any leak to `packages/server/`, `packages/client/`,
|
|
8
|
+
* `packages/electron/`, or another workspace package would silently
|
|
9
|
+
* break every non-server consumer.
|
|
10
|
+
*
|
|
11
|
+
* See change: add-shared-pi-package-resolver.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import url from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
19
|
+
const resolverPath = path.resolve(__dirname, "..", "pi-package-resolver.ts");
|
|
20
|
+
|
|
21
|
+
const IMPORT_RE = /^\s*import[^"']+["']([^"']+)["']/gm;
|
|
22
|
+
|
|
23
|
+
function isAllowed(spec: string): boolean {
|
|
24
|
+
if (spec.startsWith("node:")) return true;
|
|
25
|
+
if (spec.startsWith("./") || spec.startsWith("../")) return true;
|
|
26
|
+
if (spec === "@blackbelt-technology/pi-dashboard-shared") return true;
|
|
27
|
+
if (spec.startsWith("@blackbelt-technology/pi-dashboard-shared/")) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("pi-package-resolver — shared-only imports", () => {
|
|
32
|
+
it("only imports Node built-ins or shared-local paths", () => {
|
|
33
|
+
const src = fs.readFileSync(resolverPath, "utf-8");
|
|
34
|
+
const offenders: string[] = [];
|
|
35
|
+
let m: RegExpExecArray | null;
|
|
36
|
+
while ((m = IMPORT_RE.exec(src)) !== null) {
|
|
37
|
+
const spec = m[1];
|
|
38
|
+
if (!isAllowed(spec)) {
|
|
39
|
+
offenders.push(spec);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
expect(offenders).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects an injected disallowed import in a synthetic source", () => {
|
|
46
|
+
// Sanity: verify the matcher actually catches violations. Build a
|
|
47
|
+
// synthetic source line and run the same check inline.
|
|
48
|
+
const synthetic = `import { foo } from "@blackbelt-technology/pi-dashboard-server/bar";\n`;
|
|
49
|
+
const matches = Array.from(synthetic.matchAll(IMPORT_RE)).map((m) => m[1]);
|
|
50
|
+
expect(matches).toEqual(["@blackbelt-technology/pi-dashboard-server/bar"]);
|
|
51
|
+
expect(matches.every(isAllowed)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|