Skip to main content

Best Boilerplates for Building Developer Tools 2026

·StarterPick Team
developer-toolsclivscode-extensionnpm-packagetypescript2026

TL;DR

There's no single "developer tools boilerplate" — you pick the right starter for your tool type. CLI tools: use create-typescript-app or oclif. VS Code extensions: the official Yeoman generator or generator-code. npm packages: tsup-starter or pkgroll setup. Language servers: vscode-languageserver-node. Each category has established patterns in 2026 with TypeScript as the non-negotiable baseline.

Key Takeaways

  • CLI tools: oclif (Heroku, complex CLIs) or commander/meow (lightweight)
  • VS Code extensions: Official yo code generator, then customize
  • npm packages: tsup + changesets + GitHub Actions release workflow
  • Language servers: LSP protocol via vscode-languageserver-node or @volar/language-server
  • Browser DevTools: Chrome DevTools extension template
  • TypeScript: Non-negotiable baseline for all categories in 2026

CLI Tools: Two Approaches

Lightweight CLI (commander/meow pattern)

npm create typescript-app@latest my-cli
# Or from scratch:
mkdir my-cli && cd my-cli
npm init -y
npm install typescript commander chalk ora
npm install -D @types/node tsup
// src/index.ts — production CLI pattern:
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';

const packageJson = JSON.parse(
  await readFile(new URL('../package.json', import.meta.url), 'utf-8')
);

program
  .name('my-cli')
  .description('My developer tool CLI')
  .version(packageJson.version);

program
  .command('analyze <path>')
  .description('Analyze files in a directory')
  .option('-r, --recursive', 'Analyze recursively', false)
  .option('--format <type>', 'Output format (json|table)', 'table')
  .action(async (path, options) => {
    const spinner = ora('Analyzing...').start();
    
    try {
      const result = await analyze(resolve(path), options);
      spinner.succeed(chalk.green('Analysis complete!'));
      
      if (options.format === 'json') {
        console.log(JSON.stringify(result, null, 2));
      } else {
        printTable(result);
      }
    } catch (err) {
      spinner.fail(chalk.red(`Failed: ${err.message}`));
      process.exit(1);
    }
  });

program.parse();
// package.json for CLI:
{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "my-cli": "./dist/index.js"
  },
  "type": "module",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsup src/index.ts --format esm --target node18",
    "dev": "tsx src/index.ts",
    "test": "vitest"
  },
  "publishConfig": {
    "access": "public"
  }
}
npx oclif generate my-cli
cd my-cli
// src/commands/analyze.ts — oclif command:
import { Command, Flags, Args } from '@oclif/core';
import chalk from 'chalk';

export default class Analyze extends Command {
  static override description = 'Analyze files in a directory';
  
  static override args = {
    path: Args.string({ description: 'Path to analyze', required: true }),
  };
  
  static override flags = {
    recursive: Flags.boolean({ char: 'r', description: 'Analyze recursively' }),
    format: Flags.string({ char: 'f', default: 'table', options: ['json', 'table'] }),
    output: Flags.file({ char: 'o', description: 'Write output to file' }),
  };
  
  async run(): Promise<void> {
    const { args, flags } = await this.parse(Analyze);
    
    this.log(chalk.blue(`Analyzing ${args.path}...`));
    const result = await analyze(args.path, flags);
    
    if (flags.format === 'json') {
      this.log(JSON.stringify(result, null, 2));
    } else {
      this.table(result.files, {
        name: { header: 'File' },
        size: { header: 'Size' },
        issues: { header: 'Issues' },
      });
    }
  }
}
oclif gives you:
  → Auto-generated help (--help on every command)
  → Plugin system (users can extend your CLI)
  → Auto-update mechanism
  → Multi-command CLIs with subcommands
  → Tab completion
  
Used by: Heroku CLI, Salesforce CLI, Twilio CLI

VS Code Extension Boilerplate

# Official generator:
npm install -g yo generator-code
yo code

# Choose:
# - Extension (TypeScript/JavaScript)
# - Color Theme
# - Language Support
# - Snippet pack
Generated structure:
my-extension/
  src/
    extension.ts     ← Main entry (activate/deactivate)
    test/
      suite/
  package.json       ← Contributes section = extension manifest
  .vscode/
    launch.json      ← Debug configuration
  tsconfig.json
// src/extension.ts — VS Code extension pattern:
import * as vscode from 'vscode';

let statusBarItem: vscode.StatusBarItem;

export function activate(context: vscode.ExtensionContext) {
  // Register a command:
  const analyzeCommand = vscode.commands.registerCommand(
    'myExtension.analyze',
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showWarningMessage('No active editor');
        return;
      }
      
      const document = editor.document;
      const text = document.getText();
      
      await vscode.window.withProgress(
        {
          location: vscode.ProgressLocation.Notification,
          title: 'Analyzing...',
          cancellable: false,
        },
        async (progress) => {
          progress.report({ increment: 0 });
          const result = await analyze(text);
          progress.report({ increment: 100 });
          
          vscode.window.showInformationMessage(
            `Analysis complete: ${result.issues} issues found`
          );
        }
      );
    }
  );
  
  // Status bar item:
  statusBarItem = vscode.window.createStatusBarItem(
    vscode.StatusBarAlignment.Right,
    100
  );
  statusBarItem.command = 'myExtension.analyze';
  statusBarItem.text = '$(search) Analyze';
  statusBarItem.show();
  
  // Register a code action provider:
  const codeActionProvider = vscode.languages.registerCodeActionsProvider(
    ['typescript', 'javascript'],
    new MyCodeActionProvider(),
    { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }
  );
  
  context.subscriptions.push(analyzeCommand, statusBarItem, codeActionProvider);
}

export function deactivate() {
  statusBarItem?.dispose();
}

class MyCodeActionProvider implements vscode.CodeActionProvider {
  provideCodeActions(
    document: vscode.TextDocument,
    range: vscode.Range,
    context: vscode.CodeActionContext
  ): vscode.CodeAction[] {
    return context.diagnostics
      .filter(d => d.code === 'MY_RULE')
      .map(diagnostic => {
        const fix = new vscode.CodeAction('Fix: auto-fix', vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();
        fix.edit.replace(document.uri, diagnostic.range, 'corrected-value');
        fix.diagnostics = [diagnostic];
        fix.isPreferred = true;
        return fix;
      });
  }
}
// package.json contributes section:
{
  "contributes": {
    "commands": [
      {
        "command": "myExtension.analyze",
        "title": "Analyze Current File",
        "category": "My Extension"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "myExtension.analyze",
          "when": "editorHasSelection",
          "group": "myExtension"
        }
      ]
    },
    "configuration": {
      "title": "My Extension",
      "properties": {
        "myExtension.autoAnalyze": {
          "type": "boolean",
          "default": false,
          "description": "Auto-analyze on save"
        }
      }
    },
    "keybindings": [
      {
        "command": "myExtension.analyze",
        "key": "ctrl+shift+a",
        "mac": "cmd+shift+a",
        "when": "editorTextFocus"
      }
    ]
  }
}

npm Package: Release Automation

# Modern npm package starter:
npm create typescript-app@latest my-package
# Or manual setup:
mkdir my-package && cd my-package
npm init -y
npm install -D typescript tsup vitest @changesets/cli
// package.json — dual CJS/ESM exports:
{
  "name": "my-package",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "test": "vitest run",
    "release": "changeset publish"
  }
}
// tsup.config.ts:
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  treeshake: true,
});
# .github/workflows/release.yml — automated releases:
name: Release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: 'https://registry.npmjs.org'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
      - run: pnpm build
      - name: Create release PR or publish
        uses: changesets/action@v1
        with:
          publish: pnpm release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Decision Guide

Building a CLI tool:
  → Simple single-command: commander + tsup
  → Multi-command with plugins: oclif
  → Maximum speed (Bun-based): oclif on Bun or commander + Bun

Building a VS Code extension:
  → Start with yo code (official generator)
  → Add language server if syntax highlighting/intellisense needed
  → Use vscode-test for integration testing

Building an npm package:
  → tsup + changesets is the 2026 standard
  → Dual CJS/ESM exports via exports field
  → Automated releases via changesets/action GitHub Action

Building a language server:
  → vscode-languageserver-node for VS Code integration
  → Volar (@volar/language-server) for Vue-ecosystem tools
  → Both implement the Language Server Protocol (LSP)

Find developer tool boilerplates at StarterPick.

Comments