always-on-run-logs.js 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import { promises as fs } from 'node:fs';
  2. import path from 'node:path';
  3. import { getAlwaysOnRoot } from './always-on-paths.js';
  4. function normalizeRunId(runId) {
  5. return typeof runId === 'string'
  6. ? runId.trim().replace(/[^a-zA-Z0-9._:-]/g, '-')
  7. : '';
  8. }
  9. function ensureTrailingNewline(value) {
  10. return value.endsWith('\n') ? value : `${value}\n`;
  11. }
  12. function getAlwaysOnRunsDir(projectRoot) {
  13. return path.join(getAlwaysOnRoot(projectRoot), 'runs');
  14. }
  15. function getRunLogPath(projectRoot, runId) {
  16. const safeRunId = normalizeRunId(runId);
  17. if (!safeRunId) {
  18. throw new Error('runId is required');
  19. }
  20. return path.join(getAlwaysOnRunsDir(projectRoot), `${safeRunId}.log`);
  21. }
  22. function getRunEventsPath(projectRoot, runId) {
  23. const safeRunId = normalizeRunId(runId);
  24. if (!safeRunId) {
  25. throw new Error('runId is required');
  26. }
  27. return path.join(getAlwaysOnRunsDir(projectRoot), `${safeRunId}.events.jsonl`);
  28. }
  29. export function formatAlwaysOnPlanLogLine({
  30. timestamp = new Date().toISOString(),
  31. level = 'info',
  32. runId,
  33. planId,
  34. phase,
  35. message,
  36. }) {
  37. const safeMessage = String(message || '').replace(/\s+/g, ' ').trim();
  38. return `[AlwaysOnPlanRun] ts=${timestamp} level=${level} runId=${runId} planId=${planId} phase=${phase} message=${JSON.stringify(safeMessage)}`;
  39. }
  40. export async function appendAlwaysOnRunLog(projectRoot, runId, lines) {
  41. const values = Array.isArray(lines) ? lines : [lines];
  42. const content = values
  43. .map((line) => (typeof line === 'string' ? line : String(line ?? '')))
  44. .filter((line) => line.length > 0)
  45. .map(ensureTrailingNewline)
  46. .join('');
  47. if (!content) {
  48. return;
  49. }
  50. await fs.mkdir(getAlwaysOnRunsDir(projectRoot), { recursive: true });
  51. await fs.appendFile(getRunLogPath(projectRoot, runId), content, 'utf8');
  52. }
  53. export async function appendAlwaysOnRunLogEvent(projectRoot, runId, event) {
  54. await fs.mkdir(getAlwaysOnRunsDir(projectRoot), { recursive: true });
  55. await fs.appendFile(
  56. getRunEventsPath(projectRoot, runId),
  57. `${JSON.stringify({
  58. timestamp: new Date().toISOString(),
  59. ...event,
  60. runId,
  61. })}\n`,
  62. 'utf8',
  63. );
  64. }