Created a vs code extension with Claude

This commit is contained in:
Pierre committed 2026-04-17 13:35:57 +02:00
1 parent 923cfd4152
commit 3e1f0f3ca9
12 files changed
+1217

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
import * as vscode from 'vscode';
import { HAClient } from './haClient';
import { ADClient } from './adClient';
import { StatusBarManager } from './statusBar';
import { EntityCompletionProvider, EntityHoverProvider } from './entityProvider';
import { ErrorViewer } from './errorViewer';
let haClient: HAClient;
let adClient: ADClient;
let statusBar: StatusBarManager;
let errorViewer: ErrorViewer;
let outputChannel: vscode.OutputChannel;
// Non-app top-level YAML keys to ignore
const NON_APP_KEYS = new Set([
'global_modules', 'global_dependencies', 'secrets',
'appdaemon', 'http', 'hadashboard', 'admin', 'api', 'plugins'
]);
/**
* Parse AppDaemon app names from a YAML document.
* Returns only top-level keys whose block contains a `module:` line.
*/
function parseAppsFromDocument(doc: vscode.TextDocument): string[] {
if (doc.languageId !== 'yaml') { return []; }
const lines = doc.getText().split('\n');
const apps: string[] = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^([a-z][a-z0-9_]*):\s*$/);
if (!m) { continue; }
const key = m[1];
if (NON_APP_KEYS.has(key)) { continue; }
// Scan the indented block below for a `module:` key
for (let j = i + 1; j < Math.min(i + 20, lines.length); j++) {
if (lines[j].match(/^\S/)) { break; } // end of block
if (lines[j].match(/^\s+module\s*:/)) { apps.push(key); break; }
}
}
return apps;
}
function updateContextualButton(editor: vscode.TextEditor | undefined) {
if (!editor) { statusBar.updateContextualApps([]); return; }
const apps = parseAppsFromDocument(editor.document);
statusBar.updateContextualApps(apps);
}
export async function activate(context: vscode.ExtensionContext) {
// ── Output channel for debug ─────────────────────────────────────────────
outputChannel = vscode.window.createOutputChannel('AppDaemon');
outputChannel.appendLine('AppDaemon extension activating...');
const config = vscode.workspace.getConfiguration('appdaemon');
outputChannel.appendLine(` haUrl = ${config.get('haUrl')}`);
outputChannel.appendLine(` haToken = ${config.get<string>('haToken', '') ? '***set***' : '*** NOT SET ***'}`);
outputChannel.appendLine(` adUrl = ${config.get('adUrl')}`);
// ── Clients ──────────────────────────────────────────────────────────────
haClient = new HAClient(outputChannel);
adClient = new ADClient(outputChannel);
errorViewer = new ErrorViewer();
statusBar = new StatusBarManager(adClient);
// Wire error count → status bar badge
errorViewer.onErrorCountChanged((count) => statusBar.setErrorCount(count));
// ── Contextual per-file restart button ───────────────────────────────────
updateContextualButton(vscode.window.activeTextEditor);
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(updateContextualButton),
vscode.workspace.onDidSaveTextDocument(doc => {
if (vscode.window.activeTextEditor?.document === doc) {
updateContextualButton(vscode.window.activeTextEditor);
}
})
);
// Initial entity load (non-blocking)
haClient.fetchEntities().then(entities => {
if (entities.length > 0) {
outputChannel.appendLine(`Ready: ${entities.length} entities loaded`);
} else {
outputChannel.appendLine('Warning: 0 entities loaded — check haUrl and haToken settings');
}
});
// Try to read current production mode from AD
adClient.fetchProductionMode().catch(() => { /* ignore on startup */ });
// ── Language features (YAML + Python) ────────────────────────────────────
const selector: vscode.DocumentSelector = [
{ scheme: 'file', language: 'yaml' },
{ scheme: 'file', language: 'python' }
];
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
selector,
new EntityCompletionProvider(haClient),
'.' // trigger completion when user types a dot
),
vscode.languages.registerHoverProvider(
selector,
new EntityHoverProvider(haClient)
)
);
// ── Commands ─────────────────────────────────────────────────────────────
context.subscriptions.push(
vscode.commands.registerCommand('appdaemon.restartCurrentFileApps', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('AppDaemon: No active file');
return;
}
const apps = parseAppsFromDocument(editor.document);
if (apps.length === 0) {
vscode.window.showWarningMessage('AppDaemon: No app definitions found in this file');
return;
}
try {
await adClient.restartApps(apps);
vscode.window.showInformationMessage(`AppDaemon: Restarted ${apps.join(', ')}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
vscode.window.showErrorMessage(`AppDaemon: Restart failed — ${msg}`);
}
}),
vscode.commands.registerCommand('appdaemon.restartAD', async () => {
try {
await adClient.reloadApps();
vscode.window.showInformationMessage('AppDaemon: Apps reloaded');
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
vscode.window.showErrorMessage(`AppDaemon: Reload failed — ${msg}`);
}
}),
vscode.commands.registerCommand('appdaemon.restartHA', async () => {
const answer = await vscode.window.showWarningMessage(
'Restart Home Assistant?',
{ modal: true },
'Restart HA',
'Restart Host'
);
if (!answer) {
return;
}
try {
if (answer === 'Restart Host') {
await haClient.restartHost();
vscode.window.showInformationMessage('Home Assistant: Host restart initiated');
} else {
await haClient.restartHA();
vscode.window.showInformationMessage('Home Assistant: Restart initiated');
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
vscode.window.showErrorMessage(`Home Assistant: Restart failed — ${msg}`);
}
}),
vscode.commands.registerCommand('appdaemon.toggleProductionMode', async () => {
try {
await adClient.toggleProductionMode();
const mode = adClient.isProductionMode() ? 'ON' : 'OFF';
vscode.window.showInformationMessage(`AppDaemon: Production mode ${mode}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
vscode.window.showErrorMessage(`AppDaemon: Toggle failed — ${msg}`);
}
}),
vscode.commands.registerCommand('appdaemon.showErrors', () => {
errorViewer.show();
}),
vscode.commands.registerCommand('appdaemon.refreshEntities', async () => {
const entities = await haClient.fetchEntities();
vscode.window.showInformationMessage(`AppDaemon: ${entities.length} entities loaded`);
}),
vscode.commands.registerCommand('appdaemon.clearErrors', () => {
errorViewer.clearDiagnostics();
vscode.window.showInformationMessage('AppDaemon: Error diagnostics cleared');
})
);
// ── Disposables ──────────────────────────────────────────────────────────
context.subscriptions.push(
outputChannel,
{ dispose: () => haClient.dispose() },
{ dispose: () => adClient.dispose() },
{ dispose: () => statusBar.dispose() },
{ dispose: () => errorViewer.dispose() }
);
}
export function deactivate() {
haClient?.dispose();
adClient?.dispose();
statusBar?.dispose();
errorViewer?.dispose();
}