@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0
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 +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- 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/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- 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 +71 -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__/force-kill-handler.test.ts +57 -8
- 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 +165 -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 +83 -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 +256 -32
- 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 +196 -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 +130 -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__/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__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -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 +15 -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/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/tool-registry/definitions.ts +342 -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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/commands.ts.
|
|
3
|
+
* Platform behavior exercised via injected `platform` + `exec` / `asyncExec`.
|
|
4
|
+
* See change: consolidate-platform-handlers.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from "vitest";
|
|
7
|
+
import { openBrowser, isVirtualMachine } from "../platform/commands.js";
|
|
8
|
+
|
|
9
|
+
describe("openBrowser", () => {
|
|
10
|
+
it("uses `open` on macOS", () => {
|
|
11
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(null));
|
|
12
|
+
openBrowser("https://example.com", { platform: "darwin", asyncExec });
|
|
13
|
+
expect(asyncExec).toHaveBeenCalledOnce();
|
|
14
|
+
expect(asyncExec.mock.calls[0][0]).toMatch(/^open\s+"https:\/\/example\.com"$/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("uses `start` on Windows", () => {
|
|
18
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(null));
|
|
19
|
+
openBrowser("https://example.com", { platform: "win32", asyncExec });
|
|
20
|
+
expect(asyncExec.mock.calls[0][0]).toMatch(/^start\s+""\s+"https:\/\/example\.com"$/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("uses `xdg-open` on Linux", () => {
|
|
24
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(null));
|
|
25
|
+
openBrowser("https://example.com", { platform: "linux", asyncExec });
|
|
26
|
+
expect(asyncExec.mock.calls[0][0]).toMatch(/^xdg-open\s+"https:\/\/example\.com"$/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("escapes URLs via JSON.stringify (quotes, newlines, backslashes)", () => {
|
|
30
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(null));
|
|
31
|
+
openBrowser('https://example.com/?q="escaped"', { platform: "linux", asyncExec });
|
|
32
|
+
// JSON.stringify converts " → \"
|
|
33
|
+
expect(asyncExec.mock.calls[0][0]).toContain('\\"escaped\\"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("invokes onError callback when async exec fails", () => {
|
|
37
|
+
const err = new Error("nope");
|
|
38
|
+
const onError = vi.fn();
|
|
39
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(err));
|
|
40
|
+
openBrowser("https://example.com", { platform: "linux", asyncExec, onError });
|
|
41
|
+
expect(onError).toHaveBeenCalledWith(err);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not throw when onError is absent", () => {
|
|
45
|
+
const err = new Error("nope");
|
|
46
|
+
const asyncExec = vi.fn((_cmd, cb: (e: Error | null) => void) => cb(err));
|
|
47
|
+
expect(() =>
|
|
48
|
+
openBrowser("https://example.com", { platform: "linux", asyncExec }),
|
|
49
|
+
).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("isVirtualMachine", () => {
|
|
54
|
+
it("detects VMware via sysctl on macOS", () => {
|
|
55
|
+
const exec = vi.fn().mockReturnValue("VMware7,1\n");
|
|
56
|
+
expect(isVirtualMachine({ platform: "darwin", exec })).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("detects VirtualBox via sysctl on macOS", () => {
|
|
60
|
+
const exec = vi.fn().mockReturnValue("VirtualBox6,0\n");
|
|
61
|
+
expect(isVirtualMachine({ platform: "darwin", exec })).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns false on physical macOS hardware", () => {
|
|
65
|
+
const exec = vi.fn().mockReturnValue("MacBookPro18,3\n");
|
|
66
|
+
expect(isVirtualMachine({ platform: "darwin", exec })).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("detects VM via systemd-detect-virt on Linux", () => {
|
|
70
|
+
const exec = vi.fn().mockReturnValue("kvm\n");
|
|
71
|
+
expect(isVirtualMachine({ platform: "linux", exec })).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns false on bare-metal Linux", () => {
|
|
75
|
+
const exec = vi.fn().mockReturnValue("none\n");
|
|
76
|
+
expect(isVirtualMachine({ platform: "linux", exec })).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("detects VMware via wmic on Windows", () => {
|
|
80
|
+
const exec = vi.fn().mockImplementation((cmd: string) => {
|
|
81
|
+
if (cmd.includes("bios")) return "SerialNumber\nVMware-42 AA BB\n";
|
|
82
|
+
return "";
|
|
83
|
+
});
|
|
84
|
+
expect(isVirtualMachine({ platform: "win32", exec })).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("detects Hyper-V via wmic computersystem on Windows", () => {
|
|
88
|
+
const exec = vi.fn().mockImplementation((cmd: string) => {
|
|
89
|
+
if (cmd.includes("bios")) throw new Error("no serial");
|
|
90
|
+
if (cmd.includes("computersystem")) return "Manufacturer Model\nMicrosoft Corporation Virtual Machine\n";
|
|
91
|
+
return "";
|
|
92
|
+
});
|
|
93
|
+
expect(isVirtualMachine({ platform: "win32", exec })).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns false on physical Windows when no VM markers found", () => {
|
|
97
|
+
const exec = vi.fn().mockReturnValue("SerialNumber\nR90ABCDE\n");
|
|
98
|
+
expect(isVirtualMachine({ platform: "win32", exec })).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns false when exec throws unexpectedly", () => {
|
|
102
|
+
const exec = vi.fn().mockImplementation(() => {
|
|
103
|
+
throw new Error("boom");
|
|
104
|
+
});
|
|
105
|
+
expect(isVirtualMachine({ platform: "darwin", exec })).toBe(false);
|
|
106
|
+
expect(isVirtualMachine({ platform: "linux", exec })).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/exec.ts — the thin wrapper over
|
|
3
|
+
* node:child_process that sets `windowsHide: true` by default.
|
|
4
|
+
*
|
|
5
|
+
* These tests assert the *option passthrough* contract; they do not spawn
|
|
6
|
+
* real subprocesses. The wrapper is a few lines per function — its only
|
|
7
|
+
* job is to forward arguments with `windowsHide: true` layered on top.
|
|
8
|
+
*
|
|
9
|
+
* See change: platform-command-executor.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { execSync, spawn, spawnSync, exec, execFile } from "../platform/exec.js";
|
|
13
|
+
|
|
14
|
+
describe("platform/exec wrappers", () => {
|
|
15
|
+
// ── execSync ────────────────────────────────────────────────────────────
|
|
16
|
+
// Real invocation: we pick commands that exit 0 on every OS (node itself).
|
|
17
|
+
|
|
18
|
+
it("execSync exits 0 for `node --version`", () => {
|
|
19
|
+
const out = execSync(`"${process.execPath}" --version`, { encoding: "utf-8" });
|
|
20
|
+
expect(String(out).trim()).toMatch(/^v\d+\.\d+\.\d+/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ── spawnSync ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
it("spawnSync runs `node --version` and captures stdout", () => {
|
|
26
|
+
const result = spawnSync(process.execPath, ["--version"], { encoding: "utf-8" });
|
|
27
|
+
expect(result.status).toBe(0);
|
|
28
|
+
expect(String(result.stdout).trim()).toMatch(/^v\d+\.\d+\.\d+/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("spawnSync accepts undefined args and defaults to []", () => {
|
|
32
|
+
// Should not throw — wrapper must coerce undefined args to []
|
|
33
|
+
const result = spawnSync(process.execPath, undefined, {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
input: "process.stdout.write('ok')",
|
|
36
|
+
});
|
|
37
|
+
// May or may not work depending on shell, but the call itself must not throw.
|
|
38
|
+
expect(typeof result).toBe("object");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ── windowsHide default ─────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
// The key invariant: wrappers set windowsHide: true when caller omits it.
|
|
44
|
+
// We verify this by inspecting the spawn metadata (spawnargs / opts).
|
|
45
|
+
// Node doesn't expose the final options object, so we check by spawning
|
|
46
|
+
// with a non-overridden call and verifying it completes successfully
|
|
47
|
+
// (a misconfigured windowsHide would not change functional behavior,
|
|
48
|
+
// so the real assertion is in D10 below via source inspection).
|
|
49
|
+
|
|
50
|
+
it("spawn returns a ChildProcess object", async () => {
|
|
51
|
+
const child = spawn(process.execPath, ["--version"]);
|
|
52
|
+
expect(child.pid).toBeGreaterThan(0);
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
child.on("exit", () => resolve());
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── exec (callback form) ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
it("exec(cmd, cb) invokes callback with stdout", async () => {
|
|
61
|
+
const out = await new Promise<string>((resolve, reject) => {
|
|
62
|
+
exec(`"${process.execPath}" --version`, (err, stdout) => {
|
|
63
|
+
if (err) reject(err);
|
|
64
|
+
else resolve(String(stdout));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
expect(out.trim()).toMatch(/^v\d+\.\d+\.\d+/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── execFile ────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
it("execFile(file, args, cb) works", async () => {
|
|
73
|
+
const out = await new Promise<string>((resolve, reject) => {
|
|
74
|
+
execFile(process.execPath, ["--version"], (err, stdout) => {
|
|
75
|
+
if (err) reject(err);
|
|
76
|
+
else resolve(String(stdout));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
expect(out.trim()).toMatch(/^v\d+\.\d+\.\d+/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("platform/exec — windowsHide default (source-level assertion)", () => {
|
|
84
|
+
// Since Node doesn't expose the spawn options after the call, we verify
|
|
85
|
+
// the windowsHide default by reading the wrapper source and asserting
|
|
86
|
+
// that every public export merges `windowsHide: true` into its options.
|
|
87
|
+
//
|
|
88
|
+
// This catches refactors that accidentally drop the default.
|
|
89
|
+
|
|
90
|
+
it("exec.ts source sets windowsHide: true by default", async () => {
|
|
91
|
+
const fs = await import("node:fs/promises");
|
|
92
|
+
const path = await import("node:path");
|
|
93
|
+
const url = await import("node:url");
|
|
94
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
95
|
+
const src = await fs.readFile(path.resolve(here, "../platform/exec.ts"), "utf-8");
|
|
96
|
+
|
|
97
|
+
// Must define a withHide helper and apply it uniformly.
|
|
98
|
+
expect(src).toMatch(/windowsHide\??:\s*boolean/);
|
|
99
|
+
expect(src).toMatch(/windowsHide:\s*hide/);
|
|
100
|
+
// Default must be true (not false) when caller omits it.
|
|
101
|
+
expect(src).toMatch(/opts\?\.windowsHide\s*\?\?\s*true/);
|
|
102
|
+
});
|
|
103
|
+
});
|