@zenbu-labs/pixel 0.0.0 → 0.0.13
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/dist/bin.d.ts +2 -0
- package/dist/bin.js +161 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +75 -0
- package/dist/context-menu.d.ts +20 -0
- package/dist/context-menu.js +57 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +9 -0
- package/dist/devtools-layout.d.ts +21 -0
- package/dist/devtools-layout.js +45 -0
- package/dist/devtools.d.ts +33 -0
- package/dist/devtools.js +125 -0
- package/dist/electron/index.d.ts +3 -0
- package/dist/electron/index.js +4 -0
- package/dist/host/guest.d.ts +5 -0
- package/dist/host/guest.js +178 -0
- package/dist/host/server.d.ts +91 -0
- package/dist/host/server.js +296 -0
- package/dist/host/shell.d.ts +35 -0
- package/dist/host/shell.js +113 -0
- package/dist/host/strip.d.ts +6 -0
- package/dist/host/strip.js +63 -0
- package/dist/icons.d.ts +30 -0
- package/dist/icons.js +58 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +39 -0
- package/dist/instances.d.ts +30 -0
- package/dist/instances.js +177 -0
- package/dist/layout.d.ts +5 -0
- package/dist/layout.js +34 -0
- package/dist/popup.d.ts +21 -0
- package/dist/popup.js +47 -0
- package/dist/preload/index.d.ts +15 -0
- package/dist/preload/index.js +3 -0
- package/dist/react/colors.d.ts +3 -0
- package/dist/react/colors.js +25 -0
- package/dist/react/components.d.ts +21 -0
- package/dist/react/components.js +45 -0
- package/dist/react/devtools/app.d.ts +3 -0
- package/dist/react/devtools/app.js +89 -0
- package/dist/react/devtools/console-capture.d.ts +1 -0
- package/dist/react/devtools/console-capture.js +41 -0
- package/dist/react/devtools/controller.d.ts +45 -0
- package/dist/react/devtools/controller.js +252 -0
- package/dist/react/devtools/elements.d.ts +4 -0
- package/dist/react/devtools/elements.js +152 -0
- package/dist/react/devtools/export-profile.d.ts +2 -0
- package/dist/react/devtools/export-profile.js +225 -0
- package/dist/react/devtools/fiber-hook.d.ts +1 -0
- package/dist/react/devtools/fiber-hook.js +89 -0
- package/dist/react/devtools/log-panel.d.ts +4 -0
- package/dist/react/devtools/log-panel.js +113 -0
- package/dist/react/devtools/profiler.d.ts +11 -0
- package/dist/react/devtools/profiler.js +374 -0
- package/dist/react/devtools/store.d.ts +27 -0
- package/dist/react/devtools/store.js +61 -0
- package/dist/react/devtools/stores.d.ts +92 -0
- package/dist/react/devtools/stores.js +41 -0
- package/dist/react/devtools/theme.d.ts +35 -0
- package/dist/react/devtools/theme.js +77 -0
- package/dist/react/devtools/ui.d.ts +24 -0
- package/dist/react/devtools/ui.js +59 -0
- package/dist/react/index.d.ts +110 -0
- package/dist/react/index.js +538 -0
- package/dist/react/markdown.d.ts +25 -0
- package/dist/react/markdown.js +270 -0
- package/dist/react/native.d.ts +116 -0
- package/dist/react/native.js +56 -0
- package/dist/react/reconciler-config.d.ts +241 -0
- package/dist/react/reconciler-config.js +412 -0
- package/dist/react/styles.d.ts +88 -0
- package/dist/react/styles.js +85 -0
- package/dist/react/surface.d.ts +55 -0
- package/dist/react/surface.js +74 -0
- package/dist/registry.d.ts +53 -0
- package/dist/registry.js +193 -0
- package/dist/root.d.ts +19 -0
- package/dist/root.js +342 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +84 -0
- package/dist/scale.d.ts +7 -0
- package/dist/scale.js +31 -0
- package/dist/ssh/index.d.ts +58 -0
- package/dist/ssh/index.js +445 -0
- package/dist/terminal/applescript.d.ts +2 -0
- package/dist/terminal/applescript.js +20 -0
- package/dist/terminal/detect.d.ts +10 -0
- package/dist/terminal/detect.js +34 -0
- package/dist/terminal/graphics.d.ts +9 -0
- package/dist/terminal/graphics.js +142 -0
- package/dist/terminal/index.d.ts +12 -0
- package/dist/terminal/index.js +23 -0
- package/dist/terminal/run.d.ts +2 -0
- package/dist/terminal/run.js +15 -0
- package/dist/terminal/shared.d.ts +20 -0
- package/dist/terminal/shared.js +88 -0
- package/dist/terminal/terminal.d.ts +66 -0
- package/dist/terminal/terminal.js +6 -0
- package/dist/terminal/terminals/cmux.d.ts +2 -0
- package/dist/terminal/terminals/cmux.js +98 -0
- package/dist/terminal/terminals/ghostty-script.d.ts +1 -0
- package/dist/terminal/terminals/ghostty-script.js +179 -0
- package/dist/terminal/terminals/ghostty.d.ts +2 -0
- package/dist/terminal/terminals/ghostty.js +336 -0
- package/dist/terminal/terminals/herdr.d.ts +2 -0
- package/dist/terminal/terminals/herdr.js +141 -0
- package/dist/terminal/terminals/index.d.ts +5 -0
- package/dist/terminal/terminals/index.js +26 -0
- package/dist/terminal/terminals/kitty.d.ts +2 -0
- package/dist/terminal/terminals/kitty.js +209 -0
- package/dist/terminal/terminals/supacode.d.ts +2 -0
- package/dist/terminal/terminals/supacode.js +79 -0
- package/dist/terminal/terminals/tmux.d.ts +2 -0
- package/dist/terminal/terminals/tmux.js +132 -0
- package/dist/terminal/terminals/tty7.d.ts +2 -0
- package/dist/terminal/terminals/tty7.js +58 -0
- package/dist/terminal/terminals/vscode.d.ts +2 -0
- package/dist/terminal/terminals/vscode.js +13 -0
- package/dist/terminal/terminals/wezterm.d.ts +2 -0
- package/dist/terminal/terminals/wezterm.js +59 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +40 -0
- package/dist/web/browser-session.d.ts +13 -0
- package/dist/web/browser-session.js +186 -0
- package/dist/web/cursor.d.ts +1 -0
- package/dist/web/cursor.js +24 -0
- package/dist/web/devtools.d.ts +39 -0
- package/dist/web/devtools.js +300 -0
- package/dist/web/favicon.d.ts +7 -0
- package/dist/web/favicon.js +73 -0
- package/dist/web/frame-rate.d.ts +1 -0
- package/dist/web/frame-rate.js +12 -0
- package/dist/web/host.d.ts +123 -0
- package/dist/web/host.js +573 -0
- package/dist/web/input.d.ts +49 -0
- package/dist/web/input.js +468 -0
- package/dist/web/offscreen.d.ts +2 -0
- package/dist/web/offscreen.js +23 -0
- package/dist/web/paint.d.ts +11 -0
- package/dist/web/paint.js +128 -0
- package/dist/web/popup.d.ts +40 -0
- package/dist/web/popup.js +144 -0
- package/dist/web/session.d.ts +20 -0
- package/dist/web/session.js +110 -0
- package/dist/web/types.d.ts +38 -0
- package/dist/web/types.js +39 -0
- package/dist/web/zoom.d.ts +5 -0
- package/dist/web/zoom.js +36 -0
- package/dist/webview.d.ts +55 -0
- package/dist/webview.js +481 -0
- package/package.json +69 -3
- package/scripts/apparmor.sh +71 -0
- package/scripts/postinstall.mjs +150 -0
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sandboxRefusal = sandboxRefusal;
|
|
7
|
+
exports.deniedRefusal = deniedRefusal;
|
|
8
|
+
exports.linuxSandboxError = linuxSandboxError;
|
|
9
|
+
exports.apparmorSetup = apparmorSetup;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const REFUSAL = `[placeholder copy: the codex sandbox blocks commands pixel needs to run to determine the terminal pane it is running in.]
|
|
15
|
+
Re-run this command with escalated permissions.`;
|
|
16
|
+
function inCodex() {
|
|
17
|
+
return Boolean(process.env.CODEX_THREAD_ID ?? process.env.CODEX_PERMISSION_PROFILE);
|
|
18
|
+
}
|
|
19
|
+
function sandboxRefusal() {
|
|
20
|
+
return process.env.CODEX_SANDBOX ? REFUSAL : null;
|
|
21
|
+
}
|
|
22
|
+
function deniedRefusal() {
|
|
23
|
+
return inCodex() ? REFUSAL : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The reason we need to write this file is chromium
|
|
27
|
+
* uses a linux feature (unprivelged user namespaces)
|
|
28
|
+
* that ubuntu will not let a process use without a specific
|
|
29
|
+
* "grant" (userns) that can be given to a process by making
|
|
30
|
+
* this special file that creates an apparmor profile
|
|
31
|
+
*/
|
|
32
|
+
const APPARMOR_SCRIPT = node_path_1.default.resolve(__dirname, "..", "scripts", "apparmor.sh");
|
|
33
|
+
function kernelSetting(file) {
|
|
34
|
+
if (!node_fs_1.default.existsSync(file))
|
|
35
|
+
return null;
|
|
36
|
+
return node_fs_1.default.readFileSync(file, "utf8").trim();
|
|
37
|
+
}
|
|
38
|
+
function setuidSandbox(electronBinary) {
|
|
39
|
+
const helper = node_path_1.default.join(node_path_1.default.dirname(electronBinary), "chrome-sandbox");
|
|
40
|
+
const stat = node_fs_1.default.statSync(helper, { throwIfNoEntry: false });
|
|
41
|
+
return stat !== undefined && stat.uid === 0 && (stat.mode & 0o4000) !== 0;
|
|
42
|
+
}
|
|
43
|
+
function apparmorProfile(electronBinary) {
|
|
44
|
+
try {
|
|
45
|
+
const resolved = node_fs_1.default.realpathSync(electronBinary);
|
|
46
|
+
const slug = node_crypto_1.default.createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
47
|
+
return node_fs_1.default.readFileSync(`/etc/apparmor.d/pixel-${slug}`, "utf8").includes(resolved);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function linuxSandboxError(electronBinary) {
|
|
54
|
+
if (process.getuid?.() === 0) {
|
|
55
|
+
return "Linux sandbox cannot run as root. Run pixel as a non-root user.]";
|
|
56
|
+
}
|
|
57
|
+
// look into all these cases some are new
|
|
58
|
+
if (setuidSandbox(electronBinary))
|
|
59
|
+
return null;
|
|
60
|
+
if (apparmorProfile(electronBinary))
|
|
61
|
+
return null;
|
|
62
|
+
if (kernelSetting("/proc/sys/user/max_user_namespaces") === "0") {
|
|
63
|
+
return "This machine sets user.max_user_namespaces to 0, so nothing can create the user namespaces the chromium sandbox needs. Raise it with: sudo sysctl -w user.max_user_namespaces=15000 (write it to /etc/sysctl.d/ to keep it across reboots), or install a root-owned setuid chrome-sandbox helper.]";
|
|
64
|
+
}
|
|
65
|
+
if (kernelSetting("/proc/sys/kernel/apparmor_restrict_unprivileged_userns") === "1") {
|
|
66
|
+
return "AppArmor blocks unprivileged user namespaces, which the chromium sandbox needs. Run: sudo bash node_modules/@zenbu-labs/pixel/scripts/apparmor.sh <electron binary>]";
|
|
67
|
+
}
|
|
68
|
+
if (kernelSetting("/proc/sys/kernel/unprivileged_userns_clone") === "0") {
|
|
69
|
+
return "Linux disables unprivileged user namespaces. Enable them or install a root-owned setuid chrome-sandbox helper.";
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function apparmorSetup(electronBinary) {
|
|
74
|
+
if (process.platform !== "linux")
|
|
75
|
+
return 0;
|
|
76
|
+
try {
|
|
77
|
+
(0, node_child_process_1.execFileSync)("bash", [APPARMOR_SCRIPT, electronBinary], { stdio: "inherit" });
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
process.stderr.write(`could not install the AppArmor profile: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/scale.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EngineInfo } from "./react";
|
|
2
|
+
import type { Terminal } from "./terminal";
|
|
3
|
+
export declare function hostDisplayScale(terminal: Terminal | null, env: NodeJS.ProcessEnv): number;
|
|
4
|
+
export declare class CellZoomFollower {
|
|
5
|
+
private last;
|
|
6
|
+
ratio(info: EngineInfo): number | null;
|
|
7
|
+
}
|
package/dist/scale.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CellZoomFollower = void 0;
|
|
4
|
+
exports.hostDisplayScale = hostDisplayScale;
|
|
5
|
+
const electron_1 = require("electron");
|
|
6
|
+
function hostDisplayScale(terminal, env) {
|
|
7
|
+
const explicit = Number(env.PIXEL_DISPLAY_SCALE);
|
|
8
|
+
if (Number.isFinite(explicit) && explicit > 0)
|
|
9
|
+
return explicit;
|
|
10
|
+
if (terminal?.reportsCssPixels)
|
|
11
|
+
return 1;
|
|
12
|
+
return electron_1.screen.getDisplayNearestPoint(electron_1.screen.getCursorScreenPoint()).scaleFactor;
|
|
13
|
+
}
|
|
14
|
+
class CellZoomFollower {
|
|
15
|
+
last = null;
|
|
16
|
+
ratio(info) {
|
|
17
|
+
const { height, basePx } = info;
|
|
18
|
+
const prev = this.last;
|
|
19
|
+
this.last = { height, basePx };
|
|
20
|
+
if (!prev || !prev.basePx || !prev.height)
|
|
21
|
+
return null;
|
|
22
|
+
const ratio = basePx / prev.basePx;
|
|
23
|
+
if (!Number.isFinite(ratio) || ratio <= 0 || Math.abs(ratio - 1) < 0.01)
|
|
24
|
+
return null;
|
|
25
|
+
const paneRatio = height / prev.height;
|
|
26
|
+
if (Math.abs(paneRatio - ratio) < 0.04 * ratio)
|
|
27
|
+
return null;
|
|
28
|
+
return ratio;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.CellZoomFollower = CellZoomFollower;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface SshTunnel {
|
|
2
|
+
destination: string;
|
|
3
|
+
socksPort: number;
|
|
4
|
+
controlPath: string;
|
|
5
|
+
stop(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface RemoteBundle {
|
|
8
|
+
url: string;
|
|
9
|
+
stop(): void;
|
|
10
|
+
}
|
|
11
|
+
export type StatusLine = (line: string) => void;
|
|
12
|
+
export interface SshOptions {
|
|
13
|
+
/** [placeholder copy: Whether ssh may use this process's terminal for password and host key prompts. Off when called from inside a running app, where the terminal is already taken; ssh then fails instead of prompting.] */
|
|
14
|
+
terminal?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseSshTarget(target: string): {
|
|
17
|
+
destination: string;
|
|
18
|
+
sshPort: string | null;
|
|
19
|
+
};
|
|
20
|
+
interface ResolvedTarget {
|
|
21
|
+
destination: string;
|
|
22
|
+
hostArgs: string[];
|
|
23
|
+
aliasCommand: string | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveSshTarget(target: string): ResolvedTarget;
|
|
26
|
+
export declare function validateSshTarget(target: string): void;
|
|
27
|
+
export declare function openSshTunnel(target: string, status: StatusLine, options?: SshOptions): Promise<SshTunnel>;
|
|
28
|
+
export interface SshSessionOptions extends SshOptions {
|
|
29
|
+
/** [placeholder copy: Where to connect, as you would type it after ssh: user@host, host:port, a full ssh command with flags, or a shell alias.] */
|
|
30
|
+
target: string;
|
|
31
|
+
/** [placeholder copy: A directory to install and start on the host. It must hold an executable start that prints READY <url>; setup and stop are optional.] */
|
|
32
|
+
bundle?: string | null;
|
|
33
|
+
/** [placeholder copy: Directory on the host that bundles are installed under.] */
|
|
34
|
+
remoteBase?: string | null;
|
|
35
|
+
status?: StatusLine;
|
|
36
|
+
}
|
|
37
|
+
export interface SshSession {
|
|
38
|
+
destination: string;
|
|
39
|
+
socksPort: number;
|
|
40
|
+
/** [placeholder copy: The url the bundle's start script announced, when a bundle was given.] */
|
|
41
|
+
url: string | undefined;
|
|
42
|
+
/** [placeholder copy: Props for the WebView that should browse through this host: spread them onto it.] */
|
|
43
|
+
view: {
|
|
44
|
+
proxy: string;
|
|
45
|
+
partition: string;
|
|
46
|
+
};
|
|
47
|
+
stop(): void;
|
|
48
|
+
}
|
|
49
|
+
export declare function connectSsh(options: SshSessionOptions): Promise<SshSession>;
|
|
50
|
+
export declare function socksProxyRules(tunnel: SshTunnel): string;
|
|
51
|
+
export declare function validateBundleDir(dir: string): void;
|
|
52
|
+
export declare const REMOTE_BUNDLES_DIR = "${XDG_DATA_HOME:-$HOME/.local/share}/pixel/bundles";
|
|
53
|
+
export interface BundleOptions extends SshOptions {
|
|
54
|
+
status: StatusLine;
|
|
55
|
+
remoteBase?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function startBundle(tunnel: SshTunnel, dir: string, options: BundleOptions): Promise<RemoteBundle>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.REMOTE_BUNDLES_DIR = void 0;
|
|
7
|
+
exports.parseSshTarget = parseSshTarget;
|
|
8
|
+
exports.resolveSshTarget = resolveSshTarget;
|
|
9
|
+
exports.validateSshTarget = validateSshTarget;
|
|
10
|
+
exports.openSshTunnel = openSshTunnel;
|
|
11
|
+
exports.connectSsh = connectSsh;
|
|
12
|
+
exports.socksProxyRules = socksProxyRules;
|
|
13
|
+
exports.validateBundleDir = validateBundleDir;
|
|
14
|
+
exports.startBundle = startBundle;
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
19
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
function parseSshTarget(target) {
|
|
22
|
+
const match = /^([A-Za-z0-9._-]+@)?([A-Za-z0-9._-]+)(:(\d+))?$/.exec(target);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(`[placeholder copy: invalid ssh target ${target} (user@host, host, user@host:port, or a shell alias for ssh)]`);
|
|
25
|
+
}
|
|
26
|
+
return { destination: `${match[1] ?? ""}${match[2]}`, sshPort: match[4] ?? null };
|
|
27
|
+
}
|
|
28
|
+
function resolveSshTarget(target) {
|
|
29
|
+
const words = target.trim().split(/\s+/).filter(Boolean);
|
|
30
|
+
if (words.length > 1) {
|
|
31
|
+
return { ...parseSshWords(words, target), aliasCommand: null };
|
|
32
|
+
}
|
|
33
|
+
const single = words[0] ?? "";
|
|
34
|
+
if (!single.includes("@") && !single.includes(":")) {
|
|
35
|
+
const alias = shellAlias(single);
|
|
36
|
+
if (alias) {
|
|
37
|
+
const parsed = parseSshCommand(alias);
|
|
38
|
+
if (parsed)
|
|
39
|
+
return { ...parsed, aliasCommand: alias.join(" ") };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const { destination, sshPort } = parseSshTarget(single);
|
|
43
|
+
return { destination, hostArgs: sshPort ? ["-p", sshPort] : [], aliasCommand: null };
|
|
44
|
+
}
|
|
45
|
+
function validateSshTarget(target) {
|
|
46
|
+
const words = target.trim().split(/\s+/).filter(Boolean);
|
|
47
|
+
if (words.length > 1)
|
|
48
|
+
parseSshWords(words, target);
|
|
49
|
+
else
|
|
50
|
+
parseSshTarget(words[0] ?? "");
|
|
51
|
+
}
|
|
52
|
+
function parseSshWords(words, target) {
|
|
53
|
+
const tokens = words[0] === "ssh" ? words.slice(1) : words;
|
|
54
|
+
const hostArgs = [];
|
|
55
|
+
let found = null;
|
|
56
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
57
|
+
const token = tokens[i];
|
|
58
|
+
if (token.startsWith("-")) {
|
|
59
|
+
hostArgs.push(token);
|
|
60
|
+
if (SSH_VALUE_FLAGS.has(token) && i + 1 < tokens.length)
|
|
61
|
+
hostArgs.push(tokens[++i]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (found) {
|
|
65
|
+
throw new Error(`[placeholder copy: invalid ssh target ${target} (both ${found} and ${token} look like destinations)]`);
|
|
66
|
+
}
|
|
67
|
+
found = token;
|
|
68
|
+
}
|
|
69
|
+
if (!found)
|
|
70
|
+
throw new Error(`[placeholder copy: invalid ssh target ${target} (no destination)]`);
|
|
71
|
+
const { destination, sshPort } = parseSshTarget(found);
|
|
72
|
+
return { destination, hostArgs: [...hostArgs, ...(sshPort ? ["-p", sshPort] : [])] };
|
|
73
|
+
}
|
|
74
|
+
function shellAlias(name) {
|
|
75
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name))
|
|
76
|
+
return null;
|
|
77
|
+
const shell = process.env.SHELL ?? "/bin/sh";
|
|
78
|
+
const result = (0, node_child_process_1.spawnSync)(shell, ["-ic", `alias ${name}`], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
timeout: 5000,
|
|
82
|
+
});
|
|
83
|
+
if (result.status !== 0 || !result.stdout)
|
|
84
|
+
return null;
|
|
85
|
+
const line = result.stdout.trim().split("\n").pop() ?? "";
|
|
86
|
+
const eq = line.indexOf("=");
|
|
87
|
+
if (eq < 0)
|
|
88
|
+
return null;
|
|
89
|
+
const tokens = unquote(line.slice(eq + 1).trim())
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
if (!/(^|\/)ssh$/.test(tokens[0] ?? ""))
|
|
93
|
+
return null;
|
|
94
|
+
return tokens.slice(1);
|
|
95
|
+
}
|
|
96
|
+
function unquote(value) {
|
|
97
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
98
|
+
return value.slice(1, -1).replaceAll(`'\\''`, "'");
|
|
99
|
+
}
|
|
100
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
101
|
+
return value.slice(1, -1);
|
|
102
|
+
return value.replaceAll("\\ ", " ");
|
|
103
|
+
}
|
|
104
|
+
const SSH_VALUE_FLAGS = new Set("-B -b -c -D -E -e -F -I -i -J -L -l -m -O -o -P -p -R -S -W -w".split(" "));
|
|
105
|
+
function parseSshCommand(tokens) {
|
|
106
|
+
const hostArgs = [];
|
|
107
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
108
|
+
const token = tokens[i];
|
|
109
|
+
if (token.startsWith("-")) {
|
|
110
|
+
hostArgs.push(token);
|
|
111
|
+
if (SSH_VALUE_FLAGS.has(token) && i + 1 < tokens.length)
|
|
112
|
+
hostArgs.push(tokens[++i]);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
return { destination: token, hostArgs };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
async function openSshTunnel(target, status, options = {}) {
|
|
120
|
+
const terminal = options.terminal ?? true;
|
|
121
|
+
const { destination, hostArgs, aliasCommand } = resolveSshTarget(target);
|
|
122
|
+
if (aliasCommand)
|
|
123
|
+
status(`${target} is an alias for ssh ${aliasCommand}`);
|
|
124
|
+
const controlPath = freshControlPath();
|
|
125
|
+
const socksPort = await freePort();
|
|
126
|
+
const args = [
|
|
127
|
+
...(terminal ? [] : ["-o", "BatchMode=yes"]),
|
|
128
|
+
"-f",
|
|
129
|
+
"-N",
|
|
130
|
+
"-M",
|
|
131
|
+
"-S",
|
|
132
|
+
controlPath,
|
|
133
|
+
"-D",
|
|
134
|
+
`127.0.0.1:${socksPort}`,
|
|
135
|
+
"-o",
|
|
136
|
+
"ExitOnForwardFailure=yes",
|
|
137
|
+
"-o",
|
|
138
|
+
"ServerAliveInterval=15",
|
|
139
|
+
"-o",
|
|
140
|
+
"ServerAliveCountMax=3",
|
|
141
|
+
...hostArgs,
|
|
142
|
+
destination,
|
|
143
|
+
];
|
|
144
|
+
status(`connecting to ${destination}`);
|
|
145
|
+
const stderr = [];
|
|
146
|
+
const code = await new Promise((resolve, reject) => {
|
|
147
|
+
const child = (0, node_child_process_1.spawn)("ssh", args, { stdio: terminal ? "inherit" : ["ignore", "ignore", "pipe"] });
|
|
148
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk.toString("utf8").trim()));
|
|
149
|
+
child.once("error", reject);
|
|
150
|
+
child.once("exit", (exitCode) => resolve(exitCode ?? 1));
|
|
151
|
+
});
|
|
152
|
+
if (code !== 0) {
|
|
153
|
+
const detail = stderr.filter(Boolean).join("\n");
|
|
154
|
+
throw new Error(`[placeholder copy: ssh to ${destination} failed${detail ? `: ${detail}` : ""}]`);
|
|
155
|
+
}
|
|
156
|
+
await waitForSocks(socksPort, destination);
|
|
157
|
+
status(`connected ${destination}`);
|
|
158
|
+
return {
|
|
159
|
+
destination,
|
|
160
|
+
socksPort,
|
|
161
|
+
controlPath,
|
|
162
|
+
stop: () => {
|
|
163
|
+
try {
|
|
164
|
+
(0, node_child_process_1.spawnSync)("ssh", ["-S", controlPath, "-O", "exit", destination], {
|
|
165
|
+
stdio: "ignore",
|
|
166
|
+
timeout: 5000,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch { }
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// The whole ssh mode in one step: tunnel, then bundle, then the props a page
|
|
174
|
+
// needs to browse through the host. Stopping runs in reverse and also happens
|
|
175
|
+
// when the process exits.
|
|
176
|
+
async function connectSsh(options) {
|
|
177
|
+
const status = options.status ?? (() => { });
|
|
178
|
+
const terminal = options.terminal ?? true;
|
|
179
|
+
if (options.bundle)
|
|
180
|
+
validateBundleDir(options.bundle);
|
|
181
|
+
const tunnel = await openSshTunnel(options.target, status, { terminal });
|
|
182
|
+
let bundle = null;
|
|
183
|
+
let stopped = false;
|
|
184
|
+
const stop = () => {
|
|
185
|
+
if (stopped)
|
|
186
|
+
return;
|
|
187
|
+
stopped = true;
|
|
188
|
+
bundle?.stop();
|
|
189
|
+
tunnel.stop();
|
|
190
|
+
};
|
|
191
|
+
process.on("exit", stop);
|
|
192
|
+
if (options.bundle) {
|
|
193
|
+
try {
|
|
194
|
+
bundle = await startBundle(tunnel, node_path_1.default.resolve(options.bundle), {
|
|
195
|
+
status,
|
|
196
|
+
remoteBase: options.remoteBase ?? undefined,
|
|
197
|
+
terminal,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
stop();
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
destination: tunnel.destination,
|
|
207
|
+
socksPort: tunnel.socksPort,
|
|
208
|
+
url: bundle?.url,
|
|
209
|
+
view: {
|
|
210
|
+
proxy: socksProxyRules(tunnel),
|
|
211
|
+
partition: `ssh-${tunnel.destination.replace(/[^A-Za-z0-9@._-]/g, "-")}`,
|
|
212
|
+
},
|
|
213
|
+
stop,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function socksProxyRules(tunnel) {
|
|
217
|
+
return `socks5://127.0.0.1:${tunnel.socksPort}`;
|
|
218
|
+
}
|
|
219
|
+
function validateBundleDir(dir) {
|
|
220
|
+
let stat;
|
|
221
|
+
try {
|
|
222
|
+
stat = node_fs_1.default.statSync(dir);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
throw new Error(`[placeholder copy: bundle ${dir} does not exist]`);
|
|
226
|
+
}
|
|
227
|
+
if (!stat.isDirectory())
|
|
228
|
+
throw new Error(`[placeholder copy: bundle ${dir} is not a directory]`);
|
|
229
|
+
let start;
|
|
230
|
+
try {
|
|
231
|
+
start = node_fs_1.default.statSync(node_path_1.default.join(dir, "start"));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new Error(`[placeholder copy: bundle ${dir} has no start script]`);
|
|
235
|
+
}
|
|
236
|
+
if (!start.isFile() || !(start.mode & 0o111)) {
|
|
237
|
+
throw new Error(`[placeholder copy: bundle ${dir}/start is not executable]`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const READY_TIMEOUT_MS = 120_000;
|
|
241
|
+
exports.REMOTE_BUNDLES_DIR = '${XDG_DATA_HOME:-$HOME/.local/share}/pixel/bundles';
|
|
242
|
+
async function startBundle(tunnel, dir, options) {
|
|
243
|
+
const { status, remoteBase = exports.REMOTE_BUNDLES_DIR, terminal = true } = options;
|
|
244
|
+
validateBundleDir(dir);
|
|
245
|
+
const name = bundleName(dir);
|
|
246
|
+
const remoteDir = `${remoteBase}/${name}-${bundleHash(dir).slice(0, 12)}`;
|
|
247
|
+
if (!installed(tunnel, remoteDir)) {
|
|
248
|
+
status(`installing ${name} on ${tunnel.destination}`);
|
|
249
|
+
upload(tunnel, dir, remoteDir);
|
|
250
|
+
setup(tunnel, remoteDir, terminal, status);
|
|
251
|
+
}
|
|
252
|
+
status(`starting ${name}`);
|
|
253
|
+
return launch(tunnel, name, remoteDir, status);
|
|
254
|
+
}
|
|
255
|
+
function bundleName(dir) {
|
|
256
|
+
let name = node_path_1.default.basename(dir);
|
|
257
|
+
try {
|
|
258
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "manifest.json"), "utf8"));
|
|
259
|
+
if (typeof manifest.name === "string" && manifest.name)
|
|
260
|
+
name = manifest.name;
|
|
261
|
+
}
|
|
262
|
+
catch { }
|
|
263
|
+
return name.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
264
|
+
}
|
|
265
|
+
function bundleHash(dir) {
|
|
266
|
+
const hash = node_crypto_1.default.createHash("sha256");
|
|
267
|
+
const walk = (rel) => {
|
|
268
|
+
const entries = node_fs_1.default
|
|
269
|
+
.readdirSync(node_path_1.default.join(dir, rel), { withFileTypes: true })
|
|
270
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const childRel = node_path_1.default.join(rel, entry.name);
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
walk(childRel);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!entry.isFile())
|
|
278
|
+
continue;
|
|
279
|
+
const stat = node_fs_1.default.statSync(node_path_1.default.join(dir, childRel));
|
|
280
|
+
hash.update(`${childRel}\0${stat.mode & 0o777}\0`);
|
|
281
|
+
hash.update(node_fs_1.default.readFileSync(node_path_1.default.join(dir, childRel)));
|
|
282
|
+
hash.update("\0");
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
walk("");
|
|
286
|
+
return hash.digest("hex");
|
|
287
|
+
}
|
|
288
|
+
function run(tunnel, command, options = {}) {
|
|
289
|
+
return (0, node_child_process_1.spawnSync)("ssh", ["-S", tunnel.controlPath, tunnel.destination, command], {
|
|
290
|
+
input: options.input,
|
|
291
|
+
stdio: options.input ? undefined : options.stdio ?? "ignore",
|
|
292
|
+
timeout: options.timeout,
|
|
293
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
294
|
+
encoding: "utf8",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function installed(tunnel, remoteDir) {
|
|
298
|
+
return run(tunnel, `test -f "${remoteDir}/.ready"`).status === 0;
|
|
299
|
+
}
|
|
300
|
+
function upload(tunnel, dir, remoteDir) {
|
|
301
|
+
const tar = (0, node_child_process_1.spawnSync)("tar", ["-cz", "-C", dir, "."], {
|
|
302
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
303
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
304
|
+
});
|
|
305
|
+
if (tar.status !== 0 || !tar.stdout)
|
|
306
|
+
throw new Error(`[placeholder copy: could not pack ${dir}]`);
|
|
307
|
+
const result = run(tunnel, `mkdir -p "${remoteDir}" && tar -xz -C "${remoteDir}"`, {
|
|
308
|
+
input: tar.stdout,
|
|
309
|
+
});
|
|
310
|
+
if (result.status !== 0) {
|
|
311
|
+
throw new Error(`[placeholder copy: could not upload the bundle to ${tunnel.destination}]`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function setup(tunnel, remoteDir, terminal, status) {
|
|
315
|
+
const result = run(tunnel, `cd "${remoteDir}" && { [ ! -x ./setup ] || ./setup; } && touch .ready`, { stdio: terminal ? "inherit" : "pipe" });
|
|
316
|
+
if (!terminal) {
|
|
317
|
+
for (const line of `${result.stdout ?? ""}${result.stderr ?? ""}`.split("\n"))
|
|
318
|
+
if (line.trim())
|
|
319
|
+
status(line);
|
|
320
|
+
}
|
|
321
|
+
if (result.status !== 0) {
|
|
322
|
+
throw new Error(`[placeholder copy: the bundle's setup script failed on ${tunnel.destination}]`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function launch(tunnel, name, remoteDir, status) {
|
|
326
|
+
const tail = [];
|
|
327
|
+
const child = (0, node_child_process_1.spawn)("ssh", [
|
|
328
|
+
"-tt",
|
|
329
|
+
"-S",
|
|
330
|
+
tunnel.controlPath,
|
|
331
|
+
tunnel.destination,
|
|
332
|
+
`cd "${remoteDir}" || exit 9; [ -x ./stop ] && ./stop >/dev/null 2>&1; exec ./start`,
|
|
333
|
+
], { stdio: ["ignore", "pipe", "ignore"] });
|
|
334
|
+
const stop = () => {
|
|
335
|
+
try {
|
|
336
|
+
run(tunnel, `cd "${remoteDir}" && [ -x ./stop ] && ./stop`, { timeout: 10_000 });
|
|
337
|
+
}
|
|
338
|
+
catch { }
|
|
339
|
+
try {
|
|
340
|
+
child.kill();
|
|
341
|
+
}
|
|
342
|
+
catch { }
|
|
343
|
+
};
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
let buffer = "";
|
|
346
|
+
let done = false;
|
|
347
|
+
const finish = (result) => {
|
|
348
|
+
if (done)
|
|
349
|
+
return;
|
|
350
|
+
done = true;
|
|
351
|
+
clearTimeout(timer);
|
|
352
|
+
if (result instanceof Error) {
|
|
353
|
+
stop();
|
|
354
|
+
reject(result);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
resolve(result);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const timer = setTimeout(() => {
|
|
361
|
+
finish(new Error(`[placeholder copy: ${name} never printed READY <url>]\n${tail.join("\n")}`));
|
|
362
|
+
}, READY_TIMEOUT_MS);
|
|
363
|
+
const sawLine = (line) => {
|
|
364
|
+
tail.push(line);
|
|
365
|
+
if (tail.length > 20)
|
|
366
|
+
tail.shift();
|
|
367
|
+
const ready = /^READY\s+(\S+)/.exec(line);
|
|
368
|
+
if (!ready)
|
|
369
|
+
return;
|
|
370
|
+
const url = pageUrl(ready[1]);
|
|
371
|
+
if (!url) {
|
|
372
|
+
finish(new Error(`[placeholder copy: ${name} printed READY ${ready[1]}, which is not an http(s) url]`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
status(`${name} is up at ${url}`);
|
|
376
|
+
finish({ url, stop });
|
|
377
|
+
};
|
|
378
|
+
child.stdout.on("data", (chunk) => {
|
|
379
|
+
const text = chunk.toString("utf8");
|
|
380
|
+
buffer += text;
|
|
381
|
+
let newline = buffer.indexOf("\n");
|
|
382
|
+
while (newline !== -1) {
|
|
383
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
384
|
+
buffer = buffer.slice(newline + 1);
|
|
385
|
+
newline = buffer.indexOf("\n");
|
|
386
|
+
sawLine(line);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
child.once("error", (error) => finish(error));
|
|
390
|
+
child.once("close", (code) => {
|
|
391
|
+
if (buffer.trim())
|
|
392
|
+
sawLine(buffer.trim());
|
|
393
|
+
finish(new Error(`[placeholder copy: ${name} exited with code ${code} before printing READY <url>]\n${tail.join("\n")}`));
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function pageUrl(token) {
|
|
398
|
+
try {
|
|
399
|
+
const url = new URL(token);
|
|
400
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function freshControlPath() {
|
|
407
|
+
const name = `${process.pid.toString(36)}-${Date.now().toString(36).slice(-5)}`;
|
|
408
|
+
const dir = node_path_1.default.join(node_os_1.default.tmpdir(), "pixel-ssh");
|
|
409
|
+
const candidate = node_path_1.default.join(dir, name);
|
|
410
|
+
if (candidate.length <= 80) {
|
|
411
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
412
|
+
return candidate;
|
|
413
|
+
}
|
|
414
|
+
node_fs_1.default.mkdirSync("/tmp/pixel-ssh", { recursive: true, mode: 0o700 });
|
|
415
|
+
return node_path_1.default.join("/tmp/pixel-ssh", name);
|
|
416
|
+
}
|
|
417
|
+
function freePort() {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const server = node_net_1.default.createServer();
|
|
420
|
+
server.once("error", reject);
|
|
421
|
+
server.listen(0, "127.0.0.1", () => {
|
|
422
|
+
const address = server.address();
|
|
423
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
424
|
+
server.close(() => (port ? resolve(port) : reject(new Error("no free port"))));
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function waitForSocks(port, destination) {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const socket = node_net_1.default.connect(port, "127.0.0.1");
|
|
431
|
+
const timer = setTimeout(() => {
|
|
432
|
+
socket.destroy();
|
|
433
|
+
reject(new Error(`[placeholder copy: the ssh proxy for ${destination} never started listening]`));
|
|
434
|
+
}, 5000);
|
|
435
|
+
socket.once("connect", () => {
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
socket.end();
|
|
438
|
+
resolve();
|
|
439
|
+
});
|
|
440
|
+
socket.once("error", (error) => {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
reject(error);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appleScript = appleScript;
|
|
4
|
+
function appleScript(app, run, language) {
|
|
5
|
+
const flags = language ? ["-l", language] : [];
|
|
6
|
+
return async function osascript(script, args) {
|
|
7
|
+
try {
|
|
8
|
+
return (await run("osascript", [...flags, "-", ...args], script)).trim();
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.signal === "SIGTERM") {
|
|
12
|
+
throw new Error(`controlling ${app} timed out — this environment may lack macOS automation permission for ${app}`);
|
|
13
|
+
}
|
|
14
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
15
|
+
if (stderr)
|
|
16
|
+
throw new Error(`controlling ${app} failed: ${stderr}`);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GraphicsSupport } from "./graphics";
|
|
2
|
+
import type { Run } from "./run";
|
|
3
|
+
import type { Terminal } from "./terminal";
|
|
4
|
+
export declare function detect(env?: NodeJS.ProcessEnv, run?: Run): Terminal | null;
|
|
5
|
+
export interface TerminalCheck {
|
|
6
|
+
terminal: Terminal | null;
|
|
7
|
+
graphics: GraphicsSupport;
|
|
8
|
+
}
|
|
9
|
+
export declare function checkTerminal(terminal: Terminal | null, env?: NodeJS.ProcessEnv): Promise<TerminalCheck>;
|
|
10
|
+
export declare function cannotOpenPanes(terminal: Terminal | null): string;
|