always-on-events.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { readdir, readFile } from 'node:fs/promises';
  2. import { resolve } from 'node:path';
  3. import { resolvePilotHome, createProjectId } from '../utils/pilotPaths.js';
  4. import { getPilotDeckGateway } from '../pilotdeck-bridge.js';
  5. /**
  6. * Read phase events from a single project's events.jsonl.
  7. *
  8. * @param {string} projectDir Absolute path to the project's always-on dir
  9. * (e.g. `~/.pilotdeck/always-on/projects/<id>`)
  10. * @returns {Array<object>}
  11. */
  12. async function readProjectEvents(projectDir) {
  13. const eventsFile = resolve(projectDir, 'events.jsonl');
  14. let raw;
  15. try {
  16. raw = await readFile(eventsFile, 'utf-8');
  17. } catch {
  18. return [];
  19. }
  20. const events = [];
  21. for (const line of raw.trim().split('\n')) {
  22. if (!line) continue;
  23. try {
  24. events.push(JSON.parse(line));
  25. } catch {
  26. // skip malformed
  27. }
  28. }
  29. return events;
  30. }
  31. /**
  32. * Build a lookup from projectKey -> { projectName, projectDisplayName }.
  33. */
  34. async function buildProjectLookup() {
  35. const gateway = await getPilotDeckGateway();
  36. const { projects } = await gateway.listProjects();
  37. const lookup = new Map();
  38. for (const project of projects) {
  39. const key = resolve(project.projectKey ?? project.fullPath ?? '');
  40. if (!key) continue;
  41. const name = createProjectId(key);
  42. const displayName = project.displayName || key.split(/[\\/]/).pop() || name;
  43. lookup.set(key, { projectName: name, projectDisplayName: displayName });
  44. }
  45. return lookup;
  46. }
  47. /**
  48. * Aggregate Always-On phase events across all projects.
  49. *
  50. * @param {{ limit?: number; since?: string }} [opts]
  51. * @returns {Promise<{ events: Array<object> }>}
  52. */
  53. export async function getAlwaysOnDashboardEvents(opts = {}) {
  54. const { limit = 200, since } = opts;
  55. const pilotHome = resolvePilotHome();
  56. const projectsDir = resolve(pilotHome, 'always-on', 'projects');
  57. let projectDirs;
  58. try {
  59. projectDirs = await readdir(projectsDir, { withFileTypes: true });
  60. } catch {
  61. return { events: [] };
  62. }
  63. const lookup = await buildProjectLookup().catch(() => new Map());
  64. const allEvents = [];
  65. for (const entry of projectDirs) {
  66. if (!entry.isDirectory()) continue;
  67. const dir = resolve(projectsDir, entry.name);
  68. const events = await readProjectEvents(dir);
  69. allEvents.push(...events);
  70. }
  71. let filtered = allEvents;
  72. if (since) {
  73. const sinceMs = Date.parse(since);
  74. if (Number.isFinite(sinceMs)) {
  75. filtered = filtered.filter((e) => Date.parse(e.timestamp) >= sinceMs);
  76. }
  77. }
  78. filtered.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
  79. if (limit > 0) {
  80. filtered = filtered.slice(0, limit);
  81. }
  82. const events = filtered.map((event) => {
  83. const key = resolve(event.projectKey || '');
  84. const info = lookup.get(key) || {
  85. projectName: createProjectId(key || 'unknown'),
  86. projectDisplayName: key.split(/[\\/]/).pop() || 'Unknown',
  87. };
  88. return {
  89. ...event,
  90. projectName: info.projectName,
  91. projectDisplayName: info.projectDisplayName,
  92. };
  93. });
  94. return { events };
  95. }