← back to Abrams
scripts/inventory.js
132 lines
#!/usr/bin/env node
// inventory.js — discover every Abrams / AgentAbrams asset on this machine
// and stamp data/projects.json. Read-only. No prod writes.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const http = require('http');
const ROOT = path.resolve(__dirname, '..');
const HOME = process.env.HOME;
const OUT = path.join(ROOT, 'data', 'projects.json');
const LOG = path.join(ROOT, 'data', 'build-log.jsonl');
const log = (type, payload) => fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), type, ...payload }) + '\n');
// ── 1) walk ~/Projects/ for abrams / agentabrams dirs
const PROJECTS_DIR = path.join(HOME, 'Projects');
let dirs = [];
try {
dirs = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.filter(d => /abrams/i.test(d.name))
.map(d => path.join(PROJECTS_DIR, d.name));
} catch (e) { /* swallow */ }
// ── 2) pull pm2 jlist for live ports + status
let pm2 = [];
try {
const raw = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8' });
pm2 = JSON.parse(raw);
} catch (e) { pm2 = []; }
const pm2For = (dir) => pm2.find(p => (p.pm2_env?.pm_cwd || '').toLowerCase() === dir.toLowerCase());
const pm2ForName = (name) => pm2.find(p => p.name === name);
// ── 3) build project entries
const projects = [];
for (const dir of dirs) {
const name = path.basename(dir);
let pkg = {};
try { pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); } catch {}
let readme = '';
try { readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf8').split('\n').slice(0,8).join('\n'); } catch {}
const description = pkg.description || readme.match(/^[A-Z][^\n]{30,200}/m)?.[0] || '';
const p = pm2For(dir);
const status = p ? (p.pm2_env?.status === 'online' ? 'live' : 'dev') : 'dev';
const port = p?.pm2_env?.env?.PORT || (readme.match(/[Pp]ort[: ]+(\d{4})/) || [])[1] || null;
// git stats
let lastCommit = null, commitCount = 0;
try {
lastCommit = execSync(`cd "${dir}" && git log -1 --format="%ai %s" 2>/dev/null`, { encoding: 'utf8' }).trim();
commitCount = parseInt(execSync(`cd "${dir}" && git rev-list --count HEAD 2>/dev/null`, { encoding: 'utf8' }).trim(), 10) || 0;
} catch {}
projects.push({
slug: name.toLowerCase(),
name,
path: dir,
description: description.slice(0, 240),
status,
port,
domain: pkg.homepage || guessDomain(name),
category: classifyCategory(name, description),
pm2_name: p?.name || null,
pm2_pid: p?.pid || null,
pm2_uptime_ms: p?.pm2_env?.pm_uptime ? Date.now() - p.pm2_env.pm_uptime : null,
commit_count: commitCount,
last_commit: lastCommit,
created_at: tryCtime(dir),
});
}
function classifyCategory(name, desc) {
const n = (name + ' ' + desc).toLowerCase();
if (/report|news|drudge/.test(n)) return 'media';
if (/mp3|audio|video|podcast/.test(n)) return 'media';
if (/agentos|abramsos|rights|warranty|receipt|claim/.test(n)) return 'household-os';
if (/discovery|viewer|panel|dashboard/.test(n)) return 'panel';
if (/agentabrams|public/.test(n)) return 'umbrella';
if (/worker|cron|tick/.test(n)) return 'infra';
return 'misc';
}
function guessDomain(name) {
const n = name.toLowerCase();
if (n === 'agentabrams') return 'agentabrams.com';
if (n === 'abrams-report') return 'report.agentabrams.com';
if (n === 'mp3-agentabrams') return 'mp3.agentabrams.com';
if (n === 'abramsos') return 'os.agentabrams.com';
if (n === 'abrams-discovery-viewer') return 'discovery.agentabrams.com';
if (n === 'videos-agentabrams-worker') return 'videos.agentabrams.com';
if (n === 'abrams') return 'abrams.agentabrams.com (this panel)';
return null;
}
function tryCtime(dir) {
try { return new Date(fs.statSync(dir).birthtime || fs.statSync(dir).ctime).toISOString(); } catch { return null; }
}
// ── 4) also include known agentabrams.com subdomain projects living outside dirs
const externalKnown = [
{ name: 'lawyers.agentabrams.com', slug: 'lawyers-agentabrams', description: 'California bar attorney directory (lawyer-directory-builder).', status: 'live', domain: 'lawyers.agentabrams.com', category: 'directory', path: path.join(HOME, 'Projects/lawyer-directory-builder') },
{ name: 'sdccanalysis.agentabrams.com', slug: 'sdcc-agentabrams', description: 'Student loan & SDCC analysis hub.', status: 'live', domain: 'sdccanalysis.agentabrams.com', category: 'analysis', path: null },
{ name: 'butlr.agentabrams.com', slug: 'butlr-agentabrams', description: 'Butlr — AI dialer that holds for you.', status: 'live', domain: 'butlr.agentabrams.com', category: 'saas', path: path.join(HOME, 'Projects/butlr') },
];
for (const ek of externalKnown) {
if (!projects.find(p => p.slug === ek.slug)) projects.push({ ...ek, port: null, commit_count: null, last_commit: null, created_at: null });
}
// ── 5) self-reference: the Panel itself
projects.unshift({
slug: 'abrams-panel',
name: 'Abrams Panel',
path: ROOT,
description: 'The umbrella — one screen for every Abrams / AgentAbrams project. Live build log, lead capture, trademark hub, SWOT ledger.',
status: pm2ForName('abrams-panel') ? 'live' : 'dev',
port: 9898,
domain: 'panel.agentabrams.com',
category: 'panel',
created_at: tryCtime(ROOT),
});
// ── 6) write
const out = { updated_at: new Date().toISOString(), count: projects.length, projects };
fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
log('inventory', { count: projects.length, categories: [...new Set(projects.map(p => p.category))] });
console.log(`inventory: ${projects.length} projects → ${OUT}`);