npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

doe-sdk

v1.1.0

Published

SDK for building DOE extensions

Readme

doe-sdk

SDK for building DOE extensions and themes.

Quick Start

# Interactive setup (recommended)
npx doe-sdk init

# Or quick create with a name
npx doe-sdk create my-extension

Then:

cd my-extension
pnpm install
pnpm dev

CLI Commands

# Project creation
npx doe-sdk init                  # Interactive project setup (extension or theme)
npx doe-sdk create <name>        # Quick scaffold with a name
npx doe-sdk create <name> --type theme  # Create a theme project

# Development
npx doe-sdk validate              # Validate manifest and code
npx doe-sdk validate --strict     # Strict validation
npx doe-sdk validate --fix        # Auto-fix issues

# Authentication
npx doe-sdk login                 # Login to developer account
npx doe-sdk logout                # Logout
npx doe-sdk whoami                # Show current auth status

# Publishing
npx doe-sdk publish               # Submit to marketplace
npx doe-sdk publish --dry-run     # Validate without submitting
npx doe-sdk status [submissionId] # Check submission status

Updating / Publishing a New Version

Bump the version in manifest.json and publish again:

# 1. Update version in manifest.json (e.g., "1.0.0" → "1.1.0")
# 2. Publish the new version
npx doe-sdk publish

The marketplace treats each publish as a new version. Users with the extension installed will see the update available.

Build and dev server are handled by the template's tooling (Vite):

pnpm dev      # Vite watch mode with hot reload
pnpm build    # Production build + validation

Project Structure

Extension

my-extension/
├── manifest.json              # Extension metadata and permissions
├── src/
│   ├── main.tsx               # Entry point
│   ├── App.tsx                # Root component
│   ├── styles.ts              # Styles
│   ├── hooks/
│   │   ├── useDoe.ts          # DOE API hook
│   │   └── useOnboarding.ts   # Onboarding state
│   └── components/            # UI components
├── dist/                      # Built output
├── icon.png                   # Extension icon
├── package.json
├── tsconfig.json
└── vite.config.ts

Theme

my-theme/
├── manifest.json              # Theme metadata
├── themes/
│   └── theme.json             # Theme token definitions
├── styles/                    # Optional custom styles
├── package.json
└── vite.config.ts

Manifest Configuration

Extension Manifest

{
  "id": "@your-username/my-extension",
  "version": "1.0.0",
  "name": "My Extension",
  "description": "A helpful description",
  "author": "Your Name",
  "permissions": ["canvas:read", "canvas:write", "storage:read", "storage:write"],
  "icon": "icon.png",
  "main": "dist/main.js",
  "ui": "dist/index.html",
  "categories": ["productivity"],
  "keywords": ["example"],
  "minDOEVersion": "1.0.0",
  "shapeBehavior": {
    "defaultWidth": 500,
    "defaultHeight": 700,
    "minWidth": 320,
    "minHeight": 400,
    "canResize": true,
    "canScroll": true
  }
}

Theme Manifest

{
  "id": "@your-username/my-theme",
  "version": "1.0.0",
  "name": "My Custom Theme",
  "description": "A beautiful custom theme",
  "author": "Your Name",
  "permissions": [],
  "categories": ["theme"],
  "minDOEVersion": "1.0.0",
  "contributes": {
    "themes": [
      {
        "path": "./themes/theme.json",
        "label": "My Theme"
      }
    ]
  }
}

Permissions Reference

Standard Permissions

| Permission | Description | |------------|-------------| | canvas:read | Read shapes created by this extension | | canvas:write | Create/modify/delete shapes | | storage:read | Read extension storage | | storage:write | Write extension storage | | clipboard:write | Write to clipboard | | ui:show-panel | Display extension panel | | config:read | Read extension configuration | | config:write | Write extension configuration | | profile:read-basic | Read user display name and avatar | | workspace:read-basic | Read workspace ID and name | | theme:read | Read theme tokens and subscribe to changes | | theme:write | Inject custom styles | | navigation:open-url | Open URLs in popup, browser, or as canvas shapes |

Sensitive Permissions (require user consent)

| Permission | Description | |------------|-------------| | canvas:read:all | Read ALL shapes on canvas | | clipboard:read | Read clipboard content | | network:fetch | Make HTTP requests (requires hostPermissions) |

Extension API

Extensions receive an injected API at window.__DOE_EXTENSION_API__:

import type { DOEExtensionAPI } from "doe-sdk";

const api = window.__DOE_EXTENSION_API__!;

// Read canvas shapes
const shapes = await api.canvas.getShapes();

// Store data
await api.storage.set("key", { data: "value" });

// Subscribe to events
api.events.onSelectionChange((payload) => {
  console.log("Selected:", payload.shapeIds);
});

// Show right-side panel
await api.ui.showPanel({ title: "My Extension", width: 400 });

// Show centered modal
await api.ui.showModal({ title: "Settings", width: 500, height: 400 });

Using the useDoe Hook

The template includes a useDoe hook for React extensions:

import { useDoe } from "./hooks/useDoe";

function MyComponent() {
  const { api, isReady, error } = useDoe();

  if (error) return <div>Standalone Mode</div>;
  if (!isReady) return <div>Connecting...</div>;

  return (
    <button onClick={() => api.ui.showToast({ message: "Hello!" })}>
      Show Toast
    </button>
  );
}

The extension works in both modes:

  • Standalone: For development (pnpm dev)
  • In DOE: Full API access when loaded as extension

Canvas API

// Read operations (requires canvas:read)
const shapes = await api.canvas.getShapes();
const shape = await api.canvas.getShape("shape-id");
const selected = await api.canvas.getSelectedShapes();
const viewport = await api.canvas.getViewport();
const info = await api.canvas.getCanvasInfo();

// Write operations (requires canvas:write)
const newShape = await api.canvas.createShape({
  type: "note",
  x: 100,
  y: 100,
  props: { text: "Hello!", color: "yellow" }
});
await api.canvas.updateShape("shape-id", { props: { text: "Updated" } });
await api.canvas.deleteShape("shape-id");
await api.canvas.selectShapes(["shape-1", "shape-2"]);
await api.canvas.panTo(500, 500);
await api.canvas.zoomTo(1.5);

// Smart placement with arrow linking (requires canvas:write)
const linked = await api.canvas.createLinkedShape({
  type: "geo",
  props: { text: "Related Note" },
  placement: "relative",       // "viewport-center" or "relative"
  relativePosition: "right",   // "right" | "left" | "above" | "below"
  spacing: 100,                // distance from extension shape
  arrow: "to",                 // "to" | "from" | "both" | "none"
});
console.log(linked.shapeId, linked.arrowId);

Storage API

// Requires storage:read and storage:write
await api.storage.set("myKey", { data: "value" });
const value = await api.storage.get<{ data: string }>("myKey");
const keys = await api.storage.keys();
await api.storage.delete("myKey");
await api.storage.clear();

Config API

Read and write extension configuration. Supports secure storage for secrets.

// Requires config:read and config:write
const config = await api.config.getConfig();
const apiKey = await api.config.get<string>("apiKey");
await api.config.set("theme", "dark");

// Open the extension's settings UI
await api.config.openSettings();

// Subscribe to configuration changes
const unsubscribe = api.config.onConfigChange((key, value) => {
  console.log(`Config ${key} changed to:`, value);
});

Configuration schema in manifest.json:

{
  "configurationSchema": {
    "type": "object",
    "properties": {
      "apiKey": {
        "type": "string",
        "title": "API Key",
        "description": "Your API key for the service",
        "format": "password"
      },
      "refreshInterval": {
        "type": "integer",
        "title": "Refresh Interval",
        "description": "Minutes between refreshes",
        "default": 30,
        "minimum": 1,
        "maximum": 60
      }
    },
    "required": ["apiKey"]
  }
}

Fields with format: "password" are securely stored in the OS keychain.

Events API

Subscribe to application events for reactive extensions.

// No permission required for basic events
const unsubscribe1 = api.events.onSelectionChange((payload) => {
  console.log("Selected shapes:", payload.shapeIds);
});

const unsubscribe2 = api.events.onCanvasChange((payload) => {
  console.log(`Canvas ${payload.type}:`, payload.shapeIds);
});

const unsubscribe3 = api.events.onThemeChange((payload) => {
  console.log("Theme:", payload.mode); // "light" or "dark"
});

// Requires config:read permission
const unsubscribe4 = api.events.onConfigChange((payload) => {
  console.log(`Config ${payload.key} changed to:`, payload.value);
});

// Clean up when done
unsubscribe1();
unsubscribe2();
unsubscribe3();
unsubscribe4();

UI API

// Show right-side panel (requires ui:show-panel)
await api.ui.showPanel({
  title: "My Extension",
  width: 400,
});
await api.ui.hidePanel();

// Show centered modal dialog (requires ui:show-panel)
await api.ui.showModal({
  title: "My Modal",
  width: 500,
  height: 400,
});
await api.ui.hideModal();

// Show toast notification
await api.ui.showToast("Operation complete!", {
  duration: 3000,
  type: "success" // "info" | "success" | "warning" | "error"
});

// Confirmation dialog
const confirmed = await api.ui.confirm("Delete this item?", {
  title: "Confirm Delete",
  confirmText: "Delete",
  cancelText: "Cancel"
});

// Get current theme (no permission required)
const theme = await api.ui.getTheme();
console.log(theme.mode); // "light" or "dark"

Network API

// Requires network:fetch permission + hostPermissions in manifest
const response = await api.network!.fetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: "test" }),
  timeout: 5000
});

if (response.ok) {
  const data = await response.json();
}

Manifest must include hostPermissions:

{
  "permissions": ["network:fetch"],
  "hostPermissions": ["https://api.example.com/*"]
}

Workspace API

// Get workspace info (requires workspace:read-basic)
const workspace = await api.workspace.getInfo();
console.log(workspace.id, workspace.name);

// Get current user (requires profile:read-basic)
const user = await api.workspace.getCurrentUser();
console.log(user?.displayName, user?.avatarUrl);

Navigation API

// Requires navigation:open-url permission

// Prompt user to choose how to open (default)
const result = await api.navigation.openUrl("https://example.com");
// result.action: "popup" | "default-browser" | "cancelled"

// Open directly in system default browser
await api.navigation.openUrl("https://docs.example.com", {
  mode: "default-browser",
});

// Open in a native popup window
await api.navigation.openUrl("https://app.example.com", {
  mode: "popup",
});

Clipboard API

// Write to clipboard (requires clipboard:write)
await api.clipboard!.writeText("Copied text");

// Read from clipboard (requires clipboard:read)
const text = await api.clipboard!.readText();
const hasContent = await api.clipboard!.hasContent();

Theme API

Extensions can access theme information and inject custom styles:

const api = window.__DOE_EXTENSION_API__!;

// Get current theme info
const themeId = await api.theme.getActiveThemeId();
const themeType = await api.theme.getThemeType(); // "light" or "dark"

// Get theme tokens
const accent = await api.theme.getToken("colors.accent");
const tokens = await api.theme.getTokens(["colors.background", "colors.text"]);

// Get CSS variable name for a token
const cssVar = await api.theme.getCSSVariable("colors.accent"); // "--doe-accent"

// List all installed themes
const themes = await api.theme.list();

// Subscribe to theme changes
const unsubscribe = api.theme.onThemeChange((event) => {
  console.log("Theme changed:", event.themeId, event.themeType);
});

// Inject custom styles (requires theme:write permission)
const injection = await api.theme.injectStyles(`
  .my-extension-panel {
    background: var(--doe-surface);
    color: var(--doe-text);
    border: 1px solid var(--doe-border);
  }
`);

// Later, remove the injected styles
await injection.remove();

Available Theme Tokens

| Token Path | CSS Variable | Description | |------------|--------------|-------------| | colors.background | --doe-bg | App background | | colors.surface | --doe-surface | Panel/card background | | colors.text | --doe-text | Primary text | | colors.textMuted | --doe-text-muted | Secondary text | | colors.accent | --doe-accent | Accent/brand color | | colors.border | --doe-border | Borders and dividers | | colors.error | --doe-error | Error states | | colors.warning | --doe-warning | Warning states | | colors.success | --doe-success | Success states | | typography.fontFamily | --doe-font-family | Primary font | | typography.fontFamilyMono | --doe-font-family-mono | Monospace font | | shapes.borderRadiusSm | --doe-radius-sm | Small radius | | shapes.borderRadiusMd | --doe-radius-md | Medium radius | | effects.shadowSm | --doe-shadow-sm | Small shadow | | effects.shadowMd | --doe-shadow-md | Medium shadow |

Creating Theme Extensions

npx doe-sdk init --type theme
# or
npx doe-sdk create my-theme --type theme

Theme JSON file structure (themes/theme.json):

{
  "id": "my-theme",
  "name": "My Theme",
  "version": "1.0.0",
  "author": "Your Name",
  "type": "dark",
  "colors": {
    "background": "#1a1a2e",
    "surface": "#252541",
    "text": "#eaeaea",
    "textMuted": "#a0a0a0",
    "accent": "#e94560",
    "border": "#3d3d5c"
  }
}

TypeScript Support

Full TypeScript support with type definitions:

import type {
  DOEExtensionAPI,
  ExtensionManifest,
  Shape,
  CanvasReadAPI,
  ConfigAPI,
  EventsAPI,
  UIAPI,
  PanelOptions,
  ModalOptions,
  SelectionChangePayload,
  CanvasChangePayload,
  ThemeChangePayload,
  ConfigChangePayload,
  NavigationAPI,
  OpenUrlMode,
  OpenUrlOptions,
  OpenUrlResult,
  UserBasicInfo,
  WorkspaceInfo,
} from "doe-sdk";

License

MIT