pi-ui-extend 1.0.41 → 1.0.45
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/README.md +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
|
|
5
|
+
const args = parseArgs(process.argv.slice(2));
|
|
6
|
+
const socket = net.createConnection({ host: "127.0.0.1", port: args.port });
|
|
7
|
+
|
|
8
|
+
socket.once("connect", () => {
|
|
9
|
+
if (args.title) process.stdout.write(`\u001b]0;${args.title}\u0007`);
|
|
10
|
+
const handshake = {
|
|
11
|
+
token: args.token,
|
|
12
|
+
pid: process.pid,
|
|
13
|
+
ppid: process.ppid,
|
|
14
|
+
cols: boundedDimension(process.stdout.columns, 80),
|
|
15
|
+
rows: boundedDimension(process.stdout.rows, 24),
|
|
16
|
+
};
|
|
17
|
+
socket.write(`${JSON.stringify(handshake)}\n`);
|
|
18
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
19
|
+
process.stdin.resume();
|
|
20
|
+
process.stdin.on("data", (chunk) => socket.write(chunk));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
socket.on("data", (chunk) => process.stdout.write(chunk));
|
|
24
|
+
socket.on("end", () => process.exit(0));
|
|
25
|
+
socket.on("error", (error) => {
|
|
26
|
+
process.stderr.write(`native terminal bridge failed: ${error.message}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function parseArgs(values) {
|
|
31
|
+
const result = {};
|
|
32
|
+
for (let index = 0; index < values.length; index += 2) {
|
|
33
|
+
const key = values[index];
|
|
34
|
+
const value = values[index + 1];
|
|
35
|
+
if (!key?.startsWith("--") || value === undefined) throw new Error(`invalid bridge argument: ${key ?? "(missing)"}`);
|
|
36
|
+
result[key.slice(2)] = value;
|
|
37
|
+
}
|
|
38
|
+
const port = Number(result.port);
|
|
39
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("native terminal bridge requires a valid --port");
|
|
40
|
+
if (!/^[a-f0-9]{32,128}$/.test(result.token ?? "")) throw new Error("native terminal bridge requires a valid --token");
|
|
41
|
+
const title = result.title;
|
|
42
|
+
if (title !== undefined && (title.length < 1 || title.length > 120 || /[\x00-\x1f\x7f]/.test(title))) {
|
|
43
|
+
throw new Error("native terminal bridge requires a safe bounded --title");
|
|
44
|
+
}
|
|
45
|
+
return { port, token: result.token, title };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function boundedDimension(value, fallback) {
|
|
49
|
+
return Number.isInteger(value) && value >= 10 && value <= 500 ? value : fallback;
|
|
50
|
+
}
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createWindowEvidenceController, probeDesktopBackend } from "../../backends/desktop.mjs";
|
|
8
|
+
|
|
9
|
+
export const NATIVE_TERMINAL_PRESENTATION = "native-terminal";
|
|
10
|
+
|
|
11
|
+
const ITERM2_EXECUTABLE = "/Applications/iTerm.app/Contents/MacOS/iTerm2";
|
|
12
|
+
const MACOS_TERMINAL_EXECUTABLE = "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal";
|
|
13
|
+
const BRIDGE_CLIENT = fileURLToPath(new URL("./bridge-client.mjs", import.meta.url));
|
|
14
|
+
const HANDSHAKE_MAX_BYTES = 4 * 1024;
|
|
15
|
+
const PENDING_INPUT_MAX_BYTES = 64 * 1024;
|
|
16
|
+
const CLEANUP_GRACE_MS = 1_000;
|
|
17
|
+
|
|
18
|
+
export async function probeNativeTerminalBackend(context) {
|
|
19
|
+
const supportedCapabilities = [
|
|
20
|
+
"launch", "nativeTerminalWindow", "ptyInput", "screenState", "ansiVt", "alternateScreen",
|
|
21
|
+
"stableFrame", "visibleTextAssertions", "cursorAssertions", "processState", "terminalCapture",
|
|
22
|
+
"terminalRecording", "ownedCleanup",
|
|
23
|
+
];
|
|
24
|
+
const provider = selectNativeTerminalProvider();
|
|
25
|
+
if (!provider) {
|
|
26
|
+
const blocker = nativeTerminalProviderBlocker(process.platform);
|
|
27
|
+
return {
|
|
28
|
+
available: false,
|
|
29
|
+
platformDriver: blocker.platformDriver,
|
|
30
|
+
supportedCapabilities,
|
|
31
|
+
missingCapabilities: ["nativeTerminalWindow", "windowScreenshot", "windowVideo"],
|
|
32
|
+
reason: blocker.reason,
|
|
33
|
+
remediation: blocker.remediation,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!fs.existsSync(BRIDGE_CLIENT)) {
|
|
37
|
+
return {
|
|
38
|
+
available: false,
|
|
39
|
+
platformDriver: provider.platformDriver,
|
|
40
|
+
supportedCapabilities,
|
|
41
|
+
missingCapabilities: ["nativeTerminalWindow", "windowScreenshot", "windowVideo"],
|
|
42
|
+
reason: "the bundled native-terminal bridge client is unavailable",
|
|
43
|
+
remediation: "restore the bundled UI QA native-terminal driver files",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const desktop = await probeDesktopBackend(context);
|
|
47
|
+
const screenshot = desktop.supportedCapabilities?.includes("windowScreenshot") === true;
|
|
48
|
+
const supported = [...supportedCapabilities];
|
|
49
|
+
if (screenshot) supported.push("windowScreenshot");
|
|
50
|
+
if (desktop.supportedCapabilities?.includes("windowVideo")) supported.push("windowVideo");
|
|
51
|
+
const missing = [];
|
|
52
|
+
if (!screenshot) missing.push("windowScreenshot");
|
|
53
|
+
if (!supported.includes("windowVideo")) missing.push("windowVideo");
|
|
54
|
+
return {
|
|
55
|
+
available: desktop.available && screenshot,
|
|
56
|
+
platformDriver: provider.platformDriver,
|
|
57
|
+
supportedCapabilities: supported,
|
|
58
|
+
missingCapabilities: [...missing, "terminalResize"],
|
|
59
|
+
reason: desktop.available && screenshot
|
|
60
|
+
? `PTY control plus a runner-owned ${provider.displayName} mirror and exact-window capture are available for pixel-faithful terminal QA`
|
|
61
|
+
: desktop.reason ?? `native-terminal presentation requires ${provider.displayName} plus desktop accessibility and exact-window screenshot capabilities`,
|
|
62
|
+
remediation: desktop.remediation ?? (desktop.available && !screenshot
|
|
63
|
+
? nativeTerminalScreenshotRemediation(process.platform)
|
|
64
|
+
: undefined),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function startNativeTerminalDisplay(context, observations, artifacts) {
|
|
69
|
+
const provider = selectNativeTerminalProvider();
|
|
70
|
+
if (!provider) throw new Error("no supported native terminal host is available");
|
|
71
|
+
const token = randomBytes(32).toString("hex");
|
|
72
|
+
const title = `Pi UI QA ${String(context.runId).slice(0, 72)} ${randomBytes(6).toString("hex")}`;
|
|
73
|
+
const bridge = await createBridge(token);
|
|
74
|
+
const commandFile = path.join(context.workspaceDir, `native-terminal-${context.runId}.command`);
|
|
75
|
+
let host;
|
|
76
|
+
let evidence;
|
|
77
|
+
try {
|
|
78
|
+
const command = provider.launchKind === "macos-shell"
|
|
79
|
+
? nativeTerminalBridgeShellCommand({ nodePath: process.execPath, bridgePath: BRIDGE_CLIENT, port: bridge.port, token, title })
|
|
80
|
+
: undefined;
|
|
81
|
+
if (provider.commandFile) {
|
|
82
|
+
fs.writeFileSync(commandFile, nativeTerminalBridgeCommandFile({ command }), { encoding: "utf8", mode: 0o700, flag: "wx" });
|
|
83
|
+
fs.chmodSync(commandFile, 0o700);
|
|
84
|
+
}
|
|
85
|
+
host = launchNativeTerminalProvider(provider, { command, commandFile, nodePath: process.execPath, bridgePath: BRIDGE_CLIENT, port: bridge.port, token, title });
|
|
86
|
+
if (!host.pid) throw new Error("native terminal host launch did not return a process id");
|
|
87
|
+
const handshake = await bridge.waitForHandshake(15_000);
|
|
88
|
+
if (provider.requireDescendantBridge && !isDescendantProcess(handshake.pid, host.pid)) {
|
|
89
|
+
throw new Error("native terminal bridge did not originate from the runner-owned terminal process");
|
|
90
|
+
}
|
|
91
|
+
const selector = provider.evidenceSelector === "pid"
|
|
92
|
+
? ["--pid", String(host.pid)]
|
|
93
|
+
: ["--title", title];
|
|
94
|
+
evidence = await createWindowEvidenceController({ context, selector, observations, artifacts });
|
|
95
|
+
observations.push({
|
|
96
|
+
action: "nativeTerminalHost",
|
|
97
|
+
presentation: NATIVE_TERMINAL_PRESENTATION,
|
|
98
|
+
provider: provider.id,
|
|
99
|
+
pid: host.pid,
|
|
100
|
+
launcherPid: host.pid,
|
|
101
|
+
platformDriver: provider.platformDriver,
|
|
102
|
+
windowTitle: provider.evidenceSelector === "title" ? title : undefined,
|
|
103
|
+
cols: handshake.cols,
|
|
104
|
+
rows: handshake.rows,
|
|
105
|
+
});
|
|
106
|
+
context.progress("tui_native_terminal_ready", { pid: host.pid, provider: provider.id, cols: handshake.cols, rows: handshake.rows });
|
|
107
|
+
return nativeDisplayHandle({ bridge, evidence, host, commandFile, handshake });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
await evidence?.finish().catch(() => {});
|
|
110
|
+
await bridge.close().catch(() => {});
|
|
111
|
+
if (host) await cleanupNativeTerminalHost(host);
|
|
112
|
+
removeCommandFile(commandFile);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function nativeTerminalBridgeShellCommand(options) {
|
|
118
|
+
const { nodePath, bridgePath, port, token } = options;
|
|
119
|
+
const title = options.title;
|
|
120
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("native-terminal bridge port is invalid");
|
|
121
|
+
if (!/^[a-f0-9]{32,128}$/.test(token)) throw new Error("native-terminal bridge token is invalid");
|
|
122
|
+
const titleArgument = title === undefined ? "" : ` --title ${shellQuoteTrustedTitle(title)}`;
|
|
123
|
+
return `exec ${shellQuoteTrustedPath(nodePath)} ${shellQuoteTrustedPath(bridgePath)} --port ${port} --token ${token}${titleArgument}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function nativeTerminalBridgeCommandFile({ command }) {
|
|
127
|
+
if (typeof command !== "string" || !command.startsWith("exec ") || command.includes("\n") || command.includes("\r")) {
|
|
128
|
+
throw new Error("native-terminal bridge command is invalid");
|
|
129
|
+
}
|
|
130
|
+
return `#!/bin/zsh\n${command}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function chooseNativeTerminalProvider(options = {}) {
|
|
134
|
+
const platform = options.platform ?? process.platform;
|
|
135
|
+
if (platform === "darwin" && options.itermAvailable) {
|
|
136
|
+
return {
|
|
137
|
+
id: "iterm2",
|
|
138
|
+
displayName: "iTerm2",
|
|
139
|
+
executable: options.itermExecutable ?? ITERM2_EXECUTABLE,
|
|
140
|
+
platformDriver: "macos-iterm2-window",
|
|
141
|
+
launchKind: "macos-shell",
|
|
142
|
+
commandFile: false,
|
|
143
|
+
requireDescendantBridge: false,
|
|
144
|
+
evidenceSelector: "pid",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (platform === "darwin" && options.terminalAvailable) {
|
|
148
|
+
return {
|
|
149
|
+
id: "terminal-app",
|
|
150
|
+
displayName: "Terminal.app",
|
|
151
|
+
executable: options.terminalExecutable ?? MACOS_TERMINAL_EXECUTABLE,
|
|
152
|
+
platformDriver: "macos-terminal-window",
|
|
153
|
+
launchKind: "macos-shell",
|
|
154
|
+
commandFile: true,
|
|
155
|
+
requireDescendantBridge: true,
|
|
156
|
+
evidenceSelector: "pid",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (platform === "win32" && options.windowsTerminalAvailable) {
|
|
160
|
+
return {
|
|
161
|
+
id: "windows-terminal",
|
|
162
|
+
displayName: "Windows Terminal",
|
|
163
|
+
executable: options.windowsTerminalExecutable ?? "wt.exe",
|
|
164
|
+
platformDriver: "windows-terminal-window",
|
|
165
|
+
launchKind: "direct",
|
|
166
|
+
commandFile: false,
|
|
167
|
+
requireDescendantBridge: false,
|
|
168
|
+
evidenceSelector: "title",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (platform === "linux") {
|
|
172
|
+
const candidates = [
|
|
173
|
+
["kittyAvailable", "kittyExecutable", "kitty", "kitty", "kitty", "linux-kitty-window"],
|
|
174
|
+
["alacrittyAvailable", "alacrittyExecutable", "alacritty", "alacritty", "Alacritty", "linux-alacritty-window"],
|
|
175
|
+
["gnomeTerminalAvailable", "gnomeTerminalExecutable", "gnome-terminal", "gnome-terminal", "GNOME Terminal", "linux-gnome-terminal-window"],
|
|
176
|
+
["konsoleAvailable", "konsoleExecutable", "konsole", "konsole", "Konsole", "linux-konsole-window"],
|
|
177
|
+
["xtermAvailable", "xtermExecutable", "xterm", "xterm", "xterm", "linux-xterm-window"],
|
|
178
|
+
];
|
|
179
|
+
for (const [availableKey, executableKey, fallbackExecutable, id, displayName, platformDriver] of candidates) {
|
|
180
|
+
if (!options[availableKey]) continue;
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
displayName,
|
|
184
|
+
executable: options[executableKey] ?? fallbackExecutable,
|
|
185
|
+
platformDriver,
|
|
186
|
+
launchKind: "direct",
|
|
187
|
+
commandFile: false,
|
|
188
|
+
requireDescendantBridge: false,
|
|
189
|
+
evidenceSelector: "title",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function selectNativeTerminalProvider() {
|
|
197
|
+
if (process.platform === "darwin") {
|
|
198
|
+
return chooseNativeTerminalProvider({
|
|
199
|
+
platform: "darwin",
|
|
200
|
+
itermAvailable: fs.existsSync(ITERM2_EXECUTABLE),
|
|
201
|
+
terminalAvailable: fs.existsSync(MACOS_TERMINAL_EXECUTABLE),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (process.platform === "win32") {
|
|
205
|
+
const executable = findExecutable(["wt.exe", "wt"]);
|
|
206
|
+
return chooseNativeTerminalProvider({
|
|
207
|
+
platform: "win32",
|
|
208
|
+
windowsTerminalAvailable: Boolean(executable),
|
|
209
|
+
windowsTerminalExecutable: executable,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (process.platform === "linux") {
|
|
213
|
+
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return null;
|
|
214
|
+
const kitty = findExecutable(["kitty"]);
|
|
215
|
+
const alacritty = findExecutable(["alacritty"]);
|
|
216
|
+
const gnomeTerminal = findExecutable(["gnome-terminal"]);
|
|
217
|
+
const konsole = findExecutable(["konsole"]);
|
|
218
|
+
const xterm = findExecutable(["xterm"]);
|
|
219
|
+
return chooseNativeTerminalProvider({
|
|
220
|
+
platform: "linux",
|
|
221
|
+
kittyAvailable: Boolean(kitty), kittyExecutable: kitty,
|
|
222
|
+
alacrittyAvailable: Boolean(alacritty), alacrittyExecutable: alacritty,
|
|
223
|
+
gnomeTerminalAvailable: Boolean(gnomeTerminal), gnomeTerminalExecutable: gnomeTerminal,
|
|
224
|
+
konsoleAvailable: Boolean(konsole), konsoleExecutable: konsole,
|
|
225
|
+
xtermAvailable: Boolean(xterm), xtermExecutable: xterm,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function nativeTerminalProviderLaunchArgs(provider, launch) {
|
|
232
|
+
const { nodePath, bridgePath, port, token, title, command, commandFile } = launch;
|
|
233
|
+
if (provider.launchKind === "macos-shell") return provider.commandFile ? [commandFile] : [`--command=${command}`];
|
|
234
|
+
const bridgeArgs = [bridgePath, "--port", String(port), "--token", token, "--title", title];
|
|
235
|
+
if (provider.id === "windows-terminal") {
|
|
236
|
+
return ["-w", "new", "new-tab", "--title", title, "--suppressApplicationTitle", nodePath, ...bridgeArgs];
|
|
237
|
+
}
|
|
238
|
+
if (provider.id === "kitty") return ["--title", title, nodePath, ...bridgeArgs];
|
|
239
|
+
if (provider.id === "alacritty") return ["--title", title, "-e", nodePath, ...bridgeArgs];
|
|
240
|
+
if (provider.id === "gnome-terminal") return ["--wait", "--window", `--title=${title}`, "--", nodePath, ...bridgeArgs];
|
|
241
|
+
if (provider.id === "konsole") return ["--nofork", "-p", `tabtitle=${title}`, "-e", nodePath, ...bridgeArgs];
|
|
242
|
+
if (provider.id === "xterm") return ["-T", title, "-e", nodePath, ...bridgeArgs];
|
|
243
|
+
throw new Error(`unsupported native terminal provider: ${provider.id}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function launchNativeTerminalProvider(provider, launch) {
|
|
247
|
+
const args = nativeTerminalProviderLaunchArgs(provider, launch);
|
|
248
|
+
return spawn(provider.executable, args, { stdio: "ignore", detached: false, windowsHide: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function nativeDisplayHandle({ bridge, evidence, host, commandFile, handshake }) {
|
|
252
|
+
let finished = false;
|
|
253
|
+
return {
|
|
254
|
+
viewport: { cols: handshake.cols, rows: handshake.rows },
|
|
255
|
+
write(data) { bridge.write(data); },
|
|
256
|
+
setInputHandler(handler) { bridge.setInputHandler(handler); },
|
|
257
|
+
connected() { return bridge.connected(); },
|
|
258
|
+
async capture(name) { await evidence.capture(name); },
|
|
259
|
+
async finish() {
|
|
260
|
+
if (finished) return;
|
|
261
|
+
finished = true;
|
|
262
|
+
await evidence.finish().catch(() => {});
|
|
263
|
+
await bridge.close();
|
|
264
|
+
await cleanupNativeTerminalHost(host);
|
|
265
|
+
removeCommandFile(commandFile);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function createBridge(token) {
|
|
271
|
+
let socket;
|
|
272
|
+
let authenticated = false;
|
|
273
|
+
let closed = false;
|
|
274
|
+
let inputHandler;
|
|
275
|
+
let pendingInput = Buffer.alloc(0);
|
|
276
|
+
let resolveHandshake;
|
|
277
|
+
let rejectHandshake;
|
|
278
|
+
const handshakePromise = new Promise((resolve, reject) => { resolveHandshake = resolve; rejectHandshake = reject; });
|
|
279
|
+
const server = net.createServer((candidate) => {
|
|
280
|
+
if (socket || authenticated || closed) {
|
|
281
|
+
candidate.destroy();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
socket = candidate;
|
|
285
|
+
let header = Buffer.alloc(0);
|
|
286
|
+
candidate.on("data", (chunk) => {
|
|
287
|
+
if (authenticated) {
|
|
288
|
+
forwardInput(chunk);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
header = Buffer.concat([header, Buffer.from(chunk)]);
|
|
292
|
+
if (header.length > HANDSHAKE_MAX_BYTES) {
|
|
293
|
+
candidate.destroy();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const newline = header.indexOf(0x0a);
|
|
297
|
+
if (newline < 0) return;
|
|
298
|
+
const line = header.subarray(0, newline).toString("utf8");
|
|
299
|
+
const remainder = header.subarray(newline + 1);
|
|
300
|
+
try {
|
|
301
|
+
const handshake = validateHandshake(line, token);
|
|
302
|
+
authenticated = true;
|
|
303
|
+
resolveHandshake(handshake);
|
|
304
|
+
if (remainder.length > 0) forwardInput(remainder);
|
|
305
|
+
} catch {
|
|
306
|
+
candidate.destroy();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
candidate.once("error", () => {
|
|
310
|
+
if (!authenticated && socket === candidate) socket = undefined;
|
|
311
|
+
});
|
|
312
|
+
candidate.once("close", () => {
|
|
313
|
+
if (!authenticated && socket === candidate) socket = undefined;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
const forwardInput = (chunk) => {
|
|
317
|
+
if (inputHandler) {
|
|
318
|
+
inputHandler(chunk);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (pendingInput.length >= PENDING_INPUT_MAX_BYTES) return;
|
|
322
|
+
pendingInput = Buffer.concat([pendingInput, Buffer.from(chunk)]).subarray(0, PENDING_INPUT_MAX_BYTES);
|
|
323
|
+
};
|
|
324
|
+
server.once("error", (error) => rejectHandshake(error));
|
|
325
|
+
await new Promise((resolve, reject) => {
|
|
326
|
+
server.once("error", reject);
|
|
327
|
+
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, resolve);
|
|
328
|
+
});
|
|
329
|
+
const address = server.address();
|
|
330
|
+
if (!address || typeof address === "string") throw new Error("native terminal bridge did not bind a TCP port");
|
|
331
|
+
return {
|
|
332
|
+
port: address.port,
|
|
333
|
+
async waitForHandshake(timeoutMs) { return waitForPromise(handshakePromise, timeoutMs, "native terminal bridge did not connect"); },
|
|
334
|
+
write(data) {
|
|
335
|
+
if (!authenticated || socket?.destroyed) throw new Error("native terminal bridge is not connected");
|
|
336
|
+
socket.write(data);
|
|
337
|
+
},
|
|
338
|
+
setInputHandler(handler) {
|
|
339
|
+
inputHandler = handler;
|
|
340
|
+
if (pendingInput.length > 0) {
|
|
341
|
+
const buffered = pendingInput;
|
|
342
|
+
pendingInput = Buffer.alloc(0);
|
|
343
|
+
handler(buffered);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
connected() { return authenticated && !closed && Boolean(socket) && !socket.destroyed; },
|
|
347
|
+
async close() {
|
|
348
|
+
if (closed) return;
|
|
349
|
+
closed = true;
|
|
350
|
+
try { socket?.end(); } catch { /* best effort */ }
|
|
351
|
+
try { socket?.destroy(); } catch { /* best effort */ }
|
|
352
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validateHandshake(line, token) {
|
|
358
|
+
let value;
|
|
359
|
+
try { value = JSON.parse(line); } catch { throw new Error("native terminal bridge returned invalid handshake JSON"); }
|
|
360
|
+
if (!isRecord(value) || value.token !== token) throw new Error("native terminal bridge authentication failed");
|
|
361
|
+
const pid = boundedInteger(value.pid, 1, 2_147_483_647, "native terminal bridge pid");
|
|
362
|
+
const ppid = boundedInteger(value.ppid, 1, 2_147_483_647, "native terminal bridge ppid");
|
|
363
|
+
const cols = boundedInteger(value.cols, 10, 500, "native terminal cols");
|
|
364
|
+
const rows = boundedInteger(value.rows, 10, 500, "native terminal rows");
|
|
365
|
+
return { pid, ppid, cols, rows };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function isDescendantProcess(pid, ancestorPid) {
|
|
369
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ancestorPid) || pid <= 0 || ancestorPid <= 0) return false;
|
|
370
|
+
let current = pid;
|
|
371
|
+
const seen = new Set();
|
|
372
|
+
for (let depth = 0; depth < 64 && current > 1 && !seen.has(current); depth += 1) {
|
|
373
|
+
if (current === ancestorPid) return true;
|
|
374
|
+
seen.add(current);
|
|
375
|
+
try {
|
|
376
|
+
const output = execFileSync("/bin/ps", ["-o", "ppid=", "-p", String(current)], { encoding: "utf8", timeout: 1_000 }).trim();
|
|
377
|
+
current = Number(output);
|
|
378
|
+
if (!Number.isInteger(current) || current <= 0) return false;
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return current === ancestorPid;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function cleanupNativeTerminalHost(child) {
|
|
387
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return;
|
|
388
|
+
const exited = new Promise((resolve) => child.once("exit", resolve));
|
|
389
|
+
try { child.kill("SIGTERM"); } catch { /* already exited */ }
|
|
390
|
+
await Promise.race([exited, sleep(CLEANUP_GRACE_MS)]);
|
|
391
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
392
|
+
try { child.kill("SIGKILL"); } catch { /* already exited */ }
|
|
393
|
+
await Promise.race([exited, sleep(CLEANUP_GRACE_MS)]);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function removeCommandFile(commandFile) {
|
|
397
|
+
try { fs.rmSync(commandFile, { force: true }); } catch { /* best effort */ }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function nativeTerminalProviderBlocker(platform) {
|
|
401
|
+
if (platform === "darwin") {
|
|
402
|
+
return {
|
|
403
|
+
platformDriver: "macos-native-terminal-window",
|
|
404
|
+
reason: "no supported native terminal host is available on macOS",
|
|
405
|
+
remediation: "install iTerm2, restore the system Terminal application, or use presentation=pty when pixel fidelity is not required",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (platform === "win32") {
|
|
409
|
+
return {
|
|
410
|
+
platformDriver: "windows-terminal-window",
|
|
411
|
+
reason: "Windows Terminal (wt.exe) is unavailable",
|
|
412
|
+
remediation: "install or repair Windows Terminal, or use presentation=pty when pixel fidelity is not required",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (platform === "linux") {
|
|
416
|
+
return {
|
|
417
|
+
platformDriver: "linux-native-terminal-window",
|
|
418
|
+
reason: process.env.DISPLAY || process.env.WAYLAND_DISPLAY
|
|
419
|
+
? "no supported Linux terminal emulator is available"
|
|
420
|
+
: "no graphical Linux session is available for native-terminal presentation",
|
|
421
|
+
remediation: process.env.DISPLAY || process.env.WAYLAND_DISPLAY
|
|
422
|
+
? "install kitty, Alacritty, GNOME Terminal, Konsole, or xterm; otherwise use presentation=pty when pixel fidelity is not required"
|
|
423
|
+
: "run in an X11/Wayland graphical session with a supported terminal emulator, or use presentation=pty when pixel fidelity is not required",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
platformDriver: `${platform}-native-terminal-unsupported`,
|
|
428
|
+
reason: `native-terminal presentation is not implemented on ${platform}`,
|
|
429
|
+
remediation: "use presentation=pty when pixel fidelity is not required, or add a native terminal-window driver for this platform",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function nativeTerminalScreenshotRemediation(platform) {
|
|
434
|
+
if (platform === "darwin") return "grant Screen Recording permission to the terminal/Pi host, then rerun";
|
|
435
|
+
if (platform === "linux") return "install a supported exact-window screenshot producer (gnome-screenshot or scrot) and ensure AT-SPI can focus the terminal window, then rerun";
|
|
436
|
+
if (platform === "win32") return "repair the bundled Windows UI Automation screenshot capability, then rerun";
|
|
437
|
+
return "provide exact-window screenshot capability for the selected platform driver, then rerun";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function shellQuoteTrustedPath(value) {
|
|
441
|
+
if (typeof value !== "string" || value.length < 1 || value.includes("\n") || value.includes("\r") || value.includes(String.fromCharCode(0))) {
|
|
442
|
+
throw new Error("native-terminal bridge path is invalid");
|
|
443
|
+
}
|
|
444
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function shellQuoteTrustedTitle(value) {
|
|
448
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 120 || /[\x00-\x1f\x7f]/.test(value)) {
|
|
449
|
+
throw new Error("native-terminal bridge title is invalid");
|
|
450
|
+
}
|
|
451
|
+
return shellQuoteTrustedPath(value);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function findExecutable(candidates) {
|
|
455
|
+
const directories = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
456
|
+
const windowsExtensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
457
|
+
for (const candidate of candidates) {
|
|
458
|
+
const hasExtension = path.extname(candidate).length > 0;
|
|
459
|
+
const extensions = process.platform === "win32" && !hasExtension ? windowsExtensions : [""];
|
|
460
|
+
for (const directory of directories) {
|
|
461
|
+
for (const extension of extensions) {
|
|
462
|
+
const resolved = path.join(directory, `${candidate}${extension}`);
|
|
463
|
+
try {
|
|
464
|
+
const stat = fs.statSync(resolved);
|
|
465
|
+
if (!stat.isFile()) continue;
|
|
466
|
+
if (process.platform !== "win32") fs.accessSync(resolved, fs.constants.X_OK);
|
|
467
|
+
return resolved;
|
|
468
|
+
} catch { /* continue */ }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function boundedInteger(value, min, max, label) {
|
|
476
|
+
if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${label} is invalid`);
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isRecord(value) {
|
|
481
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function waitForPromise(promise, timeoutMs, reason) {
|
|
485
|
+
let timer;
|
|
486
|
+
try {
|
|
487
|
+
return await Promise.race([
|
|
488
|
+
promise,
|
|
489
|
+
new Promise((_, reject) => {
|
|
490
|
+
timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
|
|
491
|
+
timer.unref?.();
|
|
492
|
+
}),
|
|
493
|
+
]);
|
|
494
|
+
} finally {
|
|
495
|
+
if (timer) clearTimeout(timer);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function sleep(ms) {
|
|
500
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(1, ms)));
|
|
501
|
+
}
|