feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @file Shared helpers for starting and stopping the npm-managed monitoring stack.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawn, spawnSync } = require("node:child_process");
|
||||
const {
|
||||
DATA_ROOT,
|
||||
RUNTIME_PROVISIONING,
|
||||
GRAFANA_DASHBOARDS,
|
||||
GRAFANA_DATASOURCE_TEMPLATE,
|
||||
PROMETHEUS_PID,
|
||||
GRAFANA_PID,
|
||||
PROMETHEUS_LOG,
|
||||
GRAFANA_LOG,
|
||||
isWindows,
|
||||
toGrafanaPath,
|
||||
} = require("./paths");
|
||||
|
||||
function readPid(file) {
|
||||
try {
|
||||
const raw = fs.readFileSync(file, "utf8").trim();
|
||||
const pid = Number(raw);
|
||||
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isRunning(pid) {
|
||||
if (!pid) return false;
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writePid(file, pid) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, String(pid));
|
||||
}
|
||||
|
||||
function removePid(file) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
} catch {
|
||||
/* already gone */
|
||||
}
|
||||
}
|
||||
|
||||
function killProcess(pid) {
|
||||
if (!pid) return;
|
||||
if (isWindows()) {
|
||||
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
||||
return;
|
||||
}
|
||||
process.kill(pid, "SIGTERM");
|
||||
}
|
||||
|
||||
function stopPid(file, label) {
|
||||
const pid = readPid(file);
|
||||
if (!pid) return false;
|
||||
if (!isRunning(pid)) {
|
||||
removePid(file);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
killProcess(pid);
|
||||
console.log(`Stopped ${label} (pid ${pid})`);
|
||||
} catch (err) {
|
||||
console.warn(`Could not stop ${label} (pid ${pid}): ${err.message}`);
|
||||
}
|
||||
removePid(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
function writeGrafanaProvisioning() {
|
||||
const dashboardsDir = path.join(RUNTIME_PROVISIONING, "dashboards");
|
||||
const datasourcesDir = path.join(RUNTIME_PROVISIONING, "datasources");
|
||||
fs.mkdirSync(dashboardsDir, { recursive: true });
|
||||
fs.mkdirSync(datasourcesDir, { recursive: true });
|
||||
|
||||
fs.copyFileSync(GRAFANA_DATASOURCE_TEMPLATE, path.join(datasourcesDir, "datasource.yml"));
|
||||
|
||||
const provider = [
|
||||
"apiVersion: 1",
|
||||
"",
|
||||
"providers:",
|
||||
" - name: CCAM",
|
||||
" orgId: 1",
|
||||
" type: file",
|
||||
" disableDeletion: false",
|
||||
" updateIntervalSeconds: 30",
|
||||
" allowUiUpdates: true",
|
||||
" options:",
|
||||
` path: ${toGrafanaPath(GRAFANA_DASHBOARDS)}`,
|
||||
" foldersFromFilesStructure: false",
|
||||
"",
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(dashboardsDir, "provider.yml"), provider);
|
||||
}
|
||||
|
||||
function spawnDetached(binary, args, env, logFile) {
|
||||
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
||||
const logFd = fs.openSync(logFile, "a");
|
||||
const child = spawn(binary, args, {
|
||||
detached: !isWindows(),
|
||||
stdio: ["ignore", logFd, logFd],
|
||||
env: { ...process.env, ...env },
|
||||
windowsHide: true,
|
||||
});
|
||||
if (!isWindows()) child.unref();
|
||||
fs.closeSync(logFd);
|
||||
return child.pid;
|
||||
}
|
||||
|
||||
function spawnForeground(binary, args, env) {
|
||||
return spawn(binary, args, {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, ...env },
|
||||
windowsHide: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForHttp(url, attempts = 30, intervalMs = 500) {
|
||||
for (let i = 0; i < attempts; i += 1) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return true;
|
||||
} catch {
|
||||
/* retry */
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, intervalMs));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readPid,
|
||||
isRunning,
|
||||
writePid,
|
||||
removePid,
|
||||
killProcess,
|
||||
stopPid,
|
||||
writeGrafanaProvisioning,
|
||||
spawnDetached,
|
||||
spawnForeground,
|
||||
waitForHttp,
|
||||
PROMETHEUS_PID,
|
||||
GRAFANA_PID,
|
||||
PROMETHEUS_LOG,
|
||||
GRAFANA_LOG,
|
||||
DATA_ROOT,
|
||||
RUNTIME_PROVISIONING,
|
||||
};
|
||||
Reference in New Issue
Block a user