import * as vscode from 'vscode'; import * as https from 'https'; import * as http from 'http'; import { URL } from 'url'; /** Ensure URL has http(s):// prefix and no trailing slash */ function normalizeUrl(raw: string): string { let url = raw.trim().replace(/\/+$/, ''); if (!/^https?:\/\//i.test(url)) { url = 'http://' + url; } return url; } export class ADClient { private productionMode = false; private _onProductionModeChanged = new vscode.EventEmitter(); readonly onProductionModeChanged = this._onProductionModeChanged.event; private log: vscode.OutputChannel; constructor(log: vscode.OutputChannel) { this.log = log; } private getConfig() { const config = vscode.workspace.getConfiguration('appdaemon'); return { url: normalizeUrl(config.get('adUrl', 'http://localhost:5050')) }; } private async request(method: string, path: string, body?: unknown): Promise { const { url } = this.getConfig(); const fullUrl = new URL(path, url); const isHttps = fullUrl.protocol === 'https:'; const lib = isHttps ? https : http; this.log.appendLine(`[AD] ${method} ${fullUrl.href}`); return new Promise((resolve, reject) => { const options: http.RequestOptions = { hostname: fullUrl.hostname, port: parseInt(fullUrl.port, 10) || (isHttps ? 443 : 5050), path: fullUrl.pathname + fullUrl.search, method, headers: { 'Content-Type': 'application/json' } }; const req = lib.request(options, (res) => { let data = ''; res.on('data', (chunk: Buffer) => { data += chunk; }); res.on('end', () => { this.log.appendLine(`[AD] ${res.statusCode} (${data.length} bytes)`); if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(data) as T); } catch { resolve(data as unknown as T); } } else { reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)); } }); }); req.on('error', (err) => { this.log.appendLine(`[AD] ERROR: ${err.message}`); reject(err); }); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timed out')); }); if (body) { req.write(JSON.stringify(body)); } req.end(); }); } /** * Restart a specific list of AppDaemon apps by name. */ async restartApps(appNames: string[]): Promise { this.log.appendLine(`[AD] Restarting specific apps: ${appNames.join(', ')}`); await Promise.all(appNames.map(app => this.request('POST', '/api/appdaemon/service/admin/app/restart', { app }) )); } /** * Reload all AppDaemon apps by fetching the app list then restarting each one. */ async reloadApps(): Promise { // Fetch app names from admin state type AdminState = { state: Record }; const resp = await this.request('GET', '/api/appdaemon/state/admin'); const stateMap = resp?.state ?? (resp as unknown as Record); const appNames = Object.keys(stateMap) .filter(k => k.startsWith('app.')) .map(k => k.slice(4)); // strip "app." prefix if (appNames.length === 0) { throw new Error('No apps found in AppDaemon state'); } this.log.appendLine(`[AD] Restarting ${appNames.length} apps: ${appNames.join(', ')}`); await Promise.all(appNames.map(app => this.request('POST', '/api/appdaemon/service/admin/app/restart', { app }) )); } /** * Set AppDaemon production mode on/off via the admin API. */ async setProductionMode(mode: boolean): Promise { await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode }); this.productionMode = mode; this._onProductionModeChanged.fire(mode); } /** * Try to read the current production mode from AppDaemon state. */ async fetchProductionMode(): Promise { try { const resp = await this.request<{ state: Record }>('GET', '/api/appdaemon/state/admin'); const state = resp?.state ?? resp as unknown as Record; if (state && typeof state === 'object') { for (const key of Object.keys(state)) { if (key.includes('production_mode')) { const val = (state[key] as { state?: unknown })?.state; this.productionMode = val === true || val === 'True' || val === 'true'; this._onProductionModeChanged.fire(this.productionMode); return this.productionMode; } } } } catch { // Fallback to cached value } return this.productionMode; } isProductionMode(): boolean { return this.productionMode; } async toggleProductionMode(): Promise { await this.setProductionMode(!this.productionMode); } dispose() { this._onProductionModeChanged.dispose(); } }