Files
uptimerunner/ts/cli.ts
T

210 lines
6.3 KiB
TypeScript
Raw Normal View History

2026-04-29 19:48:14 +00:00
import { CheckExecutor } from './check-executor.ts';
import { DEFAULT_CONFIG_PATH, loadConfig, validateConfig, writeConfig } from './config.ts';
import type { IUptimeRunnerConfig, TCheckJob } from './interfaces.ts';
import { UptimeRunner } from './runner.ts';
import { UptimeRunnerSystemd } from './systemd.ts';
import denoConfig from '../deno.json' with { type: 'json' };
export class UptimeRunnerCli {
public async parseAndExecute(argsArg: string[]): Promise<void> {
const command = argsArg[0] ?? 'help';
const commandArgs = argsArg.slice(1);
switch (command) {
case 'run':
await this.run(commandArgs, false);
break;
case 'once':
await this.run(commandArgs, true);
break;
case 'check':
await this.check(commandArgs);
break;
case 'config':
await this.config(commandArgs);
break;
case 'service':
await this.service(commandArgs);
break;
case '--version':
case '-v':
case 'version':
console.log(denoConfig.version);
break;
case 'help':
case '--help':
case '-h':
this.showHelp();
break;
default:
throw new Error(`Unknown command: ${command}`);
}
}
private async run(argsArg: string[], onceArg: boolean): Promise<void> {
const flags = parseFlags(argsArg);
const config = await this.loadConfigFromFlags(flags);
const runner = new UptimeRunner(config);
if (onceArg) {
const result = await runner.runOnce();
console.log(JSON.stringify(result, null, 2));
return;
}
await runner.run();
}
private async check(argsArg: string[]): Promise<void> {
const url = argsArg.find((arg) => !arg.startsWith('--'));
if (!url) {
throw new Error('Usage: uptimerunner check <url>');
}
const check: TCheckJob = {
id: `manual-${Date.now().toString(36)}`,
type: 'http',
url,
expectedStatusCodes: [200],
};
const executor = new CheckExecutor('manual');
const result = await executor.execute(check);
console.log(JSON.stringify(result, null, 2));
if (result.status !== 'ok') {
Deno.exit(2);
}
}
private async config(argsArg: string[]): Promise<void> {
const subcommand = argsArg[0] ?? 'show';
const flags = parseFlags(argsArg.slice(1));
const configPath = flags.config ?? DEFAULT_CONFIG_PATH;
switch (subcommand) {
case 'show': {
const config = await loadConfig(configPath);
console.log(JSON.stringify({ ...config, token: mask(config.token) }, null, 2));
break;
}
case 'write': {
const config: IUptimeRunnerConfig = {
instanceUrl: requiredFlag(flags, 'url'),
runnerId: requiredFlag(flags, 'runner-id'),
token: requiredFlag(flags, 'token'),
pollIntervalMs: flags.interval ? Number(flags.interval) : 30000,
labels: flags.labels?.split(',').map((labelArg) => labelArg.trim()).filter(Boolean),
maxConcurrentChecks: flags.concurrency ? Number(flags.concurrency) : 8,
};
await writeConfig(config, configPath);
console.log(`Config written to ${configPath}`);
break;
}
default:
throw new Error(`Unknown config command: ${subcommand}`);
}
}
private async service(argsArg: string[]): Promise<void> {
const subcommand = argsArg[0] ?? 'status';
const systemd = new UptimeRunnerSystemd();
switch (subcommand) {
case 'install':
await systemd.install();
break;
case 'uninstall':
await systemd.uninstall();
break;
case 'start':
await systemd.start();
break;
case 'stop':
await systemd.stop();
break;
case 'restart':
await systemd.restart();
break;
case 'status':
await systemd.status();
break;
case 'logs':
await systemd.logs();
break;
default:
throw new Error(`Unknown service command: ${subcommand}`);
}
}
private async loadConfigFromFlags(
flagsArg: Record<string, string>,
): Promise<IUptimeRunnerConfig> {
const configPath = flagsArg.config ?? DEFAULT_CONFIG_PATH;
const fileConfig = await loadConfig(configPath).catch((error) => {
if (flagsArg.url && flagsArg.token && flagsArg['runner-id']) {
return {} as Partial<IUptimeRunnerConfig>;
}
throw error;
});
const config = {
...fileConfig,
instanceUrl: flagsArg.url ?? fileConfig.instanceUrl,
runnerId: flagsArg['runner-id'] ?? fileConfig.runnerId,
token: flagsArg.token ?? fileConfig.token,
pollIntervalMs: flagsArg.interval ? Number(flagsArg.interval) : fileConfig.pollIntervalMs,
labels: flagsArg.labels
? flagsArg.labels.split(',').map((labelArg) => labelArg.trim()).filter(Boolean)
: fileConfig.labels,
maxConcurrentChecks: flagsArg.concurrency
? Number(flagsArg.concurrency)
: fileConfig.maxConcurrentChecks,
};
validateConfig(config);
return config;
}
private showHelp(): void {
console.log(`uptimerunner ${denoConfig.version}
Usage:
uptimerunner run [--config path] [--url https://uptime.link] [--token token] [--runner-id id]
uptimerunner once [--config path]
uptimerunner check <url>
uptimerunner config write --url https://uptime.link --runner-id edge-1 --token token
uptimerunner service install|start|stop|restart|status|logs|uninstall
Environment:
UPTIMERUNNER_CONFIG
UPTIMERUNNER_INSTANCE_URL
UPTIMERUNNER_RUNNER_ID
UPTIMERUNNER_TOKEN
UPTIMERUNNER_LABELS
`);
}
}
function parseFlags(argsArg: string[]): Record<string, string> {
const flags: Record<string, string> = {};
for (let index = 0; index < argsArg.length; index++) {
const arg = argsArg[index];
if (!arg.startsWith('--')) {
continue;
}
const [rawName, inlineValue] = arg.slice(2).split('=', 2);
flags[rawName] = inlineValue ?? argsArg[++index];
}
return flags;
}
function requiredFlag(flagsArg: Record<string, string>, nameArg: string): string {
const value = flagsArg[nameArg];
if (!value) {
throw new Error(`Missing required --${nameArg} flag.`);
}
return value;
}
function mask(valueArg: string): string {
return valueArg.length <= 8 ? '********' : `${valueArg.slice(0, 4)}...${valueArg.slice(-4)}`;
}