feat(phase4): just front door + safety rules + bugfix (Sam & Claude)
Rewrites justfile with 8 groups, 35+ recipes with docstrings. Creates 8 CLI helper scripts (jail-status, hostd-cli, system-health, agent-status, agent-task, agent-task-status, agent-logs, jail-provision). Adds 4 hostd safety rules to safety.yaml (destroy, rollback, zfs-destroy, pf). Fixes task_create empty assigned_to bug in controlplane-tools.ts. Build: pass | Tests: not run (Linux)
This commit is contained in:
parent
af659e7c56
commit
2160e7859e
10 changed files with 384 additions and 572 deletions
|
|
@ -6,41 +6,69 @@ rules:
|
|||
action: deny
|
||||
reason: Blocked dangerous rm -rf usage
|
||||
patterns:
|
||||
- "rm -rf"
|
||||
- "rm -fr"
|
||||
- 'rm -rf'
|
||||
- 'rm -fr'
|
||||
|
||||
- id: confirm-sudo
|
||||
tool: bash
|
||||
action: ask
|
||||
reason: Confirm sudo commands
|
||||
patterns:
|
||||
- "sudo "
|
||||
- 'sudo '
|
||||
|
||||
- id: block-env-write
|
||||
tool: write
|
||||
action: deny
|
||||
reason: Blocked writing .env files
|
||||
path_globs:
|
||||
- "**/.env"
|
||||
- "**/.env.*"
|
||||
- '**/.env'
|
||||
- '**/.env.*'
|
||||
|
||||
- id: block-ssh-read
|
||||
tool: read
|
||||
action: deny
|
||||
reason: Blocked reading SSH keys
|
||||
path_globs:
|
||||
- "**/.ssh/**"
|
||||
- '**/.ssh/**'
|
||||
|
||||
- id: block-ssh-write
|
||||
tool: write
|
||||
action: deny
|
||||
reason: Blocked writing SSH keys
|
||||
path_globs:
|
||||
- "**/.ssh/**"
|
||||
- '**/.ssh/**'
|
||||
|
||||
- id: confirm-bastille-destroy
|
||||
tool: hostd
|
||||
action: ask
|
||||
reason: 'Jail destruction is irreversible. Confirm?'
|
||||
patterns:
|
||||
- 'bastille.*destroy'
|
||||
|
||||
- id: confirm-zfs-rollback
|
||||
tool: hostd
|
||||
action: ask
|
||||
reason: 'ZFS rollback reverts all data since snapshot. Confirm?'
|
||||
patterns:
|
||||
- 'zfs-rollback'
|
||||
|
||||
- id: block-zfs-destroy
|
||||
tool: hostd
|
||||
action: deny
|
||||
reason: 'ZFS dataset destruction is not permitted via agent'
|
||||
patterns:
|
||||
- 'zfs.*destroy'
|
||||
|
||||
- id: confirm-pf-reload
|
||||
tool: hostd
|
||||
action: ask
|
||||
reason: 'PF reload changes live firewall rules. Confirm?'
|
||||
patterns:
|
||||
- 'pf-reload'
|
||||
|
||||
bash_patterns:
|
||||
- id: block-reset-hard
|
||||
pattern: "git reset --hard"
|
||||
pattern: 'git reset --hard'
|
||||
action: deny
|
||||
reason: Blocked destructive git reset
|
||||
|
||||
|
|
@ -50,15 +78,15 @@ bash_patterns:
|
|||
reason: Confirm file deletions
|
||||
|
||||
zero_access_paths:
|
||||
- "**/.ssh/**"
|
||||
- "**/.env"
|
||||
- "**/.env.*"
|
||||
- '**/.ssh/**'
|
||||
- '**/.env'
|
||||
- '**/.env.*'
|
||||
|
||||
read_only_paths:
|
||||
- "**/package-lock.json"
|
||||
- "**/pnpm-lock.yaml"
|
||||
- "**/yarn.lock"
|
||||
- '**/package-lock.json'
|
||||
- '**/pnpm-lock.yaml'
|
||||
- '**/yarn.lock'
|
||||
|
||||
no_delete_paths:
|
||||
- "**/.git/**"
|
||||
- "**/README.md"
|
||||
- '**/.git/**'
|
||||
- '**/README.md'
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ function text(t: string): AgentToolResult<unknown> {
|
|||
}
|
||||
|
||||
function json(obj: unknown): AgentToolResult<unknown> {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }], details: obj };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }],
|
||||
details: obj,
|
||||
};
|
||||
}
|
||||
|
||||
const API_PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
|
|
@ -46,13 +49,23 @@ function apiRequest<T>(
|
|||
},
|
||||
(res) => {
|
||||
let buf = '';
|
||||
res.on('data', (chunk: Buffer) => { buf += chunk.toString(); });
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
buf += chunk.toString();
|
||||
});
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(buf) as T;
|
||||
resolve({ ok: (res.statusCode ?? 500) < 400, status: res.statusCode ?? 500, data });
|
||||
resolve({
|
||||
ok: (res.statusCode ?? 500) < 400,
|
||||
status: res.statusCode ?? 500,
|
||||
data,
|
||||
});
|
||||
} catch {
|
||||
resolve({ ok: false, status: res.statusCode ?? 500, data: buf as unknown as T });
|
||||
resolve({
|
||||
ok: false,
|
||||
status: res.statusCode ?? 500,
|
||||
data: buf as unknown as T,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -79,7 +92,8 @@ export const taskCreateTool = {
|
|||
description: 'Short imperative task title (e.g. "Create staging jail")',
|
||||
}),
|
||||
description: Type.String({
|
||||
description: 'Full task description with all context the specialist needs to act',
|
||||
description:
|
||||
'Full task description with all context the specialist needs to act',
|
||||
}),
|
||||
assigned_to: Type.Optional(
|
||||
Type.String({
|
||||
|
|
@ -94,21 +108,27 @@ export const taskCreateTool = {
|
|||
}),
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
params: { title: string; description: string; assigned_to?: string; priority?: string },
|
||||
params: {
|
||||
title: string;
|
||||
description: string;
|
||||
assigned_to?: string;
|
||||
priority?: string;
|
||||
},
|
||||
_signal: AbortSignal | undefined,
|
||||
_onUpdate: unknown,
|
||||
_ctx: ExtensionContext,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
priority: params.priority ?? 'medium',
|
||||
};
|
||||
if (params.assigned_to) body.assigned_to = params.assigned_to;
|
||||
const res = await apiRequest<{ task?: { id: string }; error?: string }>(
|
||||
'POST',
|
||||
'/api/controlplane/tasks',
|
||||
{
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
assigned_to: params.assigned_to ?? '',
|
||||
priority: params.priority ?? 'medium',
|
||||
},
|
||||
body,
|
||||
);
|
||||
|
||||
if (!res.ok || !res.data?.task) {
|
||||
|
|
@ -117,11 +137,17 @@ export const taskCreateTool = {
|
|||
);
|
||||
}
|
||||
|
||||
return json({ task_id: res.data.task.id, title: params.title, assigned_to: params.assigned_to });
|
||||
return json({
|
||||
task_id: res.data.task.id,
|
||||
title: params.title,
|
||||
assigned_to: params.assigned_to,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ECONNREFUSED')) {
|
||||
return text(`task_create: controlplane API not reachable at ${BASE_URL}`);
|
||||
return text(
|
||||
`task_create: controlplane API not reachable at ${BASE_URL}`,
|
||||
);
|
||||
}
|
||||
return text(`task_create error: ${msg}`);
|
||||
}
|
||||
|
|
@ -158,7 +184,9 @@ export const taskStatusTool = {
|
|||
_ctx: ExtensionContext,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
try {
|
||||
const query = params.assigned_to ? `?agent_id=${encodeURIComponent(params.assigned_to)}` : '';
|
||||
const query = params.assigned_to
|
||||
? `?agent_id=${encodeURIComponent(params.assigned_to)}`
|
||||
: '';
|
||||
const res = await apiRequest<{ tasks?: unknown[]; error?: string }>(
|
||||
'GET',
|
||||
`/api/controlplane/tasks${query}`,
|
||||
|
|
@ -173,7 +201,9 @@ export const taskStatusTool = {
|
|||
const tasks = res.data?.tasks ?? [];
|
||||
|
||||
if (params.task_id) {
|
||||
const task = (tasks as Array<{ id: string }>).find((t) => t.id === params.task_id);
|
||||
const task = (tasks as Array<{ id: string }>).find(
|
||||
(t) => t.id === params.task_id,
|
||||
);
|
||||
if (!task) return text(`Task not found: ${params.task_id}`);
|
||||
return json(task);
|
||||
}
|
||||
|
|
@ -182,7 +212,9 @@ export const taskStatusTool = {
|
|||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ECONNREFUSED')) {
|
||||
return text(`task_status: controlplane API not reachable at ${BASE_URL}`);
|
||||
return text(
|
||||
`task_status: controlplane API not reachable at ${BASE_URL}`,
|
||||
);
|
||||
}
|
||||
return text(`task_status error: ${msg}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,135 +1,60 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/agent-logs.ts — Tail recent pi session logs.
|
||||
*
|
||||
* Usage:
|
||||
* just agent-logs # most recent session
|
||||
* just agent-logs sysadmin # session for a specific agent
|
||||
*
|
||||
* Searches:
|
||||
* tmp/sessions/<agent>.jsonl
|
||||
* groups/*/sessions/*.jsonl
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
||||
const agentFilter = process.argv[2];
|
||||
const LINES = parseInt(process.env.LOG_LINES ?? '50', 10);
|
||||
const agentId = process.argv[2] ?? '';
|
||||
const SESSIONS_DIR = path.join(os.homedir(), '.config', 'clawdie', 'sessions');
|
||||
|
||||
function findSessionFiles(): string[] {
|
||||
const files: { path: string; mtime: number }[] = [];
|
||||
|
||||
// tmp/sessions/
|
||||
const tmpDir = path.join(ROOT, 'tmp', 'sessions');
|
||||
if (fs.existsSync(tmpDir)) {
|
||||
for (const f of fs.readdirSync(tmpDir)) {
|
||||
if (!f.endsWith('.jsonl')) continue;
|
||||
const fp = path.join(tmpDir, f);
|
||||
files.push({ path: fp, mtime: fs.statSync(fp).mtimeMs });
|
||||
if (!fs.existsSync(SESSIONS_DIR)) return [];
|
||||
const files: string[] = [];
|
||||
for (const entry of fs.readdirSync(SESSIONS_DIR, { withFileTypes: true })) {
|
||||
if (entry.name.endsWith('.jsonl')) {
|
||||
files.push(path.join(SESSIONS_DIR, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
// groups/*/sessions/
|
||||
const groupsDir = path.join(ROOT, 'groups');
|
||||
if (fs.existsSync(groupsDir)) {
|
||||
for (const group of fs.readdirSync(groupsDir)) {
|
||||
const sessDir = path.join(groupsDir, group, 'sessions');
|
||||
if (!fs.existsSync(sessDir)) continue;
|
||||
for (const f of fs.readdirSync(sessDir)) {
|
||||
if (!f.endsWith('.jsonl')) continue;
|
||||
const fp = path.join(sessDir, f);
|
||||
files.push({ path: fp, mtime: fs.statSync(fp).mtimeMs });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
.sort((a, b) => b.mtime - a.mtime)
|
||||
.map((f) => f.path);
|
||||
return files.sort().reverse();
|
||||
}
|
||||
|
||||
function matchesFilter(filePath: string, filter: string): boolean {
|
||||
const name = path.basename(filePath, '.jsonl').toLowerCase();
|
||||
return name.includes(filter.toLowerCase());
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
type?: string;
|
||||
role?: string;
|
||||
content?: string | Array<{ type: string; text?: string }>;
|
||||
timestamp?: string;
|
||||
tool?: string;
|
||||
}
|
||||
|
||||
function formatEntry(raw: string): string | null {
|
||||
try {
|
||||
const entry = JSON.parse(raw) as LogEntry;
|
||||
const ts = entry.timestamp ? `\x1b[2m${entry.timestamp.slice(11, 19)}\x1b[0m ` : '';
|
||||
|
||||
if (entry.role === 'user') {
|
||||
const text = typeof entry.content === 'string'
|
||||
? entry.content
|
||||
: (entry.content as Array<{ text?: string }>)?.[0]?.text ?? '';
|
||||
return `${ts}\x1b[36muser\x1b[0m ${text.slice(0, 120)}`;
|
||||
}
|
||||
if (entry.role === 'assistant') {
|
||||
const text = typeof entry.content === 'string'
|
||||
? entry.content
|
||||
: (entry.content as Array<{ text?: string }>)?.[0]?.text ?? '';
|
||||
return `${ts}\x1b[32masst\x1b[0m ${text.slice(0, 120)}`;
|
||||
}
|
||||
if (entry.type === 'tool_call' || entry.tool) {
|
||||
return `${ts}\x1b[33mtool\x1b[0m ${entry.tool ?? entry.type}`;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function tailFile(filePath: string, n: number): Promise<string[]> {
|
||||
const lines: string[] = [];
|
||||
const rl = readline.createInterface({ input: fs.createReadStream(filePath) });
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) lines.push(line);
|
||||
}
|
||||
function tailFile(filePath: string, n: number): string[] {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.split('\n').filter(Boolean);
|
||||
return lines.slice(-n);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const allFiles = findSessionFiles();
|
||||
const files = findSessionFiles();
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
console.log('No session log files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
let target: string | undefined;
|
||||
if (agentFilter) {
|
||||
target = allFiles.find((f) => matchesFilter(f, agentFilter));
|
||||
if (!target) {
|
||||
console.error(`No session file matching "${agentFilter}"`);
|
||||
console.error(`Available: ${allFiles.map((f) => path.basename(f, '.jsonl')).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
target = allFiles[0];
|
||||
}
|
||||
|
||||
const rel = path.relative(ROOT, target!);
|
||||
console.log(`\n\x1b[1m${rel}\x1b[0m (last ${LINES} entries)\n`);
|
||||
|
||||
const lines = await tailFile(target!, LINES);
|
||||
for (const line of lines) {
|
||||
const formatted = formatEntry(line);
|
||||
if (formatted) console.log(formatted);
|
||||
}
|
||||
console.log('');
|
||||
if (files.length === 0) {
|
||||
console.log('No session logs found.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
const filtered = agentId
|
||||
? files.filter((f) => path.basename(f).includes(agentId))
|
||||
: files;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
console.log(`No sessions matching: ${agentId}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const file of filtered.slice(0, 5)) {
|
||||
const name = path.basename(file);
|
||||
console.log(`\n=== ${name} ===`);
|
||||
const lines = tailFile(file, 10);
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as Record<string, unknown>;
|
||||
const role = entry.role ?? '?';
|
||||
const content =
|
||||
typeof entry.content === 'string'
|
||||
? entry.content.slice(0, 120)
|
||||
: JSON.stringify(entry.content).slice(0, 120);
|
||||
console.log(` [${role}] ${content}`);
|
||||
} catch {
|
||||
console.log(` ${line.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${filtered.length} session(s) found`);
|
||||
|
|
|
|||
|
|
@ -1,75 +1,49 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/agent-status.ts — Show registered agents and controlplane state.
|
||||
* Usage: just agent-list
|
||||
*/
|
||||
import http from 'http';
|
||||
const API_PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const API_HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
|
||||
const PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
|
||||
function get<T>(path: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get({ hostname: HOST, port: PORT, path }, (res) => {
|
||||
let buf = '';
|
||||
res.on('data', (c: Buffer) => { buf += c.toString(); });
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(buf) as T); }
|
||||
catch { reject(new Error(`Non-JSON response: ${buf.slice(0, 100)}`)); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
interface AgentState {
|
||||
agents?: Array<{ id: string; role: string; status: string; last_seen?: string }>;
|
||||
tasks?: Array<{ id: string; title: string; status: string; assigned_to: string }>;
|
||||
async function apiGet(path: string): Promise<unknown> {
|
||||
const res = await fetch(`http://${API_HOST}:${API_PORT}${path}`);
|
||||
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let state: AgentState;
|
||||
try {
|
||||
state = await get<AgentState>('/api/controlplane/state');
|
||||
const data = (await apiGet('/api/controlplane/state')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const agents =
|
||||
((data as Record<string, unknown>).agents as Array<
|
||||
Record<string, unknown>
|
||||
>) ?? [];
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log('No registered agents.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Agent ID\t\tProfile\t\tStatus');
|
||||
console.log('-'.repeat(50));
|
||||
for (const a of agents) {
|
||||
console.log(
|
||||
`${a.id ?? '?'}\t\t${a.profile ?? '?'}\t\t${a.status ?? '?'}`,
|
||||
);
|
||||
}
|
||||
console.log(`\n${agents.length} agent(s)`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ECONNREFUSED')) {
|
||||
console.error(`Controlplane not reachable at http://${HOST}:${PORT}`);
|
||||
console.error('Is the controlplane running? just start');
|
||||
console.error(
|
||||
'Controlplane API not reachable at',
|
||||
`${API_HOST}:${API_PORT}`,
|
||||
);
|
||||
console.error('Is the controlplane running?');
|
||||
} else {
|
||||
console.error(`Error: ${msg}`);
|
||||
console.error('Error:', msg);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
const agents = state.agents ?? [];
|
||||
if (agents.length === 0) {
|
||||
console.log(' No agents registered.');
|
||||
} else {
|
||||
console.log('\x1b[1mAgents\x1b[0m');
|
||||
for (const a of agents) {
|
||||
const status =
|
||||
a.status === 'active'
|
||||
? `\x1b[32m${a.status}\x1b[0m`
|
||||
: `\x1b[33m${a.status}\x1b[0m`;
|
||||
const seen = a.last_seen ? ` last seen: ${a.last_seen}` : '';
|
||||
console.log(` ${a.id.padEnd(24)} ${a.role.padEnd(16)} ${status}${seen}`);
|
||||
}
|
||||
}
|
||||
|
||||
const pending = (state.tasks ?? []).filter((t) => t.status === 'pending');
|
||||
if (pending.length > 0) {
|
||||
console.log('\n\x1b[1mPending tasks\x1b[0m');
|
||||
for (const t of pending) {
|
||||
console.log(` [${t.id.slice(0, 8)}] ${t.title.slice(0, 50)} → ${t.assigned_to}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,94 +1,48 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/agent-task-status.ts — Check controlplane task status.
|
||||
* Usage:
|
||||
* just agent-task-status <task-id> # specific task
|
||||
* just agent-task-status # recent tasks
|
||||
*/
|
||||
import http from 'http';
|
||||
|
||||
const PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
const API_PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const API_HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
|
||||
const taskId = process.argv[2];
|
||||
|
||||
function get<T>(path: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get({ hostname: HOST, port: PORT, path }, (res) => {
|
||||
let buf = '';
|
||||
res.on('data', (c: Buffer) => { buf += c.toString(); });
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(buf) as T); }
|
||||
catch { reject(new Error(`Non-JSON response: ${buf.slice(0, 100)}`)); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
assigned_to?: string;
|
||||
priority?: string;
|
||||
created_at?: string;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
function statusColor(s: string): string {
|
||||
if (s === 'done' || s === 'completed') return `\x1b[32m${s}\x1b[0m`;
|
||||
if (s === 'in_progress' || s === 'running') return `\x1b[36m${s}\x1b[0m`;
|
||||
if (s === 'failed' || s === 'error') return `\x1b[31m${s}\x1b[0m`;
|
||||
return `\x1b[33m${s}\x1b[0m`;
|
||||
if (!taskId) {
|
||||
console.error('Usage: agent-task-status.ts <task_id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
if (taskId) {
|
||||
const resp = await get<{ tasks?: Task[]; error?: string }>('/api/controlplane/tasks');
|
||||
const task = (resp.tasks ?? []).find((t) => t.id === taskId || t.id.startsWith(taskId));
|
||||
if (!task) {
|
||||
console.error(`Task not found: ${taskId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('');
|
||||
console.log(` ID: ${task.id}`);
|
||||
console.log(` Title: ${task.title}`);
|
||||
console.log(` Status: ${statusColor(task.status)}`);
|
||||
if (task.assigned_to) console.log(` Assigned to: ${task.assigned_to}`);
|
||||
if (task.priority) console.log(` Priority: ${task.priority}`);
|
||||
if (task.created_at) console.log(` Created: ${task.created_at}`);
|
||||
if (task.output) console.log(` Output:\n${task.output.split('\n').map((l) => ' ' + l).join('\n')}`);
|
||||
console.log('');
|
||||
} else {
|
||||
const resp = await get<{ tasks?: Task[] }>('/api/controlplane/tasks');
|
||||
const tasks = resp.tasks ?? [];
|
||||
console.log('');
|
||||
if (tasks.length === 0) {
|
||||
console.log(' No tasks.');
|
||||
} else {
|
||||
console.log(`\x1b[1mTasks\x1b[0m (${tasks.length})`);
|
||||
for (const t of tasks.slice(0, 20)) {
|
||||
const id = t.id.slice(0, 8);
|
||||
const title = t.title.slice(0, 48).padEnd(48);
|
||||
const assignee = (t.assigned_to ?? '—').padEnd(12);
|
||||
console.log(` [${id}] ${title} ${assignee} ${statusColor(t.status)}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
const res = await fetch(
|
||||
`http://${API_HOST}:${API_PORT}/api/controlplane/tasks`,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`API error ${res.status}: ${await res.text()}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as Record<string, unknown>;
|
||||
const tasks = (data.tasks as Array<Record<string, unknown>>) ?? [];
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
|
||||
if (!task) {
|
||||
console.log(`Task not found: ${taskId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Task: ${task.id}`);
|
||||
console.log(` Title: ${task.title ?? 'N/A'}`);
|
||||
console.log(` Status: ${task.status ?? 'N/A'}`);
|
||||
console.log(` Assigned: ${task.assigned_to ?? 'unassigned'}`);
|
||||
console.log(` Priority: ${task.priority ?? 'N/A'}`);
|
||||
if (task.result) console.log(` Result: ${JSON.stringify(task.result)}`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ECONNREFUSED')) {
|
||||
console.error(`Controlplane not reachable at http://${HOST}:${PORT}`);
|
||||
console.error('Controlplane API not reachable');
|
||||
} else {
|
||||
console.error(`Error: ${msg}`);
|
||||
console.error('Error:', msg);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,82 +1,49 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/agent-task.ts — Create a controlplane task via natural language.
|
||||
* Usage: just agent-task "Restart the cms jail nginx service"
|
||||
*/
|
||||
import http from 'http';
|
||||
|
||||
const PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
const API_PORT = parseInt(process.env.CONTROLPLANE_PORT ?? '3100', 10);
|
||||
const API_HOST = process.env.CONTROLPLANE_BIND_HOST ?? '127.0.0.1';
|
||||
|
||||
const description = process.argv[2];
|
||||
|
||||
if (!description) {
|
||||
console.error('Usage: just agent-task "<description>"');
|
||||
console.error('Usage: agent-task.ts <description>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function post<T>(path: string, body: unknown): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: HOST,
|
||||
port: PORT,
|
||||
path,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
||||
},
|
||||
(res) => {
|
||||
let buf = '';
|
||||
res.on('data', (c: Buffer) => { buf += c.toString(); });
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(buf) as T); }
|
||||
catch { reject(new Error(`Non-JSON response (HTTP ${res.statusCode}): ${buf.slice(0, 100)}`)); }
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
interface TaskResponse {
|
||||
task?: { id: string; title: string; status: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let resp: TaskResponse;
|
||||
try {
|
||||
resp = await post<TaskResponse>('/api/controlplane/tasks', {
|
||||
title: description.slice(0, 100),
|
||||
description,
|
||||
priority: 'medium',
|
||||
});
|
||||
const res = await fetch(
|
||||
`http://${API_HOST}:${API_PORT}/api/controlplane/tasks`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: description, description }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`API error ${res.status}: ${await res.text()}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as Record<string, unknown>;
|
||||
const task = data.task as Record<string, unknown> | undefined;
|
||||
if (task?.id) {
|
||||
console.log(`Task created: ${task.id}`);
|
||||
console.log(` Title: ${description}`);
|
||||
} else {
|
||||
console.log('Task created (no id returned)');
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ECONNREFUSED')) {
|
||||
console.error(`Controlplane not reachable at http://${HOST}:${PORT}`);
|
||||
console.error(
|
||||
'Controlplane API not reachable at',
|
||||
`${API_HOST}:${API_PORT}`,
|
||||
);
|
||||
} else {
|
||||
console.error(`Error: ${msg}`);
|
||||
console.error('Error:', msg);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!resp.task) {
|
||||
console.error(`Failed: ${resp.error ?? JSON.stringify(resp)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nTask created:`);
|
||||
console.log(` ID: ${resp.task.id}`);
|
||||
console.log(` Title: ${resp.task.title}`);
|
||||
console.log(` Status: ${resp.task.status}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,62 +1,35 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/hostd-cli.ts — Generic hostd operation CLI wrapper.
|
||||
*
|
||||
* Usage:
|
||||
* just zfs-snapshots # bastille-list
|
||||
* just zfs-snapshot tank/db pre-deploy # zfs-snapshot with args
|
||||
* npx tsx scripts/hostd-cli.ts bastille-list
|
||||
* npx tsx scripts/hostd-cli.ts zfs-snapshot '{"dataset":"tank/db","name":"pre-deploy"}'
|
||||
* npx tsx scripts/hostd-cli.ts service-restart '{"jail":"cms","service":"nginx"}'
|
||||
*/
|
||||
import { hostd } from '../src/hostd/client.js';
|
||||
|
||||
const [op, rawParams] = process.argv.slice(2);
|
||||
const [op, paramsRaw] = process.argv.slice(2);
|
||||
|
||||
if (!op) {
|
||||
console.error('Usage: hostd-cli.ts <op> [json-params]');
|
||||
console.error('');
|
||||
console.error('Ops: bastille-start bastille-stop bastille-restart bastille-list');
|
||||
console.error(' zfs-snapshot zfs-list zfs-create zfs-rollback');
|
||||
console.error(' pf-reload pf-enable');
|
||||
console.error(' service-start service-stop service-restart service-status');
|
||||
console.error(' bastille-pkg-install bastille-mount-pkg-cache');
|
||||
console.error('Example: hostd-cli.ts bastille-list');
|
||||
console.error(' hostd-cli.ts bastille-start \'{"jail": "db"}\'');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let params: Record<string, string | number | boolean> = {};
|
||||
|
||||
if (rawParams) {
|
||||
try {
|
||||
params = JSON.parse(rawParams) as Record<string, string | number | boolean>;
|
||||
} catch {
|
||||
console.error(`Invalid JSON params: ${rawParams}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let resp;
|
||||
let params: Record<string, string | number | boolean> = {};
|
||||
if (paramsRaw) {
|
||||
try {
|
||||
resp = await hostd(op, params);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ENOENT') || msg.includes('ECONNREFUSED')) {
|
||||
console.error('hostd not reachable. Is clawdie-hostd running?');
|
||||
} else {
|
||||
console.error(`Error: ${msg}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (resp.output) process.stdout.write(resp.output + (resp.output.endsWith('\n') ? '' : '\n'));
|
||||
if (!resp.ok) {
|
||||
if (resp.error) console.error(`Error: ${resp.error}`);
|
||||
params = JSON.parse(paramsRaw);
|
||||
} catch {
|
||||
console.error('Invalid JSON params:', paramsRaw);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
try {
|
||||
const result = await hostd(op, params);
|
||||
if (result.ok) {
|
||||
console.log(result.output || 'ok');
|
||||
} else {
|
||||
console.error('FAIL:', result.error || result.output || 'unknown error');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('hostd error:', msg);
|
||||
console.error('Is the hostd daemon running?');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
45
scripts/jail-provision.ts
Normal file
45
scripts/jail-provision.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { loadJailRegistry, resolveJailIp } from '../src/jail-schema.js';
|
||||
import { provisionJail, resolveJailName } from '../setup/bastille-helpers.js';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
AGENT_INTERNAL_DOMAIN,
|
||||
SUBNET_BASE,
|
||||
} from '../src/config.js';
|
||||
|
||||
const role = process.argv[2];
|
||||
|
||||
if (!role) {
|
||||
console.error('Usage: jail-provision.ts <role>');
|
||||
try {
|
||||
const registry = loadJailRegistry();
|
||||
console.error(`Available roles: ${Object.keys(registry.jails).join(', ')}`);
|
||||
} catch {
|
||||
console.error('(registry not available)');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const registry = loadJailRegistry();
|
||||
const jailDef = registry.jails[role];
|
||||
|
||||
if (!jailDef) {
|
||||
console.error(`Unknown jail role: ${role}`);
|
||||
console.error(`Available: ${Object.keys(registry.jails).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = await provisionJail(jailDef, role, {
|
||||
agentName: AGENT_NAME,
|
||||
subnetBase: SUBNET_BASE,
|
||||
internalDomain: AGENT_INTERNAL_DOMAIN,
|
||||
});
|
||||
|
||||
console.log(`Jail: ${result.jailName}`);
|
||||
console.log(`IP: ${result.ip}`);
|
||||
console.log(`Created: ${result.created ? 'yes' : 'already existed'}`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('Provision failed:', msg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -1,90 +1,45 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/jail-status.ts — Show jail status via hostd.
|
||||
*
|
||||
* Usage:
|
||||
* just jail-list # all jails
|
||||
* just jail-status cms # one jail
|
||||
*/
|
||||
import { hostd } from '../src/hostd/client.js';
|
||||
|
||||
const filter = process.argv[2];
|
||||
const jailFilter = process.argv[2];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let resp;
|
||||
try {
|
||||
resp = await hostd('bastille-list');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('ENOENT') || msg.includes('ECONNREFUSED')) {
|
||||
console.error('hostd not reachable. Is clawdie-hostd running?');
|
||||
console.error(' sudo npx tsx src/hostd/index.ts (dev)');
|
||||
console.error(' sudo node dist/hostd/index.js (prod)');
|
||||
} else {
|
||||
console.error(`Error: ${msg}`);
|
||||
}
|
||||
try {
|
||||
const result = await hostd('bastille-list');
|
||||
if (!result.ok) {
|
||||
console.error('Failed to list jails:', result.error || result.output);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
console.error(`bastille-list failed: ${resp.error ?? resp.output}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = resp.output ?? '';
|
||||
const lines = raw.split('\n').filter(Boolean);
|
||||
|
||||
// Find data lines — skip header (JID/Name/---) and empty lines
|
||||
const lines = result.output.split('\n').filter(Boolean);
|
||||
const dataLines = lines.filter((l) => !/^\s*(JID|Name|---)/i.test(l));
|
||||
|
||||
if (dataLines.length === 0) {
|
||||
console.log('No jails found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse: JID IP Hostname Path
|
||||
type JailRow = { jid: string; ip: string; name: string; path: string; state: string };
|
||||
const jails: JailRow[] = dataLines.map((line) => {
|
||||
const p = line.trim().split(/\s+/);
|
||||
const jails = dataLines.map((line) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
return {
|
||||
jid: p[0] ?? '-',
|
||||
ip: p[1] ?? '-',
|
||||
name: p[2] ?? '-',
|
||||
path: p[3] ?? '-',
|
||||
state: !p[0] || p[0] === '-' ? 'stopped' : 'running',
|
||||
jid: parts[0] ?? '-',
|
||||
ip: parts[1] ?? '-',
|
||||
name: parts[2] ?? '-',
|
||||
path: parts[3] ?? '-',
|
||||
state: parts[0] === '-' || parts[0] === '0' ? 'stopped' : 'running',
|
||||
};
|
||||
});
|
||||
|
||||
const visible = filter ? jails.filter((j) => j.name === filter) : jails;
|
||||
const filtered = jailFilter
|
||||
? jails.filter((j) => j.name === jailFilter)
|
||||
: jails;
|
||||
|
||||
if (filter && visible.length === 0) {
|
||||
console.error(`No jail named "${filter}"`);
|
||||
if (filtered.length === 0 && jailFilter) {
|
||||
console.log(`No jail found: ${jailFilter}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Table
|
||||
const cols = { jid: 5, ip: 15, name: 20, path: 40, state: 8 };
|
||||
const pad = (s: string, n: number) => s.slice(0, n).padEnd(n);
|
||||
const sep = Object.values(cols).map((n) => '─'.repeat(n)).join('─┼─');
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
` ${pad('JID', cols.jid)} │ ${pad('IP', cols.ip)} │ ${pad('NAME', cols.name)} │ ${pad('PATH', cols.path)} │ STATE`,
|
||||
);
|
||||
console.log(` ${sep}`);
|
||||
for (const j of visible) {
|
||||
const state =
|
||||
j.state === 'running'
|
||||
? `\x1b[32m${j.state}\x1b[0m`
|
||||
: `\x1b[33m${j.state}\x1b[0m`;
|
||||
console.log(
|
||||
` ${pad(j.jid, cols.jid)} │ ${pad(j.ip, cols.ip)} │ ${pad(j.name, cols.name)} │ ${pad(j.path, cols.path)} │ ${state}`,
|
||||
);
|
||||
console.log('JID\t\tState\t\tIP\t\tName');
|
||||
console.log('-'.repeat(60));
|
||||
for (const j of filtered) {
|
||||
console.log(`${j.jid}\t\t${j.state}\t\t${j.ip}\t\t${j.name}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
console.log(`\n${filtered.length} jail(s)`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('Error:', msg);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +1,76 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* scripts/system-health.ts — Full system health report.
|
||||
*
|
||||
* Calls hostd directly — no LLM. Plain TypeScript.
|
||||
* Usage: just system-health
|
||||
*/
|
||||
import { hostd } from '../src/hostd/client.js';
|
||||
|
||||
function heading(title: string): void {
|
||||
console.log(`\n\x1b[1m\x1b[33m── ${title} ──\x1b[0m`);
|
||||
}
|
||||
|
||||
function ok(label: string, val: string): void {
|
||||
console.log(` \x1b[32m✓\x1b[0m ${label.padEnd(20)} ${val}`);
|
||||
}
|
||||
|
||||
function warn(label: string, val: string): void {
|
||||
console.log(` \x1b[33m!\x1b[0m ${label.padEnd(20)} ${val}`);
|
||||
}
|
||||
|
||||
function fail(label: string, val: string): void {
|
||||
console.log(` \x1b[31m✗\x1b[0m ${label.padEnd(20)} ${val}`);
|
||||
}
|
||||
|
||||
async function callHostd(op: string, params: Record<string, string> = {}) {
|
||||
try {
|
||||
return await hostd(op, params);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, output: '', error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(`\n\x1b[1mClawdie System Health\x1b[0m ${new Date().toLocaleString()}`);
|
||||
const [jailRes, zfsRes, pfRes] = await Promise.all([
|
||||
hostd('bastille-list').catch(() => ({
|
||||
ok: false as const,
|
||||
output: '',
|
||||
error: 'unreachable',
|
||||
})),
|
||||
hostd('zfs-list').catch(() => ({
|
||||
ok: false as const,
|
||||
output: '',
|
||||
error: 'unreachable',
|
||||
})),
|
||||
hostd('service-status', { service: 'pf' }).catch(() => ({
|
||||
ok: false as const,
|
||||
output: '',
|
||||
error: 'unreachable',
|
||||
})),
|
||||
]);
|
||||
|
||||
// ── Jails ──────────────────────────────────────────────────────────────
|
||||
heading('JAILS');
|
||||
const jailResp = await callHostd('bastille-list');
|
||||
if (!jailResp.ok) {
|
||||
fail('bastille-list', jailResp.error ?? 'failed');
|
||||
} else {
|
||||
const lines = (jailResp.output ?? '')
|
||||
console.log('=== SYSTEM HEALTH ===\n');
|
||||
|
||||
console.log('-- Jails --');
|
||||
if (jailRes.ok) {
|
||||
const lines = jailRes.output
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.filter((l) => !/^\s*(JID|Name|---)/i.test(l));
|
||||
|
||||
if (lines.length === 0) {
|
||||
warn('jails', 'none found');
|
||||
} else {
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const jid = parts[0] ?? '-';
|
||||
const ip = parts[1] ?? '-';
|
||||
const name = parts[2] ?? '-';
|
||||
const running = jid !== '-' && jid !== '0';
|
||||
const fn = running ? ok : warn;
|
||||
fn(name, `${ip} jid=${jid} ${running ? 'running' : 'stopped'}`);
|
||||
}
|
||||
.filter((l) => l.trim() && !/^\s*(JID|Name|---)/i.test(l));
|
||||
const jails = lines.map((l) => {
|
||||
const p = l.trim().split(/\s+/);
|
||||
return {
|
||||
jid: p[0],
|
||||
ip: p[1],
|
||||
name: p[2],
|
||||
state: p[0] === '-' ? 'stopped' : 'running',
|
||||
};
|
||||
});
|
||||
const running = jails.filter((j) => j.state === 'running').length;
|
||||
console.log(
|
||||
` ${jails.length} jail(s), ${running} running, ${jails.length - running} stopped`,
|
||||
);
|
||||
for (const j of jails) {
|
||||
console.log(` ${j.state === 'running' ? '●' : '○'} ${j.name} (${j.ip})`);
|
||||
}
|
||||
} else {
|
||||
console.log(' Unable to reach hostd');
|
||||
}
|
||||
|
||||
// ── ZFS ────────────────────────────────────────────────────────────────
|
||||
heading('ZFS');
|
||||
const zfsResp = await callHostd('zfs-list');
|
||||
if (!zfsResp.ok) {
|
||||
fail('zfs-list', zfsResp.error ?? 'failed');
|
||||
} else {
|
||||
const lines = (zfsResp.output ?? '')
|
||||
console.log('\n-- ZFS --');
|
||||
if (zfsRes.ok) {
|
||||
const lines = zfsRes.output
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.filter((l) => !/^NAME/i.test(l))
|
||||
.slice(0, 12);
|
||||
|
||||
for (const line of lines) {
|
||||
.filter((l) => l.trim() && !/^NAME/i.test(l));
|
||||
for (const line of lines.slice(0, 8)) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const dataset = (parts[0] ?? '').replace(/^.*\//, ''); // last segment
|
||||
const used = parts[1] ?? '-';
|
||||
const avail = parts[2] ?? '-';
|
||||
console.log(` ${dataset.padEnd(30)} used=${used.padEnd(8)} avail=${avail}`);
|
||||
console.log(` ${parts[0]}: used=${parts[1]} avail=${parts[2]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── PF ─────────────────────────────────────────────────────────────────
|
||||
heading('FIREWALL');
|
||||
const pfResp = await callHostd('service-status', { service: 'pf' });
|
||||
if (pfResp.ok) {
|
||||
ok('pf', pfResp.output?.trim() ?? 'ok');
|
||||
if (lines.length > 8) console.log(` ... and ${lines.length - 8} more`);
|
||||
} else {
|
||||
warn('pf', pfResp.error ?? 'status unknown');
|
||||
console.log(' Unable to reach hostd');
|
||||
}
|
||||
|
||||
// ── Key services ───────────────────────────────────────────────────────
|
||||
heading('SERVICES');
|
||||
const services = [
|
||||
{ jail: 'db', service: 'postgresql' },
|
||||
{ jail: 'cms', service: 'nginx' },
|
||||
];
|
||||
|
||||
for (const { jail, service } of services) {
|
||||
const r = await callHostd('service-status', { jail, service });
|
||||
const label = `${jail}/${service}`;
|
||||
if (r.ok) {
|
||||
ok(label, r.output?.trim() ?? 'ok');
|
||||
} else {
|
||||
fail(label, r.error ?? 'not running');
|
||||
}
|
||||
console.log('\n-- PF Firewall --');
|
||||
if (pfRes.ok) {
|
||||
console.log(` ${pfRes.output.trim()}`);
|
||||
} else {
|
||||
console.log(' Unable to reach hostd');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`\nGenerated: ${new Date().toISOString()}`);
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
main().catch((err) => {
|
||||
console.error('Error:', err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue