syntaur 0.17.0 → 0.17.1
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/dist/dashboard/server.js +182 -58
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +184 -60
- package/dist/index.js.map +1 -1
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -4270,13 +4270,16 @@ __export(scanner_exports, {
|
|
|
4270
4270
|
autoLinkPane: () => autoLinkPane,
|
|
4271
4271
|
checkTmuxAvailable: () => checkTmuxAvailable,
|
|
4272
4272
|
clearScanCache: () => clearScanCache,
|
|
4273
|
+
descendantPidsFromSnapshot: () => descendantPidsFromSnapshot,
|
|
4273
4274
|
execQuiet: () => execQuiet,
|
|
4274
4275
|
findListeningPorts: () => findListeningPorts,
|
|
4275
4276
|
getDescendantPids: () => getDescendantPids,
|
|
4276
4277
|
getGitInfo: () => getGitInfo,
|
|
4277
4278
|
getLsofOutput: () => getLsofOutput,
|
|
4279
|
+
getProcessSnapshot: () => getProcessSnapshot,
|
|
4278
4280
|
listTmuxPanes: () => listTmuxPanes,
|
|
4279
4281
|
loadWorkspaceRecords: () => loadWorkspaceRecords,
|
|
4282
|
+
parseProcessSnapshot: () => parseProcessSnapshot,
|
|
4280
4283
|
parseTmuxPaneOutput: () => parseTmuxPaneOutput,
|
|
4281
4284
|
resolveAndNormalize: () => resolveAndNormalize,
|
|
4282
4285
|
scanAllSessions: () => scanAllSessions,
|
|
@@ -4289,6 +4292,23 @@ import { resolve as resolve12 } from "path";
|
|
|
4289
4292
|
import { realpath, readdir as readdir7, readFile as readFile9 } from "fs/promises";
|
|
4290
4293
|
function clearScanCache() {
|
|
4291
4294
|
cache = null;
|
|
4295
|
+
forceFreshNext = true;
|
|
4296
|
+
scanEpoch++;
|
|
4297
|
+
inFlight = null;
|
|
4298
|
+
}
|
|
4299
|
+
function emptyScan() {
|
|
4300
|
+
return { sessions: [], tmuxAvailable: false };
|
|
4301
|
+
}
|
|
4302
|
+
function scanKey(serversDir2, projectsDir, assignmentsDir2) {
|
|
4303
|
+
return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
|
|
4304
|
+
}
|
|
4305
|
+
function delay(ms) {
|
|
4306
|
+
return new Promise((resolve29) => {
|
|
4307
|
+
const timer2 = setTimeout(resolve29, ms);
|
|
4308
|
+
if (typeof timer2.unref === "function") {
|
|
4309
|
+
timer2.unref();
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4292
4312
|
}
|
|
4293
4313
|
function parseTmuxPaneOutput(output) {
|
|
4294
4314
|
return output.trim().split("\n").filter((line) => line.length > 0).map((line) => {
|
|
@@ -4322,7 +4342,10 @@ function findListeningPorts(lsofOutput, pids) {
|
|
|
4322
4342
|
}
|
|
4323
4343
|
async function execQuiet(cmd, args) {
|
|
4324
4344
|
try {
|
|
4325
|
-
const { stdout } = await exec(cmd, args
|
|
4345
|
+
const { stdout } = await exec(cmd, args, {
|
|
4346
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
4347
|
+
maxBuffer: PROBE_MAX_BUFFER
|
|
4348
|
+
});
|
|
4326
4349
|
return stdout.trim();
|
|
4327
4350
|
} catch {
|
|
4328
4351
|
return "";
|
|
@@ -4370,6 +4393,40 @@ async function getDescendantPids(rootPid, maxDepth = 4) {
|
|
|
4370
4393
|
}
|
|
4371
4394
|
return all;
|
|
4372
4395
|
}
|
|
4396
|
+
function parseProcessSnapshot(psOutput) {
|
|
4397
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4398
|
+
for (const line of psOutput.split("\n")) {
|
|
4399
|
+
const parts = line.trim().split(/\s+/);
|
|
4400
|
+
if (parts.length < 2) continue;
|
|
4401
|
+
const pid = parseInt(parts[0], 10);
|
|
4402
|
+
const ppid = parseInt(parts[1], 10);
|
|
4403
|
+
if (isNaN(pid) || isNaN(ppid)) continue;
|
|
4404
|
+
const siblings = childrenOf.get(ppid);
|
|
4405
|
+
if (siblings) siblings.push(pid);
|
|
4406
|
+
else childrenOf.set(ppid, [pid]);
|
|
4407
|
+
}
|
|
4408
|
+
return { childrenOf };
|
|
4409
|
+
}
|
|
4410
|
+
async function getProcessSnapshot() {
|
|
4411
|
+
return parseProcessSnapshot(await execQuiet("ps", ["-axo", "pid=,ppid="]));
|
|
4412
|
+
}
|
|
4413
|
+
function descendantPidsFromSnapshot(rootPid, snapshot, maxDepth = 4) {
|
|
4414
|
+
const all = /* @__PURE__ */ new Set([rootPid]);
|
|
4415
|
+
let frontier = [rootPid];
|
|
4416
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
4417
|
+
const nextFrontier = [];
|
|
4418
|
+
for (const pid of frontier) {
|
|
4419
|
+
for (const child of snapshot.childrenOf.get(pid) ?? []) {
|
|
4420
|
+
if (!all.has(child)) {
|
|
4421
|
+
all.add(child);
|
|
4422
|
+
nextFrontier.push(child);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
frontier = nextFrontier;
|
|
4427
|
+
}
|
|
4428
|
+
return all;
|
|
4429
|
+
}
|
|
4373
4430
|
async function getLsofOutput() {
|
|
4374
4431
|
return execQuiet("lsof", ["-i", "-P", "-n", "-sTCP:LISTEN"]);
|
|
4375
4432
|
}
|
|
@@ -4475,7 +4532,7 @@ async function autoLinkPane(cwd, branch, records) {
|
|
|
4475
4532
|
}
|
|
4476
4533
|
return null;
|
|
4477
4534
|
}
|
|
4478
|
-
async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
4535
|
+
async function scanSession(sessionData, lsofOutput, workspaceRecords, procSnapshot) {
|
|
4479
4536
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4480
4537
|
const alive = await sessionAlive(sessionData.session);
|
|
4481
4538
|
if (!alive) {
|
|
@@ -4498,13 +4555,13 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4498
4555
|
windowMap.get(rp.windowIndex).panes.push(rp);
|
|
4499
4556
|
}
|
|
4500
4557
|
const cwdSet = new Set(rawPanes.map((p) => p.cwd));
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4558
|
+
const gitInfoEntries = await Promise.all(
|
|
4559
|
+
[...cwdSet].map(async (cwd) => [cwd, await getGitInfo(cwd)])
|
|
4560
|
+
);
|
|
4561
|
+
const gitInfoCache = new Map(gitInfoEntries);
|
|
4505
4562
|
const pidToPaneKey = /* @__PURE__ */ new Map();
|
|
4506
4563
|
for (const rp of rawPanes) {
|
|
4507
|
-
const descendants =
|
|
4564
|
+
const descendants = descendantPidsFromSnapshot(rp.pid, procSnapshot);
|
|
4508
4565
|
const key = `${rp.windowIndex}:${rp.paneIndex}`;
|
|
4509
4566
|
for (const pid of descendants) {
|
|
4510
4567
|
pidToPaneKey.set(pid, key);
|
|
@@ -4529,37 +4586,38 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4529
4586
|
}
|
|
4530
4587
|
const windows = [];
|
|
4531
4588
|
for (const [windowIndex, { name, panes: rawPanesInWindow }] of windowMap) {
|
|
4532
|
-
const panes =
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4589
|
+
const panes = await Promise.all(
|
|
4590
|
+
rawPanesInWindow.map(async (rp) => {
|
|
4591
|
+
const key = `${rp.windowIndex}:${rp.paneIndex}`;
|
|
4592
|
+
const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
|
|
4593
|
+
const ports = panePorts.get(key) ?? [];
|
|
4594
|
+
const urls = ports.map((p) => `http://localhost:${p}`);
|
|
4595
|
+
const override = sessionData.overrides[key];
|
|
4596
|
+
let assignment = null;
|
|
4597
|
+
if (override) {
|
|
4598
|
+
const rec = workspaceRecords.find(
|
|
4599
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
4600
|
+
);
|
|
4601
|
+
assignment = {
|
|
4602
|
+
project: override.project,
|
|
4603
|
+
slug: override.assignment,
|
|
4604
|
+
title: rec?.assignmentTitle ?? override.assignment
|
|
4605
|
+
};
|
|
4606
|
+
} else {
|
|
4607
|
+
assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
|
|
4608
|
+
}
|
|
4609
|
+
return {
|
|
4610
|
+
index: rp.paneIndex,
|
|
4611
|
+
command: rp.command,
|
|
4612
|
+
cwd: rp.cwd,
|
|
4613
|
+
branch: gitInfo.branch,
|
|
4614
|
+
worktree: gitInfo.worktree,
|
|
4615
|
+
ports,
|
|
4616
|
+
urls,
|
|
4617
|
+
assignment
|
|
4548
4618
|
};
|
|
4549
|
-
}
|
|
4550
|
-
|
|
4551
|
-
}
|
|
4552
|
-
panes.push({
|
|
4553
|
-
index: rp.paneIndex,
|
|
4554
|
-
command: rp.command,
|
|
4555
|
-
cwd: rp.cwd,
|
|
4556
|
-
branch: gitInfo.branch,
|
|
4557
|
-
worktree: gitInfo.worktree,
|
|
4558
|
-
ports,
|
|
4559
|
-
urls,
|
|
4560
|
-
assignment
|
|
4561
|
-
});
|
|
4562
|
-
}
|
|
4619
|
+
})
|
|
4620
|
+
);
|
|
4563
4621
|
windows.push({ index: windowIndex, name, panes });
|
|
4564
4622
|
}
|
|
4565
4623
|
windows.sort((a, b) => a.index - b.index);
|
|
@@ -4631,39 +4689,92 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4631
4689
|
windows: [{ index: 0, name: "process", panes: [pane] }]
|
|
4632
4690
|
};
|
|
4633
4691
|
}
|
|
4692
|
+
async function performScan(serversDir2, projectsDir, options) {
|
|
4693
|
+
const epoch = scanEpoch;
|
|
4694
|
+
const [tmuxAvailable, names, lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
4695
|
+
checkTmuxAvailable(),
|
|
4696
|
+
listSessionFiles(serversDir2),
|
|
4697
|
+
getLsofOutput(),
|
|
4698
|
+
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
4699
|
+
getProcessSnapshot()
|
|
4700
|
+
]);
|
|
4701
|
+
const datas = await Promise.all(names.map((name) => readSessionFile(serversDir2, name)));
|
|
4702
|
+
const scanned = await Promise.all(
|
|
4703
|
+
datas.map(async (data) => {
|
|
4704
|
+
if (!data) return null;
|
|
4705
|
+
if (data.kind === "process") {
|
|
4706
|
+
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
4707
|
+
}
|
|
4708
|
+
if (tmuxAvailable) {
|
|
4709
|
+
return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
|
|
4710
|
+
}
|
|
4711
|
+
return null;
|
|
4712
|
+
})
|
|
4713
|
+
);
|
|
4714
|
+
const result = {
|
|
4715
|
+
sessions: scanned.filter((s) => s !== null),
|
|
4716
|
+
tmuxAvailable
|
|
4717
|
+
};
|
|
4718
|
+
if (epoch === scanEpoch) {
|
|
4719
|
+
cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
|
|
4720
|
+
lastKnown = result;
|
|
4721
|
+
forceFreshNext = false;
|
|
4722
|
+
}
|
|
4723
|
+
return result;
|
|
4724
|
+
}
|
|
4725
|
+
function refreshInBackground(serversDir2, projectsDir, options) {
|
|
4726
|
+
if (!inFlight) {
|
|
4727
|
+
const p = performScan(serversDir2, projectsDir, options);
|
|
4728
|
+
p.finally(() => {
|
|
4729
|
+
if (inFlight === p) inFlight = null;
|
|
4730
|
+
}).catch(() => {
|
|
4731
|
+
});
|
|
4732
|
+
inFlight = p;
|
|
4733
|
+
}
|
|
4734
|
+
return inFlight;
|
|
4735
|
+
}
|
|
4634
4736
|
async function scanAllSessions(serversDir2, projectsDir, options) {
|
|
4737
|
+
const key = scanKey(serversDir2, projectsDir, options?.assignmentsDir);
|
|
4738
|
+
if (key !== cacheKey) {
|
|
4739
|
+
cache = null;
|
|
4740
|
+
lastKnown = null;
|
|
4741
|
+
inFlight = null;
|
|
4742
|
+
forceFreshNext = false;
|
|
4743
|
+
scanEpoch++;
|
|
4744
|
+
cacheKey = key;
|
|
4745
|
+
}
|
|
4635
4746
|
if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
|
|
4636
4747
|
return cache.data;
|
|
4637
4748
|
}
|
|
4638
|
-
const
|
|
4639
|
-
const
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
const data = await readSessionFile(serversDir2, name);
|
|
4645
|
-
if (!data) continue;
|
|
4646
|
-
if (data.kind === "process") {
|
|
4647
|
-
sessions.push(await scanProcessSession(data, lsofOutput, workspaceRecords));
|
|
4648
|
-
} else if (tmuxAvailable) {
|
|
4649
|
-
sessions.push(await scanSession(data, lsofOutput, workspaceRecords));
|
|
4650
|
-
}
|
|
4749
|
+
const refresh = refreshInBackground(serversDir2, projectsDir, options);
|
|
4750
|
+
const canServeStale = lastKnown !== null && !options?.bypassCache && !forceFreshNext;
|
|
4751
|
+
if (canServeStale) {
|
|
4752
|
+
void refresh.catch(() => {
|
|
4753
|
+
});
|
|
4754
|
+
return lastKnown;
|
|
4651
4755
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4756
|
+
if (options?.nonBlocking) {
|
|
4757
|
+
return Promise.race([
|
|
4758
|
+
refresh.catch(() => emptyScan()),
|
|
4759
|
+
delay(COLD_WAIT_BUDGET_MS).then(() => emptyScan())
|
|
4760
|
+
]);
|
|
4761
|
+
}
|
|
4762
|
+
return refresh;
|
|
4655
4763
|
}
|
|
4656
4764
|
async function scanSingleSession(serversDir2, projectsDir, name, options) {
|
|
4657
4765
|
const data = await readSessionFile(serversDir2, name);
|
|
4658
4766
|
if (!data) return null;
|
|
4659
|
-
const lsofOutput = await
|
|
4660
|
-
|
|
4767
|
+
const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
4768
|
+
getLsofOutput(),
|
|
4769
|
+
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
4770
|
+
getProcessSnapshot()
|
|
4771
|
+
]);
|
|
4661
4772
|
if (data.kind === "process") {
|
|
4662
4773
|
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
4663
4774
|
}
|
|
4664
|
-
return scanSession(data, lsofOutput, workspaceRecords);
|
|
4775
|
+
return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
|
|
4665
4776
|
}
|
|
4666
|
-
var exec, cache, CACHE_TTL_MS;
|
|
4777
|
+
var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER;
|
|
4667
4778
|
var init_scanner = __esm({
|
|
4668
4779
|
"src/dashboard/scanner.ts"() {
|
|
4669
4780
|
"use strict";
|
|
@@ -4672,7 +4783,15 @@ var init_scanner = __esm({
|
|
|
4672
4783
|
init_parser();
|
|
4673
4784
|
exec = promisify(execFile);
|
|
4674
4785
|
cache = null;
|
|
4786
|
+
lastKnown = null;
|
|
4787
|
+
inFlight = null;
|
|
4788
|
+
forceFreshNext = false;
|
|
4789
|
+
scanEpoch = 0;
|
|
4790
|
+
cacheKey = null;
|
|
4675
4791
|
CACHE_TTL_MS = 1e4;
|
|
4792
|
+
COLD_WAIT_BUDGET_MS = 2500;
|
|
4793
|
+
PROBE_TIMEOUT_MS = 15e3;
|
|
4794
|
+
PROBE_MAX_BUFFER = 32 * 1024 * 1024;
|
|
4676
4795
|
}
|
|
4677
4796
|
});
|
|
4678
4797
|
|
|
@@ -4927,7 +5046,12 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
4927
5046
|
const servers = await timed(
|
|
4928
5047
|
traces,
|
|
4929
5048
|
"scan-tmux-sessions",
|
|
4930
|
-
() =>
|
|
5049
|
+
() => (
|
|
5050
|
+
// Overview only needs aggregate counts — never block its render on a
|
|
5051
|
+
// live scan; serve last-known stats and let the scan refresh in the
|
|
5052
|
+
// background (stale-while-revalidate).
|
|
5053
|
+
scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2, nonBlocking: true })
|
|
5054
|
+
)
|
|
4931
5055
|
);
|
|
4932
5056
|
if (servers.tmuxAvailable) {
|
|
4933
5057
|
const alive = servers.sessions.filter((s) => s.alive).length;
|