Created a vs code extension with Claude
This commit is contained in:
1 parent
923cfd4152
commit
3e1f0f3ca9
12 files changed
+1217
No files matched your search
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user