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-extensionThen:
cd my-extension
pnpm install
pnpm devCLI 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 statusUpdating / 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 publishThe 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 + validationProject 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.tsTheme
my-theme/
├── manifest.json # Theme metadata
├── themes/
│ └── theme.json # Theme token definitions
├── styles/ # Optional custom styles
├── package.json
└── vite.config.tsManifest 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 themeTheme 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
