@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -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__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +83 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +111 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +310 -39
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +207 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +141 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -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__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/protocol.ts +23 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: `process.platform === "<os>"` branches (and the
|
|
3
|
+
* inverse `!==` form) MUST NOT appear outside the canonical platform
|
|
4
|
+
* primitive locations. All OS-specific behaviour lives under
|
|
5
|
+
* `packages/shared/src/platform/**` (and `packages/electron/src/platform/**`
|
|
6
|
+
* for Electron-specific primitives) plus a small documented allowlist.
|
|
7
|
+
*
|
|
8
|
+
* If this test fails, either:
|
|
9
|
+
* (a) Move the platform-aware logic into a platform/* primitive that
|
|
10
|
+
* takes an optional `platform: NodeJS.Platform` parameter, OR
|
|
11
|
+
* (b) Add an opt-out marker `// platform-branch-ok` on the same line
|
|
12
|
+
* for a genuine, localised OS probe (e.g. a top-level env fingerprint).
|
|
13
|
+
*
|
|
14
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from "vitest";
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import url from "node:url";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Files / directory-prefixes where platform branches are allowed.
|
|
23
|
+
*
|
|
24
|
+
* Each entry is a repo-relative path using forward slashes. Entries
|
|
25
|
+
* ending in `/` match any file under that directory (prefix match);
|
|
26
|
+
* entries without a trailing slash must match exactly.
|
|
27
|
+
*
|
|
28
|
+
* Every entry has a one-line reason and a follow-up owner.
|
|
29
|
+
*/
|
|
30
|
+
const ALLOWLIST: readonly string[] = [
|
|
31
|
+
// Canonical platform primitives — the whole POINT is platform branching.
|
|
32
|
+
"packages/shared/src/platform/",
|
|
33
|
+
// Electron-specific platform primitives.
|
|
34
|
+
"packages/electron/src/platform/",
|
|
35
|
+
|
|
36
|
+
// ── Seed allowlist (documented follow-ups, out of scope for this change)
|
|
37
|
+
|
|
38
|
+
// Extension's pgid/ps scanner — platform-aware but uses `_platform` test
|
|
39
|
+
// hooks rather than shared primitives; consolidating into
|
|
40
|
+
// shared/platform/process-scan.ts is a separate change.
|
|
41
|
+
"packages/extension/src/process-scanner.ts",
|
|
42
|
+
|
|
43
|
+
// Electron dependency detection predates the tool-registry; migration
|
|
44
|
+
// to ToolRegistry.resolve is a separate change.
|
|
45
|
+
"packages/electron/src/lib/dependency-detector.ts",
|
|
46
|
+
|
|
47
|
+
// Electron top-level bootstrap: process.platform printed in log output,
|
|
48
|
+
// legitimate observability use.
|
|
49
|
+
"packages/electron/src/main.ts",
|
|
50
|
+
|
|
51
|
+
// Electron doctor: reports process.platform to the user.
|
|
52
|
+
"packages/electron/src/lib/doctor.ts",
|
|
53
|
+
|
|
54
|
+
// Electron forge config: build-time darwin special-case.
|
|
55
|
+
"packages/electron/forge.config.ts",
|
|
56
|
+
|
|
57
|
+
// Server process-manager: one domain branch in spawnHeadless picking
|
|
58
|
+
// Unix "sh -c tail -f" wrapper vs Windows direct node.exe spawn.
|
|
59
|
+
// The wrapper is genuinely Unix-only (sh+tail); splitting the headless
|
|
60
|
+
// mechanism into two is tracked as a follow-up.
|
|
61
|
+
"packages/server/src/process-manager.ts",
|
|
62
|
+
|
|
63
|
+
// Server editor registry: selects per-OS process patterns from a data
|
|
64
|
+
// table. Genuine data-lookup branching, benign.
|
|
65
|
+
"packages/server/src/editor-registry.ts",
|
|
66
|
+
|
|
67
|
+
// Server tunnel: surfaces process.platform in a response body.
|
|
68
|
+
"packages/server/src/tunnel.ts",
|
|
69
|
+
|
|
70
|
+
// Server browse: returns process.platform in BrowseResult for the
|
|
71
|
+
// client path-picker (protocol surface).
|
|
72
|
+
"packages/server/src/browse.ts",
|
|
73
|
+
|
|
74
|
+
// Client session-grouping: reads process.platform in a comment-only
|
|
75
|
+
// doc reference and uses inferPlatform heuristic; no actual branch.
|
|
76
|
+
"packages/client/src/lib/session-grouping.ts",
|
|
77
|
+
|
|
78
|
+
// ── Follow-up: migrate to electron/src/platform/ per deferred
|
|
79
|
+
// consolidate-platform-handlers (18→13 file refactor).
|
|
80
|
+
|
|
81
|
+
// App menu: darwin detection for role:appMenu (Electron convention).
|
|
82
|
+
"packages/electron/src/lib/app-menu.ts",
|
|
83
|
+
// Bundled node: win32 binary name suffix; data-lookup branch.
|
|
84
|
+
"packages/electron/src/lib/bundled-node.ts",
|
|
85
|
+
// Server lifecycle: win32 managed-tsx.cmd + which/where probes.
|
|
86
|
+
"packages/electron/src/lib/server-lifecycle.ts",
|
|
87
|
+
// Tray icon: platform-specific asset selection; will move to
|
|
88
|
+
// electron/src/platform/tray-icon.ts in deferred consolidation.
|
|
89
|
+
"packages/electron/src/lib/tray.ts",
|
|
90
|
+
|
|
91
|
+
// Server editor PID registry: per-OS process pattern matching for
|
|
92
|
+
// orphan detection on boot. Genuine data-table branching.
|
|
93
|
+
"packages/server/src/editor-pid-registry.ts",
|
|
94
|
+
// Electron dependency installer: Windows npm is npm.cmd (batch wrapper);
|
|
95
|
+
// spawn('npm') without .cmd extension fails ENOENT on Windows. The branch
|
|
96
|
+
// routes around this by preferring bundled node+npm-cli.js on Windows.
|
|
97
|
+
// Follow-up: migrate to a platform/exec npm-resolver primitive.
|
|
98
|
+
"packages/electron/src/lib/dependency-installer.ts",
|
|
99
|
+
// fix-pty-permissions: Windows short-circuit (no chmod needed).
|
|
100
|
+
"packages/server/src/fix-pty-permissions.ts",
|
|
101
|
+
// package-manager-wrapper: comment-only reference; no runtime branch.
|
|
102
|
+
"packages/server/src/package-manager-wrapper.ts",
|
|
103
|
+
// terminal-manager: win32 branch for node-pty spawnOptions; will move
|
|
104
|
+
// to platform/terminal in deferred consolidation.
|
|
105
|
+
"packages/server/src/terminal-manager.ts",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const PLATFORM_BRANCH_RE = /process\.platform\s*(===|!==)\s*["'](win32|linux|darwin)["']/;
|
|
109
|
+
|
|
110
|
+
const OPT_OUT_MARKER = "platform-branch-ok";
|
|
111
|
+
|
|
112
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
113
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const full = path.join(dir, entry.name);
|
|
116
|
+
if (entry.isDirectory()) {
|
|
117
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__") continue;
|
|
118
|
+
yield* walk(full);
|
|
119
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
120
|
+
yield full;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Check if a repo-relative normalised path is covered by the allowlist. */
|
|
126
|
+
function isAllowed(relPath: string, allow: readonly string[]): boolean {
|
|
127
|
+
for (const entry of allow) {
|
|
128
|
+
if (entry.endsWith("/")) {
|
|
129
|
+
if (relPath.startsWith(entry)) return true;
|
|
130
|
+
} else {
|
|
131
|
+
if (relPath === entry) return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("no direct process.platform branches outside platform/**", () => {
|
|
138
|
+
it("only allowlisted files contain process.platform === \"<os>\" branches", async () => {
|
|
139
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
140
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
141
|
+
const packagesDir = path.resolve(repoRoot, "packages");
|
|
142
|
+
|
|
143
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
144
|
+
|
|
145
|
+
for (const pkg of await fs.readdir(packagesDir, { withFileTypes: true })) {
|
|
146
|
+
if (!pkg.isDirectory()) continue;
|
|
147
|
+
const srcDir = path.join(packagesDir, pkg.name, "src");
|
|
148
|
+
try { await fs.access(srcDir); } catch { continue; }
|
|
149
|
+
|
|
150
|
+
for await (const file of walk(srcDir)) {
|
|
151
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
|
|
152
|
+
if (isAllowed(relPath, ALLOWLIST)) continue;
|
|
153
|
+
|
|
154
|
+
const content = await fs.readFile(file, "utf-8");
|
|
155
|
+
const lines = content.split(/\r?\n/);
|
|
156
|
+
lines.forEach((line, idx) => {
|
|
157
|
+
if (!PLATFORM_BRANCH_RE.test(line)) return;
|
|
158
|
+
if (line.includes(OPT_OUT_MARKER)) return;
|
|
159
|
+
violations.push({ file: relPath, line: idx + 1, text: line.trim() });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (violations.length > 0) {
|
|
165
|
+
const msg =
|
|
166
|
+
`Direct process.platform branches found outside the allowlist.\n` +
|
|
167
|
+
`Move the logic into a platform/* primitive or add a ` +
|
|
168
|
+
`\`// ${OPT_OUT_MARKER}\` comment on the line with a justification.\n\n` +
|
|
169
|
+
`Offenders (${violations.length}):\n` +
|
|
170
|
+
violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
|
|
171
|
+
expect(violations, msg).toEqual([]);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: `process.kill(...)` MUST NOT be called directly
|
|
3
|
+
* outside `packages/shared/src/platform/`. All termination / liveness must
|
|
4
|
+
* go through the platform helpers (`isProcessAlive`, `killProcess`,
|
|
5
|
+
* `killPidWithGroup`) so that Windows tree-kill (taskkill /F /T /PID) and
|
|
6
|
+
* POSIX process-group semantics are applied uniformly.
|
|
7
|
+
*
|
|
8
|
+
* If this test fails, migrate the offending file's call to:
|
|
9
|
+
* import { isProcessAlive, killProcess, killPidWithGroup }
|
|
10
|
+
* from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
11
|
+
*
|
|
12
|
+
* See change: route-kill-paths-through-platform.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from "vitest";
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import url from "node:url";
|
|
18
|
+
|
|
19
|
+
/** Files or directories allowed to call `process.kill(...)` directly. */
|
|
20
|
+
const ALLOWLIST_DIRS: readonly string[] = [
|
|
21
|
+
"packages/shared/src/platform",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Regex catches any textual reference to `process.kill(...)`. We match on
|
|
26
|
+
* whole-word `process` to avoid flagging `childProcess.kill(...)`, which
|
|
27
|
+
* is the `ChildProcess#kill()` instance method, not the global
|
|
28
|
+
* `process.kill`. Calls on `ChildProcess` instances are banned separately
|
|
29
|
+
* via code review / type-guided refactors, not this lint.
|
|
30
|
+
*/
|
|
31
|
+
const PROCESS_KILL_RE = /(?:^|[^.\w])process\.kill\s*\(/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Per-line opt-out marker. Use for embedded scripts that run in a
|
|
35
|
+
* separate Node process (e.g. the `node -e` orchestrator string in
|
|
36
|
+
* restart-helper.ts):
|
|
37
|
+
* const orchestrator = `process.kill(pid, 0);` // ban:process-kill-ok
|
|
38
|
+
*/
|
|
39
|
+
const OPT_OUT_MARKER = "ban:process-kill-ok";
|
|
40
|
+
|
|
41
|
+
/** Recursively walk a directory, yielding all .ts / .tsx files. */
|
|
42
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
43
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const full = path.join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__") continue;
|
|
48
|
+
yield* walk(full);
|
|
49
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
50
|
+
yield full;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("no direct process.kill outside packages/shared/src/platform/", () => {
|
|
56
|
+
it("only allowlisted paths call process.kill directly", async () => {
|
|
57
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
58
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
59
|
+
const packagesDir = path.resolve(repoRoot, "packages");
|
|
60
|
+
|
|
61
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
62
|
+
const allowPrefixes = ALLOWLIST_DIRS.map((p) =>
|
|
63
|
+
path.resolve(repoRoot, p).replace(/\\/g, "/") + "/",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const pkg of await fs.readdir(packagesDir, { withFileTypes: true })) {
|
|
67
|
+
if (!pkg.isDirectory()) continue;
|
|
68
|
+
const srcDir = path.join(packagesDir, pkg.name, "src");
|
|
69
|
+
try {
|
|
70
|
+
await fs.access(srcDir);
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for await (const file of walk(srcDir)) {
|
|
75
|
+
const normalized = file.replace(/\\/g, "/");
|
|
76
|
+
if (allowPrefixes.some((prefix) => normalized.startsWith(prefix))) continue;
|
|
77
|
+
|
|
78
|
+
const content = await fs.readFile(file, "utf-8");
|
|
79
|
+
const lines = content.split(/\r?\n/);
|
|
80
|
+
lines.forEach((line, idx) => {
|
|
81
|
+
if (!PROCESS_KILL_RE.test(line)) return;
|
|
82
|
+
if (line.includes(OPT_OUT_MARKER)) return;
|
|
83
|
+
violations.push({
|
|
84
|
+
file: path.relative(repoRoot, file),
|
|
85
|
+
line: idx + 1,
|
|
86
|
+
text: line.trim(),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (violations.length > 0) {
|
|
93
|
+
const msg =
|
|
94
|
+
`Direct process.kill(...) calls found outside packages/shared/src/platform/.\n` +
|
|
95
|
+
`Migrate each to a platform helper:\n` +
|
|
96
|
+
` import { isProcessAlive, killProcess, killPidWithGroup }\n` +
|
|
97
|
+
` from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";\n\n` +
|
|
98
|
+
`Offenders (${violations.length}):\n` +
|
|
99
|
+
violations
|
|
100
|
+
.map((v) => ` ${v.file}:${v.line} ${v.text}`)
|
|
101
|
+
.join("\n");
|
|
102
|
+
expect(violations, msg).toEqual([]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: build-time scripts (CI workflows, Dockerfiles,
|
|
3
|
+
* shell scripts, root-level CJS helpers) MUST NOT hardcode
|
|
4
|
+
* `node_modules/electron` or `node_modules/node-pty` paths. Instead, they
|
|
5
|
+
* MUST resolve through the tool registry — either via the shared shell
|
|
6
|
+
* wrapper at `packages/shared/bin/pi-dashboard-resolve-tool.cjs`, or
|
|
7
|
+
* (for postinstall paths that run before the shared package is built)
|
|
8
|
+
* via `require.resolve("<pkg>/package.json")` matching the registry's
|
|
9
|
+
* `bare-import` strategy semantics.
|
|
10
|
+
*
|
|
11
|
+
* This invariant exists because npm workspace hoisting moves these
|
|
12
|
+
* packages between `packages/<workspace>/node_modules/<pkg>/` (nested)
|
|
13
|
+
* and `<repoRoot>/node_modules/<pkg>/` (hoisted) depending on the
|
|
14
|
+
* workspaces config and npm version. The v0.4.0 release crisis was
|
|
15
|
+
* caused exactly by this: `cd packages/electron/node_modules/electron`
|
|
16
|
+
* stopped working after `f51e352` switched workspace cross-refs to
|
|
17
|
+
* plain semver.
|
|
18
|
+
*
|
|
19
|
+
* If this test fails, replace the offending substring with one of:
|
|
20
|
+
*
|
|
21
|
+
* # Shell / YAML / Dockerfile (build-time, has access to repo source):
|
|
22
|
+
* ELECTRON_DIR=$(node packages/shared/bin/pi-dashboard-resolve-tool.cjs electron)
|
|
23
|
+
* cd "$ELECTRON_DIR" && ...
|
|
24
|
+
*
|
|
25
|
+
* # CJS root postinstall (runs DURING npm install — must inline):
|
|
26
|
+
* const ptyPkg = require.resolve("node-pty/package.json");
|
|
27
|
+
* const prebuildsDir = path.join(path.dirname(ptyPkg), "prebuilds");
|
|
28
|
+
*
|
|
29
|
+
* See change: register-build-time-tools.
|
|
30
|
+
*/
|
|
31
|
+
import { describe, expect, it } from "vitest";
|
|
32
|
+
import fs from "node:fs";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import url from "node:url";
|
|
35
|
+
|
|
36
|
+
/** Banned substrings (after comment-stripping). */
|
|
37
|
+
const PATTERNS: readonly { re: RegExp; suggestion: string }[] = [
|
|
38
|
+
{
|
|
39
|
+
re: /node_modules\/electron(?:\/|\b)/,
|
|
40
|
+
suggestion:
|
|
41
|
+
"Use `node packages/shared/bin/pi-dashboard-resolve-tool.cjs electron`",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
re: /node_modules\/node-pty(?:\/|\b)/,
|
|
45
|
+
suggestion:
|
|
46
|
+
'Use `require.resolve("node-pty/package.json")` (mirrors the registry\'s bare-import strategy)',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Files explicitly allowed to contain the banned substrings. Each entry
|
|
52
|
+
* is a repo-relative path matched exactly. Add an entry only when the
|
|
53
|
+
* substring appears as a non-path token (e.g. an argument to
|
|
54
|
+
* `require.resolve`, a comment quoting an example, or this lint file
|
|
55
|
+
* itself). Document the reason inline.
|
|
56
|
+
*/
|
|
57
|
+
const ALLOWLIST: readonly string[] = [
|
|
58
|
+
// The lint file itself contains every banned substring as test data.
|
|
59
|
+
"packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts",
|
|
60
|
+
// Root postinstall — uses `require.resolve("node-pty/package.json")`,
|
|
61
|
+
// which contains "node-pty" as an argument string but not as a
|
|
62
|
+
// hardcoded path. Allowlisted because it must run before the shared
|
|
63
|
+
// package is unpacked. See file header for full reasoning.
|
|
64
|
+
"scripts/fix-pty-permissions.cjs",
|
|
65
|
+
// Sister postinstall script (workspace-scoped) — same rationale.
|
|
66
|
+
"packages/server/scripts/fix-pty-permissions.cjs",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Repo-relative file list to scan.
|
|
71
|
+
*
|
|
72
|
+
* The scope is intentionally narrow: only the build-time sites that the
|
|
73
|
+
* `register-build-time-tools` change migrated, plus the postinstall
|
|
74
|
+
* scripts that mirror the registry's `bare-import` semantics. Bundle /
|
|
75
|
+
* Docker entrypoint scripts (`bundle-server.sh`, `docker-make.sh`,
|
|
76
|
+
* `test-electron-install-inner.sh`, etc.) are NOT in scope: those
|
|
77
|
+
* operate on a known WORKDIR with deterministic node_modules layout
|
|
78
|
+
* inside the build image and are not affected by host-side hoisting.
|
|
79
|
+
*/
|
|
80
|
+
const SCAN_FILES: readonly string[] = [
|
|
81
|
+
".github/workflows/publish.yml",
|
|
82
|
+
".github/workflows/ci.yml",
|
|
83
|
+
"packages/electron/scripts/Dockerfile.build",
|
|
84
|
+
"scripts/fix-pty-permissions.cjs",
|
|
85
|
+
"packages/server/scripts/fix-pty-permissions.cjs",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
interface Violation {
|
|
89
|
+
file: string;
|
|
90
|
+
line: number;
|
|
91
|
+
col: number;
|
|
92
|
+
text: string;
|
|
93
|
+
suggestion: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Strip a single line's trailing comment for YAML / shell / JS-style
|
|
98
|
+
* line comments. Preserves substring matches inside strings as actual
|
|
99
|
+
* content (we don't try to parse string literals — keeping it simple).
|
|
100
|
+
*
|
|
101
|
+
* Specifically:
|
|
102
|
+
* - `# ...` (YAML, shell): everything from a `#` not preceded by a
|
|
103
|
+
* non-space alphanumeric is dropped. Matches GitHub Actions /
|
|
104
|
+
* bash conventions.
|
|
105
|
+
* - `// ...` (JS): everything from `//` to end of line is dropped.
|
|
106
|
+
*
|
|
107
|
+
* This is intentionally simple. False positives only matter if a banned
|
|
108
|
+
* pattern appears INSIDE a string literal (which would still be the
|
|
109
|
+
* bug we want to catch); false negatives only matter for inline
|
|
110
|
+
* comments (`echo foo # comment node_modules/electron`), which we
|
|
111
|
+
* exclude correctly.
|
|
112
|
+
*/
|
|
113
|
+
function stripLineComment(line: string): string {
|
|
114
|
+
// JS-style first.
|
|
115
|
+
const jsIdx = line.indexOf("//");
|
|
116
|
+
if (jsIdx >= 0) line = line.slice(0, jsIdx);
|
|
117
|
+
// Shell/YAML `#` — only when preceded by whitespace or start-of-line.
|
|
118
|
+
const hashMatch = line.match(/(^|\s)#/);
|
|
119
|
+
if (hashMatch && typeof hashMatch.index === "number") {
|
|
120
|
+
line = line.slice(0, hashMatch.index);
|
|
121
|
+
}
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("no hardcoded node_modules/<dep> paths in build-time files", () => {
|
|
126
|
+
it("only allowlisted files reference node_modules/electron or node_modules/node-pty", () => {
|
|
127
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
128
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
129
|
+
|
|
130
|
+
const violations: Violation[] = [];
|
|
131
|
+
const allowSet = new Set(
|
|
132
|
+
ALLOWLIST.map((p) => path.resolve(repoRoot, p).replace(/\\/g, "/")),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
for (const rel of SCAN_FILES) {
|
|
136
|
+
const file = path.resolve(repoRoot, rel);
|
|
137
|
+
if (!fs.existsSync(file)) continue;
|
|
138
|
+
const normalized = file.replace(/\\/g, "/");
|
|
139
|
+
if (allowSet.has(normalized)) continue;
|
|
140
|
+
|
|
141
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
142
|
+
const lines = content.split(/\r?\n/);
|
|
143
|
+
|
|
144
|
+
lines.forEach((rawLine, idx) => {
|
|
145
|
+
const stripped = stripLineComment(rawLine);
|
|
146
|
+
for (const { re, suggestion } of PATTERNS) {
|
|
147
|
+
const m = stripped.match(re);
|
|
148
|
+
if (!m) continue;
|
|
149
|
+
const col = rawLine.indexOf(m[0]);
|
|
150
|
+
violations.push({
|
|
151
|
+
file: path.relative(repoRoot, file),
|
|
152
|
+
line: idx + 1,
|
|
153
|
+
col: col >= 0 ? col + 1 : 1,
|
|
154
|
+
text: rawLine.trim(),
|
|
155
|
+
suggestion,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (violations.length > 0) {
|
|
162
|
+
const msg =
|
|
163
|
+
`Hardcoded \`node_modules/<dep>\` path(s) found in build-time files.\n` +
|
|
164
|
+
`These break under npm workspace hoisting changes (see v0.4.0 release crisis).\n` +
|
|
165
|
+
`Use the tool registry instead. See change: register-build-time-tools.\n\n` +
|
|
166
|
+
`Offenders (${violations.length}):\n` +
|
|
167
|
+
violations
|
|
168
|
+
.map(
|
|
169
|
+
(v) =>
|
|
170
|
+
` ${v.file}:${v.line}:${v.col} ${v.text}\n → ${v.suggestion}`,
|
|
171
|
+
)
|
|
172
|
+
.join("\n");
|
|
173
|
+
expect(violations, msg).toEqual([]);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: any source file that passes an argv to Node
|
|
3
|
+
* with `--import` or `--loader` MUST wrap the following positions
|
|
4
|
+
* (loader and entry script) in `file://` URLs via `toFileUrl(...)` or
|
|
5
|
+
* `pathToFileURL(...).href`. Raw OS paths on Windows drives whose
|
|
6
|
+
* letter collides with URL-scheme parsing (e.g. `B:\`) crash Node with
|
|
7
|
+
* `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
|
|
8
|
+
*
|
|
9
|
+
* If this test fails, migrate the offending file to use
|
|
10
|
+
* `spawnNodeScript` or wrap the entry/loader with `toFileUrl` from
|
|
11
|
+
* `@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js`.
|
|
12
|
+
*
|
|
13
|
+
* See change: fix-windows-entry-script-url.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from "vitest";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import url from "node:url";
|
|
19
|
+
|
|
20
|
+
/** Files allowed to reference --import / --loader with raw identifiers. */
|
|
21
|
+
const ALLOWLIST: readonly string[] = [
|
|
22
|
+
"packages/shared/src/platform/node-spawn.ts",
|
|
23
|
+
// resolve-jiti.ts returns a file:// URL to callers; it does not itself
|
|
24
|
+
// build a `["--import", X, Y]` argv. Allowlisted as the documented
|
|
25
|
+
// source of loader URLs referenced in server spawn call sites.
|
|
26
|
+
"packages/shared/src/resolve-jiti.ts",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Per-line opt-out for intentional usages (e.g. comment examples). */
|
|
30
|
+
const OPT_OUT_MARKER = "ban:raw-node-import-ok";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect argv arrays containing `"--import"` or `"--loader"` followed by
|
|
34
|
+
* a bare identifier (not a string literal and not a wrapped call).
|
|
35
|
+
*
|
|
36
|
+
* We match the argv-literal shape:
|
|
37
|
+
* ["--import", X, Y]
|
|
38
|
+
* args: ["--import", X, Y, ...]
|
|
39
|
+
*
|
|
40
|
+
* Then check that both X and Y are either:
|
|
41
|
+
* - a string literal starting with "file:" (already a URL)
|
|
42
|
+
* - a call expression to toFileUrl(...) or pathToFileURL(...).href
|
|
43
|
+
* - the identifier resolveJitiImport() / resolveJitiFromAnchor() (which
|
|
44
|
+
* are documented to return file:// URLs — allowlisted by name)
|
|
45
|
+
*
|
|
46
|
+
* Anything else is flagged.
|
|
47
|
+
*/
|
|
48
|
+
const IMPORT_ARGV_RE =
|
|
49
|
+
/["']--(?:import|loader)["']\s*,\s*([^,\]]+?)\s*,\s*([^,\]]+?)(?:\s*,|\s*\])/g;
|
|
50
|
+
|
|
51
|
+
const URL_LOOKING_RE =
|
|
52
|
+
/^(?:["']file:|toFileUrl\s*\(|pathToFileURL\s*\([^)]*\)\s*\.href|resolveJitiImport\s*\(|resolveJitiFromAnchor\s*\()/;
|
|
53
|
+
|
|
54
|
+
/** Recursively walk a directory, yielding .ts / .tsx files. */
|
|
55
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
56
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const full = path.join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__") continue;
|
|
61
|
+
yield* walk(full);
|
|
62
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
63
|
+
yield full;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("no raw paths passed to node --import / --loader", () => {
|
|
69
|
+
it("only URL-wrapped or allowlisted argv positions follow --import / --loader", async () => {
|
|
70
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
71
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
72
|
+
const packagesDir = path.resolve(repoRoot, "packages");
|
|
73
|
+
|
|
74
|
+
const allowSet = new Set(
|
|
75
|
+
ALLOWLIST.map((p) => path.resolve(repoRoot, p).replace(/\\/g, "/")),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
79
|
+
|
|
80
|
+
for (const pkg of await fs.readdir(packagesDir, { withFileTypes: true })) {
|
|
81
|
+
if (!pkg.isDirectory()) continue;
|
|
82
|
+
const srcDir = path.join(packagesDir, pkg.name, "src");
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(srcDir);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for await (const file of walk(srcDir)) {
|
|
89
|
+
const normalized = file.replace(/\\/g, "/");
|
|
90
|
+
if (allowSet.has(normalized)) continue;
|
|
91
|
+
|
|
92
|
+
const content = await fs.readFile(file, "utf-8");
|
|
93
|
+
const lines = content.split(/\r?\n/);
|
|
94
|
+
|
|
95
|
+
// Walk each line and check for the argv pattern. Track byte
|
|
96
|
+
// offsets so we can compute line numbers for multi-line matches.
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
const line = lines[i]!;
|
|
100
|
+
// Fast path: only inspect lines that mention --import or --loader.
|
|
101
|
+
if (!line.includes("--import") && !line.includes("--loader")) {
|
|
102
|
+
offset += line.length + 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.includes(OPT_OUT_MARKER)) {
|
|
106
|
+
offset += line.length + 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Check the current line alone (we allow argv to be on one line;
|
|
110
|
+
// multi-line argv arrays are a rare style and would still trip
|
|
111
|
+
// the quick search above).
|
|
112
|
+
IMPORT_ARGV_RE.lastIndex = 0;
|
|
113
|
+
let m: RegExpExecArray | null;
|
|
114
|
+
while ((m = IMPORT_ARGV_RE.exec(line)) !== null) {
|
|
115
|
+
const loaderArg = m[1]!.trim();
|
|
116
|
+
const entryArg = m[2]!.trim();
|
|
117
|
+
const loaderOk = URL_LOOKING_RE.test(loaderArg);
|
|
118
|
+
const entryOk = URL_LOOKING_RE.test(entryArg);
|
|
119
|
+
if (!loaderOk || !entryOk) {
|
|
120
|
+
violations.push({
|
|
121
|
+
file: path.relative(repoRoot, file),
|
|
122
|
+
line: i + 1,
|
|
123
|
+
text: line.trim(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
offset += line.length + 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (violations.length > 0) {
|
|
133
|
+
const msg =
|
|
134
|
+
`Raw filesystem paths passed to node --import / --loader found.\n` +
|
|
135
|
+
`Migrate each call site to use spawnNodeScript() or wrap the\n` +
|
|
136
|
+
`loader/entry with toFileUrl(...) from:\n` +
|
|
137
|
+
` import { toFileUrl, spawnNodeScript } from\n` +
|
|
138
|
+
` "@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js";\n\n` +
|
|
139
|
+
`Offenders (${violations.length}):\n` +
|
|
140
|
+
violations
|
|
141
|
+
.map((v) => ` ${v.file}:${v.line} ${v.text}`)
|
|
142
|
+
.join("\n");
|
|
143
|
+
expect(violations, msg).toEqual([]);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|