Build VS Code extensions that wrap CLI tools with panels, commands, and webviews
Expert guidance for building VS Code extensions that provide UI shells for CLI tools, with patterns specific to the Power Platform Developer Suite extension architecture.
Extensions that:
**The extension is a UI shell.** Heavy logic lives in the CLI tool being wrapped. The extension:
1. Use `any` without explicit type annotation - use interfaces or `unknown` with type narrowing
2. Add `eslint-disable` comments - fix root causes instead
3. Use non-null assertions (`!`) - write explicit null checks: `if (x === null) return;`
4. Duplicate code 3+ times - create abstraction on 2nd copy
5. Embed HTML in TypeScript files - extract to separate view files
1. Enable TypeScript strict mode for compile-time safety
2. Add explicit return types to all public methods
3. Use singleton pattern for VS Code panels (`createOrShow()` factory)
4. Search codebase for existing patterns before implementing new ones
5. Test manually via F5 (Extension Development Host) as primary validation
```typescript
export class MyPanel {
private static currentPanel: MyPanel | undefined;
public static createOrShow(context: vscode.ExtensionContext): void {
if (MyPanel.currentPanel) {
MyPanel.currentPanel.panel.reveal();
return;
}
const panel = vscode.window.createWebviewPanel(
'myPanel',
'Panel Title',
vscode.ViewColumn.One,
{ enableScripts: true }
);
MyPanel.currentPanel = new MyPanel(panel, context);
}
private constructor(
public readonly panel: vscode.WebviewPanel,
private readonly context: vscode.ExtensionContext
) {
this.panel.onDidDispose(() => {
MyPanel.currentPanel = undefined;
});
this.panel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'refresh':
await this.refresh();
break;
}
});
}
private async refresh(): Promise<void> {
const data = await this.fetchData();
this.panel.webview.postMessage({ type: 'data', payload: data });
}
}
```
```typescript
import { execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
interface CliResult {
success: boolean;
data: unknown;
error?: string;
}
async function callCli(args: string[]): Promise<CliResult> {
try {
const { stdout, stderr } = await execFileAsync('ppds', args);
if (stderr) {
return { success: false, error: stderr, data: null };
}
return { success: true, data: JSON.parse(stdout) };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
data: null
};
}
}
// Usage
const result = await callCli(['plugins', 'list', '--json']);
if (result.success) {
// Parse result.data into typed interface
}
```
```typescript
this.panel.webview.postMessage({
type: 'update',
payload: { plugins: pluginList }
});
```
```typescript
this.panel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'refresh':
await this.loadData();
break;
case 'select':
await this.handleSelection(message.id);
break;
}
});
```
1. Press F5 to launch Extension Development Host
2. Manually test your feature end-to-end
3. Verify UI rendering, commands, error handling
Write tests for:
Skip tests for:
```bash
npm test # Run all tests
npm run compile # Full build with lint + tests
npm run compile:fast # Quick build, no lint/tests
```
1. **Implement** - Write the feature code
2. **F5 Test** - Launch Extension Development Host and test manually
3. **Iterate** - Fix issues until feature works correctly
4. **Add Tests** - Only if logic is complex enough to warrant tests
5. **Prepare PR** - Update CHANGELOG.md, run full validation
1. Reproduce bug via F5
2. Fix the root cause
3. Verify fix via F5 again
4. Add regression test if bug was in complex logic
5. Commit with conventional commit message
```bash
ppds plugins list --json # List registered plugins
ppds plugins register <path> # Register plugin assembly
ppds env list --json # List environments
ppds solution export <name> # Export solution
```
```
extension/
├── src/
│ ├── panels/ # VS Code panel implementations
│ ├── commands/ # Command handlers
│ ├── services/ # CLI wrappers and utilities
│ └── extension.ts # Extension entry point
├── test/ # Unit tests
├── e2e/ # E2E tests (sparingly)
└── package.json # Extension manifest
```
```
feat: add plugin registration panel
fix: prevent null reference in environment activation
docs: update panel development guide
```
1. Update `CHANGELOG.md` with your changes
2. Run full compile: `npm run compile`
3. Ensure all tests pass
4. Verify no TypeScript errors
Before implementing new functionality, search codebase for existing patterns:
If your extension wraps a CLI you control, coordinate changes:
1. **New CLI command needed** - Implement in CLI repo first, then extension
2. **CLI output changed** - Update parsing logic in extension
3. **Cross-repo feature** - Work in parent workspace containing both repos
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/power-platform-vs-code-extension-development/raw