Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/desktop/scripts/neovim-terminal/launch.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
param(
[Parameter(Mandatory=$true)][string]$Runtime,
[Parameter(Mandatory=$true)][string]$Token
)

$ErrorActionPreference = 'Stop'
try {
if ($Token -cnotmatch '^[A-Za-z0-9_-]+$' -or $Token.Length -gt 21846) {
throw 'Invalid terminal-editor payload.'
}
# The packaged Electron executable supplies Node; no separate Windows runtime is required.
$env:ELECTRON_RUN_AS_NODE = '1'
# Start-Process waits for Electron's GUI-subsystem executable without
# PowerShell competing with the child for console input.
$helper = Join-Path $PSScriptRoot 'session.mjs'
$child = Start-Process -FilePath $Runtime -ArgumentList @(('"' + $helper + '"'), $Token) -NoNewWindow -Wait -PassThru
if ($child.ExitCode -ne 0) {
throw "The editor session exited with code $($child.ExitCode)."
}
} catch {
Write-Host $_ -ForegroundColor Red
Read-Host 'Press Enter to close this window'
exit 1
}
133 changes: 133 additions & 0 deletions apps/desktop/scripts/neovim-terminal/session.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as NodeCrypto from "node:crypto";
import * as NodeFSP from "node:fs/promises";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";
import * as NodeOS from "node:os";
import {
decodeRequest,
encodeRequest,
findNeovim,
neovimArgs,
quotePosix,
run,
sshArgs,
wslArgs,
} from "./transport.mjs";

const sourceDirectory = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
let sources;
async function loadSources() {
sources ??= Promise.all(
["session.mjs", "transport.mjs"].map(async (name) => [
name,
await NodeFSP.readFile(NodePath.join(sourceDirectory, name), "utf8"),
]),
);
return Object.fromEntries(await sources);
}

/** Source and payload use staging stdin; the final invocation contains only a private script path. */
export async function stagingProgram(request, _node, directory, stagingUser) {
const files = await loadSources();
files.request = encodeRequest(request);
return `import * as fs from 'node:fs/promises';
import * as os from 'node:os';
const directory = ${JSON.stringify(directory)};
const files = ${JSON.stringify(files)};
const expectedUser = ${JSON.stringify(stagingUser ?? null)};
if (expectedUser && os.userInfo().username !== expectedUser) throw new Error('The WSL account changed. Reconnect before opening Neovim.');
const quote = value => "'" + value.replaceAll("'", "'\\\\''") + "'";
files['launch.sh'] = '#!/bin/bash\\nexec ' + quote(process.execPath) + ' ' + quote(directory + '/session.mjs') + ' --staged ' + quote(directory) + '\\n';
// Consumed launchers are removed before editing; only abandoned preparation remains here.
let cleaned = 0;
for (const name of await fs.readdir('/tmp')) {
if (cleaned >= 64) break;
if (!/^t3code-neovim-[a-f0-9-]{36}$/.test(name)) continue;
const candidate = '/tmp/' + name;
try {
const stat = await fs.lstat(candidate);
if (stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === process.getuid() && Date.now() - stat.mtimeMs > 86400000) {
await fs.rm(candidate, { recursive: true, force: true });
cleaned++;
}
} catch { /* Another launcher may have consumed the directory. */ }
}
await fs.mkdir(directory, { mode: 0o700 });
try {
for (const [name, data] of Object.entries(files)) await fs.writeFile(directory + '/' + name, data, { mode: 0o600, flag: 'wx' });
} catch (error) { await fs.rm(directory, { recursive: true, force: true }); throw error; }
`;
}

function bootstrap(program, node) {
return `set -eu\n${node ? `node_path=${quotePosix(node)}` : "node_path=$(command -v node) || { echo 'Node.js is missing from this account’s login PATH.' >&2; exit 72; }"}\n"$node_path" --input-type=module <<'T3_NEOVIM_STAGE'\n${program}\nT3_NEOVIM_STAGE\n`;
}

export async function launchSession(request) {
const route = request.route;
if (route.kind === "native") {
if (request.expectedAccount && NodeOS.userInfo().username !== request.expectedAccount)
throw new Error("The target account changed. Reconnect and check Neovim again.");
const executable = await findNeovim(request.executable);
await run(executable, await neovimArgs(request.target), { cwd: request.workspace });
return;
}
const directory = `/tmp/t3code-neovim-${NodeCrypto.randomUUID()}`;
if (route.kind === "wsl" || route.kind === "wsl-ssh") {
const next = {
...request,
platform: "linux",
route:
route.kind === "wsl"
? { kind: "native" }
: {
kind: "ssh",
host: route.host,
sshUser: route.sshUser,
port: route.port,
remoteNode: route.remoteNode,
},
};
const prefix = wslArgs(route);
await run("wsl.exe", [...prefix, "/bin/bash", "-l", "-s"], {
input: bootstrap(await stagingProgram(next, route.node, directory, route.user), route.node),
timeout: 30_000,
});
await run("wsl.exe", [...prefix, "/bin/bash", "-l", `${directory}/launch.sh`]);
return;
}
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone packaged helper has no Effect runtime.
const ssh = process.platform === "win32" ? "ssh.exe" : "ssh";
const next = { ...request, platform: "linux", route: { kind: "native" } };
// SSH authenticates on the terminal; the preparation program has a separate stdin pipe.
await run(ssh, [...sshArgs(route, false), "/bin/bash -l -s"], {
input: bootstrap(await stagingProgram(next, route.remoteNode, directory), route.remoteNode),
});
await run(ssh, [...sshArgs(route, true), `/bin/bash -l ${quotePosix(`${directory}/launch.sh`)}`]);
}

async function main() {
delete process.env.ELECTRON_RUN_AS_NODE;
let token;
if (process.argv[2] === "--staged") {
const directory = process.argv[3];
if (!/^\/tmp\/t3code-neovim-[a-f0-9-]{36}$/u.test(directory))
throw new Error("Invalid staging directory.");
try {
token = await NodeFSP.readFile(`${directory}/request`, "utf8");
await loadSources();
} finally {
await NodeFSP.rm(directory, { recursive: true, force: true });
}
} else token = process.argv[2];
await launchSession(decodeRequest(token));
}
if (
process.argv[1] &&
NodeURL.pathToFileURL(NodePath.resolve(process.argv[1])).href === import.meta.url
) {
main().catch((error) => {
console.error(`Neovim could not complete the session: ${error.message}`);
process.exitCode = 1;
});
}
47 changes: 47 additions & 0 deletions apps/desktop/scripts/neovim-terminal/spike.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as NodeChildProcess from "node:child_process";
import * as NodeFSP from "node:fs/promises";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";
import { encodeRequest, windowsTerminalArgs } from "./transport.mjs";

// Invoke using the packaged Electron executable with ELECTRON_RUN_AS_NODE=1.
// This harness intentionally has no product IPC or editor-preference entry point.
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone packaged helper has no Effect runtime.
if (process.platform !== "win32" || !process.versions.electron) {
throw new Error("Run this spike with the packaged Windows Electron runtime.");
}
const request = JSON.parse(await NodeFSP.readFile(process.argv[2], "utf8"));
const powershell = NodePath.join(
process.env.SystemRoot,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
);
const bootstrap = NodePath.join(
NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)),
"launch.ps1",
);
const terminal = NodePath.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps", "wt.exe");
await Promise.all([
NodeFSP.access(terminal),
NodeFSP.access(powershell),
NodeFSP.access(bootstrap),
]);
const args = windowsTerminalArgs({
powershell,
bootstrap,
runtime: process.execPath,
token: encodeRequest(request),
});
const child = NodeChildProcess.spawn(terminal, args, {
detached: true,
stdio: "ignore",
shell: false,
});
await new Promise((resolve, reject) => {
child.once("error", reject);
child.once("spawn", resolve);
});
child.unref();
console.log("Windows Terminal accepted the launch. This does not confirm SSH or Neovim readiness.");
52 changes: 52 additions & 0 deletions apps/desktop/scripts/neovim-terminal/transport.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { EditorOpenTarget } from "@t3tools/contracts";
export type TerminalRoute =
| { kind: "native" }
| { kind: "wsl"; distro: string; user: string; node?: string }
| { kind: "ssh"; host: string; sshUser?: string; port?: number; remoteNode?: string }
| {
kind: "wsl-ssh";
distro: string;
user: string;
node?: string;
host: string;
sshUser?: string;
port?: number;
remoteNode?: string;
};
export interface LaunchPayload {
version: 1;
id: string;
platform: string;
route: TerminalRoute;
workspace: string;
target: EditorOpenTarget;
executable?: string;
expectedAccount?: string;
}
export function encodeRequest(input: LaunchPayload): string;
export function decodeRequest(token: string): LaunchPayload;
export function quotePosix(value: string): string;
export function windowsTerminalArgs(input: {
powershell: string;
bootstrap: string;
runtime: string;
token: string;
}): string[];
export function sshArgs(
route: Extract<TerminalRoute, { kind: "ssh" | "wsl-ssh" }>,
tty: boolean,
): string[];
export function wslArgs(route: Extract<TerminalRoute, { kind: "wsl" | "wsl-ssh" }>): string[];
export function run(
command: string,
args: readonly string[],
options?: {
input?: string;
timeout?: number;
cwd?: string;
capture?: boolean;
env?: NodeJS.ProcessEnv;
},
): Promise<string>;
export function findNeovim(override?: string, environment?: NodeJS.ProcessEnv): Promise<string>;
export function neovimArgs(target: EditorOpenTarget): Promise<string[]>;
Loading