@autoscanlab/connect 0.4.0 → 0.4.2
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/connect.mjs +61 -12
- package/package.json +1 -1
package/connect.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, hostname, platform } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,7 @@ import manifest from "./package.json" with { type: "json" };
|
|
|
10
10
|
|
|
11
11
|
const IDLE_EXIT_MS = 30 * 60_000;
|
|
12
12
|
const RETRY_MS = 5_000;
|
|
13
|
+
const OUTPUT_TAIL = 1_000;
|
|
13
14
|
const AGENT_LABELS = { claude: "Claude Code", codex: "Codex" };
|
|
14
15
|
const SCHEME = "autoscan";
|
|
15
16
|
const KEYCHAIN_SERVICE = "autoscan-connect";
|
|
@@ -210,6 +211,7 @@ async function install(value, code) {
|
|
|
210
211
|
const check = await reach(`${helperUrl(base, agents)}&wait=0`, { headers: { Authorization: `Bearer ${key}` } });
|
|
211
212
|
if (!check.ok) fail((await check.text()) || "That key doesn't work for this course.");
|
|
212
213
|
mkdirSync(APP_DIR, { recursive: true });
|
|
214
|
+
stopRunning(base);
|
|
213
215
|
for (const file of ["connect.mjs", "package.json"]) copyFileSync(join(HERE, file), join(APP_DIR, file));
|
|
214
216
|
saveKey(base, key);
|
|
215
217
|
registerLink(join(APP_DIR, "connect.mjs"));
|
|
@@ -232,9 +234,34 @@ function uninstall() {
|
|
|
232
234
|
console.log("Removed autOScan from this computer.");
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
const lockOf = (base) => join(APP_DIR, `running-${createHash("sha256").update(base).digest("hex").slice(0, 12)}.pid`);
|
|
238
|
+
|
|
239
|
+
function stopRunning(base) {
|
|
240
|
+
const lock = lockOf(base);
|
|
241
|
+
if (!existsSync(lock)) return;
|
|
242
|
+
try {
|
|
243
|
+
process.kill(Number(readFileSync(lock, "utf8")), "SIGTERM");
|
|
244
|
+
} catch {}
|
|
245
|
+
rmSync(lock, { force: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function updateItself(base) {
|
|
249
|
+
console.log("Updating the helper…");
|
|
250
|
+
rmSync(lockOf(base), { force: true });
|
|
251
|
+
const npx = OS === "win32" ? "npx.cmd" : "npx";
|
|
252
|
+
const updated = spawnSync(npx, ["--prefer-online", "-y", `${manifest.name}@latest`, "install", base], { stdio: "inherit", shell: OS === "win32" });
|
|
253
|
+
if (updated.status !== 0) fail("Couldn't update the helper. Run the install command from autOScan's Agents page.");
|
|
254
|
+
// Restarting the same version would be told to update again, round and round.
|
|
255
|
+
if (JSON.parse(readFileSync(join(APP_DIR, "package.json"), "utf8")).version === manifest.version) fail("No newer helper is published yet; try again later.");
|
|
256
|
+
const log = openSync(join(APP_DIR, "helper.log"), "a");
|
|
257
|
+
const link = `${SCHEME}://connect?base=${encodeURIComponent(base)}`;
|
|
258
|
+
spawn(process.execPath, [join(APP_DIR, "connect.mjs"), "open", link], { detached: true, stdio: ["ignore", log, log] }).unref();
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
235
262
|
// One helper per course: a second autoscan:// link while one runs leaves it be.
|
|
236
263
|
function holdLock(base) {
|
|
237
|
-
const lock =
|
|
264
|
+
const lock = lockOf(base);
|
|
238
265
|
if (existsSync(lock)) {
|
|
239
266
|
try {
|
|
240
267
|
process.kill(Number(readFileSync(lock, "utf8")), 0);
|
|
@@ -249,14 +276,15 @@ function holdLock(base) {
|
|
|
249
276
|
async function run(base, key) {
|
|
250
277
|
const agents = agentsOrFail();
|
|
251
278
|
const headers = { Authorization: `Bearer ${key}` };
|
|
252
|
-
const report = (running) =>
|
|
279
|
+
const report = (running, exit = null) =>
|
|
253
280
|
fetch(`${base}/agent/helper`, {
|
|
254
281
|
method: "POST",
|
|
255
282
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
256
|
-
body: JSON.stringify({ running }),
|
|
283
|
+
body: JSON.stringify({ running, exit }),
|
|
257
284
|
}).catch(() => {});
|
|
258
285
|
|
|
259
286
|
let child = null;
|
|
287
|
+
let stopping = false;
|
|
260
288
|
let idleSince = Date.now();
|
|
261
289
|
let reachable = null;
|
|
262
290
|
const announce = (now) => {
|
|
@@ -267,14 +295,29 @@ async function run(base, key) {
|
|
|
267
295
|
const start = async ({ agent, workspace }) => {
|
|
268
296
|
const { command, args, env } = agentCommand(agent, { mcpUrl: `${base}/mcp/grading`, key, workspace });
|
|
269
297
|
console.log(`Grading ${workspace} with ${AGENT_LABELS[agent]}…`);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
298
|
+
const running = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
|
|
299
|
+
child = running;
|
|
300
|
+
let tail = "";
|
|
301
|
+
for (const [stream, log] of [[running.stdout, process.stdout], [running.stderr, process.stderr]]) {
|
|
302
|
+
stream.on("data", (chunk) => {
|
|
303
|
+
log.write(chunk);
|
|
304
|
+
tail = (tail + chunk).slice(-OUTPUT_TAIL);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
let ended = false;
|
|
308
|
+
const end = (code, error) => {
|
|
309
|
+
if (ended) return;
|
|
310
|
+
ended = true;
|
|
273
311
|
child = null;
|
|
274
312
|
idleSince = Date.now();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
313
|
+
const failed = !stopping && (error !== null || code !== 0);
|
|
314
|
+
stopping = false;
|
|
315
|
+
console.log(failed ? `Agent stopped unexpectedly (${error?.message ?? `exit code ${code}`}).` : "Agent stopped.");
|
|
316
|
+
void report(null, failed ? { agent, code, message: error?.message ?? (tail.trim() || `${AGENT_LABELS[agent]} exited with code ${code}.`) } : null);
|
|
317
|
+
};
|
|
318
|
+
running.on("error", (error) => end(null, error));
|
|
319
|
+
running.on("close", (code) => end(code, null));
|
|
320
|
+
await report({ workspace, agent });
|
|
278
321
|
};
|
|
279
322
|
|
|
280
323
|
while (child || Date.now() - idleSince < IDLE_EXIT_MS) {
|
|
@@ -288,8 +331,14 @@ async function run(base, key) {
|
|
|
288
331
|
announce(true);
|
|
289
332
|
if (response.status !== 200) continue;
|
|
290
333
|
const command = await response.json();
|
|
291
|
-
if (command.kind === "
|
|
292
|
-
|
|
334
|
+
if (command.kind === "update" && !child) {
|
|
335
|
+
updateItself(base);
|
|
336
|
+
} else if (command.kind === "stop" && child) {
|
|
337
|
+
stopping = true;
|
|
338
|
+
child.kill("SIGTERM");
|
|
339
|
+
} else if (command.kind === "start" && !child && agents.includes(command.agent)) {
|
|
340
|
+
await start(command);
|
|
341
|
+
}
|
|
293
342
|
}
|
|
294
343
|
console.log("Idle for 30 minutes; disconnected.");
|
|
295
344
|
}
|