dsh-vscode-workbench 0.2.3 → 0.2.5
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/README.en.md +27 -10
- package/README.md +26 -11
- package/cordis.patch.yml +2 -0
- package/lib/client.js +1039 -417
- package/lib/client.js.map +1 -1
- package/lib/index.js +15 -249
- package/lib/index.js.map +1 -1
- package/lib/types/client/DshBrowserPage.d.ts +6 -0
- package/lib/types/client/DshPanelPage.d.ts +9 -0
- package/lib/types/client/Icon.d.ts +1 -1
- package/lib/types/client/TerminalPanel.d.ts +5 -2
- package/lib/types/client/Workbench.d.ts +7 -1
- package/lib/types/client/browser.d.ts +26 -0
- package/lib/types/client/dshPanels.d.ts +4 -0
- package/lib/types/client/shortcuts.d.ts +2 -0
- package/lib/types/client/terminal.d.ts +37 -32
- package/package.json +15 -4
- package/src/client/DshBrowserPage.tsx +61 -0
- package/src/client/DshPanelPage.tsx +63 -0
- package/src/client/Icon.tsx +1 -1
- package/src/client/TerminalPanel.tsx +63 -40
- package/src/client/Workbench.tsx +72 -11
- package/src/client/browser.ts +68 -0
- package/src/client/dshPanels.ts +16 -0
- package/src/client/index.tsx +25 -6
- package/src/client/shortcuts.ts +9 -0
- package/src/client/styles.ts +18 -7
- package/src/client/terminal.ts +125 -78
- package/src/index.ts +0 -2
- package/src/lsp.ts +22 -14
- package/lib/types/terminal.d.ts +0 -46
- package/src/terminal.ts +0 -173
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { constants, createReadStream } from "node:fs";
|
|
3
3
|
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -9,7 +9,6 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
|
-
import * as pty from "node-pty";
|
|
13
12
|
//#region src/commit-message.ts
|
|
14
13
|
const DEFAULT_COMMIT_PREFERENCES = {
|
|
15
14
|
detail: "standard",
|
|
@@ -284,7 +283,7 @@ function authorize(req, res, connection) {
|
|
|
284
283
|
}
|
|
285
284
|
//#endregion
|
|
286
285
|
//#region src/host.ts
|
|
287
|
-
const require
|
|
286
|
+
const require = createRequire(import.meta.url);
|
|
288
287
|
const TEXT_LIMIT = 5242880;
|
|
289
288
|
const PROCESS_TIMEOUT = 3e4;
|
|
290
289
|
function cwdFor(ctx, sessionId) {
|
|
@@ -577,7 +576,7 @@ async function workspaceDownloadFile(cwd, path) {
|
|
|
577
576
|
/** Resolve the PDF.js worker shipped with this plugin's runtime dependency. */
|
|
578
577
|
function pdfWorkerAsset() {
|
|
579
578
|
return {
|
|
580
|
-
path: require
|
|
579
|
+
path: require.resolve("pdfjs-dist/legacy/build/pdf.worker.min.mjs"),
|
|
581
580
|
type: "text/javascript; charset=utf-8"
|
|
582
581
|
};
|
|
583
582
|
}
|
|
@@ -638,7 +637,7 @@ async function searchWorkspace(cwd, request, signal) {
|
|
|
638
637
|
limited: false
|
|
639
638
|
};
|
|
640
639
|
const root = await canonicalRoot(cwd);
|
|
641
|
-
const rg = require
|
|
640
|
+
const rg = require("@vscode/ripgrep").rgPath;
|
|
642
641
|
const args = [
|
|
643
642
|
"--json",
|
|
644
643
|
"--line-number",
|
|
@@ -1138,7 +1137,7 @@ async function gitAction(cwd, action, path, value) {
|
|
|
1138
1137
|
}
|
|
1139
1138
|
/** Resolve the official VS Code Codicon font shipped by @vscode/codicons. */
|
|
1140
1139
|
function codiconAsset() {
|
|
1141
|
-
const packageRoot = dirname(require
|
|
1140
|
+
const packageRoot = dirname(require.resolve("@vscode/codicons/package.json"));
|
|
1142
1141
|
return {
|
|
1143
1142
|
path: resolve(packageRoot, "dist", "codicon.ttf"),
|
|
1144
1143
|
type: "font/ttf"
|
|
@@ -1146,14 +1145,14 @@ function codiconAsset() {
|
|
|
1146
1145
|
}
|
|
1147
1146
|
/** Browser build of SheetJS served as an asset so the client bundle stays free of Node builtins. */
|
|
1148
1147
|
function xlsxAsset() {
|
|
1149
|
-
const packageRoot = dirname(require
|
|
1148
|
+
const packageRoot = dirname(require.resolve("xlsx/package.json"));
|
|
1150
1149
|
return {
|
|
1151
1150
|
path: resolve(packageRoot, "dist", "xlsx.full.min.js"),
|
|
1152
1151
|
type: "text/javascript; charset=utf-8"
|
|
1153
1152
|
};
|
|
1154
1153
|
}
|
|
1155
1154
|
function monacoAsset(raw) {
|
|
1156
|
-
const packageRoot = dirname(require
|
|
1155
|
+
const packageRoot = dirname(require.resolve("monaco-editor/package.json"));
|
|
1157
1156
|
const path = resolve(packageRoot, "min", raw);
|
|
1158
1157
|
if (!inside(resolve(packageRoot, "min"), path)) throw new WorkbenchError("asset-invalid", "invalid Monaco asset path", 403);
|
|
1159
1158
|
return {
|
|
@@ -1164,7 +1163,7 @@ function monacoAsset(raw) {
|
|
|
1164
1163
|
/** Resolve a vscode-icons SVG shipped by @vscode-icons/vscode-icons-api. */
|
|
1165
1164
|
function fileIconAsset(raw) {
|
|
1166
1165
|
if (!/^[a-z0-9_-]+\.svg$/i.test(raw)) throw new WorkbenchError("asset-invalid", "invalid file icon path", 403);
|
|
1167
|
-
const packageRoot = dirname(require
|
|
1166
|
+
const packageRoot = dirname(require.resolve("@vscode-icons/vscode-icons-api/package.json"));
|
|
1168
1167
|
return {
|
|
1169
1168
|
path: resolve(packageRoot, "icons", raw),
|
|
1170
1169
|
type: "image/svg+xml"
|
|
@@ -1400,7 +1399,12 @@ async function exists$1(path) {
|
|
|
1400
1399
|
*/
|
|
1401
1400
|
function lspSearchDirs(env = process.env, home = homedir()) {
|
|
1402
1401
|
const fromPath = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
1403
|
-
const extras = [
|
|
1402
|
+
const extras = process.platform === "win32" ? [
|
|
1403
|
+
join(home, "go", "bin"),
|
|
1404
|
+
join(home, ".cargo", "bin"),
|
|
1405
|
+
join(home, ".dotnet", "tools"),
|
|
1406
|
+
join(home, "bin")
|
|
1407
|
+
] : [
|
|
1404
1408
|
join(home, "go", "bin"),
|
|
1405
1409
|
join(home, ".cargo", "bin"),
|
|
1406
1410
|
join(home, ".dotnet", "tools"),
|
|
@@ -1420,7 +1424,7 @@ async function executableIn(directory, command) {
|
|
|
1420
1424
|
`${command}.exe`,
|
|
1421
1425
|
`${command}.cmd`,
|
|
1422
1426
|
`${command}.bat`,
|
|
1423
|
-
command
|
|
1427
|
+
`${command}.com`
|
|
1424
1428
|
] : [command];
|
|
1425
1429
|
for (const name of names) {
|
|
1426
1430
|
const file = join(directory, name);
|
|
@@ -1689,243 +1693,6 @@ async function officePdf(cwd, path) {
|
|
|
1689
1693
|
}
|
|
1690
1694
|
}
|
|
1691
1695
|
//#endregion
|
|
1692
|
-
//#region src/terminal.ts
|
|
1693
|
-
const require = createRequire(import.meta.url);
|
|
1694
|
-
const SENSITIVE = /(api[_-]?key|token|credential|secret|password|passwd|private[_-]?key|^dsh_.*(?:key|token|secret))/i;
|
|
1695
|
-
const MAX_TERMINALS = 8;
|
|
1696
|
-
const REPLAY_BYTES = 2097152;
|
|
1697
|
-
/** Remove credentials while retaining ordinary interactive-shell variables. */
|
|
1698
|
-
function sanitizedTerminalEnv(env) {
|
|
1699
|
-
return Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== void 0 && !SENSITIVE.test(key)));
|
|
1700
|
-
}
|
|
1701
|
-
/** Select the user's interactive shell with platform-specific fallbacks. */
|
|
1702
|
-
function defaultTerminalShell(platform = process.platform, env = process.env) {
|
|
1703
|
-
if (platform === "win32") return {
|
|
1704
|
-
shell: env.COMSPEC?.toLowerCase().includes("powershell") ? env.COMSPEC : "powershell.exe",
|
|
1705
|
-
args: ["-NoLogo"]
|
|
1706
|
-
};
|
|
1707
|
-
return {
|
|
1708
|
-
shell: env.SHELL || (platform === "darwin" ? "/bin/zsh" : "/bin/bash"),
|
|
1709
|
-
args: ["-l"]
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
/** Add execute bits when a registry package extracted a native helper without them. */
|
|
1713
|
-
function ensureExecutable(path) {
|
|
1714
|
-
if (process.platform === "win32") return true;
|
|
1715
|
-
try {
|
|
1716
|
-
const mode = statSync(path).mode;
|
|
1717
|
-
if ((mode & 73) === 0) chmodSync(path, mode | 73);
|
|
1718
|
-
return true;
|
|
1719
|
-
} catch {
|
|
1720
|
-
return false;
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
/** Repair node-pty's macOS/Linux helper, whose npm package currently loses its execute bit. */
|
|
1724
|
-
function repairNodePtyHelper() {
|
|
1725
|
-
if (process.platform === "win32") return;
|
|
1726
|
-
const packageDirectory = dirname(require.resolve("node-pty/package.json"));
|
|
1727
|
-
ensureExecutable(join(packageDirectory, "build", "Release", "spawn-helper"));
|
|
1728
|
-
ensureExecutable(join(packageDirectory, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"));
|
|
1729
|
-
}
|
|
1730
|
-
/** Own interactive terminals by their canonical workspace directory. */
|
|
1731
|
-
var TerminalManager = class {
|
|
1732
|
-
workspaces = /* @__PURE__ */ new Map();
|
|
1733
|
-
constructor() {
|
|
1734
|
-
repairNodePtyHelper();
|
|
1735
|
-
}
|
|
1736
|
-
workspace(cwd) {
|
|
1737
|
-
return realpathSync(cwd);
|
|
1738
|
-
}
|
|
1739
|
-
list(cwd) {
|
|
1740
|
-
return [...this.workspaces.get(this.workspace(cwd))?.values() ?? []].map((handle) => ({ ...handle.info }));
|
|
1741
|
-
}
|
|
1742
|
-
create(cwd, title) {
|
|
1743
|
-
const key = this.workspace(cwd);
|
|
1744
|
-
const terminals = this.workspaces.get(key) ?? /* @__PURE__ */ new Map();
|
|
1745
|
-
this.workspaces.set(key, terminals);
|
|
1746
|
-
if (terminals.size >= MAX_TERMINALS) throw new Error(`terminal limit reached (${MAX_TERMINALS})`);
|
|
1747
|
-
const { shell, args } = defaultTerminalShell();
|
|
1748
|
-
const child = pty.spawn(shell, args, {
|
|
1749
|
-
name: "xterm-256color",
|
|
1750
|
-
cols: 80,
|
|
1751
|
-
rows: 24,
|
|
1752
|
-
cwd: key,
|
|
1753
|
-
env: sanitizedTerminalEnv(process.env)
|
|
1754
|
-
});
|
|
1755
|
-
const info = {
|
|
1756
|
-
id: randomUUID(),
|
|
1757
|
-
title: title || basename(shell),
|
|
1758
|
-
cwd: key,
|
|
1759
|
-
running: true,
|
|
1760
|
-
sequence: 0
|
|
1761
|
-
};
|
|
1762
|
-
const handle = {
|
|
1763
|
-
info,
|
|
1764
|
-
process: child,
|
|
1765
|
-
replay: "",
|
|
1766
|
-
listeners: /* @__PURE__ */ new Set()
|
|
1767
|
-
};
|
|
1768
|
-
terminals.set(info.id, handle);
|
|
1769
|
-
child.onData((data) => {
|
|
1770
|
-
info.sequence++;
|
|
1771
|
-
handle.replay += data;
|
|
1772
|
-
if (Buffer.byteLength(handle.replay) > REPLAY_BYTES) handle.replay = handle.replay.slice(-2097152);
|
|
1773
|
-
for (const listener of handle.listeners) listener({
|
|
1774
|
-
sequence: info.sequence,
|
|
1775
|
-
data
|
|
1776
|
-
});
|
|
1777
|
-
});
|
|
1778
|
-
child.onExit(({ exitCode }) => {
|
|
1779
|
-
info.running = false;
|
|
1780
|
-
info.exitCode = exitCode;
|
|
1781
|
-
info.sequence++;
|
|
1782
|
-
const data = `\r\n[process exited with code ${exitCode}]\r\n`;
|
|
1783
|
-
for (const listener of handle.listeners) listener({
|
|
1784
|
-
sequence: info.sequence,
|
|
1785
|
-
data
|
|
1786
|
-
});
|
|
1787
|
-
});
|
|
1788
|
-
return { ...info };
|
|
1789
|
-
}
|
|
1790
|
-
get(cwd, id) {
|
|
1791
|
-
return this.workspaces.get(this.workspace(cwd))?.get(id);
|
|
1792
|
-
}
|
|
1793
|
-
close(cwd, id) {
|
|
1794
|
-
const key = this.workspace(cwd);
|
|
1795
|
-
const terminals = this.workspaces.get(key);
|
|
1796
|
-
const handle = terminals?.get(id);
|
|
1797
|
-
if (handle === void 0 || terminals === void 0) return;
|
|
1798
|
-
terminals.delete(id);
|
|
1799
|
-
try {
|
|
1800
|
-
handle.process.kill();
|
|
1801
|
-
} catch {}
|
|
1802
|
-
if (terminals.size === 0) this.workspaces.delete(key);
|
|
1803
|
-
}
|
|
1804
|
-
rename(cwd, id, title) {
|
|
1805
|
-
const handle = this.get(cwd, id);
|
|
1806
|
-
if (handle === void 0) throw new Error("terminal not found");
|
|
1807
|
-
handle.info.title = title;
|
|
1808
|
-
return { ...handle.info };
|
|
1809
|
-
}
|
|
1810
|
-
dispose() {
|
|
1811
|
-
for (const [cwd, terminals] of this.workspaces) for (const id of terminals.keys()) this.close(cwd, id);
|
|
1812
|
-
}
|
|
1813
|
-
};
|
|
1814
|
-
function terminalRequestAllowed(req) {
|
|
1815
|
-
const host = req.headers.host;
|
|
1816
|
-
if (host === void 0 || req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
1817
|
-
let hostname;
|
|
1818
|
-
try {
|
|
1819
|
-
hostname = new URL(`http://${host}`).hostname;
|
|
1820
|
-
} catch {
|
|
1821
|
-
return false;
|
|
1822
|
-
}
|
|
1823
|
-
if (hostname !== "localhost" && hostname !== "[::1]" && !hostname.startsWith("127.")) return false;
|
|
1824
|
-
const origin = req.headers.origin;
|
|
1825
|
-
try {
|
|
1826
|
-
return origin === void 0 || new URL(origin).host === host;
|
|
1827
|
-
} catch {
|
|
1828
|
-
return false;
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
/** Register the private local terminal API and WebSocket transport. */
|
|
1832
|
-
function installTerminal(ctx) {
|
|
1833
|
-
const manager = new TerminalManager();
|
|
1834
|
-
ctx.effect(() => ctx.webServer.register({
|
|
1835
|
-
kind: "prefix",
|
|
1836
|
-
path: "/dsh-vscode-terminal/api",
|
|
1837
|
-
handler: async (req, res) => {
|
|
1838
|
-
if (!authorize(req, res, ctx.connection)) return;
|
|
1839
|
-
if (!terminalRequestAllowed(req)) return send(res, 403, {
|
|
1840
|
-
ok: false,
|
|
1841
|
-
error: { message: "forbidden" }
|
|
1842
|
-
});
|
|
1843
|
-
if (req.method !== "POST") return send(res, 405, {
|
|
1844
|
-
ok: false,
|
|
1845
|
-
error: { message: "POST required" }
|
|
1846
|
-
});
|
|
1847
|
-
try {
|
|
1848
|
-
const body = await readBody(req);
|
|
1849
|
-
const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
|
|
1850
|
-
if (sessionId === "") throw new Error("sessionId required");
|
|
1851
|
-
const cwd = cwdFor(ctx, sessionId);
|
|
1852
|
-
const method = new URL(req.url ?? "/", "http://dsh.local").pathname.slice(25);
|
|
1853
|
-
let value;
|
|
1854
|
-
if (method === "list") value = manager.list(cwd);
|
|
1855
|
-
else if (method === "create") value = manager.create(cwd, typeof body.title === "string" ? body.title : void 0);
|
|
1856
|
-
else if (method === "close") {
|
|
1857
|
-
manager.close(cwd, typeof body.id === "string" ? body.id : "");
|
|
1858
|
-
value = manager.list(cwd);
|
|
1859
|
-
} else if (method === "rename") value = manager.rename(cwd, typeof body.id === "string" ? body.id : "", typeof body.title === "string" ? body.title : "");
|
|
1860
|
-
else throw new Error("unknown terminal method");
|
|
1861
|
-
send(res, 200, {
|
|
1862
|
-
ok: true,
|
|
1863
|
-
value
|
|
1864
|
-
});
|
|
1865
|
-
} catch (error) {
|
|
1866
|
-
sendError(res, error);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}), "dsh-vscode-workbench: terminal API");
|
|
1870
|
-
const webSocket = new WebSocketServer({ noServer: true });
|
|
1871
|
-
ctx.effect(() => ctx.webServer.registerUpgrade({
|
|
1872
|
-
path: "/dsh-vscode-terminal/ws",
|
|
1873
|
-
handler(req, socket, head) {
|
|
1874
|
-
if (ctx.connection.requestRejection(req) !== void 0 || !terminalRequestAllowed(req)) {
|
|
1875
|
-
socket.destroy();
|
|
1876
|
-
return;
|
|
1877
|
-
}
|
|
1878
|
-
webSocket.handleUpgrade(req, socket, head, (connection) => {
|
|
1879
|
-
const url = new URL(req.url ?? "/", "http://dsh.local");
|
|
1880
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
1881
|
-
const id = url.searchParams.get("id");
|
|
1882
|
-
if (sessionId === null || id === null) {
|
|
1883
|
-
connection.close(1008, "sessionId and id required");
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
let handle;
|
|
1887
|
-
try {
|
|
1888
|
-
handle = manager.get(cwdFor(ctx, sessionId), id);
|
|
1889
|
-
} catch {
|
|
1890
|
-
connection.close(1008, "session workspace unavailable");
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
if (handle === void 0) {
|
|
1894
|
-
connection.close(1008, "terminal not found");
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
if (handle.replay !== "") connection.send(JSON.stringify({
|
|
1898
|
-
type: "replay",
|
|
1899
|
-
sequence: handle.info.sequence,
|
|
1900
|
-
data: handle.replay
|
|
1901
|
-
}));
|
|
1902
|
-
const listener = (output) => {
|
|
1903
|
-
if (connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify({
|
|
1904
|
-
type: "output",
|
|
1905
|
-
...output
|
|
1906
|
-
}));
|
|
1907
|
-
};
|
|
1908
|
-
handle.listeners.add(listener);
|
|
1909
|
-
connection.on("message", (raw) => {
|
|
1910
|
-
const text = raw.toString();
|
|
1911
|
-
try {
|
|
1912
|
-
const message = JSON.parse(text);
|
|
1913
|
-
if (message !== null && typeof message === "object" && "type" in message && message.type === "input" && "data" in message && typeof message.data === "string") handle.process.write(message.data);
|
|
1914
|
-
else if (message !== null && typeof message === "object" && "type" in message && message.type === "resize" && "cols" in message && "rows" in message && typeof message.cols === "number" && typeof message.rows === "number") handle.process.resize(Math.max(2, Math.floor(message.cols)), Math.max(2, Math.floor(message.rows)));
|
|
1915
|
-
} catch {
|
|
1916
|
-
handle.process.write(text);
|
|
1917
|
-
}
|
|
1918
|
-
});
|
|
1919
|
-
connection.on("close", () => handle.listeners.delete(listener));
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
}), "dsh-vscode-workbench: terminal WebSocket");
|
|
1923
|
-
ctx.effect(() => () => {
|
|
1924
|
-
webSocket.close();
|
|
1925
|
-
manager.dispose();
|
|
1926
|
-
}, "dsh-vscode-workbench: terminal teardown");
|
|
1927
|
-
}
|
|
1928
|
-
//#endregion
|
|
1929
1696
|
//#region src/watch-ignore.ts
|
|
1930
1697
|
/** Exclusion rules for the recursive workspace file watcher. */
|
|
1931
1698
|
/** Directory names skipped by default; large dependency and build trees exhaust inotify watches. */
|
|
@@ -2082,7 +1849,6 @@ function apply(ctx) {
|
|
|
2082
1849
|
fiber.dispose?.();
|
|
2083
1850
|
};
|
|
2084
1851
|
}, "dsh-vscode-workbench: optional DSH services");
|
|
2085
|
-
installTerminal(ctx);
|
|
2086
1852
|
ctx.effect(() => ctx.webServer.register({
|
|
2087
1853
|
kind: "exact",
|
|
2088
1854
|
path: "/dsh-vscode/codicons/codicon.ttf",
|