syntaur 0.16.2 → 0.16.3
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 +178 -70
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +178 -70
- package/dist/index.js.map +1 -1
- package/dist/launch/index.js +97 -55
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -4658,6 +4658,30 @@ function setUpdatedField(content, value) {
|
|
|
4658
4658
|
function escapeRegExp2(value) {
|
|
4659
4659
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4660
4660
|
}
|
|
4661
|
+
function createTraces() {
|
|
4662
|
+
return { entries: [], subPhases: /* @__PURE__ */ new Map() };
|
|
4663
|
+
}
|
|
4664
|
+
async function timed(traces, label, fn) {
|
|
4665
|
+
if (!traces) return fn();
|
|
4666
|
+
const start = performance.now();
|
|
4667
|
+
try {
|
|
4668
|
+
return await fn();
|
|
4669
|
+
} finally {
|
|
4670
|
+
traces.entries.push({ label, ms: performance.now() - start });
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
function accumulatePhase(traces, label, ms) {
|
|
4674
|
+
if (!traces) return;
|
|
4675
|
+
traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);
|
|
4676
|
+
}
|
|
4677
|
+
function emitTrace(traces, meta) {
|
|
4678
|
+
if (process.env.SYNTAUR_PERF_TRACE !== "1") return;
|
|
4679
|
+
const totalMs = traces.entries.reduce((sum, entry) => sum + entry.ms, 0);
|
|
4680
|
+
const subPhases = Object.fromEntries(traces.subPhases);
|
|
4681
|
+
console.log(
|
|
4682
|
+
JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
|
|
4683
|
+
);
|
|
4684
|
+
}
|
|
4661
4685
|
async function listStandaloneRecords(assignmentsDir2) {
|
|
4662
4686
|
if (!assignmentsDir2) return [];
|
|
4663
4687
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
@@ -4823,8 +4847,19 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
4823
4847
|
return { rewroteFiles };
|
|
4824
4848
|
}
|
|
4825
4849
|
async function getOverview(projectsDir, serversDir2, assignmentsDir2, options = {}) {
|
|
4826
|
-
const
|
|
4827
|
-
const
|
|
4850
|
+
const traceEnabled = process.env.SYNTAUR_PERF_TRACE === "1";
|
|
4851
|
+
const traces = traceEnabled ? createTraces() : void 0;
|
|
4852
|
+
const overallStart = traceEnabled ? performance.now() : 0;
|
|
4853
|
+
const projectRecords = await timed(
|
|
4854
|
+
traces,
|
|
4855
|
+
"list-project-records",
|
|
4856
|
+
() => listProjectRecords(projectsDir, traces)
|
|
4857
|
+
);
|
|
4858
|
+
const standaloneRecords = await timed(
|
|
4859
|
+
traces,
|
|
4860
|
+
"list-standalone-records",
|
|
4861
|
+
() => listStandaloneRecords(assignmentsDir2)
|
|
4862
|
+
);
|
|
4828
4863
|
const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
|
|
4829
4864
|
const staleLimit = clamp(
|
|
4830
4865
|
Number.isFinite(options.staleLimit) ? Number(options.staleLimit) : STALE_LIMIT_DEFAULT,
|
|
@@ -4832,12 +4867,16 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
4832
4867
|
STALE_LIMIT_MAX
|
|
4833
4868
|
);
|
|
4834
4869
|
const staleOffset = Math.max(0, Number.isFinite(options.staleOffset) ? Number(options.staleOffset) : 0);
|
|
4835
|
-
const buckets = await
|
|
4870
|
+
const buckets = await timed(
|
|
4871
|
+
traces,
|
|
4872
|
+
"build-segment-buckets",
|
|
4873
|
+
() => buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces)
|
|
4874
|
+
);
|
|
4836
4875
|
const segments = toOverviewSegments(buckets, { staleLimit, staleOffset });
|
|
4837
4876
|
const hero = pickOverviewHero(buckets);
|
|
4838
4877
|
let recentSessions = [];
|
|
4839
4878
|
try {
|
|
4840
|
-
const all = await listAllSessions(projectsDir);
|
|
4879
|
+
const all = await timed(traces, "list-recent-sessions", () => listAllSessions(projectsDir));
|
|
4841
4880
|
recentSessions = all.slice(0, RECENT_SESSIONS_LIMIT);
|
|
4842
4881
|
} catch {
|
|
4843
4882
|
}
|
|
@@ -4845,7 +4884,11 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
4845
4884
|
if (serversDir2) {
|
|
4846
4885
|
try {
|
|
4847
4886
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
4848
|
-
const servers = await
|
|
4887
|
+
const servers = await timed(
|
|
4888
|
+
traces,
|
|
4889
|
+
"scan-tmux-sessions",
|
|
4890
|
+
() => scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 })
|
|
4891
|
+
);
|
|
4849
4892
|
if (servers.tmuxAvailable) {
|
|
4850
4893
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
4851
4894
|
const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
|
|
@@ -4859,6 +4902,14 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
4859
4902
|
} catch {
|
|
4860
4903
|
}
|
|
4861
4904
|
}
|
|
4905
|
+
if (traces) {
|
|
4906
|
+
const wallMs = performance.now() - overallStart;
|
|
4907
|
+
const totalAssignments = projectRecords.reduce((sum, r) => sum + r.assignments.length, 0) + standaloneRecords.length;
|
|
4908
|
+
emitTrace(traces, {
|
|
4909
|
+
wallMs,
|
|
4910
|
+
fixture: { projects: projectRecords.length, assignments: totalAssignments }
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
4862
4913
|
return {
|
|
4863
4914
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4864
4915
|
firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,
|
|
@@ -5389,7 +5440,7 @@ async function buildStandaloneAssignmentDetail(resolved) {
|
|
|
5389
5440
|
};
|
|
5390
5441
|
return detail;
|
|
5391
5442
|
}
|
|
5392
|
-
async function listProjectRecords(projectsDir) {
|
|
5443
|
+
async function listProjectRecords(projectsDir, traces) {
|
|
5393
5444
|
if (!await fileExists(projectsDir)) {
|
|
5394
5445
|
return [];
|
|
5395
5446
|
}
|
|
@@ -5399,61 +5450,75 @@ async function listProjectRecords(projectsDir) {
|
|
|
5399
5450
|
}
|
|
5400
5451
|
const entries = await readdir8(projectsDir, { withFileTypes: true });
|
|
5401
5452
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
}
|
|
5409
|
-
const projectContent = await readFile10(projectMdPath, "utf-8");
|
|
5410
|
-
const project = parseProject(projectContent);
|
|
5411
|
-
const assignments = await listAssignmentRecords(projectPath);
|
|
5412
|
-
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
5413
|
-
const updated = getProjectActivityTimestamp(project.updated, assignments);
|
|
5414
|
-
records.push({
|
|
5415
|
-
projectPath,
|
|
5416
|
-
project,
|
|
5417
|
-
assignments,
|
|
5418
|
-
dependencyGraph: await loadDependencyGraph(projectPath, assignments),
|
|
5419
|
-
summary: {
|
|
5420
|
-
slug: project.slug || entry.name,
|
|
5421
|
-
title: project.title,
|
|
5422
|
-
status: rollup.status,
|
|
5423
|
-
statusOverride: project.statusOverride,
|
|
5424
|
-
archived: project.archived,
|
|
5425
|
-
archivedAt: project.archivedAt,
|
|
5426
|
-
archivedReason: project.archivedReason,
|
|
5427
|
-
created: project.created,
|
|
5428
|
-
updated,
|
|
5429
|
-
tags: project.tags,
|
|
5430
|
-
progress: rollup.progress,
|
|
5431
|
-
needsAttention: rollup.needsAttention,
|
|
5432
|
-
workspace: project.workspace
|
|
5453
|
+
const maybeRecords = await Promise.all(
|
|
5454
|
+
projectDirs.map(async (entry) => {
|
|
5455
|
+
const projectPath = resolve13(projectsDir, entry.name);
|
|
5456
|
+
const projectMdPath = resolve13(projectPath, "project.md");
|
|
5457
|
+
if (!await fileExists(projectMdPath)) {
|
|
5458
|
+
return null;
|
|
5433
5459
|
}
|
|
5434
|
-
|
|
5435
|
-
|
|
5460
|
+
const t0 = traces ? performance.now() : 0;
|
|
5461
|
+
const projectContent = await readFile10(projectMdPath, "utf-8");
|
|
5462
|
+
const project = parseProject(projectContent);
|
|
5463
|
+
if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
|
|
5464
|
+
const t1 = traces ? performance.now() : 0;
|
|
5465
|
+
const assignments = await listAssignmentRecords(projectPath, traces);
|
|
5466
|
+
if (traces) accumulatePhase(traces, "list-assignments", performance.now() - t1);
|
|
5467
|
+
const t2 = traces ? performance.now() : 0;
|
|
5468
|
+
const rollup = await buildProjectRollup(projectPath, project, assignments, traces);
|
|
5469
|
+
if (traces) accumulatePhase(traces, "build-rollup", performance.now() - t2);
|
|
5470
|
+
const updated = getProjectActivityTimestamp(project.updated, assignments);
|
|
5471
|
+
const t3 = traces ? performance.now() : 0;
|
|
5472
|
+
const dependencyGraph = await loadDependencyGraph(projectPath, assignments);
|
|
5473
|
+
if (traces) accumulatePhase(traces, "load-dep-graph", performance.now() - t3);
|
|
5474
|
+
return {
|
|
5475
|
+
projectPath,
|
|
5476
|
+
project,
|
|
5477
|
+
assignments,
|
|
5478
|
+
dependencyGraph,
|
|
5479
|
+
summary: {
|
|
5480
|
+
slug: project.slug || entry.name,
|
|
5481
|
+
title: project.title,
|
|
5482
|
+
status: rollup.status,
|
|
5483
|
+
statusOverride: project.statusOverride,
|
|
5484
|
+
archived: project.archived,
|
|
5485
|
+
archivedAt: project.archivedAt,
|
|
5486
|
+
archivedReason: project.archivedReason,
|
|
5487
|
+
created: project.created,
|
|
5488
|
+
updated,
|
|
5489
|
+
tags: project.tags,
|
|
5490
|
+
progress: rollup.progress,
|
|
5491
|
+
needsAttention: rollup.needsAttention,
|
|
5492
|
+
workspace: project.workspace
|
|
5493
|
+
}
|
|
5494
|
+
};
|
|
5495
|
+
})
|
|
5496
|
+
);
|
|
5497
|
+
const records = maybeRecords.filter((r) => r !== null);
|
|
5436
5498
|
records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
|
|
5437
5499
|
return records;
|
|
5438
5500
|
}
|
|
5439
|
-
async function listAssignmentRecords(projectPath) {
|
|
5501
|
+
async function listAssignmentRecords(projectPath, traces) {
|
|
5440
5502
|
const assignmentsDir2 = resolve13(projectPath, "assignments");
|
|
5441
5503
|
if (!await fileExists(assignmentsDir2)) {
|
|
5442
5504
|
return [];
|
|
5443
5505
|
}
|
|
5444
5506
|
const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
|
|
5445
|
-
const
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5507
|
+
const dirEntries = entries.filter((entry) => entry.isDirectory());
|
|
5508
|
+
const maybeRecords = await Promise.all(
|
|
5509
|
+
dirEntries.map(async (entry) => {
|
|
5510
|
+
const assignmentMd = resolve13(assignmentsDir2, entry.name, "assignment.md");
|
|
5511
|
+
if (!await fileExists(assignmentMd)) {
|
|
5512
|
+
return null;
|
|
5513
|
+
}
|
|
5514
|
+
const t0 = traces ? performance.now() : 0;
|
|
5515
|
+
const content = await readFile10(assignmentMd, "utf-8");
|
|
5516
|
+
const parsed = parseAssignmentFull(content);
|
|
5517
|
+
if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
|
|
5518
|
+
return parsed;
|
|
5519
|
+
})
|
|
5520
|
+
);
|
|
5521
|
+
const records = maybeRecords.filter((r) => r !== null);
|
|
5457
5522
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
5458
5523
|
return records;
|
|
5459
5524
|
}
|
|
@@ -5611,13 +5676,20 @@ async function loadDependencyGraph(projectPath, assignments) {
|
|
|
5611
5676
|
}
|
|
5612
5677
|
return buildDependencyGraph(assignments);
|
|
5613
5678
|
}
|
|
5614
|
-
async function buildProjectRollup(projectPath, project, assignments) {
|
|
5679
|
+
async function buildProjectRollup(projectPath, project, assignments, traces) {
|
|
5615
5680
|
const progress = { total: assignments.length };
|
|
5681
|
+
const perAssignment = await Promise.all(
|
|
5682
|
+
assignments.map(async (assignment) => {
|
|
5683
|
+
const t0 = traces ? performance.now() : 0;
|
|
5684
|
+
const openQuestions2 = await countOpenQuestions(projectPath, assignment.slug);
|
|
5685
|
+
if (traces) accumulatePhase(traces, "count-open-questions", performance.now() - t0);
|
|
5686
|
+
return { status: assignment.status, openQuestions: openQuestions2 };
|
|
5687
|
+
})
|
|
5688
|
+
);
|
|
5616
5689
|
let openQuestions = 0;
|
|
5617
|
-
for (const
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
openQuestions += await countOpenQuestions(projectPath, assignment.slug);
|
|
5690
|
+
for (const entry of perAssignment) {
|
|
5691
|
+
progress[entry.status] = (progress[entry.status] ?? 0) + 1;
|
|
5692
|
+
openQuestions += entry.openQuestions;
|
|
5621
5693
|
}
|
|
5622
5694
|
const needsAttention = {
|
|
5623
5695
|
blockedCount: progress["blocked"] ?? 0,
|
|
@@ -5698,18 +5770,26 @@ function buildDependencyGraph(assignments) {
|
|
|
5698
5770
|
function findAssignmentStatus(assignments, slug) {
|
|
5699
5771
|
return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
|
|
5700
5772
|
}
|
|
5701
|
-
async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment) {
|
|
5773
|
+
async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment, options) {
|
|
5702
5774
|
const config = await getStatusConfig();
|
|
5703
5775
|
const transitionDefs = getTransitionDefinitions(config);
|
|
5704
5776
|
const actions = [];
|
|
5705
5777
|
const projectPath = resolve13(projectsDir, projectSlug);
|
|
5778
|
+
const traces = options?.traces;
|
|
5706
5779
|
for (const definition of transitionDefs) {
|
|
5707
5780
|
let warning = null;
|
|
5708
5781
|
if (definition.command === "start" && !assignment.assignee) {
|
|
5709
5782
|
warning = "No assignee set \u2014 consider assigning before starting.";
|
|
5710
5783
|
}
|
|
5711
5784
|
if (definition.command === "start" && assignment.dependsOn.length > 0) {
|
|
5712
|
-
const
|
|
5785
|
+
const t0 = traces ? performance.now() : 0;
|
|
5786
|
+
const unmetDependencies = await getUnmetDependencies(
|
|
5787
|
+
projectPath,
|
|
5788
|
+
assignment.dependsOn,
|
|
5789
|
+
config.terminalStatuses,
|
|
5790
|
+
options?.dependencyStatusMap
|
|
5791
|
+
);
|
|
5792
|
+
if (traces) accumulatePhase(traces, "get-unmet-dependencies", performance.now() - t0);
|
|
5713
5793
|
if (unmetDependencies.length > 0) {
|
|
5714
5794
|
warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
|
|
5715
5795
|
}
|
|
@@ -5728,10 +5808,19 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
5728
5808
|
}
|
|
5729
5809
|
return actions;
|
|
5730
5810
|
}
|
|
5731
|
-
async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
5811
|
+
async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, dependencyStatusMap) {
|
|
5732
5812
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
5733
5813
|
const unmet = [];
|
|
5734
5814
|
for (const dependency of dependsOn) {
|
|
5815
|
+
if (dependencyStatusMap) {
|
|
5816
|
+
const mappedStatus = dependencyStatusMap.get(dependency);
|
|
5817
|
+
if (mappedStatus !== void 0) {
|
|
5818
|
+
if (!terminals.has(mappedStatus)) {
|
|
5819
|
+
unmet.push(`${dependency} (${mappedStatus})`);
|
|
5820
|
+
}
|
|
5821
|
+
continue;
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5735
5824
|
const dependencyPath = resolve13(projectPath, "assignments", dependency, "assignment.md");
|
|
5736
5825
|
if (!await fileExists(dependencyPath)) {
|
|
5737
5826
|
unmet.push(`${dependency} (missing)`);
|
|
@@ -5769,23 +5858,35 @@ function segmentSeverity(segment) {
|
|
|
5769
5858
|
return "medium";
|
|
5770
5859
|
}
|
|
5771
5860
|
}
|
|
5772
|
-
async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords) {
|
|
5861
|
+
async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces) {
|
|
5773
5862
|
const now = Date.now();
|
|
5774
5863
|
const buckets = emptyBuckets();
|
|
5775
5864
|
const newestPool = [];
|
|
5776
5865
|
for (const record of projectRecords) {
|
|
5777
|
-
|
|
5866
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
5867
|
+
for (const a of record.assignments) {
|
|
5868
|
+
depMap.set(a.slug, a.status);
|
|
5869
|
+
}
|
|
5870
|
+
const resolvedTransitions = await Promise.all(
|
|
5871
|
+
record.assignments.map(async (assignment) => {
|
|
5872
|
+
const t0 = traces ? performance.now() : 0;
|
|
5873
|
+
const availableTransitions = await getAvailableTransitions(
|
|
5874
|
+
projectsDir,
|
|
5875
|
+
record.summary.slug,
|
|
5876
|
+
assignment.slug,
|
|
5877
|
+
assignment,
|
|
5878
|
+
{ traces, dependencyStatusMap: depMap }
|
|
5879
|
+
);
|
|
5880
|
+
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
5881
|
+
return { assignment, availableTransitions };
|
|
5882
|
+
})
|
|
5883
|
+
);
|
|
5884
|
+
for (const { assignment, availableTransitions } of resolvedTransitions) {
|
|
5778
5885
|
const segmentId = STATUS_TO_SEGMENT[assignment.status];
|
|
5779
5886
|
const stale = isStale(assignment.updated);
|
|
5780
5887
|
const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
|
|
5781
5888
|
const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
|
|
5782
5889
|
const baseId = `${record.summary.slug}:${assignment.slug}`;
|
|
5783
|
-
const availableTransitions = await getAvailableTransitions(
|
|
5784
|
-
projectsDir,
|
|
5785
|
-
record.summary.slug,
|
|
5786
|
-
assignment.slug,
|
|
5787
|
-
assignment
|
|
5788
|
-
);
|
|
5789
5890
|
const shared = {
|
|
5790
5891
|
projectSlug: record.summary.slug,
|
|
5791
5892
|
projectTitle: record.summary.title,
|
|
@@ -5835,14 +5936,21 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
5835
5936
|
}
|
|
5836
5937
|
}
|
|
5837
5938
|
}
|
|
5838
|
-
|
|
5939
|
+
const resolvedStandaloneTransitions = await Promise.all(
|
|
5940
|
+
standaloneRecords.map(async (sr) => {
|
|
5941
|
+
const t0 = traces ? performance.now() : 0;
|
|
5942
|
+
const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
|
|
5943
|
+
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
5944
|
+
return { sr, availableTransitions };
|
|
5945
|
+
})
|
|
5946
|
+
);
|
|
5947
|
+
for (const { sr, availableTransitions } of resolvedStandaloneTransitions) {
|
|
5839
5948
|
const assignment = sr.record;
|
|
5840
5949
|
const segmentId = STATUS_TO_SEGMENT[assignment.status];
|
|
5841
5950
|
const stale = isStale(assignment.updated);
|
|
5842
5951
|
const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
|
|
5843
5952
|
const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
|
|
5844
5953
|
const baseId = `standalone:${sr.id}`;
|
|
5845
|
-
const availableTransitions = await getStandaloneAvailableTransitions(assignment);
|
|
5846
5954
|
const shared = {
|
|
5847
5955
|
projectSlug: null,
|
|
5848
5956
|
projectTitle: null,
|