add terminal

This commit is contained in:
Arjun 2026-06-12 22:37:07 +05:30
parent e255420fb2
commit b6c3d11e7a
12 changed files with 463 additions and 11 deletions

View file

@ -11,6 +11,9 @@
import * as esbuild from 'esbuild';
import { readFile } from 'node:fs/promises';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// In CommonJS, import.meta.url doesn't exist. We need to polyfill it.
// The banner defines __import_meta_url at the top of the bundle,
@ -24,7 +27,11 @@ await esbuild.build({
platform: 'node',
target: 'node20',
outfile: './.package/dist/main.cjs',
external: ['electron'], // Provided by Electron runtime
// electron is provided by the runtime. node-pty is a NATIVE module: it can't
// be inlined (its loader requires .node binaries + a spawn-helper relative to
// its own package dir), so it stays external and is copied into
// .package/node_modules below, where require() from dist/main.cjs finds it.
external: ['electron', 'node-pty'],
// Use CommonJS format - many dependencies use require() which doesn't work
// well with esbuild's ESM shim. CJS handles dynamic requires natively.
format: 'cjs',
@ -42,4 +49,23 @@ await esbuild.build({
},
});
// Ship node-pty next to the bundle. Resolve through pnpm's symlink to the real
// package dir and copy only what's needed at runtime (compiled JS + prebuilt
// binaries). The macOS spawn-helper must be executable — pnpm extraction drops
// the bit, and a non-executable helper makes every PTY spawn fail.
const here = path.dirname(fileURLToPath(import.meta.url));
const ptySrc = fs.realpathSync(path.join(here, 'node_modules', 'node-pty'));
const ptyDest = path.join(here, '.package', 'node_modules', 'node-pty');
fs.rmSync(ptyDest, { recursive: true, force: true });
fs.mkdirSync(ptyDest, { recursive: true });
for (const item of ['package.json', 'lib', 'prebuilds']) {
fs.cpSync(path.join(ptySrc, item), path.join(ptyDest, item), { recursive: true, dereference: true });
}
const prebuildsDir = path.join(ptyDest, 'prebuilds');
for (const dir of fs.readdirSync(prebuildsDir)) {
const helper = path.join(prebuildsDir, dir, 'spawn-helper');
if (fs.existsSync(helper)) fs.chmodSync(helper, 0o755);
}
console.log('✅ node-pty staged in .package/node_modules');
console.log('✅ Main process bundled to .package/dist-bundle/main.js');

View file

@ -32,10 +32,12 @@ module.exports = {
// Since we bundle everything with esbuild, we don't need node_modules at all.
// These settings prevent Forge's dependency walker (flora-colossus) from trying
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
// Regexes are ANCHORED to the app root: .package/node_modules (where
// bundle.mjs stages the native node-pty module) must survive packaging.
prune: false,
ignore: [
/src\//,
/node_modules\//,
/^\/src\//,
/^\/node_modules\//,
/.gitignore/,
/bundle\.mjs/,
/tsconfig.json/,

View file

@ -21,6 +21,7 @@
"electron-squirrel-startup": "^1.0.1",
"html-to-docx": "^1.8.0",
"mammoth": "^1.11.0",
"node-pty": "^1.1.0",
"papaparse": "^5.5.3",
"pdf-parse": "^2.4.5",
"update-electron-app": "^3.1.2",

View file

@ -40,6 +40,7 @@ import { CodeSessionService } from '@x/core/dist/code-mode/sessions/service.js';
import { CodeSessionStatusTracker } from '@x/core/dist/code-mode/sessions/status-tracker.js';
import * as codeGit from '@x/core/dist/code-mode/git/service.js';
import { readProjectDir, readProjectFile } from '@x/core/dist/code-mode/projects/fs.js';
import { ensureTerminal, writeTerminal, resizeTerminal, disposeTerminal } from './terminal.js';
import type { CodeSession } from '@x/shared/dist/code-sessions.js';
import { invalidateCopilotInstructionsCache } from '@x/core/dist/application/assistant/instructions.js';
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
@ -724,6 +725,7 @@ export function setupIpcHandlers() {
},
'codeSession:delete': async (_event, args) => {
const service = container.resolve<CodeSessionService>('codeSessionService');
disposeTerminal(args.sessionId);
await service.delete(args.sessionId, {
removeWorktree: args.removeWorktree,
deleteBranch: args.deleteBranch,
@ -934,6 +936,21 @@ export function setupIpcHandlers() {
}
return { path: result.filePaths[0] ?? null };
},
'terminal:ensure': async (_event, args) => {
return ensureTerminal(args.id, args.cwd, args.cols, args.rows);
},
'terminal:input': async (_event, args) => {
writeTerminal(args.id, args.data);
return { success: true };
},
'terminal:resize': async (_event, args) => {
resizeTerminal(args.id, args.cols, args.rows);
return { success: true };
},
'terminal:dispose': async (_event, args) => {
disposeTerminal(args.id);
return { success: true };
},
'dialog:openFiles': async (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win!, {

View file

@ -12,6 +12,7 @@ import {
stopServicesWatcher,
stopWorkspaceWatcher
} from "./ipc.js";
import { disposeAllTerminals } from "./terminal.js";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname } from "node:path";
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
@ -428,6 +429,8 @@ app.on("before-quit", () => {
} catch {
// nothing live to dispose
}
// Kill embedded terminal shells.
disposeAllTerminals();
shutdownLocalSites().catch((error) => {
console.error('[LocalSites] Failed to shut down cleanly:', error);
});

View file

@ -0,0 +1,126 @@
import { BrowserWindow } from 'electron';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
// node-pty is a NATIVE module: it stays external to the esbuild bundle and is
// shipped alongside it in .package/node_modules (see bundle.mjs).
import * as pty from 'node-pty';
// One PTY per coding session, kept alive while the app runs so the terminal
// survives pane collapses and session switches. The renderer view re-attaches
// via `terminal:ensure`, which replays the recent backlog.
const BACKLOG_LIMIT = 400_000; // chars (~400KB) of scrollback replay
interface TerminalEntry {
proc: pty.IPty;
cwd: string;
backlog: string;
running: boolean;
}
const terminals = new Map<string, TerminalEntry>();
function broadcast(channel: 'terminal:data' | 'terminal:exit', payload: unknown): void {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed() && win.webContents) {
win.webContents.send(channel, payload);
}
}
}
// pnpm extracts node-pty's prebuilt macOS spawn-helper without its executable
// bit, which makes every spawn fail with "posix_spawnp failed". Repair it once.
let helperFixed = false;
function ensureSpawnHelperExecutable(): void {
if (helperFixed || process.platform === 'win32') return;
helperFixed = true;
try {
const pkgDir = path.dirname(require.resolve('node-pty/package.json'));
const helper = path.join(pkgDir, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper');
if (fs.existsSync(helper)) {
fs.chmodSync(helper, 0o755);
}
} catch {
// best effort — spawn() will surface a real error if this mattered
}
}
function defaultShell(): { file: string; args: string[] } {
if (process.platform === 'win32') {
return { file: 'powershell.exe', args: [] };
}
// Login shell so the user's PATH/aliases match their normal terminal.
return { file: process.env.SHELL || '/bin/zsh', args: ['-l'] };
}
function spawnEntry(id: string, cwd: string, cols: number, rows: number): TerminalEntry {
ensureSpawnHelperExecutable();
const { file, args } = defaultShell();
const proc = pty.spawn(file, args, {
name: 'xterm-256color',
cwd,
cols,
rows,
env: { ...process.env, TERM_PROGRAM: 'rowboat' } as Record<string, string>,
});
const entry: TerminalEntry = { proc, cwd, backlog: '', running: true };
proc.onData((data) => {
entry.backlog = (entry.backlog + data).slice(-BACKLOG_LIMIT);
broadcast('terminal:data', { id, data });
});
proc.onExit(({ exitCode }) => {
entry.running = false;
broadcast('terminal:exit', { id, exitCode });
});
terminals.set(id, entry);
return entry;
}
// Create-or-attach. A cwd change (e.g. the session's worktree was removed) or
// an exited shell gets a fresh PTY; otherwise the live one is reused and the
// caller repaints from the backlog.
export function ensureTerminal(id: string, cwd: string, cols: number, rows: number): { backlog: string; running: boolean } {
const existing = terminals.get(id);
if (existing && existing.running && existing.cwd === cwd) {
existing.proc.resize(cols, rows);
return { backlog: existing.backlog, running: true };
}
if (existing) {
disposeTerminal(id);
}
const fallbackCwd = fs.existsSync(cwd) ? cwd : os.homedir();
const entry = spawnEntry(id, fallbackCwd, cols, rows);
return { backlog: entry.backlog, running: entry.running };
}
export function writeTerminal(id: string, data: string): void {
const entry = terminals.get(id);
if (entry?.running) entry.proc.write(data);
}
export function resizeTerminal(id: string, cols: number, rows: number): void {
const entry = terminals.get(id);
if (entry?.running) {
try {
entry.proc.resize(cols, rows);
} catch {
// resizing a dying pty throws — harmless
}
}
}
export function disposeTerminal(id: string): void {
const entry = terminals.get(id);
if (!entry) return;
terminals.delete(id);
try {
entry.proc.kill();
} catch {
// already gone
}
}
export function disposeAllTerminals(): void {
for (const id of [...terminals.keys()]) disposeTerminal(id);
}

View file

@ -44,6 +44,8 @@
"@tiptap/starter-kit": "3.22.4",
"@x/preload": "workspace:*",
"@x/shared": "workspace:*",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"ai": "^5.0.117",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { Bot, Code2, GitBranch } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Bot, ChevronDown, ChevronUp, Code2, GitBranch, Terminal as TerminalIcon } from 'lucide-react'
import type { CodeSession, CodeSessionStatus } from '@x/shared/src/code-sessions.js'
import type { ApprovalPolicy } from '@x/shared/src/code-mode.js'
import { toast } from 'sonner'
@ -25,6 +25,18 @@ import { useCodeSessions } from './use-code-sessions'
import { SessionRail } from './session-rail'
import { NewSessionDialog } from './new-session-dialog'
import { WorkspacePane } from './workspace-pane'
import { TerminalPane } from './terminal-pane'
const TERMINAL_HEIGHT_STORAGE_KEY = 'x:code-terminal-height'
const TERMINAL_MIN_HEIGHT = 120
const TERMINAL_MAX_HEIGHT = 600
function readStoredTerminalHeight(): number {
if (typeof window === 'undefined') return 240
const raw = Number(window.localStorage.getItem(TERMINAL_HEIGHT_STORAGE_KEY))
if (!Number.isFinite(raw) || raw <= 0) return 240
return Math.min(TERMINAL_MAX_HEIGHT, Math.max(TERMINAL_MIN_HEIGHT, raw))
}
const AGENT_LABEL: Record<string, string> = { claude: 'Claude Code', codex: 'Codex' }
const POLICY_LABEL: Record<ApprovalPolicy, string> = {
@ -56,6 +68,32 @@ export function CodeView({
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
const [newSessionProjectId, setNewSessionProjectId] = useState<string | null>(null)
const [deleteTarget, setDeleteTarget] = useState<CodeSession | null>(null)
const [terminalOpen, setTerminalOpen] = useState(false)
const [terminalHeight, setTerminalHeight] = useState(readStoredTerminalHeight)
const dragStateRef = useRef<{ startY: number; startHeight: number } | null>(null)
useEffect(() => {
window.localStorage.setItem(TERMINAL_HEIGHT_STORAGE_KEY, String(terminalHeight))
}, [terminalHeight])
const handleTerminalDragStart = useCallback((e: React.MouseEvent) => {
e.preventDefault()
dragStateRef.current = { startY: e.clientY, startHeight: terminalHeight }
const onMove = (event: MouseEvent) => {
const drag = dragStateRef.current
if (!drag) return
// Terminal sits at the bottom: dragging up grows it.
const next = drag.startHeight + (drag.startY - event.clientY)
setTerminalHeight(Math.min(TERMINAL_MAX_HEIGHT, Math.max(TERMINAL_MIN_HEIGHT, next)))
}
const onUp = () => {
dragStateRef.current = null
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onUp)
}
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onUp)
}, [terminalHeight])
const selectedSession = sessions.find((s) => s.id === selectedSessionId) ?? null
const selectedStatus = selectedSession ? statusOf(selectedSession.id) : 'idle'
@ -191,6 +229,40 @@ export function CodeView({
onSessionChanged={() => void refresh()}
/>
</div>
{/* Embedded terminal a real shell in the session's directory
(worktree included). The PTY lives in the main process and
survives collapsing this panel. */}
<div className="shrink-0 border-t">
{terminalOpen && (
<div
onMouseDown={handleTerminalDragStart}
className="h-1 cursor-row-resize bg-transparent transition-colors hover:bg-sidebar-border"
/>
)}
<button
type="button"
onClick={() => setTerminalOpen((v) => !v)}
className="flex w-full items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
>
<TerminalIcon className="size-3.5" />
<span className="font-medium">Terminal</span>
{selectedSession.worktree && !selectedSession.worktree.removedAt && (
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px]">worktree</span>
)}
<span className="flex-1" />
{terminalOpen ? <ChevronDown className="size-3.5" /> : <ChevronUp className="size-3.5" />}
</button>
{terminalOpen && (
<div style={{ height: terminalHeight }}>
<TerminalPane
key={selectedSession.id}
terminalId={selectedSession.id}
cwd={selectedSession.cwd}
/>
</div>
)}
</div>
</>
) : (
<div className="flex h-full flex-col items-center justify-center gap-3 text-center">

View file

@ -0,0 +1,110 @@
import { useEffect, useRef } from 'react'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
import { useTheme } from '@/contexts/theme-context'
// xterm color schemes tuned to the app's light/dark backgrounds.
const DARK_THEME = {
background: '#1b1b1f',
foreground: '#d4d4d8',
cursor: '#d4d4d8',
selectionBackground: 'rgba(120, 140, 255, 0.3)',
}
const LIGHT_THEME = {
background: '#ffffff',
foreground: '#27272a',
cursor: '#27272a',
selectionBackground: 'rgba(60, 90, 220, 0.2)',
}
// One embedded terminal view, attached to a per-session PTY in the main
// process. The PTY outlives this component (collapse/switch just detaches);
// on mount we re-attach and repaint from the backlog the main process keeps.
export function TerminalPane({ terminalId, cwd }: { terminalId: string; cwd: string }) {
const { resolvedTheme } = useTheme()
const containerRef = useRef<HTMLDivElement>(null)
const termRef = useRef<Terminal | null>(null)
useEffect(() => {
const container = containerRef.current
if (!container) return
const term = new Terminal({
fontSize: 12,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
cursorBlink: true,
scrollback: 5000,
theme: resolvedTheme === 'dark' ? DARK_THEME : LIGHT_THEME,
})
const fit = new FitAddon()
term.loadAddon(fit)
term.open(container)
fit.fit()
termRef.current = term
let disposed = false
// Attach (or spawn) the PTY at the current size, then repaint history.
void window.ipc.invoke('terminal:ensure', {
id: terminalId,
cwd,
cols: term.cols,
rows: term.rows,
}).then(({ backlog }) => {
if (disposed) return
if (backlog) term.write(backlog)
term.focus()
})
const dataDisposable = term.onData((data) => {
void window.ipc.invoke('terminal:input', { id: terminalId, data })
})
const offData = window.ipc.on('terminal:data', (payload) => {
if (payload.id === terminalId) term.write(payload.data)
})
const offExit = window.ipc.on('terminal:exit', (payload) => {
if (payload.id !== terminalId) return
term.write(`\r\n\x1b[2m[process exited with code ${payload.exitCode} — press Enter to restart]\x1b[0m\r\n`)
})
// Restart the shell on Enter after it exited (ensure() respawns dead PTYs).
const keyDisposable = term.onKey(({ domEvent }) => {
if (domEvent.key !== 'Enter') return
void window.ipc.invoke('terminal:ensure', {
id: terminalId,
cwd,
cols: term.cols,
rows: term.rows,
})
})
const resizeObserver = new ResizeObserver(() => {
if (container.clientHeight === 0) return
fit.fit()
void window.ipc.invoke('terminal:resize', { id: terminalId, cols: term.cols, rows: term.rows })
})
resizeObserver.observe(container)
return () => {
disposed = true
resizeObserver.disconnect()
offData()
offExit()
dataDisposable.dispose()
keyDisposable.dispose()
term.dispose()
termRef.current = null
}
// The PTY is keyed by terminalId; cwd changes (worktree cleanup) respawn via ensure.
}, [terminalId, cwd])
// Live theme switches restyle the existing terminal without a respawn.
useEffect(() => {
const term = termRef.current
if (term) term.options.theme = resolvedTheme === 'dark' ? DARK_THEME : LIGHT_THEME
}, [resolvedTheme])
return <div ref={containerRef} className="h-full w-full overflow-hidden px-2 pt-1" />
}

View file

@ -449,6 +449,21 @@ export function SidebarContentPanel({
const [emailThreads, setEmailThreads] = useState<SidebarEmailThread[]>([])
const [meetings, setMeetings] = useState<UpcomingMeeting[]>([])
const [quickAccessExpanded, setQuickAccessExpanded] = useState(true)
// The Code section only makes sense with a coding agent available — same
// flag the chat composer's code chip uses (auto-on when Claude Code or
// Codex is installed + signed in; explicit toggle in settings wins).
const [codeModeEnabled, setCodeModeEnabled] = useState(false)
useEffect(() => {
const load = () => {
window.ipc.invoke('codeMode:getConfig', null)
.then((r) => setCodeModeEnabled(r.enabled))
.catch(() => setCodeModeEnabled(false))
}
load()
window.addEventListener('code-mode-config-changed', load)
return () => window.removeEventListener('code-mode-config-changed', load)
}, [])
useEffect(() => {
let cancelled = false
@ -837,12 +852,14 @@ export function SidebarContentPanel({
</div>
) : null}
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton isActive={activeNav === 'code'} onClick={onOpenCode}>
<Code2 className="size-4 shrink-0" />
<span className="flex-1 truncate">Code</span>
</SidebarMenuButton>
</SidebarMenuItem>
{codeModeEnabled && (
<SidebarMenuItem>
<SidebarMenuButton isActive={activeNav === 'code'} onClick={onOpenCode}>
<Code2 className="size-4 shrink-0" />
<span className="flex-1 truncate">Code</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton
isActive={activeNav === 'knowledge'}

View file

@ -632,6 +632,51 @@ const ipcSchemas = {
}),
res: z.null(),
},
// ==========================================================================
// Embedded terminal (Code section): one PTY per coding session
// ==========================================================================
// Create-or-attach. Returns the scrollback backlog so a remounted view can
// repaint what happened while it was closed.
'terminal:ensure': {
req: z.object({
id: z.string(),
cwd: z.string(),
cols: z.number().int().positive(),
rows: z.number().int().positive(),
}),
res: z.object({
backlog: z.string(),
running: z.boolean(),
}),
},
'terminal:input': {
req: z.object({
id: z.string(),
data: z.string(),
}),
res: z.object({ success: z.literal(true) }),
},
'terminal:resize': {
req: z.object({
id: z.string(),
cols: z.number().int().positive(),
rows: z.number().int().positive(),
}),
res: z.object({ success: z.literal(true) }),
},
'terminal:dispose': {
req: z.object({ id: z.string() }),
res: z.object({ success: z.literal(true) }),
},
// main → renderer streams
'terminal:data': {
req: z.object({ id: z.string(), data: z.string() }),
res: z.null(),
},
'terminal:exit': {
req: z.object({ id: z.string(), exitCode: z.number() }),
res: z.null(),
},
'granola:setConfig': {
req: z.object({
enabled: z.boolean(),

31
apps/x/pnpm-lock.yaml generated
View file

@ -76,6 +76,9 @@ importers:
mammoth:
specifier: ^1.11.0
version: 1.11.0
node-pty:
specifier: ^1.1.0
version: 1.1.0
papaparse:
specifier: ^5.5.3
version: 5.5.3
@ -249,6 +252,12 @@ importers:
'@x/shared':
specifier: workspace:*
version: link:../../packages/shared
'@xterm/addon-fit':
specifier: ^0.11.0
version: 0.11.0
'@xterm/xterm':
specifier: ^6.0.0
version: 6.0.0
ai:
specifier: ^5.0.117
version: 5.0.117(zod@4.2.1)
@ -4064,6 +4073,12 @@ packages:
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'}
'@xterm/addon-fit@0.11.0':
resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
'@xterm/xterm@6.0.0':
resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==}
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -6659,6 +6674,9 @@ packages:
resolution: {integrity: sha512-sn9Et4N3ynsetj3spsZR729DVlGH6iBG4RiDMV7HEp3guyOW6W3S0unGpLDxT50mXortGUMax/ykUNQXdqc/Xg==}
engines: {node: '>=10'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-api-version@0.2.1:
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
@ -6687,6 +6705,9 @@ packages:
node-html-parser@6.1.13:
resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
node-pty@1.1.0:
resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==}
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@ -12951,6 +12972,10 @@ snapshots:
'@xmldom/xmldom@0.9.10': {}
'@xterm/addon-fit@0.11.0': {}
'@xterm/xterm@6.0.0': {}
'@xtuc/ieee754@1.2.0': {}
'@xtuc/long@4.2.2': {}
@ -16119,6 +16144,8 @@ snapshots:
dependencies:
semver: 7.7.3
node-addon-api@7.1.1: {}
node-api-version@0.2.1:
dependencies:
semver: 7.7.3
@ -16146,6 +16173,10 @@ snapshots:
css-select: 5.2.2
he: 1.2.0
node-pty@1.1.0:
dependencies:
node-addon-api: 7.1.1
node-releases@2.0.27: {}
nopt@6.0.0: