mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(cli): codeman status probes the running server (#230)
Reported by @mtiller. `codeman status` runs in its own fresh process, and reported THAT process's always-stopped Ralph loop under a bare "Status:", which reads as "the web server is down" while the service is running fine and agents are reachable. It now probes the real server first (`CODEMAN_API_URL`, else https then http on the local port, overridable with `--url`) and reports reachability, version and live session state. Any HTTP answer proves the server is up, including a 401 from a password-protected install. The Ralph loop keeps its own `codeman ralph status`. This complements `codeman web --status` from the daemon work: that answers "did I start a daemon", this answers "is a server running at all", which is what the bare command was already being used for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+133
-28
@@ -577,47 +577,152 @@ function printStats(stats: ReturnType<ReturnType<typeof getRalphLoop>['getStats'
|
|||||||
|
|
||||||
// ============ Utility Commands ============
|
// ============ Utility Commands ============
|
||||||
|
|
||||||
|
/** What probing the web server found. */
|
||||||
|
interface WebServerProbe {
|
||||||
|
reachable: boolean;
|
||||||
|
/** The URL that answered, or the first candidate when nothing did. */
|
||||||
|
url: string;
|
||||||
|
statusCode?: number;
|
||||||
|
version?: string;
|
||||||
|
authRequired?: boolean;
|
||||||
|
/** Live session states from `/api/status`, when the probe could read them. */
|
||||||
|
sessions?: Array<{ status?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET `<base>/api/status` with a short timeout, tolerating the self-signed cert an
|
||||||
|
* `--https` install uses. ANY HTTP answer proves the server is up: a 401 just
|
||||||
|
* means it wants credentials (sent when available, same env → data-dir `.env`
|
||||||
|
* fallback as `codeman attach`).
|
||||||
|
*/
|
||||||
|
function probeWebServerAt(base: string): Promise<WebServerProbe | null> {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL('/api/status', base);
|
||||||
|
} catch {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
const envFile = readCodemanEnv();
|
||||||
|
const username = process.env.CODEMAN_USERNAME || envFile.CODEMAN_USERNAME || 'admin';
|
||||||
|
const password = process.env.CODEMAN_PASSWORD || envFile.CODEMAN_PASSWORD;
|
||||||
|
const transport = url.protocol === 'https:' ? https : http;
|
||||||
|
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||||
|
if (password) {
|
||||||
|
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = transport.request(
|
||||||
|
{
|
||||||
|
protocol: url.protocol,
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
method: 'GET',
|
||||||
|
path: url.pathname,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
headers,
|
||||||
|
timeout: 3000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let received = 0;
|
||||||
|
res.on('data', (chunk: Buffer) => {
|
||||||
|
received += chunk.length;
|
||||||
|
if (received <= 1024 * 1024) chunks.push(chunk);
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
const statusCode = res.statusCode ?? 0;
|
||||||
|
if (statusCode === 401) {
|
||||||
|
resolve({ reachable: true, url: base, statusCode, authRequired: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let version: string | undefined;
|
||||||
|
let sessions: Array<{ status?: string }> | undefined;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf-8')) as {
|
||||||
|
data?: { version?: unknown; sessions?: unknown };
|
||||||
|
};
|
||||||
|
const data = parsed?.data ?? (parsed as { version?: unknown; sessions?: unknown });
|
||||||
|
if (typeof data?.version === 'string') version = data.version;
|
||||||
|
if (Array.isArray(data?.sessions)) sessions = data.sessions as Array<{ status?: string }>;
|
||||||
|
} catch {
|
||||||
|
// Not JSON, but still an answer, so still running.
|
||||||
|
}
|
||||||
|
resolve({ reachable: true, url: base, statusCode, version, sessions });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
req.on('timeout', () => req.destroy(new Error('timeout')));
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('status')
|
.command('status')
|
||||||
.description('Show overall status')
|
.description('Show whether the Codeman web server is running, plus session/task state')
|
||||||
.action(() => {
|
.option('--url <url>', 'Server URL to probe (defaults to CODEMAN_API_URL, then local port)')
|
||||||
const manager = getSessionManager();
|
.action(async (options: { url?: string }) => {
|
||||||
const queue = getTaskQueue();
|
// Issue #230: this command runs in its own fresh process, and the old output
|
||||||
const loop = getRalphLoop();
|
// reported THAT process's (always-stopped) Ralph loop under a bare "Status:",
|
||||||
|
// reading as "the server is down" while the web service ran fine. Probe the
|
||||||
const sessions = manager.getAllSessions();
|
// real server first; the Ralph loop has its own `codeman ralph status`.
|
||||||
const stored = manager.getStoredSessions();
|
const port = process.env.CODEMAN_PORT || '3000';
|
||||||
const storedValues = Object.values(stored);
|
const candidates = options.url
|
||||||
const taskCounts = queue.getCount();
|
? [options.url]
|
||||||
const loopStatus = loop.status;
|
: process.env.CODEMAN_API_URL
|
||||||
|
? [process.env.CODEMAN_API_URL]
|
||||||
// Use live sessions if available, otherwise fall back to stored state
|
: [`https://127.0.0.1:${port}`, `http://127.0.0.1:${port}`];
|
||||||
const activeCount = sessions.length || storedValues.filter((s) => s.status !== 'stopped').length;
|
let probe: WebServerProbe = { reachable: false, url: candidates[0] };
|
||||||
const idleCount = sessions.length
|
for (const candidate of candidates) {
|
||||||
? sessions.filter((s) => s.isIdle()).length
|
const answer = await probeWebServerAt(candidate);
|
||||||
: storedValues.filter((s) => s.status === 'idle').length;
|
if (answer) {
|
||||||
const busyCount = sessions.length
|
probe = answer;
|
||||||
? sessions.filter((s) => s.isBusy()).length
|
break;
|
||||||
: storedValues.filter((s) => s.status === 'busy').length;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chalk.bold('\nCodeman Status'));
|
console.log(chalk.bold('\nCodeman Status'));
|
||||||
console.log('─'.repeat(40));
|
console.log('─'.repeat(40));
|
||||||
|
|
||||||
console.log(chalk.bold('\nSessions:'));
|
console.log(chalk.bold('\nWeb Server:'));
|
||||||
console.log(` Active: ${activeCount}`);
|
if (probe.reachable) {
|
||||||
console.log(` Idle: ${idleCount}`);
|
const version = probe.version ? ` (v${probe.version})` : '';
|
||||||
console.log(` Busy: ${busyCount}`);
|
console.log(` Status: ${chalk.green('running')}${version} at ${probe.url}`);
|
||||||
|
if (probe.authRequired) {
|
||||||
|
console.log(chalk.gray(' (answers 401: set CODEMAN_PASSWORD/CODEMAN_USERNAME to see session details)'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Status: ${chalk.red('not reachable')} at ${candidates.join(' or ')}`);
|
||||||
|
console.log(
|
||||||
|
chalk.gray(' (start it with `codeman web`, or check your service: systemctl --user status codeman-web)')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the server's live view; fall back to the shared saved state, labeled
|
||||||
|
// as such, so the numbers are never silently a different thing.
|
||||||
|
if (probe.sessions) {
|
||||||
|
const live = probe.sessions;
|
||||||
|
console.log(chalk.bold('\nSessions (live, from the server):'));
|
||||||
|
console.log(` Total: ${live.length}`);
|
||||||
|
console.log(` Idle: ${live.filter((s) => s.status === 'idle').length}`);
|
||||||
|
console.log(` Busy: ${live.filter((s) => s.status === 'busy').length}`);
|
||||||
|
} else {
|
||||||
|
const manager = getSessionManager();
|
||||||
|
const storedValues = Object.values(manager.getStoredSessions());
|
||||||
|
console.log(chalk.bold('\nSessions (from saved state):'));
|
||||||
|
console.log(` Active: ${storedValues.filter((s) => s.status !== 'stopped').length}`);
|
||||||
|
console.log(` Idle: ${storedValues.filter((s) => s.status === 'idle').length}`);
|
||||||
|
console.log(` Busy: ${storedValues.filter((s) => s.status === 'busy').length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskCounts = getTaskQueue().getCount();
|
||||||
console.log(chalk.bold('\nTasks:'));
|
console.log(chalk.bold('\nTasks:'));
|
||||||
console.log(` Total: ${taskCounts.total}`);
|
console.log(` Total: ${taskCounts.total}`);
|
||||||
console.log(` Pending: ${taskCounts.pending}`);
|
console.log(` Pending: ${taskCounts.pending}`);
|
||||||
console.log(` Running: ${taskCounts.running}`);
|
console.log(` Running: ${taskCounts.running}`);
|
||||||
console.log(` Completed: ${taskCounts.completed}`);
|
console.log(` Completed: ${taskCounts.completed}`);
|
||||||
console.log(` Failed: ${taskCounts.failed}`);
|
console.log(` Failed: ${taskCounts.failed}`);
|
||||||
|
|
||||||
const statusColor = loopStatus === 'running' ? chalk.green : loopStatus === 'paused' ? chalk.yellow : chalk.gray;
|
|
||||||
console.log(chalk.bold('\nRalph Loop:'));
|
|
||||||
console.log(` Status: ${statusColor(loopStatus)}`);
|
|
||||||
console.log('');
|
console.log('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user