Files
AgentHub/statusline/install.js
T
nntrivi2001 4aae61297f feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
  Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
  with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
  Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
  its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
  status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
  reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
  ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files

Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
2026-10-06 17:12:28 +07:00

77 lines
3.2 KiB
JavaScript

/**
* @file `agenthub statusline install|uninstall`: writes or removes the
* `statusLine` entry in Claude Code's user settings (`<CLAUDE_HOME>/settings.json`)
* — only when the user runs the command, never at bootstrap. A statusLine that
* is not AgentHub's is left alone unless `force` is set, and the file is backed
* up beside itself (`settings.json.agenthub-backup-<stamp>`) before any write.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const REPO_ROOT = path.join(__dirname, "..");
/** Our own entries: `node ".../statusline/statusline.js"` or `".../agenthub" statusline`. */
const OURS = /(statusline[\\/]statusline\.js"?|agenthub"? statusline)\s*$/;
const isOurs = (entry) => typeof entry?.command === "string" && OURS.test(entry.command);
/**
* The command to register. A plugin install runs from a cache directory that
* every update replaces, so it goes through the stable `~/.local/bin/agenthub`
* launcher the bootstrap keeps pointed at the current build; a checkout runs
* the script in place.
*/
function statuslineCommand(root = REPO_ROOT, home = os.homedir()) {
const launcher = path.join(home, ".local", "bin", "agenthub");
const inCache = root.includes(`${path.sep}plugins${path.sep}cache${path.sep}`);
if (inCache && fs.existsSync(launcher)) return `"${launcher}" statusline`;
return `node "${path.join(root, "statusline", "statusline.js")}"`;
}
function readSettings(file) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return {};
throw new Error(`cannot read ${file}: ${err.message}`);
}
}
function writeSettings(file, settings) {
if (fs.existsSync(file)) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
fs.copyFileSync(file, `${file}.agenthub-backup-${stamp}`);
} else {
fs.mkdirSync(path.dirname(file), { recursive: true });
}
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n", "utf8");
}
/** @returns {{status: "installed"|"unchanged"|"foreign", command: string, previous?: object}} */
function install(file, { force = false, command = statuslineCommand() } = {}) {
const settings = readSettings(file);
const current = settings.statusLine;
if (current && !isOurs(current) && !force)
return { status: "foreign", command, previous: current };
if (current?.type === "command" && current.command === command)
return { status: "unchanged", command };
settings.statusLine = { type: "command", command, padding: 0 };
writeSettings(file, settings);
return { status: "installed", command, previous: current };
}
/** @returns {{status: "removed"|"absent"|"foreign", previous?: object}} */
function uninstall(file, { force = false } = {}) {
const settings = readSettings(file);
const current = settings.statusLine;
if (!current) return { status: "absent" };
if (!isOurs(current) && !force) return { status: "foreign", previous: current };
delete settings.statusLine;
writeSettings(file, settings);
return { status: "removed", previous: current };
}
module.exports = { install, uninstall, isOurs, statuslineCommand };