vscode: revamp terminal UX, fix sizing, add cross-platform packaging #6
@@ -6,6 +6,13 @@
|
|||||||
build/bin/
|
build/bin/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# ── VSCode extensions: platform-specific build artifacts (never commit) ────
|
||||||
|
# The Go bridge is compiled per-host by vscode/build.sh and staged into each
|
||||||
|
# extension's bin/ — committing it ships a binary for the wrong OS/arch.
|
||||||
|
vscode/bin/
|
||||||
|
vscode/dist/
|
||||||
|
vscode/*/bin/
|
||||||
|
|
||||||
# ── Build platform resources (tracked — icons, plists, manifests) ─────────
|
# ── Build platform resources (tracked — icons, plists, manifests) ─────────
|
||||||
# build/darwin/, build/windows/, build/linux/ are tracked for icons & metadata
|
# build/darwin/, build/windows/, build/linux/ are tracked for icons & metadata
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -69,6 +69,10 @@ class Bridge {
|
|||||||
if (!fs.existsSync(bridgePath)) {
|
if (!fs.existsSync(bridgePath)) {
|
||||||
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
|
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
|
||||||
}
|
}
|
||||||
|
// VSIX extraction does not always preserve the executable bit, so ensure it.
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ }
|
||||||
|
}
|
||||||
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
|
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true
|
windowsHide: true
|
||||||
@@ -776,12 +780,19 @@ class ToolPanel {
|
|||||||
h1 { margin: 0; font-size: 15px; }
|
h1 { margin: 0; font-size: 15px; }
|
||||||
.title-wrap { min-width: 0; }
|
.title-wrap { min-width: 0; }
|
||||||
.file-state { color: var(--vscode-descriptionForeground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.file-state { color: var(--vscode-descriptionForeground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
header button, .actions button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); }
|
||||||
|
header button:hover, .actions button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
main { display: grid; grid-template-columns: minmax(260px, 360px) 1fr; height: calc(100vh - 58px); }
|
main { display: grid; grid-template-columns: minmax(260px, 360px) 1fr; height: calc(100vh - 58px); }
|
||||||
textarea { width: calc(100% - 24px); height: calc(100% - 66px); margin: 12px; resize: none; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); font-family: var(--vscode-editor-font-family); }
|
.left { border-right: 1px solid var(--vscode-panel-border); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
||||||
.left { border-right: 1px solid var(--vscode-panel-border); }
|
.params-head { padding: 12px 12px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--vscode-descriptionForeground); }
|
||||||
.actions { display: flex; gap: 6px; padding: 0 12px 12px; }
|
textarea { flex: 1; margin: 0 12px; resize: none; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 8px; box-sizing: border-box; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
|
||||||
button { padding: 5px 10px; }
|
textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
|
.actions { display: flex; gap: 6px; padding: 10px 12px; }
|
||||||
|
button { padding: 5px 10px; cursor: pointer; }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); border-color: var(--vscode-button-border, transparent); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
#out { margin: 0; padding: 12px; overflow: auto; height: calc(100% - 24px); box-sizing: border-box; }
|
#out { margin: 0; padding: 12px; overflow: auto; height: calc(100% - 24px); box-sizing: border-box; }
|
||||||
|
.out-empty { color: var(--vscode-descriptionForeground); }
|
||||||
table { border-collapse: collapse; width: 100%; margin: 0 0 12px; }
|
table { border-collapse: collapse; width: 100%; margin: 0 0 12px; }
|
||||||
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 6px; text-align: left; vertical-align: top; font-family: var(--vscode-editor-font-family); font-size: 12px; }
|
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 6px; text-align: left; vertical-align: top; font-family: var(--vscode-editor-font-family); font-size: 12px; }
|
||||||
th { background: var(--vscode-editorWidget-background); position: sticky; top: 0; }
|
th { background: var(--vscode-editorWidget-background); position: sticky; top: 0; }
|
||||||
@@ -800,15 +811,20 @@ class ToolPanel {
|
|||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section class="left">
|
<section class="left">
|
||||||
|
<div class="params-head">Parameters (JSON)</div>
|
||||||
<textarea id="params" spellcheck="false">${escapeHtml(JSON.stringify(JSON.parse(defaults), null, 2))}</textarea>
|
<textarea id="params" spellcheck="false">${escapeHtml(JSON.stringify(JSON.parse(defaults), null, 2))}</textarea>
|
||||||
<div class="actions"><button id="run">Run</button></div>
|
<div class="actions"><button id="run" class="primary">Run</button></div>
|
||||||
</section>
|
</section>
|
||||||
<section id="out">Run the tool to see results.</section>
|
<section id="out"><span class="out-empty">Adjust the parameters and select Run to see results.</span></section>
|
||||||
</main>
|
</main>
|
||||||
<script nonce="${nonce}">
|
<script nonce="${nonce}">
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
const initialFileState = ${initialFileState};
|
const initialFileState = ${initialFileState};
|
||||||
document.getElementById('run').onclick = () => vscode.postMessage({ type: 'run', params: document.getElementById('params').value });
|
const runTool = () => vscode.postMessage({ type: 'run', params: document.getElementById('params').value });
|
||||||
|
document.getElementById('run').onclick = runTool;
|
||||||
|
document.getElementById('params').addEventListener('keydown', event => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); runTool(); }
|
||||||
|
});
|
||||||
document.getElementById('undo').onclick = () => vscode.postMessage({ type: 'undo' });
|
document.getElementById('undo').onclick = () => vscode.postMessage({ type: 'undo' });
|
||||||
document.getElementById('redo').onclick = () => vscode.postMessage({ type: 'redo' });
|
document.getElementById('redo').onclick = () => vscode.postMessage({ type: 'redo' });
|
||||||
document.getElementById('revert').onclick = () => vscode.postMessage({ type: 'revert' });
|
document.getElementById('revert').onclick = () => vscode.postMessage({ type: 'revert' });
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5.5 4.2h8.7l4.3 4.3v11.3H5.5V4.2Z"/>
|
<path d="M13.4 3.5H5a1.5 1.5 0 0 0-1.5 1.5v14A1.5 1.5 0 0 0 5 20.5h5.2"/>
|
||||||
<path d="M14.2 4.2v4.3h4.3"/>
|
<path d="M6.6 7.3h7"/>
|
||||||
<path d="M8.4 8.2h2.2"/>
|
<path d="M6.6 10.4h4.4"/>
|
||||||
<path d="M8.4 11.4h4.9"/>
|
<path d="M6.6 13.5h2.2"/>
|
||||||
<path d="M8.4 14.6h2.9"/>
|
<circle cx="15.3" cy="14.1" r="3.7"/>
|
||||||
<path d="m15.1 14.3 1.5 1.5"/>
|
<path d="m18 16.8 2.8 2.8"/>
|
||||||
<circle cx="13.9" cy="13.1" r="2.1"/>
|
|
||||||
<path d="M8.4 17.8h6.8"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 396 B |
Binary file not shown.
+206
-58
@@ -22,6 +22,10 @@ class Bridge {
|
|||||||
if (!fs.existsSync(bridgePath)) {
|
if (!fs.existsSync(bridgePath)) {
|
||||||
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
|
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
|
||||||
}
|
}
|
||||||
|
// VSIX extraction does not always preserve the executable bit, so ensure it.
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
|
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
@@ -508,8 +512,6 @@ class TerminalPanel {
|
|||||||
await this.bridge.rpc('WriteSession', [this.session.id, msg.data]);
|
await this.bridge.rpc('WriteSession', [this.session.id, msg.data]);
|
||||||
} else if (msg.type === 'resize') {
|
} else if (msg.type === 'resize') {
|
||||||
await this.bridge.rpc('ResizeSession', [this.session.id, msg.cols, msg.rows]);
|
await this.bridge.rpc('ResizeSession', [this.session.id, msg.cols, msg.rows]);
|
||||||
} else if (msg.type === 'sftp') {
|
|
||||||
new SFTPPanel(this.context, this.bridge, this.session);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.panel.webview.postMessage({ type: 'status', text: err.message });
|
this.panel.webview.postMessage({ type: 'status', text: err.message });
|
||||||
@@ -536,94 +538,191 @@ class TerminalPanel {
|
|||||||
const fitJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm-addon-fit.min.js'));
|
const fitJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm-addon-fit.min.js'));
|
||||||
const xtermCss = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm.css'));
|
const xtermCss = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm.css'));
|
||||||
const nonce = String(Date.now());
|
const nonce = String(Date.now());
|
||||||
const sftpStyle = String(this.profile.protocol || this.session.protocol || '').toLowerCase() === 'ssh' ? '' : 'display:none';
|
const terminalFontFamily = JSON.stringify(this.profile.terminalFontFamily || '');
|
||||||
const terminalFontFamily = JSON.stringify(this.profile.terminalFontFamily || 'Menlo, Monaco, Consolas, "Courier New", monospace');
|
|
||||||
const terminalFontSize = Number(this.profile.terminalFontSize || 13);
|
const terminalFontSize = Number(this.profile.terminalFontSize || 13);
|
||||||
const terminalScrollback = Number(this.profile.terminalScrollback || 10000);
|
const terminalScrollback = Number(this.profile.terminalScrollback || 10000);
|
||||||
|
const terminalCursorStyle = JSON.stringify(this.profile.terminalCursorStyle || 'block');
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src ${webview.cspSource} 'nonce-${nonce}';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource} 'nonce-${nonce}'; font-src ${webview.cspSource};">
|
||||||
<link rel="stylesheet" href="${xtermCss}">
|
<link rel="stylesheet" href="${xtermCss}">
|
||||||
<style>
|
<style>
|
||||||
|
:root { color-scheme: light dark; }
|
||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f1720;
|
background: var(--vscode-terminal-background, var(--vscode-panel-background, var(--vscode-editor-background)));
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
}
|
}
|
||||||
|
/* Full-bleed terminal: occupies 100% of the tab, no chrome stealing space. */
|
||||||
#terminal {
|
#terminal {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100vw;
|
padding: 4px 6px;
|
||||||
height: 100vh;
|
box-sizing: border-box;
|
||||||
background: #0f1720;
|
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
}
|
}
|
||||||
.xterm, .xterm-viewport, .xterm-screen {
|
.xterm { height: 100%; }
|
||||||
width: 100% !important;
|
.xterm-viewport::-webkit-scrollbar { width: 10px; }
|
||||||
height: 100% !important;
|
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace !important;
|
background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, .4));
|
||||||
}
|
}
|
||||||
|
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, .7));
|
||||||
|
}
|
||||||
|
/* Floating, auto-hiding status pill following VSCode badge styling. */
|
||||||
#status {
|
#status {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
right: 10px;
|
right: 12px;
|
||||||
top: 8px;
|
top: 10px;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
color: #a7f3d0;
|
display: inline-flex;
|
||||||
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 11px;
|
||||||
|
font: 11px/1.6 var(--vscode-font-family);
|
||||||
|
color: var(--vscode-badge-foreground);
|
||||||
|
background: var(--vscode-badge-background);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: .9;
|
opacity: 0;
|
||||||
|
transition: opacity .25s ease;
|
||||||
}
|
}
|
||||||
#sftp {
|
#status.show { opacity: 1; }
|
||||||
position: fixed;
|
#status .dot {
|
||||||
left: 10px;
|
width: 7px;
|
||||||
top: 8px;
|
height: 7px;
|
||||||
z-index: 2;
|
border-radius: 50%;
|
||||||
border: 1px solid rgba(148, 163, 184, .35);
|
background: var(--vscode-charts-yellow, #e2c08d);
|
||||||
background: rgba(15, 23, 32, .82);
|
|
||||||
color: #dbeafe;
|
|
||||||
padding: 4px 9px;
|
|
||||||
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
#sftp:hover { background: rgba(30, 41, 59, .95); }
|
#status.ok .dot { background: var(--vscode-charts-green, #89d185); }
|
||||||
|
#status.bad { color: var(--vscode-statusBarItem-errorForeground, #fff); background: var(--vscode-statusBarItem-errorBackground, #c72e2e); }
|
||||||
|
#status.bad .dot { background: currentColor; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="status">connecting</div>
|
<div id="status" class="show"><span class="dot"></span><span id="statusText">Connecting…</span></div>
|
||||||
<button id="sftp" style="${sftpStyle}">SFTP</button>
|
|
||||||
<div id="terminal"></div>
|
<div id="terminal"></div>
|
||||||
<script nonce="${nonce}" src="${xtermJs}"></script>
|
<script nonce="${nonce}" src="${xtermJs}"></script>
|
||||||
<script nonce="${nonce}" src="${fitJs}"></script>
|
<script nonce="${nonce}" src="${fitJs}"></script>
|
||||||
<script nonce="${nonce}">
|
<script nonce="${nonce}">
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
// Resolve a VSCode theme colour from the CSS variables injected into the webview.
|
||||||
|
const rootStyle = getComputedStyle(document.documentElement);
|
||||||
|
function cssVar(name, fallback) {
|
||||||
|
const value = rootStyle.getPropertyValue(name);
|
||||||
|
return value && value.trim() ? value.trim() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = cssVar('--vscode-terminal-background', cssVar('--vscode-editor-background', '#1e1e1e'));
|
||||||
|
const foreground = cssVar('--vscode-terminal-foreground', cssVar('--vscode-editor-foreground', '#d4d4d4'));
|
||||||
|
// Build the xterm palette straight from the active VSCode colour theme.
|
||||||
|
const theme = {
|
||||||
|
background,
|
||||||
|
foreground,
|
||||||
|
cursor: cssVar('--vscode-terminalCursor-foreground', foreground),
|
||||||
|
cursorAccent: cssVar('--vscode-terminalCursor-background', background),
|
||||||
|
selectionBackground: cssVar('--vscode-terminal-selectionBackground', 'rgba(128,128,128,0.35)'),
|
||||||
|
black: cssVar('--vscode-terminal-ansiBlack', '#000000'),
|
||||||
|
red: cssVar('--vscode-terminal-ansiRed', '#cd3131'),
|
||||||
|
green: cssVar('--vscode-terminal-ansiGreen', '#0dbc79'),
|
||||||
|
yellow: cssVar('--vscode-terminal-ansiYellow', '#e5e510'),
|
||||||
|
blue: cssVar('--vscode-terminal-ansiBlue', '#2472c8'),
|
||||||
|
magenta: cssVar('--vscode-terminal-ansiMagenta', '#bc3fbc'),
|
||||||
|
cyan: cssVar('--vscode-terminal-ansiCyan', '#11a8cd'),
|
||||||
|
white: cssVar('--vscode-terminal-ansiWhite', '#e5e5e5'),
|
||||||
|
brightBlack: cssVar('--vscode-terminal-ansiBrightBlack', '#666666'),
|
||||||
|
brightRed: cssVar('--vscode-terminal-ansiBrightRed', '#f14c4c'),
|
||||||
|
brightGreen: cssVar('--vscode-terminal-ansiBrightGreen', '#23d18b'),
|
||||||
|
brightYellow: cssVar('--vscode-terminal-ansiBrightYellow', '#f5f543'),
|
||||||
|
brightBlue: cssVar('--vscode-terminal-ansiBrightBlue', '#3b8eea'),
|
||||||
|
brightMagenta: cssVar('--vscode-terminal-ansiBrightMagenta', '#d670d6'),
|
||||||
|
brightCyan: cssVar('--vscode-terminal-ansiBrightCyan', '#29b8db'),
|
||||||
|
brightWhite: cssVar('--vscode-terminal-ansiBrightWhite', '#e5e5e5')
|
||||||
|
};
|
||||||
|
document.body.style.background = background;
|
||||||
|
|
||||||
|
// Prefer a profile override, otherwise inherit VSCode's terminal/editor monospace font.
|
||||||
|
const fontFamily = ${terminalFontFamily}
|
||||||
|
|| cssVar('--vscode-terminal-font-family', '')
|
||||||
|
|| cssVar('--vscode-editor-font-family', 'Menlo, Monaco, "Cascadia Code", "Courier New", monospace');
|
||||||
|
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
|
cursorStyle: ${terminalCursorStyle},
|
||||||
scrollback: ${terminalScrollback},
|
scrollback: ${terminalScrollback},
|
||||||
fontFamily: ${terminalFontFamily},
|
fontFamily,
|
||||||
fontSize: ${terminalFontSize},
|
fontSize: ${terminalFontSize},
|
||||||
theme: { background: '#0f1720' }
|
lineHeight: 1.1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
allowProposedApi: true,
|
||||||
|
theme
|
||||||
});
|
});
|
||||||
const fit = new FitAddon.FitAddon();
|
const fit = new FitAddon.FitAddon();
|
||||||
term.loadAddon(fit);
|
term.loadAddon(fit);
|
||||||
term.open(document.getElementById('terminal'));
|
const container = document.getElementById('terminal');
|
||||||
fit.fit();
|
term.open(container);
|
||||||
term.onData(data => vscode.postMessage({ type: 'input', data: btoa(unescape(encodeURIComponent(data))) }));
|
|
||||||
document.getElementById('sftp').onclick = () => vscode.postMessage({ type: 'sftp' });
|
let lastCols = 0;
|
||||||
function resize() {
|
let lastRows = 0;
|
||||||
fit.fit();
|
// Push the current terminal geometry to the backend PTY. Pass force=true to
|
||||||
|
// re-send even when the dimensions are unchanged (needed right after the
|
||||||
|
// connection completes, since the remote PTY is created at a default size).
|
||||||
|
function resize(force) {
|
||||||
|
try { fit.fit(); } catch (_) { return; }
|
||||||
|
if (force || term.cols !== lastCols || term.rows !== lastRows) {
|
||||||
|
lastCols = term.cols;
|
||||||
|
lastRows = term.rows;
|
||||||
vscode.postMessage({ type: 'resize', cols: term.cols, rows: term.rows });
|
vscode.postMessage({ type: 'resize', cols: term.cols, rows: term.rows });
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', resize);
|
}
|
||||||
setTimeout(resize, 50);
|
|
||||||
|
// Refit whenever the tab/container changes size — handles splits, panel drags
|
||||||
|
// and the editor being shown after retainContextWhenHidden.
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
new ResizeObserver(() => resize()).observe(container);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', () => resize());
|
||||||
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) resize(); });
|
||||||
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => resize());
|
||||||
|
requestAnimationFrame(() => resize());
|
||||||
|
setTimeout(() => resize(), 60);
|
||||||
|
|
||||||
|
term.focus();
|
||||||
|
term.onData(data => vscode.postMessage({ type: 'input', data: btoa(unescape(encodeURIComponent(data))) }));
|
||||||
|
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
let hideTimer = null;
|
||||||
|
function setStatus(text) {
|
||||||
|
const value = String(text || '').trim();
|
||||||
|
if (!value) return;
|
||||||
|
const lower = value.toLowerCase();
|
||||||
|
const connected = /connect(ed)?$|ready|open|online/.test(lower) && !/dis|fail|error|refus|lost|closed/.test(lower);
|
||||||
|
const failed = /dis|fail|error|refus|lost|closed|timeout/.test(lower);
|
||||||
|
statusText.textContent = value.charAt(0).toUpperCase() + value.slice(1);
|
||||||
|
statusEl.classList.toggle('ok', connected && !failed);
|
||||||
|
statusEl.classList.toggle('bad', failed);
|
||||||
|
statusEl.classList.add('show');
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
// Healthy states fade out so they don't sit on top of the terminal; problems stay visible.
|
||||||
|
if (connected && !failed) hideTimer = setTimeout(() => statusEl.classList.remove('show'), 1600);
|
||||||
|
return connected && !failed;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const msg = event.data || {};
|
const msg = event.data || {};
|
||||||
if (msg.type === 'data') term.write(Uint8Array.from(atob(msg.data), c => c.charCodeAt(0)));
|
if (msg.type === 'data') term.write(Uint8Array.from(atob(msg.data), c => c.charCodeAt(0)));
|
||||||
if (msg.type === 'status') document.getElementById('status').textContent = msg.text || '';
|
if (msg.type === 'status') {
|
||||||
|
// The remote PTY is allocated at a default size before our first resize
|
||||||
|
// can land, so re-send the real geometry once the session is connected.
|
||||||
|
if (setStatus(msg.text)) { resize(true); setTimeout(() => resize(true), 250); }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -699,13 +798,18 @@ class RawPanel {
|
|||||||
.send { color: var(--vscode-terminal-ansiGreen); }
|
.send { color: var(--vscode-terminal-ansiGreen); }
|
||||||
.recv { color: var(--vscode-terminal-ansiCyan); }
|
.recv { color: var(--vscode-terminal-ansiCyan); }
|
||||||
.hex { color: var(--vscode-descriptionForeground); }
|
.hex { color: var(--vscode-descriptionForeground); }
|
||||||
|
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
|
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<select id="format"><option value="text">Text</option><option value="hex">Hex</option><option value="escaped">Escaped</option></select>
|
<select id="format"><option value="text">Text</option><option value="hex">Hex</option><option value="escaped">Escaped</option></select>
|
||||||
<input id="input" autocomplete="off" spellcheck="false">
|
<input id="input" autocomplete="off" spellcheck="false">
|
||||||
<button id="send">Send</button>
|
<button id="send" class="primary">Send</button>
|
||||||
<button id="clear">Clear</button>
|
<button id="clear">Clear</button>
|
||||||
<button id="save">Save</button>
|
<button id="save">Save</button>
|
||||||
<span id="status">connecting</span>
|
<span id="status">connecting</span>
|
||||||
@@ -858,6 +962,12 @@ class GitHubPanel {
|
|||||||
.repo { border: 1px solid var(--vscode-panel-border); padding: 8px; }
|
.repo { border: 1px solid var(--vscode-panel-border); padding: 8px; }
|
||||||
.repo h3 { margin: 0 0 4px; font-size: 14px; }
|
.repo h3 { margin: 0 0 4px; font-size: 14px; }
|
||||||
.meta { color: var(--vscode-descriptionForeground); font-size: 12px; }
|
.meta { color: var(--vscode-descriptionForeground); font-size: 12px; }
|
||||||
|
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
|
input:focus, select:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
|
.repo p { display: flex; gap: 6px; margin: 8px 0 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -951,13 +1061,18 @@ class IRCPanel {
|
|||||||
#target { width: 140px; }
|
#target { width: 140px; }
|
||||||
#message { flex: 1; }
|
#message { flex: 1; }
|
||||||
#log { height: calc(100vh - 92px); overflow: auto; padding: 8px; font-family: var(--vscode-editor-font-family), monospace; white-space: pre-wrap; }
|
#log { height: calc(100vh - 92px); overflow: auto; padding: 8px; font-family: var(--vscode-editor-font-family), monospace; white-space: pre-wrap; }
|
||||||
.status { color: var(--vscode-descriptionForeground); }
|
.status { color: var(--vscode-descriptionForeground); align-self: center; }
|
||||||
|
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); padding: 5px 10px; cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
|
input:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header><input id="room" placeholder="#room"><button id="join">Join</button><button id="part">Part</button><button id="list">List</button><span id="status" class="status">connecting</span></header>
|
<header><input id="room" placeholder="#room"><button id="join">Join</button><button id="part">Part</button><button id="list">List</button><span id="status" class="status">connecting</span></header>
|
||||||
<main id="log"></main>
|
<main id="log"></main>
|
||||||
<footer><input id="target" value="server"><input id="message" autocomplete="off" spellcheck="false"><button id="send">Send</button></footer>
|
<footer><input id="target" value="server"><input id="message" autocomplete="off" spellcheck="false"><button id="send" class="primary">Send</button></footer>
|
||||||
<script nonce="${nonce}">
|
<script nonce="${nonce}">
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
const log = document.getElementById('log');
|
const log = document.getElementById('log');
|
||||||
@@ -1041,7 +1156,8 @@ class VNCPanel {
|
|||||||
html, body { margin: 0; width: 100%; height: 100%; color: var(--vscode-foreground); background: #111; font: var(--vscode-font-size) var(--vscode-font-family); }
|
html, body { margin: 0; width: 100%; height: 100%; color: var(--vscode-foreground); background: #111; font: var(--vscode-font-size) var(--vscode-font-family); }
|
||||||
#toolbar { display: flex; gap: 8px; align-items: center; height: 36px; padding: 0 8px; box-sizing: border-box; background: var(--vscode-sideBar-background); }
|
#toolbar { display: flex; gap: 8px; align-items: center; height: 36px; padding: 0 8px; box-sizing: border-box; background: var(--vscode-sideBar-background); }
|
||||||
#screen { height: calc(100% - 36px); overflow: auto; display: flex; align-items: center; justify-content: center; }
|
#screen { height: calc(100% - 36px); overflow: auto; display: flex; align-items: center; justify-content: center; }
|
||||||
button { padding: 4px 10px; }
|
button { padding: 4px 10px; border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
#status { margin-right: auto; color: var(--vscode-descriptionForeground); }
|
#status { margin-right: auto; color: var(--vscode-descriptionForeground); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -1161,8 +1277,12 @@ class ProfileEditorPanel {
|
|||||||
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
|
||||||
label { display: grid; gap: 4px; }
|
label { display: grid; gap: 4px; }
|
||||||
input, select, textarea { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; box-sizing: border-box; }
|
input, select, textarea { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; box-sizing: border-box; }
|
||||||
|
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
textarea { width: 100%; min-height: 240px; font-family: var(--vscode-editor-font-family), monospace; }
|
textarea { width: 100%; min-height: 240px; font-family: var(--vscode-editor-font-family), monospace; }
|
||||||
button { padding: 5px 12px; }
|
button { padding: 5px 12px; border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
.hint, #status { color: var(--vscode-descriptionForeground); }
|
.hint, #status { color: var(--vscode-descriptionForeground); }
|
||||||
.danger { color: var(--vscode-errorForeground); }
|
.danger { color: var(--vscode-errorForeground); }
|
||||||
</style>
|
</style>
|
||||||
@@ -1171,7 +1291,7 @@ class ProfileEditorPanel {
|
|||||||
<header>
|
<header>
|
||||||
<h1>${this.isNew ? 'New Connection Profile' : 'Edit Connection Profile'}</h1>
|
<h1>${this.isNew ? 'New Connection Profile' : 'Edit Connection Profile'}</h1>
|
||||||
<button id="test">Test Host</button>
|
<button id="test">Test Host</button>
|
||||||
<button id="save">Save</button>
|
<button id="save" class="primary">Save</button>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
@@ -1399,14 +1519,20 @@ class SettingsPanel {
|
|||||||
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
|
||||||
label { display: grid; gap: 4px; }
|
label { display: grid; gap: 4px; }
|
||||||
input, select { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; }
|
input, select { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; }
|
||||||
|
input:focus, select:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
|
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); padding: 5px 12px; cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
table { width: 100%; border-collapse: collapse; }
|
table { width: 100%; border-collapse: collapse; }
|
||||||
td, th { border: 1px solid var(--vscode-panel-border); padding: 5px 7px; text-align: left; }
|
td, th { border: 1px solid var(--vscode-panel-border); padding: 5px 7px; text-align: left; }
|
||||||
|
td button { padding: 2px 8px; }
|
||||||
.hint, #status { color: var(--vscode-descriptionForeground); }
|
.hint, #status { color: var(--vscode-descriptionForeground); }
|
||||||
.danger { color: var(--vscode-errorForeground); }
|
.danger { color: var(--vscode-errorForeground); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header><h1>Nexus Settings</h1><button id="save">Save</button></header>
|
<header><h1>Nexus Settings</h1><button id="save" class="primary">Save</button></header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
@@ -1520,12 +1646,27 @@ class SFTPPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
// A freshly opened SFTP session may be created before the underlying SSH
|
||||||
|
// connection finishes. SFTPPwd resolves realpath(".") which the server maps
|
||||||
|
// to the user's home directory, so retry until the session is ready and land
|
||||||
|
// the browser in the home folder instead of falling back to the filesystem root.
|
||||||
|
this.panel.webview.postMessage({ type: 'status', text: 'Connecting…' });
|
||||||
|
let home = '';
|
||||||
|
for (let attempt = 0; attempt < 30 && !home; attempt++) {
|
||||||
try {
|
try {
|
||||||
this.cwd = await this.bridge.rpc('SFTPPwd', [this.session.id]);
|
const pwd = await this.bridge.rpc('SFTPPwd', [this.session.id]);
|
||||||
|
if (pwd && pwd.startsWith('/')) { home = pwd; break; }
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
this.cwd = '/';
|
// SSH session is still connecting; back off and retry.
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
this.cwd = home || '/';
|
||||||
|
try {
|
||||||
|
await this.list(this.cwd);
|
||||||
|
} catch (err) {
|
||||||
|
this.panel.webview.postMessage({ type: 'error', error: err.message || String(err) });
|
||||||
}
|
}
|
||||||
await this.list(this.cwd || '/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async list(remotePath) {
|
async list(remotePath) {
|
||||||
@@ -1595,6 +1736,12 @@ class SFTPPanel {
|
|||||||
.muted, #status { color: var(--vscode-descriptionForeground); }
|
.muted, #status { color: var(--vscode-descriptionForeground); }
|
||||||
.danger { color: var(--vscode-errorForeground); }
|
.danger { color: var(--vscode-errorForeground); }
|
||||||
main { height: calc(100vh - 43px); overflow: auto; }
|
main { height: calc(100vh - 43px); overflow: auto; }
|
||||||
|
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
|
||||||
|
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
|
||||||
|
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
|
||||||
|
td button { padding: 2px 8px; }
|
||||||
|
#path:focus { outline: none; border-color: var(--vscode-focusBorder); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1662,6 +1809,7 @@ class SFTPPanel {
|
|||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const msg = event.data || {};
|
const msg = event.data || {};
|
||||||
if (msg.type === 'entries') { cwd = msg.cwd || '/'; entries = msg.entries || []; render(); }
|
if (msg.type === 'entries') { cwd = msg.cwd || '/'; entries = msg.entries || []; render(); }
|
||||||
|
if (msg.type === 'status') setStatus(msg.text);
|
||||||
if (msg.type === 'error') setStatus(msg.error, true);
|
if (msg.type === 'error') setStatus(msg.error, true);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M12 2.8 20.2 7.4v9.2L12 21.2l-8.2-4.6V7.4L12 2.8Z"/>
|
<path d="M12 2.5 20.5 7.25v9.5L12 21.5 3.5 16.75v-9.5L12 2.5Z"/>
|
||||||
<path d="M7.2 8.6h9.6a1.6 1.6 0 0 1 1.6 1.6v5.6a1.6 1.6 0 0 1-1.6 1.6H7.2a1.6 1.6 0 0 1-1.6-1.6v-5.6a1.6 1.6 0 0 1 1.6-1.6Z"/>
|
<path d="m8.5 9.7 2.8 2.3-2.8 2.3"/>
|
||||||
<path d="m8.3 11 2 2-2 2"/>
|
<path d="M12.6 15h3.1"/>
|
||||||
<path d="M12 15h3.7"/>
|
|
||||||
<path d="M12 2.8v3.1"/>
|
|
||||||
<path d="M4.1 7.6 6.8 9"/>
|
|
||||||
<path d="m17.2 9 2.7-1.4"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 301 B |
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build platform-specific VSIX packages for the Nexus and Leviathan VSCode
|
||||||
|
# extensions, covering Windows, macOS and Linux on both x64 and arm64.
|
||||||
|
#
|
||||||
|
# Each extension embeds a Go "bridge" binary that is OS/arch specific, so we
|
||||||
|
# produce one VSIX per target using VSCode's platform-specific extension
|
||||||
|
# mechanism (`vsce package --target <target>`). VSCode then installs the VSIX
|
||||||
|
# matching the user's platform automatically (e.g. from the Marketplace, or via
|
||||||
|
# `code --install-extension <file>.vsix`).
|
||||||
|
#
|
||||||
|
# The bridge is pure Go (CGO_ENABLED=0), so every target cross-compiles from any
|
||||||
|
# host with just the Go toolchain — no C compilers or per-OS toolchains needed.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# vscode/package-all.sh # build every target
|
||||||
|
# vscode/package-all.sh linux-x64 ... # build only the named vsce target(s)
|
||||||
|
#
|
||||||
|
# Optional macOS code signing + notarization (darwin targets only):
|
||||||
|
# Runs only on a macOS host with Xcode command line tools, and only when the
|
||||||
|
# relevant env vars are set — otherwise it is skipped with a warning and the
|
||||||
|
# darwin VSIXs are produced unsigned (fine for local use, blocked by Gatekeeper
|
||||||
|
# when distributed to other machines).
|
||||||
|
#
|
||||||
|
# MACOS_SIGN_IDENTITY "Developer ID Application: Name (TEAMID)" — enables signing.
|
||||||
|
#
|
||||||
|
# Then, to also notarize, provide EITHER a stored notarytool profile:
|
||||||
|
# MACOS_NOTARY_PROFILE profile name from `xcrun notarytool store-credentials`
|
||||||
|
# OR an Apple ID + app-specific password:
|
||||||
|
# MACOS_NOTARY_APPLE_ID Apple ID email
|
||||||
|
# MACOS_NOTARY_TEAM_ID Apple Developer team id
|
||||||
|
# MACOS_NOTARY_PASSWORD app-specific password
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# MACOS_SIGN_IDENTITY="Developer ID Application: Andy Dixon (AB12CD34EF)" \
|
||||||
|
# MACOS_NOTARY_PROFILE="nexus-notary" \
|
||||||
|
# vscode/package-all.sh darwin-x64 darwin-arm64
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
VSCODE_DIR="$ROOT_DIR/vscode"
|
||||||
|
OUT_DIR="$VSCODE_DIR/dist"
|
||||||
|
EXTENSIONS=(nexus leviathan)
|
||||||
|
|
||||||
|
# vsce target -> GOOS/GOARCH
|
||||||
|
ALL_TARGETS=(
|
||||||
|
"win32-x64:windows/amd64"
|
||||||
|
"win32-arm64:windows/arm64"
|
||||||
|
"darwin-x64:darwin/amd64"
|
||||||
|
"darwin-arm64:darwin/arm64"
|
||||||
|
"linux-x64:linux/amd64"
|
||||||
|
"linux-arm64:linux/arm64"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional positional filter: only build the vsce targets passed as arguments.
|
||||||
|
SELECTED=("$@")
|
||||||
|
target_selected() {
|
||||||
|
[ ${#SELECTED[@]} -eq 0 ] && return 0
|
||||||
|
local want="$1"
|
||||||
|
for t in "${SELECTED[@]}"; do [ "$t" = "$want" ] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_vsce() {
|
||||||
|
if command -v vsce >/dev/null 2>&1; then
|
||||||
|
vsce "$@"
|
||||||
|
else
|
||||||
|
npx --yes @vscode/vsce "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Codesign (hardened runtime) and optionally notarize a single darwin bridge
|
||||||
|
# binary. Opt-in via env vars and skipped with a warning when it can't run, so
|
||||||
|
# the cross-platform build still succeeds on non-macOS hosts. Signing the staged
|
||||||
|
# binary before it is copied into each extension means both VSIXs inherit the
|
||||||
|
# signature (and notarization, which is keyed to the binary's hash).
|
||||||
|
macos_sign() {
|
||||||
|
local bin="$1"
|
||||||
|
|
||||||
|
if [ -z "${MACOS_SIGN_IDENTITY:-}" ]; then
|
||||||
|
echo "[vscode] (skip signing: MACOS_SIGN_IDENTITY not set — shipping unsigned)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$(uname -s)" != "Darwin" ] || ! command -v codesign >/dev/null 2>&1; then
|
||||||
|
echo "[vscode] WARNING: cannot sign darwin bridge here (needs macOS + Xcode tools); shipping unsigned"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[vscode] codesign ($MACOS_SIGN_IDENTITY)"
|
||||||
|
codesign --force --options runtime --timestamp --sign "$MACOS_SIGN_IDENTITY" "$bin"
|
||||||
|
codesign --verify --strict --verbose=2 "$bin"
|
||||||
|
|
||||||
|
# Notarize only when credentials are provided.
|
||||||
|
local notary_args=()
|
||||||
|
if [ -n "${MACOS_NOTARY_PROFILE:-}" ]; then
|
||||||
|
notary_args=(--keychain-profile "$MACOS_NOTARY_PROFILE")
|
||||||
|
elif [ -n "${MACOS_NOTARY_APPLE_ID:-}" ] && [ -n "${MACOS_NOTARY_TEAM_ID:-}" ] && [ -n "${MACOS_NOTARY_PASSWORD:-}" ]; then
|
||||||
|
notary_args=(--apple-id "$MACOS_NOTARY_APPLE_ID" --team-id "$MACOS_NOTARY_TEAM_ID" --password "$MACOS_NOTARY_PASSWORD")
|
||||||
|
else
|
||||||
|
echo "[vscode] (skip notarization: no notary credentials set)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! command -v xcrun >/dev/null 2>&1; then
|
||||||
|
echo "[vscode] WARNING: xcrun not found; cannot notarize"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[vscode] notarize (submitting to Apple, waiting for result…)"
|
||||||
|
local zip="${bin}.notarize.zip"
|
||||||
|
# notarytool requires an archive; a bare Mach-O binary cannot be submitted directly.
|
||||||
|
ditto -c -k --keepParent "$bin" "$zip"
|
||||||
|
xcrun notarytool submit "$zip" "${notary_args[@]}" --wait
|
||||||
|
rm -f "$zip"
|
||||||
|
# A standalone executable cannot be stapled (stapler only supports app/dmg/pkg);
|
||||||
|
# Gatekeeper verifies the notarization online via the now-registered hash, which
|
||||||
|
# matches the exact bytes embedded in every VSIX.
|
||||||
|
echo "[vscode] notarized (online check; standalone binaries are not stapleable)"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[vscode] Building frontend assets (platform independent)"
|
||||||
|
if [ ! -d "$ROOT_DIR/frontend/node_modules" ]; then
|
||||||
|
npm --prefix "$ROOT_DIR/frontend" install
|
||||||
|
fi
|
||||||
|
npm --prefix "$ROOT_DIR/frontend" run build
|
||||||
|
|
||||||
|
echo "[vscode] Staging shared media assets"
|
||||||
|
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
|
||||||
|
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
|
||||||
|
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
|
||||||
|
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
|
||||||
|
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
VERSION="$(node -p "require('$VSCODE_DIR/nexus/package.json').version")"
|
||||||
|
|
||||||
|
for entry in "${ALL_TARGETS[@]}"; do
|
||||||
|
VSCE_TARGET="${entry%%:*}"
|
||||||
|
GOPLATFORM="${entry##*:}"
|
||||||
|
GOOS="${GOPLATFORM%%/*}"
|
||||||
|
GOARCH="${GOPLATFORM##*/}"
|
||||||
|
|
||||||
|
if ! target_selected "$VSCE_TARGET"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIN_NAME="nexus-vscode-bridge"
|
||||||
|
[ "$GOOS" = "windows" ] && BIN_NAME="nexus-vscode-bridge.exe"
|
||||||
|
|
||||||
|
echo "[vscode] Building bridge for $VSCE_TARGET ($GOOS/$GOARCH)"
|
||||||
|
STAGE_BIN="$VSCODE_DIR/bin/$VSCE_TARGET/$BIN_NAME"
|
||||||
|
mkdir -p "$(dirname "$STAGE_BIN")"
|
||||||
|
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -o "$STAGE_BIN" ./cmd/vscode-bridge
|
||||||
|
|
||||||
|
# Sign + notarize macOS bridges before they are copied into the extensions.
|
||||||
|
if [ "$GOOS" = "darwin" ]; then
|
||||||
|
macos_sign "$STAGE_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for EXT in "${EXTENSIONS[@]}"; do
|
||||||
|
EXT_BIN_DIR="$VSCODE_DIR/$EXT/bin"
|
||||||
|
# Ship exactly one binary per VSIX so the package can't carry a wrong-arch
|
||||||
|
# (or wrong-OS) bridge alongside the right one.
|
||||||
|
rm -rf "$EXT_BIN_DIR"
|
||||||
|
mkdir -p "$EXT_BIN_DIR"
|
||||||
|
cp "$STAGE_BIN" "$EXT_BIN_DIR/$BIN_NAME"
|
||||||
|
chmod 0755 "$EXT_BIN_DIR/$BIN_NAME"
|
||||||
|
|
||||||
|
OUT_VSIX="$OUT_DIR/$EXT-$VERSION-$VSCE_TARGET.vsix"
|
||||||
|
( cd "$VSCODE_DIR/$EXT" && run_vsce package --target "$VSCE_TARGET" \
|
||||||
|
--no-dependencies --allow-missing-repository -o "$OUT_VSIX" >/dev/null )
|
||||||
|
echo "[vscode] wrote $(basename "$OUT_VSIX")"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Leave the working tree with a host-native bridge staged so a plain
|
||||||
|
# `code --install-extension` of an unpacked extension still works locally.
|
||||||
|
HOST_OS="$(go env GOOS)"; HOST_ARCH="$(go env GOARCH)"
|
||||||
|
HOST_BIN="nexus-vscode-bridge"; [ "$HOST_OS" = "windows" ] && HOST_BIN="nexus-vscode-bridge.exe"
|
||||||
|
for EXT in "${EXTENSIONS[@]}"; do
|
||||||
|
rm -rf "$VSCODE_DIR/$EXT/bin"; mkdir -p "$VSCODE_DIR/$EXT/bin"
|
||||||
|
GOOS="$HOST_OS" GOARCH="$HOST_ARCH" CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -o "$VSCODE_DIR/$EXT/bin/$HOST_BIN" ./cmd/vscode-bridge
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[vscode] Done. Packages in $OUT_DIR:"
|
||||||
|
ls -1 "$OUT_DIR"
|
||||||
Reference in New Issue
Block a user