CI : Deployment
Some checks failed
release / build (push) Failing after 45s
release / verify-windows (push) Has been skipped

This commit is contained in:
jeanotx32
2026-08-11 17:10:08 +02:00
parent b529417940
commit 105d07b82b
16 changed files with 1662 additions and 86 deletions

View File

@@ -9,8 +9,10 @@
},
"scripts": {
"build": "tsc -b",
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=cjs --outfile=dist-bundle/agent.cjs --external:bufferutil --external:utf-8-validate --legal-comments=none",
"dev": "npm run build -w @stream-control/shared && node --experimental-strip-types --disable-warning=ExperimentalWarning --watch src/index.ts",
"start": "node dist/index.js"
"start": "node dist/index.js",
"check": "node dist/index.js --check"
},
"dependencies": {
"@stream-control/shared": "*",
@@ -20,6 +22,7 @@
"devDependencies": {
"@types/node": "^22.10.5",
"@types/ws": "^8.5.13",
"esbuild": "^0.28.2",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,132 @@
import { execFile } from 'node:child_process';
import net from 'node:net';
import { promisify } from 'node:util';
import { CONFIG_PATH, type AgentConfig } from './config.ts';
const run = promisify(execFile);
type Verdict = 'ok' | 'warn' | 'fail';
const MARKS: Record<Verdict, string> = { ok: '✓', warn: '!', fail: '✗' };
function line(verdict: Verdict, label: string, detail: string): Verdict {
console.log(` ${MARKS[verdict]} ${label.padEnd(16)} ${detail}`);
return verdict;
}
/** Test TCP simple : le port répond-il ? */
function probeTcp(host: string, port: number, timeoutMs = 4000): Promise<string | null> {
return new Promise((resolve) => {
const socket = new net.Socket();
const done = (result: string | null) => {
socket.destroy();
resolve(result);
};
socket.setTimeout(timeoutMs);
socket.once('connect', () => done(null));
socket.once('timeout', () => done('délai dépassé'));
socket.once('error', (err: NodeJS.ErrnoException) => done(err.code ?? err.message));
socket.connect(port, host);
});
}
async function commandExists(command: string, args: string[]): Promise<boolean> {
try {
await run(command, args, { timeout: 8000, windowsHide: true });
return true;
} catch (err) {
return (err as NodeJS.ErrnoException).code !== 'ENOENT';
}
}
/**
* Vérifie une installation d'agent sans rien démarrer : configuration, joignabilité
* du serveur et d'OBS, prérequis du rappel plein écran.
*
* Sert aussi de test de fumée en CI (`node agent.cjs --check`) : un import cassé ou
* une régression de chemin sous Windows fait échouer la commande.
*/
export async function runDiagnostics(config: AgentConfig): Promise<number> {
console.log(`\nVérification de l'agent — ${process.platform} / Node ${process.versions.node}\n`);
const results: Verdict[] = [];
results.push(line('ok', 'configuration', CONFIG_PATH));
results.push(
line(
config.token ? 'ok' : 'fail',
'jeton',
config.token ? `présent (${config.token.slice(0, 6)}…)` : 'ABSENT',
),
);
results.push(line('ok', 'nom', config.name));
// --- Serveur de contrôle --------------------------------------------------
let serverUrl: URL | null = null;
try {
serverUrl = new URL(config.serverUrl);
} catch {
results.push(line('fail', 'serveur', `URL invalide : ${config.serverUrl}`));
}
if (serverUrl) {
const port = Number(serverUrl.port) || (serverUrl.protocol === 'wss:' ? 443 : 80);
const error = await probeTcp(serverUrl.hostname, port);
results.push(
error
? line('fail', 'serveur', `${serverUrl.host} injoignable (${error})`)
: line('ok', 'serveur', `${serverUrl.host} joignable`),
);
if (serverUrl.protocol === 'ws:' && !isLoopback(serverUrl.hostname)) {
line('warn', 'transport', 'ws:// non chiffré — à réserver à un réseau privé');
}
}
// --- OBS ------------------------------------------------------------------
const obsError = await probeTcp(config.obs.host, config.obs.port);
results.push(
obsError
? line('warn', 'obs-websocket', `${config.obs.host}:${config.obs.port} fermé (${obsError})`)
: line('ok', 'obs-websocket', `${config.obs.host}:${config.obs.port} ouvert`),
);
// --- Prérequis du rappel plein écran -------------------------------------
if (process.platform === 'linux') {
const hasXdotool = await commandExists('xdotool', ['--version']);
results.push(
hasXdotool
? line('ok', 'xdotool', 'installé')
: line('warn', 'xdotool', 'absent — apt install xdotool'),
);
const display = process.env.DISPLAY;
results.push(
display
? line('ok', 'DISPLAY', display)
: line('warn', 'DISPLAY', 'non défini — le rappel plein écran échouera'),
);
if (process.env.WAYLAND_DISPLAY) {
line('warn', 'session', 'Wayland détecté — xdotool exige X11');
}
} else if (process.platform === 'win32') {
const hasPowershell = await commandExists('powershell.exe', ['-NoProfile', '-Command', 'exit']);
results.push(
hasPowershell
? line('ok', 'powershell', 'disponible')
: line('warn', 'powershell', 'introuvable — le rappel plein écran échouera'),
);
}
const failed = results.filter((verdict) => verdict === 'fail').length;
const warned = results.filter((verdict) => verdict === 'warn').length;
console.log(
failed
? `\n${failed} problème(s) bloquant(s), ${warned} avertissement(s).\n`
: `\nAucun problème bloquant${warned ? `, ${warned} avertissement(s)` : ''}.\n`,
);
return failed > 0 ? 1 : 0;
}
function isLoopback(hostname: string): boolean {
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
}

View File

@@ -18,6 +18,7 @@ import {
safeJsonParse,
} from '@stream-control/shared';
import { loadConfig, persistIdentity, type AgentConfig } from './config.ts';
import { runDiagnostics } from './doctor.ts';
import { ObsController } from './obs.ts';
import { StreamWatcher } from './watcher.ts';
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
@@ -256,4 +257,10 @@ process.on('unhandledRejection', (reason) => {
});
console.log(`stream-control agent v${AGENT_VERSION}${config.name} (${process.platform})`);
connect();
if (process.argv.includes('--check')) {
// Diagnostic seul : rien n'est démarré, aucune connexion n'est maintenue.
runDiagnostics(config).then((code) => process.exit(code));
} else {
connect();
}