@zenbu-labs/pixel 0.0.21 → 0.0.23

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.js CHANGED
@@ -27,12 +27,12 @@ function resolveEntry(given) {
27
27
  }
28
28
  const entry = node_path_1.default.join(target, main);
29
29
  if (!node_fs_1.default.existsSync(entry)) {
30
- fail(`[placeholder copy: ${entry} does not exist. Build your app or point "main" in package.json at the built entry.]`);
30
+ fail(`${entry} does not exist. Build your app or point "main" in package.json at the built entry.`);
31
31
  }
32
32
  return entry;
33
33
  }
34
34
  if (!node_fs_1.default.existsSync(target))
35
- fail(`[placeholder copy: no such file ${target}]`);
35
+ fail(`no such file ${target}`);
36
36
  return target;
37
37
  }
38
38
  function appName(dir) {
@@ -55,7 +55,7 @@ function appDir(entry) {
55
55
  function electronBinary() {
56
56
  const dist = node_path_1.default.resolve(__dirname, "..", "electron", "dist");
57
57
  if (!node_fs_1.default.existsSync(node_path_1.default.join(dist, ".zenbu-electron-sha256"))) {
58
- fail("[placeholder copy: the patched electron build is not installed. Run `node node_modules/@zenbu-labs/pixel/scripts/postinstall.mjs` (npm normally runs it for you on install).]");
58
+ fail("the patched electron build is not installed. Run `node node_modules/@zenbu-labs/pixel/scripts/postinstall.mjs` (npm normally runs it for you on install).");
59
59
  }
60
60
  return process.platform === "darwin"
61
61
  ? node_path_1.default.join(dist, "Electron.app", "Contents", "MacOS", "pixel")
@@ -74,7 +74,7 @@ function ownTty() {
74
74
  return out;
75
75
  }
76
76
  catch { }
77
- return fail("[placeholder copy: could not work out which tty this shell is on]");
77
+ return fail("could not work out which tty this shell is on");
78
78
  }
79
79
  async function main() {
80
80
  const args = process.argv.slice(2);
@@ -82,22 +82,22 @@ async function main() {
82
82
  const own = passthrough < 0 ? args : args.slice(0, passthrough);
83
83
  const appArgs = passthrough < 0 ? [] : args.slice(passthrough + 1);
84
84
  if (own.includes("--help") || own.includes("-h")) {
85
- process.stdout.write("[placeholder copy: usage: pixel [entry|dir] [-- app args]\n Runs an app's main file inside the pixel runtime, in this terminal pane.]\n");
85
+ process.stdout.write("usage: pixel [entry|dir] [-- app args]\n Runs an app's main file inside the pixel runtime, in this terminal pane.\n");
86
86
  return 0;
87
87
  }
88
88
  const stray = own.slice(1).find((arg) => arg.startsWith("-")) ?? own.find((arg) => arg.startsWith("-"));
89
89
  if (stray) {
90
- fail(`[placeholder copy: unknown option ${stray}. pixel takes an entry and nothing else; put your app's arguments after --]`);
90
+ fail(`unknown option ${stray}. pixel takes an entry and nothing else; put your app's arguments after --`);
91
91
  }
92
92
  if (own.length > 1)
93
- fail(`[placeholder copy: unexpected ${own[1]}; put your app's arguments after --]`);
93
+ fail(`unexpected ${own[1]}; put your app's arguments after --`);
94
94
  const entry = resolveEntry(own[0] ?? ".");
95
95
  const tty = process.env.PIXEL_TTY ?? ownTty();
96
96
  const owner = (0, instances_1.findOwner)(tty);
97
97
  const announced = owner ? (0, instances_1.announceGuest)(owner, appName(appDir(entry))) : null;
98
98
  if (!owner && !process.env.PIXEL_EMBED) {
99
99
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
100
- fail("[placeholder copy: pixel draws into the terminal, so it needs to run on a tty]");
100
+ fail("tty required");
101
101
  }
102
102
  const check = await (0, terminal_1.checkTerminal)((0, terminal_1.detect)());
103
103
  if (check.graphics === "unsupported") {
@@ -18,16 +18,11 @@ export declare function DevtoolsDockPane({ rect, dock, rem, theme, surface, divi
18
18
  onDividerHover(hovering: boolean): void;
19
19
  }): import("react").JSX.Element;
20
20
  export interface DevToolsProps {
21
- /** [placeholder copy: The WebView to inspect, as the ref you gave it or the handle itself.] */
22
21
  target: RefObject<WebViewHandle | null> | WebViewHandle;
23
22
  style?: Style;
24
23
  dock?: DevtoolsDock;
25
- /** [placeholder copy: Devtools panel to show once open, for example "console" or "elements".] */
26
24
  panel?: string;
27
- /** [placeholder copy: The devtools toolbar asked to close or to dock elsewhere. Unmount or change dock in response.] */
28
25
  onAction?(action: "close" | "dock-bottom" | "dock-right"): void;
29
- /** [placeholder copy: Keep showing the last frame, stretched, while the pane changes size instead of clearing until devtools repaint. Defaults to true.] */
30
26
  keepFrame?: boolean;
31
27
  }
32
- /** [placeholder copy: Renders a WebView's devtools wherever this sits in the tree. Mounting opens them, unmounting closes them.] */
33
28
  export declare function DevTools({ target, style, dock, panel, onAction, keepFrame }: DevToolsProps): import("react").JSX.Element;
package/dist/devtools.js CHANGED
@@ -45,11 +45,10 @@ function resolveEntry(target) {
45
45
  const handle = "current" in target ? target.current : target;
46
46
  return handle ? registry_1.handleEntries.get(handle) ?? null : null;
47
47
  }
48
- /** [placeholder copy: Renders a WebView's devtools wherever this sits in the tree. Mounting opens them, unmounting closes them.] */
49
48
  function DevTools({ target, style, dock = "right", panel, onAction, keepFrame = true }) {
50
49
  const registry = (0, react_1.useContext)(registry_1.RootContext);
51
50
  if (!registry) {
52
- throw new Error("[placeholder copy: <DevTools> has to be rendered by a root from pixel's createRoot()]");
51
+ throw new Error("<DevTools> has to be rendered by a root from createRoot()");
53
52
  }
54
53
  const boxRef = (0, react_1.useRef)(null);
55
54
  const rect = (0, layout_1.useNodeRect)(boxRef, registry);
@@ -1,5 +1,4 @@
1
1
  /// <reference path="../../electron/electron.d.ts" preserve="true" />
2
- /** [placeholder copy: The terminal's colours as rgb triples. `ansi` has 16 slots and is null where the terminal did not report a colour.] */
3
2
  export interface TerminalTheme {
4
3
  background: number[];
5
4
  foreground: number[];
@@ -51,7 +51,6 @@ export interface HostOptions {
51
51
  socket: string;
52
52
  pane: string;
53
53
  name: string;
54
- /** [placeholder copy: Set when the host is not pixel: frames are drawn into this terminal as kitty virtual placements the host positions.] */
55
54
  tty?: string;
56
55
  }
57
56
  export interface HighlightSpan {
@@ -65,7 +65,6 @@ export interface ContainerSelection {
65
65
  export interface BoxProps {
66
66
  style?: Style;
67
67
  id?: string;
68
- /** [placeholder copy: Lays the box out as nothing and paints none of it, keeping its children mounted.] */
69
68
  hidden?: boolean;
70
69
  onClick?: (event: ClickEvent) => void;
71
70
  onClickOutside?: (event: ClickEvent) => void;
package/dist/root.js CHANGED
@@ -62,7 +62,7 @@ function waitForSocket(socket) {
62
62
  }
63
63
  function createRoot(options = {}) {
64
64
  if (!electron_1.app.isReady()) {
65
- throw new Error("[placeholder copy: createRoot() needs electron to be ready. Run your entry with `pixel .`, which waits for app.whenReady() before loading it.]");
65
+ throw new Error("createRoot() needs electron to be ready. Run your entry with `pixel .`, which waits for app.whenReady() before loading it.");
66
66
  }
67
67
  const env = options.sessionEnv ?? process.env;
68
68
  const terminal = (0, terminal_1.detect)(env);
@@ -310,7 +310,7 @@ function createRoot(options = {}) {
310
310
  root.render((0, jsx_runtime_1.jsx)(webview_1.WebView, { ref: pageRef, src: url, style: { width: "100%", height: "100%" }, ...options }));
311
311
  });
312
312
  if (!pageRef.current)
313
- throw new Error("[placeholder copy: the page did not mount]");
313
+ throw new Error("the page did not mount");
314
314
  return pageRef.current;
315
315
  };
316
316
  const root = {
package/dist/sandbox.js CHANGED
@@ -11,7 +11,8 @@ const node_child_process_1 = require("node:child_process");
11
11
  const node_crypto_1 = __importDefault(require("node:crypto"));
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
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.]
14
+ // probably need app name injection support here
15
+ const REFUSAL = `the codex sandbox blocks commands that are needed to determine the terminal pane the command is ran from.]
15
16
  Re-run this command with escalated permissions.`;
16
17
  function inCodex() {
17
18
  return Boolean(process.env.CODEX_THREAD_ID ?? process.env.CODEX_PERMISSION_PROFILE);
@@ -10,7 +10,6 @@ export interface RemoteBundle {
10
10
  }
11
11
  export type StatusLine = (line: string) => void;
12
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
13
  terminal?: boolean;
15
14
  }
16
15
  export declare function parseSshTarget(target: string): {
@@ -26,20 +25,15 @@ export declare function resolveSshTarget(target: string): ResolvedTarget;
26
25
  export declare function validateSshTarget(target: string): void;
27
26
  export declare function openSshTunnel(target: string, status: StatusLine, options?: SshOptions): Promise<SshTunnel>;
28
27
  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
28
  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
29
  bundle?: string | null;
33
- /** [placeholder copy: Directory on the host that bundles are installed under.] */
34
30
  remoteBase?: string | null;
35
31
  status?: StatusLine;
36
32
  }
37
33
  export interface SshSession {
38
34
  destination: string;
39
35
  socksPort: number;
40
- /** [placeholder copy: The url the bundle's start script announced, when a bundle was given.] */
41
36
  url: string | undefined;
42
- /** [placeholder copy: Props for the WebView that should browse through this host: spread them onto it.] */
43
37
  view: {
44
38
  proxy: string;
45
39
  partition: string;
package/dist/ssh/index.js CHANGED
@@ -21,7 +21,7 @@ const node_path_1 = __importDefault(require("node:path"));
21
21
  function parseSshTarget(target) {
22
22
  const match = /^([A-Za-z0-9._-]+@)?([A-Za-z0-9._-]+)(:(\d+))?$/.exec(target);
23
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)]`);
24
+ throw new Error(`invalid ssh target ${target} (user@host, host, user@host:port, or a shell alias for ssh)`);
25
25
  }
26
26
  return { destination: `${match[1] ?? ""}${match[2]}`, sshPort: match[4] ?? null };
27
27
  }
@@ -62,12 +62,12 @@ function parseSshWords(words, target) {
62
62
  continue;
63
63
  }
64
64
  if (found) {
65
- throw new Error(`[placeholder copy: invalid ssh target ${target} (both ${found} and ${token} look like destinations)]`);
65
+ throw new Error(`invalid ssh target ${target} (both ${found} and ${token} look like destinations)`);
66
66
  }
67
67
  found = token;
68
68
  }
69
69
  if (!found)
70
- throw new Error(`[placeholder copy: invalid ssh target ${target} (no destination)]`);
70
+ throw new Error(`invalid ssh target ${target} (no destination)`);
71
71
  const { destination, sshPort } = parseSshTarget(found);
72
72
  return { destination, hostArgs: [...hostArgs, ...(sshPort ? ["-p", sshPort] : [])] };
73
73
  }
@@ -151,7 +151,7 @@ async function openSshTunnel(target, status, options = {}) {
151
151
  });
152
152
  if (code !== 0) {
153
153
  const detail = stderr.filter(Boolean).join("\n");
154
- throw new Error(`[placeholder copy: ssh to ${destination} failed${detail ? `: ${detail}` : ""}]`);
154
+ throw new Error(`ssh to ${destination} failed${detail ? `: ${detail}` : ""}`);
155
155
  }
156
156
  await waitForSocks(socksPort, destination);
157
157
  status(`connected ${destination}`);
@@ -170,9 +170,6 @@ async function openSshTunnel(target, status, options = {}) {
170
170
  },
171
171
  };
172
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
173
  async function connectSsh(options) {
177
174
  const status = options.status ?? (() => { });
178
175
  const terminal = options.terminal ?? true;
@@ -222,19 +219,19 @@ function validateBundleDir(dir) {
222
219
  stat = node_fs_1.default.statSync(dir);
223
220
  }
224
221
  catch {
225
- throw new Error(`[placeholder copy: bundle ${dir} does not exist]`);
222
+ throw new Error(`bundle ${dir} does not exist`);
226
223
  }
227
224
  if (!stat.isDirectory())
228
- throw new Error(`[placeholder copy: bundle ${dir} is not a directory]`);
225
+ throw new Error(`bundle ${dir} is not a directory`);
229
226
  let start;
230
227
  try {
231
228
  start = node_fs_1.default.statSync(node_path_1.default.join(dir, "start"));
232
229
  }
233
230
  catch {
234
- throw new Error(`[placeholder copy: bundle ${dir} has no start script]`);
231
+ throw new Error(`bundle ${dir} has no start script`);
235
232
  }
236
233
  if (!start.isFile() || !(start.mode & 0o111)) {
237
- throw new Error(`[placeholder copy: bundle ${dir}/start is not executable]`);
234
+ throw new Error(`bundle ${dir}/start is not executable`);
238
235
  }
239
236
  }
240
237
  const READY_TIMEOUT_MS = 120_000;
@@ -303,12 +300,12 @@ function upload(tunnel, dir, remoteDir) {
303
300
  maxBuffer: 256 * 1024 * 1024,
304
301
  });
305
302
  if (tar.status !== 0 || !tar.stdout)
306
- throw new Error(`[placeholder copy: could not pack ${dir}]`);
303
+ throw new Error(`could not pack ${dir}`);
307
304
  const result = run(tunnel, `mkdir -p "${remoteDir}" && tar -xz -C "${remoteDir}"`, {
308
305
  input: tar.stdout,
309
306
  });
310
307
  if (result.status !== 0) {
311
- throw new Error(`[placeholder copy: could not upload the bundle to ${tunnel.destination}]`);
308
+ throw new Error(`could not upload the bundle to ${tunnel.destination}`);
312
309
  }
313
310
  }
314
311
  function setup(tunnel, remoteDir, terminal, status) {
@@ -319,7 +316,7 @@ function setup(tunnel, remoteDir, terminal, status) {
319
316
  status(line);
320
317
  }
321
318
  if (result.status !== 0) {
322
- throw new Error(`[placeholder copy: the bundle's setup script failed on ${tunnel.destination}]`);
319
+ throw new Error(`the bundle's setup script failed on ${tunnel.destination}`);
323
320
  }
324
321
  }
325
322
  function launch(tunnel, name, remoteDir, status) {
@@ -358,7 +355,7 @@ function launch(tunnel, name, remoteDir, status) {
358
355
  }
359
356
  };
360
357
  const timer = setTimeout(() => {
361
- finish(new Error(`[placeholder copy: ${name} never printed READY <url>]\n${tail.join("\n")}`));
358
+ finish(new Error(`${name} never printed READY <url>\n${tail.join("\n")}`));
362
359
  }, READY_TIMEOUT_MS);
363
360
  const sawLine = (line) => {
364
361
  tail.push(line);
@@ -369,7 +366,7 @@ function launch(tunnel, name, remoteDir, status) {
369
366
  return;
370
367
  const url = pageUrl(ready[1]);
371
368
  if (!url) {
372
- finish(new Error(`[placeholder copy: ${name} printed READY ${ready[1]}, which is not an http(s) url]`));
369
+ finish(new Error(`${name} printed READY ${ready[1]}, which is not an http(s) url`));
373
370
  return;
374
371
  }
375
372
  status(`${name} is up at ${url}`);
@@ -390,7 +387,7 @@ function launch(tunnel, name, remoteDir, status) {
390
387
  child.once("close", (code) => {
391
388
  if (buffer.trim())
392
389
  sawLine(buffer.trim());
393
- finish(new Error(`[placeholder copy: ${name} exited with code ${code} before printing READY <url>]\n${tail.join("\n")}`));
390
+ finish(new Error(`${name} exited with code ${code} before printing READY <url>\n${tail.join("\n")}`));
394
391
  });
395
392
  });
396
393
  }
@@ -430,7 +427,7 @@ function waitForSocks(port, destination) {
430
427
  const socket = node_net_1.default.connect(port, "127.0.0.1");
431
428
  const timer = setTimeout(() => {
432
429
  socket.destroy();
433
- reject(new Error(`[placeholder copy: the ssh proxy for ${destination} never started listening]`));
430
+ reject(new Error(`the ssh proxy for ${destination} never started listening`));
434
431
  }, 5000);
435
432
  socket.once("connect", () => {
436
433
  clearTimeout(timer);
@@ -61,13 +61,13 @@ function send(pid, eventClass, eventId, params) {
61
61
  const failure = Ref();
62
62
  const reply = event.sendEventWithOptionsTimeoutError(WAIT_FOR_REPLY, 10, failure);
63
63
  if (reply.isNil()) {
64
- throw new Error("[placeholder copy: Ghostty (pid " + pid + ") did not answer: " + ObjC.unwrap(failure[0].localizedDescription) + "]");
64
+ throw new Error("Ghostty (pid " + pid + ") did not answer: " + ObjC.unwrap(failure[0].localizedDescription) );
65
65
  }
66
66
  const number = reply.paramDescriptorForKeyword(code("errn"));
67
67
  if (!number.isNil()) {
68
68
  const message = reply.paramDescriptorForKeyword(code("errs"));
69
69
  const detail = message.isNil() ? "" : ": " + ObjC.unwrap(message.stringValue);
70
- const error = new Error("[placeholder copy: Ghostty (pid " + pid + ") refused the request (" + number.int32Value + ")" + detail + "]");
70
+ const error = new Error("Ghostty (pid " + pid + ") refused the request (" + number.int32Value + ")" + detail );
71
71
  error.code = number.int32Value;
72
72
  throw error;
73
73
  }
@@ -107,9 +107,9 @@ const ghostty = (env, run) => {
107
107
  if (running.length === 1)
108
108
  return running[0];
109
109
  if (running.length === 0) {
110
- throw new Error("[placeholder copy: Ghostty is not running, so there is no window to script]");
110
+ throw new Error("Ghostty is not running, so there is no window to script");
111
111
  }
112
- throw new Error(`[placeholder copy: ${running.length} Ghostty processes are running (pids ${running.join(", ")}) and this shell is not inside any of them, so we cannot tell which one to script]`);
112
+ throw new Error(`${running.length} Ghostty processes are running (pids ${running.join(", ")}) and this shell is not inside any of them, so we cannot tell which one to script`);
113
113
  }
114
114
  async function ghosttyCommand(command, args, tty = null) {
115
115
  return osascript(ghostty_script_1.GHOSTTY_SCRIPT, [String(await ownerPid(tty)), command, ...args]);
@@ -7,7 +7,7 @@ const CSS_CURSORS = new Set([
7
7
  "no-drop", "not-allowed", "grab", "grabbing", "zoom-in", "zoom-out",
8
8
  "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize",
9
9
  "sw-resize", "w-resize", "ns-resize", "ew-resize", "nesw-resize",
10
- "nwse-resize", "col-resize", "row-resize",
10
+ "nwse-resize",
11
11
  ]);
12
12
  function cursorShapeFor(type) {
13
13
  // chromium's "pointer" is the plain arrow; its css pointer is "hand"
@@ -15,6 +15,11 @@ function cursorShapeFor(type) {
15
15
  return "pointer";
16
16
  if (CSS_CURSORS.has(type))
17
17
  return type;
18
+ // kitty's pointer shape protocol has no col/row-resize; the axis arrows mean the same
19
+ if (type === "col-resize")
20
+ return "ew-resize";
21
+ if (type === "row-resize")
22
+ return "ns-resize";
18
23
  if (type === "nodrop")
19
24
  return "no-drop";
20
25
  if (type.endsWith("-panning"))
package/dist/webview.js CHANGED
@@ -93,8 +93,8 @@ exports.WebView = (0, react_1.forwardRef)(function WebView(props, ref) {
93
93
  const h = hover.current;
94
94
  const shape = h.divider
95
95
  ? dockRef.current === "bottom"
96
- ? "row-resize"
97
- : "col-resize"
96
+ ? "ns-resize"
97
+ : "ew-resize"
98
98
  : h.devtools
99
99
  ? host?.devtools?.cursorShape ?? "default"
100
100
  : host?.popup
@@ -361,7 +361,7 @@ exports.WebView = (0, react_1.forwardRef)(function WebView(props, ref) {
361
361
  const host = () => {
362
362
  const current = hostRef.current;
363
363
  if (!current) {
364
- throw new Error("[placeholder copy: this WebView has already been unmounted]");
364
+ throw new Error("this WebView has already been unmounted]");
365
365
  }
366
366
  return current;
367
367
  };
@@ -430,7 +430,7 @@ exports.WebView = (0, react_1.forwardRef)(function WebView(props, ref) {
430
430
  width: page.width,
431
431
  height: page.height,
432
432
  cornerRadius: props.style?.cornerRadius,
433
- background: theme.bg,
433
+ background: props.browserWindowOptions?.transparent ? undefined : theme.bg,
434
434
  opaque: !props.browserWindowOptions?.transparent,
435
435
  }, onPointer: (event) => {
436
436
  if (menuRef.current) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zenbu-labs/pixel",
3
- "version": "0.0.21",
4
- "description": "[placeholder copy: Electron apps that render into your terminal pane]",
3
+ "version": "0.0.23",
4
+ "description": "Build graphical applications in the terminal",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -58,10 +58,10 @@
58
58
  "react": "^18.3.1"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@zenbu-labs/pixel-native-darwin-arm64": "0.0.21",
62
- "@zenbu-labs/pixel-native-darwin-x64": "0.0.21",
63
- "@zenbu-labs/pixel-native-linux-arm64": "0.0.21",
64
- "@zenbu-labs/pixel-native-linux-x64": "0.0.21"
61
+ "@zenbu-labs/pixel-native-darwin-arm64": "0.0.23",
62
+ "@zenbu-labs/pixel-native-darwin-x64": "0.0.23",
63
+ "@zenbu-labs/pixel-native-linux-arm64": "0.0.23",
64
+ "@zenbu-labs/pixel-native-linux-x64": "0.0.23"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.0.0",
@@ -79,8 +79,8 @@ if (!shasums) {
79
79
  const error = lastError;
80
80
  if (skipBinary && fs.existsSync(typesFile)) process.exit(0);
81
81
  process.stderr.write(
82
- `pixel: [placeholder copy: no patched electron v${version} is published (${error.message}). ` +
83
- `This release of pixel is built against electron ${version}; a newer pixel may be needed.]\n`,
82
+ `pixel: no patched electron v${version} is published (${error.message}). ` +
83
+ `This release of pixel is built against electron ${version}; a newer pixel may be needed.\n`,
84
84
  );
85
85
  process.exit(1);
86
86
  }