← back to Desktop Dotbar

realm.js

77 lines

'use strict';
// realm.js — sort each live session into a FOLDER: 'dw' (Designer Wallcoverings work) or
// 'other' (everything else), so the bar can be scoped to one folder at a time (TK-12234).
// Signals, strongest first: the ticket's project + title (tk event log), the session's
// `doing` label, then the claude process cwd. Zero deps, $0 local.

const fs = require('fs');
const { execFile } = require('child_process');

const EVENTS = `${process.env.HOME}/.claude/tickets/events.jsonl`;

// DW vocabulary. Word-ish boundaries so "dw" doesn't match inside other words.
const DW_RE = new RegExp([
  '(^|[^a-z])dw([^a-z]|$)',            // dw, dw-commerce, dw_unified, DWPP-, "dw ops"
  'designer[\\s_-]?wall',              // designerwallcoverings / Designer-Wallcoverings
  'shopify', 'gmc', 'merchant center', 'filemaker',
  '_catalog', 'vendor_registry', 'sample[\\s_-]?follow', 'samplesshipped',
  'kravet', 'phillipe romano', 'malibu wall', 'hollywood wall', 'fentucci',
  'microsite', 'mfr[\\s_-]?sku', 'memo sample',
].join('|'), 'i');

function isDwText(s) { return !!s && DW_RE.test(String(s)); }

// ---- ticket index: TK-N -> "project title", built incrementally from the append-only log ----
const idx = new Map();
let offset = 0, carry = '';
function indexTickets(file = EVENTS) {
  let st;
  try { st = fs.statSync(file); } catch { return idx; }
  if (st.size < offset) { idx.clear(); offset = 0; carry = ''; }   // log rotated/truncated -> rebuild
  if (st.size === offset) return idx;
  const fd = fs.openSync(file, 'r');
  try {
    const buf = Buffer.alloc(st.size - offset);
    fs.readSync(fd, buf, 0, buf.length, offset);
    offset = st.size;
    const lines = (carry + buf.toString('utf8')).split('\n');
    carry = lines.pop();                                            // partial last line waits for next pass
    for (const l of lines) {
      if (!l.includes('"create"')) continue;                        // cheap pre-filter before JSON.parse
      let e; try { e = JSON.parse(l); } catch { continue; }
      if (e.type !== 'create' || !e.id) continue;
      const m = /^(TK-\d+)/i.exec(e.id);
      if (m) idx.set(m[1].toUpperCase(), `${e.project || ''} ${e.title || ''}`);
    }
  } finally { fs.closeSync(fd); }
  return idx;
}
function _resetIndex() { idx.clear(); offset = 0; carry = ''; }

// ---- cwd of a pid (lsof), cached — a session's cwd rarely changes ----
const cwdCache = new Map();
function cwdOf(pid) {
  return new Promise((resolve) => {
    if (!pid) return resolve('');
    if (cwdCache.has(pid)) return resolve(cwdCache.get(pid));
    execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { timeout: 4000 }, (err, out) => {
      const line = String(out || '').split('\n').find(x => x.startsWith('n')) || '';
      const cwd = line.slice(1);
      if (cwd) cwdCache.set(pid, cwd);
      resolve(cwd);
    });
  });
}

// Classify one session {ticket, doing, pid}. Returns 'dw' | 'other'.
async function realmOf(s, opts = {}) {
  const tix = opts.index || indexTickets();
  if (s.ticket && isDwText(tix.get(s.ticket))) return 'dw';
  if (isDwText(s.doing)) return 'dw';
  const cwd = opts.cwd !== undefined ? opts.cwd : await cwdOf(s.pid);
  if (isDwText(cwd.replace(/^.*\/Projects\//, ''))) return 'dw';   // only the project part of the path
  return 'other';
}

module.exports = { realmOf, isDwText, indexTickets, _resetIndex };