Merge pull request #2 from orus-dev/simple-working-mvp

Simple working mvp
This commit is contained in:
2026-02-08 18:34:13 +01:00
committed by GitHub
8 changed files with 493 additions and 17 deletions
+146
View File
@@ -9,8 +9,11 @@
"version": "0.0.1",
"dependencies": {
"@types/cors": "^2.8.19",
"@types/diff-match-patch": "^1.0.36",
"@types/express": "^5.0.6",
"axios": "^1.13.5",
"cors": "^2.8.6",
"diff-match-patch": "^1.0.5",
"express": "^5.2.1"
},
"devDependencies": {
@@ -301,6 +304,12 @@
"@types/node": "*"
}
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"license": "MIT"
},
"node_modules/@types/express": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
@@ -769,6 +778,23 @@
"node": ">=8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1063,6 +1089,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1186,6 +1224,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1205,6 +1252,12 @@
"node": ">=0.3.1"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"license": "Apache-2.0"
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1304,6 +1357,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1717,6 +1785,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1734,6 +1822,43 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1972,6 +2097,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -3090,6 +3230,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+34 -1
View File
@@ -31,7 +31,37 @@
"command": "devrun-vscode.stopServer",
"title": "DevRun: Stop the run server (for quick run starts)"
}
]
],
"menus": {
"commandPalette": [
{
"command": "devrun-vscode.startRun",
"when": "!devrun.runActive"
},
{
"command": "devrun-vscode.stopRun",
"when": "devrun.runActive"
},
{
"command": "devrun-vscode.startServer",
"when": "!devrun.serverActive"
},
{
"command": "devrun-vscode.stopServer",
"when": "devrun.serverActive"
}
]
},
"configuration": {
"title": "DevRun",
"properties": {
"devrun.autoStartServer": {
"type": "boolean",
"default": false,
"description": "Automatically start the server when vscode starts"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
@@ -54,8 +84,11 @@
},
"dependencies": {
"@types/cors": "^2.8.19",
"@types/diff-match-patch": "^1.0.36",
"@types/express": "^5.0.6",
"axios": "^1.13.5",
"cors": "^2.8.6",
"diff-match-patch": "^1.0.5",
"express": "^5.2.1"
}
}
+54 -10
View File
@@ -1,13 +1,57 @@
export interface LiveRunMove {
latency: number;
cursor: number;
changes?: {
from: number;
to: number;
insert: string;
};
import axios from "axios";
import { LiveRunMove } from "./types";
var cookies: string | undefined;
export function getCookies(): string | undefined {
return cookies;
}
export function setCookies(c: string) {
cookies = c;
}
export async function submitRun() {}
export async function addRun(problem: string, mode: string): Promise<string> {
return (
await axios.put(
"http://localhost:3000/api/run",
{
problem,
category: mode,
},
{
headers: {
Cookie: cookies,
},
},
)
).data.runId;
}
export async function addRunMoves() {}
export async function submitRun(runId: string) {
await axios.post(
"http://localhost:3000/api/run",
{
runId,
},
{
headers: {
Cookie: cookies,
},
},
);
}
export async function addRunMoves(runId: string, moves: LiveRunMove[]) {
await axios.post(
"http://localhost:3000/api/run/move",
{
runId,
moves,
},
{
headers: {
Cookie: cookies,
},
},
);
}
+97 -2
View File
@@ -1,10 +1,12 @@
import * as vscode from "vscode";
import { currentRun, startRun, stopRun } from "./run";
import { startServer, stopServer } from "./server";
import os from "os";
import { exec } from "child_process";
import net from "net";
export function activate(context: vscode.ExtensionContext) {
console.log("dev-run Activated");
const config = vscode.workspace.getConfiguration("devrun");
const statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
10,
@@ -49,8 +51,101 @@ export function activate(context: vscode.ExtensionContext) {
stopServer(statusBarItem),
),
);
console.log("dev-run Activated");
// Security check - Make sure there aren't any malicious servers currently running
const autoStart = config.get<boolean>("autoStartServer");
canConnect(63780)
.then((canConnect) => {
if (canConnect) {
console.error(
"Unable to start due to another process listening on port 63780",
);
console.error("Possible malicious dev-run instance running");
vscode.window
.showErrorMessage(
"Possible malicious dev-run instance running",
"Kill the process listening on port 63780",
)
.then(async (response) => {
if (!response) return;
if (response === "Kill the process listening on port 63780") {
await killProcessOnPort(63780);
if (autoStart) {
startServer(statusBarItem);
console.log("dev-run server auto started!");
}
}
});
return;
}
if (autoStart) {
startServer(statusBarItem);
console.log("dev-run server auto started!");
}
})
.catch(() => {});
}
export function deactivate() {
stopRun();
}
export function canConnect(
port: number,
host = "127.0.0.1",
timeout = 1000,
): Promise<boolean> {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.once("connect", () => {
socket.destroy();
resolve(true);
});
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
socket.once("error", () => {
resolve(false);
});
socket.connect(port, host);
});
}
export function killProcessOnPort(port: number): Promise<void> {
return new Promise((resolve, reject) => {
const platform = os.platform();
let cmd: string;
if (platform === "win32") {
// Windows
cmd = `for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port}') do taskkill /PID %a /F`;
} else {
// macOS / Linux
cmd = `lsof -ti tcp:${port} | xargs kill -9`;
}
exec(cmd, (error) => {
if (error) {
resolve();
} else {
resolve();
}
});
});
}
+112
View File
@@ -0,0 +1,112 @@
import * as vscode from "vscode";
import { diff_match_patch, Diff } from "diff-match-patch";
import { LiveRunMove } from "./types";
import { addRunMoves } from "./client";
export function startMonitoring(runId: string) {
const dmp = new diff_match_patch();
let moveId = 0;
let moves: LiveRunMove[] = [];
let lastText = "";
let lastEventTime = Date.now();
let idleTimeout: NodeJS.Timeout | undefined;
let isSending = false;
const IDLE_MS = 350;
const editor = vscode.window.activeTextEditor;
if (editor) {
lastText = editor.document.getText();
}
async function flush() {
if (!moves.length || isSending) return;
isSending = true;
const batch = moves;
moves = [];
try {
await addRunMoves(runId, batch);
} finally {
isSending = false;
}
}
const readInterval = setInterval(() => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const newText = editor.document.getText();
if (newText === lastText) return;
const now = Date.now();
const latency = capLatency(now - lastEventTime, IDLE_MS);
lastEventTime = now;
const diffs = dmp.diff_main(lastText, newText);
dmp.diff_cleanupEfficiency(diffs);
const changes = diffsToChanges(diffs);
lastText = newText;
for (const change of changes) {
moves.push({
moveId: moveId++,
latency,
cursor: editor.document.offsetAt(editor.selection.active),
changes: change,
});
}
if (idleTimeout) clearTimeout(idleTimeout);
idleTimeout = setTimeout(flush, IDLE_MS);
}, 500);
return () => {
clearInterval(readInterval);
if (idleTimeout) clearTimeout(idleTimeout);
flush();
};
}
function diffsToChanges(diffs: Diff[]) {
const changes: {
from: number;
to: number;
insert: string;
}[] = [];
let offset = 0;
for (const [op, text] of diffs) {
if (op === 0) {
// equal
offset += text.length;
} else if (op === -1) {
// delete
changes.push({
from: offset,
to: offset + text.length,
insert: "",
});
} else if (op === 1) {
// insert
changes.push({
from: offset,
to: offset,
insert: text,
});
offset += text.length;
}
}
return changes;
}
function capLatency(value: number, max: number) {
return value > max ? 0 : value;
}
+15 -1
View File
@@ -1,9 +1,13 @@
import * as vscode from "vscode";
import { addRun, submitRun } from "./client";
import { startMonitoring } from "./move";
export var currentRun: {
interval: NodeJS.Timeout;
stopMonitoring: () => void;
problemId: string;
start: Date;
id: string;
} | null = null;
export async function startRun(
@@ -36,11 +40,15 @@ export async function startRun(
if (!mode) return;
const runId = await addRun(problemId, mode);
let start = Date.now();
currentRun = {
id: runId,
problemId,
start: new Date(),
stopMonitoring: startMonitoring(runId),
interval: setInterval(() => {
const elapsed = Date.now() - start;
@@ -53,11 +61,17 @@ export async function startRun(
.padStart(2, "0")}`;
}, 50),
};
await vscode.commands.executeCommand("setContext", "devrun.runActive", true);
}
export async function stopRun() {
if (currentRun) {
clearTimeout(currentRun.interval);
clearInterval(currentRun.interval);
submitRun(currentRun.id);
currentRun = null;
}
await vscode.commands.executeCommand("setContext", "devrun.runActive", false);
}
+25 -3
View File
@@ -2,15 +2,16 @@ import * as vscode from "vscode";
import express from "express";
import * as http from "http";
import cors from "cors";
import { getCookies, setCookies } from "./client";
let server: http.Server | undefined;
var server: http.Server | undefined;
const app = express();
app.use(express.json());
app.use(
cors({
origin: (origin, callback) => {
// allow requests with no origin (e.g., direct curl)
if (
!origin ||
origin.includes("localhost") ||
@@ -25,10 +26,19 @@ app.use(
}),
);
app.post("/auth", (req, res) => {
const { saveCookies } = req.body;
setCookies(saveCookies);
res.json({ ok: true });
});
app.get("/start-run/:problemId", (req, res) => {
const { problemId } = req.params;
// trigger VS Code command
if (!getCookies()) return res.json({ ok: false, code: "auth" });
vscode.commands.executeCommand("devrun-vscode.startRun", problemId);
res.json({ ok: true, problemId });
@@ -42,6 +52,12 @@ export async function startServer(statusBarItem: vscode.StatusBarItem) {
server = app.listen(63780, "127.0.0.1", () => {
setTimeout(() => (statusBarItem.text = "$(clock) No active run"), 500);
});
await vscode.commands.executeCommand(
"setContext",
"devrun.serverActive",
true,
);
}
export async function stopServer(statusBarItem: vscode.StatusBarItem) {
@@ -57,4 +73,10 @@ export async function stopServer(statusBarItem: vscode.StatusBarItem) {
server = undefined;
setTimeout(() => (statusBarItem.text = "$(clock) No active run"), 500);
await vscode.commands.executeCommand(
"setContext",
"devrun.serverActive",
false,
);
}
+10
View File
@@ -0,0 +1,10 @@
export interface LiveRunMove {
latency: number;
cursor: number;
changes?: {
from: number;
to: number;
insert: string;
};
moveId: number;
}