57dc91585d
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.
159 lines
3.6 KiB
JavaScript
159 lines
3.6 KiB
JavaScript
/**
|
|
* @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,
|
|
};
|