Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Jolley
7ca1516940 Adding intial JS extension spec 2026-06-17 20:38:20 -05:00
6 changed files with 2253 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
# 01 - Architecture Overview
## Process Model
Each JavaScript/TypeScript extension runs as an **isolated Node.js process**. The CmdPal host manages these processes through `JsonRpcExtensionService`, which implements the same `IExtensionService` interface used by WinRT and built-in extensions.
```mermaid
flowchart LR
subgraph Host["Host Process (CmdPal)"]
JSSVC["JsonRpcExtensionService"]
WRAP["JSExtensionWrapper"]
RPC["JsonRpcConnection"]
PROXY["JSCommandProviderProxy"]
ONE["(one per extension)"]
JSSVC --> WRAP --> RPC --> PROXY
JSSVC --> ONE
end
subgraph Ext["Extension Process (Node.js)"]
SDK["@microsoft/cmdpal-sdk"]
STDIO["stdio-server.ts\n(JSON-RPC 2.0)"]
CODE["Extension code\nindex.ts"]
SDK --> STDIO
CODE --> STDIO
end
RPC -->|"stdin"| STDIO
STDIO -->|"stdout"| RPC
```
### Why Per-Process?
1. **Crash isolation** — A runaway extension cannot crash CmdPal. If an extension process dies, only that extension stops working.
2. **Resource isolation** — Each extension has independent memory, CPU, and event loop.
3. **Independent debugging** — Attach a debugger to a specific extension's Node.js process via `--inspect`.
4. **Clean lifecycle** — Stop/restart an extension by killing and re-spawning its process.
5. **Security boundary** — Extensions cannot access host memory or other extensions' state.
### Crash Recovery
The host tracks consecutive crashes per extension:
| Crash Count | Behavior |
|-------------|----------|
| 13 | Extension marked as disconnected, available for restart |
| > 3 | Extension marked as **unhealthy**, disabled until manual re-enable |
## Extension Discovery
Extensions are discovered from a well-known directory:
```
%LOCALAPPDATA%\Microsoft\PowerToys\CommandPalette\JSExtensions\
```
Each subdirectory containing a valid `package.json` with a `cmdpal` section is treated as an extension:
```mermaid
flowchart TB
ROOT["JSExtensions/"] --> MY["my-extension/"]
ROOT --> ANOTHER["another-extension/"]
MY --> PKG["package.json\nmanifest with cmdpal section (required)"]
MY --> DIST["dist/"]
DIST --> JSENTRY["index.js\ncompiled entry point"]
MY --> NM["node_modules/"]
NM --> MS["@microsoft/cmdpal-sdk"]
MY --> SRC["src/"]
SRC --> TSENTRY["index.ts\nTypeScript source"]
ANOTHER --> APKG["package.json"]
ANOTHER --> AETC["..."]
```
### Directory Watching
The service watches the `JSExtensions` directory for:
| Event | Behavior |
|-------|----------|
| **New subdirectory created** | Scans for `package.json` with `cmdpal` section, loads extension if valid |
| **Subdirectory deleted** | Stops the extension process, removes from provider list |
| **`*.js` file changed** (within an extension) | Hot-reloads the extension (debounced 500ms) |
Source file watchers ignore `node_modules/` changes.
## Extension Lifecycle
```mermaid
flowchart TD
DISC["Discover\n(find package.json with cmdpal)"] --> PARSE["Parse Manifest\n(validate manifest)"]
PARSE --> START["Start Process\n(spawn node process)"]
START --> INIT["Initialize\n(JSON-RPC initialize)"]
INIT --> READY["Ready\n(host queries commands)"]
READY -->|"on shutdown/hot-reload"| STOP["Stop\n(send dispose, kill process)"]
READY -->|"on crash or error"| STOP
```
### Startup Sequence
1. `JsonRpcExtensionService.LoadProvidersAsync()` scans `JSExtensions/`
2. For each valid `package.json` containing a `cmdpal` section:
- Creates `JSExtensionWrapper`
- Spawns `node <entrypoint>` with stdio redirection
- Creates `JsonRpcConnection` over the process's stdin/stdout
- Sends `initialize` request and waits for response
- Creates `JSCommandProviderProxy` as the `ICommandProvider` implementation
- Wraps in `CommandProviderWrapper` and returns to `TopLevelCommandManager`
### Hot-Reload (Development)
When a `*.js` file changes in an extension directory:
1. Change detected by `FileSystemWatcher`
2. Debounced 500ms to coalesce rapid saves
3. Current process receives `dispose` notification
4. Process is killed after 2s grace period
5. New process is spawned and initialized
6. Crash counter is reset on successful initialization
### Debug Mode
Set `"debug": true` in the `cmdpal` section of `package.json` to start the Node.js process with `--inspect`:
```json
{
"cmdpal": {
"debug": true,
"debugPort": 9230
}
}
```
The host logs a Chrome DevTools URL for attaching:
```
chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9230
```
If `debugPort` is not specified, ports are auto-assigned starting at 9229.
## Transport Layer
### Framing
All messages use **LSP-style framing** (Language Server Protocol):
```http
Content-Length: <byte-count>\r\n
\r\n
<UTF-8 JSON body>
```
This is the same framing used by VS Code's Language Server Protocol, making it compatible with existing JSON-RPC tooling.
### Connection Details
| Property | Value |
|----------|-------|
| Transport | Process stdio (stdin for writes, stdout for reads) |
| Encoding | UTF-8 |
| Framing | `Content-Length` header (LSP-style) |
| Protocol | JSON-RPC 2.0 |
| Request timeout | 10 seconds |
| Concurrency | Serialized writes (lock-protected), async reads |
| Error channel | stderr (logged by host, not part of protocol) |
### Message Flow
```mermaid
sequenceDiagram
participant Host
participant Extension
Host->>Extension: Content-Length: N {"jsonrpc":"2.0","id":1,...}
Extension-->>Host: Content-Length: M {"jsonrpc":"2.0","id":1,...}
Extension-->>Host: Content-Length: K {"jsonrpc":"2.0","method":...} (no id)
```
## C# Host-Side Architecture
### Key Classes
| Class | Role |
|-------|------|
| `JsonRpcExtensionService` | Discovers, loads, and manages JS extension processes |
| `JSExtensionManifest` | Parses and validates `package.json` with `cmdpal` section |
| `JSExtensionWrapper` | Manages a single Node.js process lifecycle (implements `IExtensionWrapper`) |
| `JsonRpcConnection` | Low-level JSON-RPC 2.0 transport over stdio |
| `JSCommandProviderProxy` | Translates `ICommandProvider` interface calls to JSON-RPC requests |
| `JSCommandAdapter` | Adapts JSON command data to `ICommand` interface |
| `JSListPageProxy` | Adapts JSON list page data to `IListPage` interface |
| `JSContentPageProxy` | Adapts JSON content page data to `IContentPage` interface |
| `JSIconInfoAdapter` | Adapts JSON icon data (glyphs, base64, data URIs) to `IIconInfo` |
| `JSSeparatorAdapter` | Adapts separator markers to `ISeparatorFilterItem` |
### Adapter Pattern
The host-side uses an **adapter/proxy pattern** to present JSON-RPC responses as native `ICommand`, `IListPage`, `IContentPage`, etc. interfaces. This allows the existing CmdPal UI (built for WinRT extensions) to consume JS extensions without any changes to the UI layer.
```mermaid
flowchart LR
UI["UI Layer (unchanged)"] --> CPP["JSCommandProviderProxy: ICommandProvider"] --> CONN["JsonRpcConnection"] --> NODE["Node.js"]
LPP["JSListPageProxy: IListPage"]
CPPG["JSContentPageProxy: IContentPage"]
CAD["JSCommandAdapter: ICommand"]
IAD["JSIconInfoAdapter: IIconInfo"]
CPP --> LPP
CPP --> CPPG
CPP --> CAD
CPP --> IAD
```
### Icon Data Pipeline
JS extensions can provide icons in three formats:
| Format | `IconData` field | C# handling |
|--------|------------------|-------------|
| **Font glyph** | `icon: "\uE91B"` | `IconPathConverter.IconSourceMUX``FontIconSource` |
| **File/URI path** | `icon: "C:\\path\\icon.png"` | `IconPathConverter.IconSourceMUX``BitmapImage` |
| **Base64 data** | `data: "iVBOR..."` | `JSIconDataAdapter.Data``InMemoryRandomAccessStream``BitmapImage` |
| **Data URI** | `data: "data:image/png;base64,..."` | `JSIconDataAdapter.Data` → parse data URI → decode → stream |
For base64 images, the TS extension fetches/encodes the image data at runtime. The SDK provides helper functions (`iconFromUrl`, `iconFromFile`, `iconFromBase64`) to simplify this.
## Security Considerations
### Current State (v1)
JS extensions run as **unsandboxed Node.js processes** with the same permissions as the user running CmdPal. This is equivalent to running any Node.js application.
### Mitigations
- **Process isolation** — Extensions cannot access host process memory
- **No elevated privileges** — Extensions run at the user's permission level, never elevated
- **Crash containment** — Runaway extensions are auto-disabled after 3 consecutive crashes
- **No network exposure** — Communication is via stdio, not network sockets
### Future Considerations
- Permission model for filesystem, network, and clipboard access
- Extension signing and trust verification
- Sandboxed execution environments (e.g., V8 isolates)

View File

@@ -0,0 +1,603 @@
# 02 - TypeScript SDK Reference
> **Package:** `@microsoft/cmdpal-sdk`
> **Node.js:** ≥ 22.0.0
> **TypeScript:** ≥ 5.8
## Installation
```bash
npm install @microsoft/cmdpal-sdk
```
---
## Core Types
### Icon Types
```typescript
interface IconData {
icon?: string; // Font glyph character or file/URI path
data?: string | null; // Base64-encoded image data or data URI
}
interface IconInfo {
light?: IconData; // Icon for light theme
dark?: IconData; // Icon for dark theme
}
```
Icons can be provided as:
- **Font glyphs:** `{ icon: '\uE91B' }` — Segoe Fluent Icons / MDL2 Assets
- **File paths:** `{ icon: 'C:\\path\\to\\icon.png' }`
- **Base64 data:** `{ data: 'iVBORw0KGgo...' }` — raw base64-encoded image bytes
- **Data URIs:** `{ data: 'data:image/png;base64,iVBOR...' }`
### Color Types
```typescript
interface Color {
r: number; // 0-255
g: number;
b: number;
a: number; // 0-255 (default: 255)
}
interface OptionalColor {
hasValue: boolean;
color?: Color;
}
```
### Tags
```typescript
interface Tag {
icon?: IconInfo | null;
text: string;
foreground?: OptionalColor | null;
background?: OptionalColor | null;
toolTip?: string;
}
```
### Key Chords
```typescript
interface KeyChord {
modifiers: number; // Bitmask: Ctrl=1, Alt=2, Shift=4, Win=8
vkey: number; // Virtual key code
scanCode: number;
}
```
---
## Command Types
### ICommand
The base contract for all commands and pages.
```typescript
interface ICommand {
id: string; // Unique identifier
name: string; // Display name
icon?: IconInfo | null; // Optional icon
}
```
### IInvokableCommand
A command that can be executed.
```typescript
interface IInvokableCommand extends ICommand {
invoke(): Promise<CommandResult> | CommandResult;
}
```
### CommandResult
Returned from `invoke()` to tell the host what to do next.
```typescript
type CommandResultKind =
| 'dismiss' // Close CmdPal
| 'goHome' // Navigate to home
| 'goBack' // Navigate back
| 'hide' // Hide CmdPal (keep state)
| 'keepOpen' // Stay on current page
| 'goToPage' // Navigate to a page
| 'showToast' // Show toast notification
| 'confirm'; // Show confirmation dialog
interface CommandResult {
kind: CommandResultKind;
args?: CommandResultArgs;
}
```
### Result Args
```typescript
interface GoToPageArgs {
pageId: string;
navigationMode?: 'push' | 'goBack' | 'goHome';
}
interface ToastArgs {
message: string;
result?: CommandResult; // What to do after toast is dismissed
}
interface ConfirmationArgs {
title: string;
description: string;
primaryCommand?: ICommand;
isPrimaryCommandCritical?: boolean;
}
```
### Helper: Creating Results
```typescript
// Navigate to a page
{ kind: 'goToPage', args: { pageId: 'my-page', navigationMode: 'push' } }
// Show a toast
{ kind: 'showToast', args: { message: 'Done!' } }
// Confirmation dialog
{ kind: 'confirm', args: { title: 'Delete?', description: 'This cannot be undone.' } }
```
---
## Item Types
### ICommandItem
A selectable item shown in lists.
```typescript
interface ICommandItem {
command: ICommand;
moreCommands?: ContextItem[]; // Right-click / overflow menu
icon?: IconInfo | null;
title: string;
subtitle?: string;
}
```
### IListItem
Extended list item with metadata.
```typescript
interface IListItem extends ICommandItem {
tags?: Tag[];
details?: Details;
section?: string; // Section header text (creates visual grouping)
textToSuggest?: string; // Text to fill into search box on selection
}
```
### IFallbackCommandItem
A command that receives the user's search query in real-time.
```typescript
interface IFallbackCommandItem extends ICommandItem {
fallbackHandler?: IFallbackHandler;
displayTitle?: string; // Dynamic title that updates as user types
}
interface IFallbackHandler {
updateQuery(query: string): void;
}
```
### ContextItem
An action in a right-click or overflow menu.
```typescript
interface ContextItem {
command: ICommand;
icon?: IconInfo | null;
title: string;
subtitle?: string;
isCritical?: boolean; // Show in red/destructive style
requestedShortcut?: KeyChord; // Keyboard shortcut hint
}
```
### Separator
A visual divider in lists.
```typescript
import { Separator } from '@microsoft/cmdpal-sdk';
// Untitled separator (horizontal line)
new Separator()
// Section header separator
new Separator('Section Title')
```
---
## Details Panel
Rich metadata shown alongside a selected list item.
```typescript
interface Details {
heroImage?: IconInfo | null;
title?: string;
body?: string; // Markdown-formatted body text
metadata?: DetailsElement[];
}
interface DetailsElement {
key: string; // Label shown to the left
data: DetailsData; // Value shown to the right
}
// Discriminated union of detail data types
type DetailsData =
| DetailsTags // { type: 'tags', tags: Tag[] }
| DetailsLink // { type: 'link', link: string, text: string }
| DetailsCommands // { type: 'commands', commands: ICommand[] }
| DetailsSeparator; // { type: 'separator' }
```
---
## Page Types
### IListPage
A page that shows a scrollable list of items.
```typescript
interface IListPage extends IPage {
searchText?: string;
placeholderText?: string;
showDetails?: boolean; // Show details panel
filters?: Filters | null; // Filter bar
gridProperties?: GridProperties | null; // Grid/gallery layout
hasMoreItems?: boolean; // Infinite scroll
emptyContent?: ICommandItem | null;
getItems(): IListItem[] | Promise<IListItem[]>;
loadMore?(): void | Promise<void>;
}
```
### IDynamicListPage
A list page that receives search input in real-time.
```typescript
interface IDynamicListPage extends IListPage {
setSearchText(text: string): void;
}
```
### IContentPage
A page that displays rich content (markdown, forms, images, trees).
```typescript
interface IContentPage extends IPage {
getContent(): Content[] | Promise<Content[]>;
details?: Details | null;
commands?: ContextItem[];
}
```
### Filters
```typescript
interface Filter {
id: string;
name: string;
icon?: IconInfo | null;
}
interface Filters {
currentFilterId: string;
filters: Array<Filter | { separator: true }>;
}
```
### Grid Properties
```typescript
type GridLayoutType = 'small' | 'medium' | 'gallery';
interface GridProperties {
type: GridLayoutType;
showTitle?: boolean;
showSubtitle?: boolean;
}
```
---
## Content Types
Content pages display an array of `Content` items:
```typescript
type ContentType = 'markdown' | 'form' | 'tree' | 'plainText' | 'image';
interface MarkdownContent {
type: 'markdown';
body: string;
}
interface FormContent {
type: 'form';
templateJson: string; // Adaptive Card JSON template
dataJson: string; // Form data values JSON
stateJson?: string;
submitForm(inputs: string, data: string): CommandResult | Promise<CommandResult>;
}
interface ImageContent {
type: 'image';
image: IconInfo; // Base64-encoded image data (use iconFromUrl/iconFromFile)
maxWidth?: number;
maxHeight?: number;
}
interface PlainTextContent {
type: 'plainText';
text: string;
fontFamily?: 'userInterface' | 'monospace';
wrapWords?: boolean;
}
interface TreeContent {
type: 'tree';
rootContent: Content;
getChildren(): Content[] | Promise<Content[]>;
}
```
---
## Settings
```typescript
import { Settings, ToggleSetting, TextSetting, ChoiceSetSetting } from '@microsoft/cmdpal-sdk';
const settings = new Settings();
// Toggle (boolean)
settings.add(new ToggleSetting('darkMode', 'Dark Mode', true, 'Enable dark theme'));
// Text input
settings.add(new TextSetting('apiKey', 'API Key', '', 'Your API key'));
// Choice set (dropdown)
settings.add(new ChoiceSetSetting('language', 'Language', [
{ title: 'English', value: 'en' },
{ title: 'Spanish', value: 'es' },
], 'en'));
// Read values
const darkMode = settings.getSetting<ToggleSetting>('darkMode')?.value;
// Expose in provider
class MyProvider extends CommandProviderBase {
settings = settings;
// ... settings page auto-generated from settings definitions
}
```
---
## Base Classes
### CommandProviderBase
The entry point for every extension.
```typescript
class MyProvider extends CommandProviderBase implements ICommandProvider {
readonly id = 'my-extension';
readonly displayName = 'My Extension';
readonly icon = iconFromGlyph('\uE8A5');
topLevelCommands(): ICommandItem[] {
return [ /* ... */ ];
}
// Optional
fallbackCommands?(): IFallbackCommandItem[];
getCommand?(id: string): ICommand | null;
settings?: ICommandSettings;
initializeWithHost?(host: IExtensionHost): void;
dispose?(): void;
}
```
### ListPageBase
```typescript
class MyListPage extends ListPageBase implements IListPage {
readonly id = 'my-list';
readonly name = 'My List';
readonly title = 'My List Page';
getItems(): IListItem[] {
return [ /* ... */ ];
}
// Optional
showDetails?: boolean;
filters?: Filters;
gridProperties?: GridProperties;
placeholderText?: string;
}
```
### DynamicListPageBase
```typescript
class MySearchPage extends DynamicListPageBase implements IDynamicListPage {
readonly id = 'my-search';
readonly name = 'Search';
readonly title = 'Search Page';
private query = '';
setSearchText(text: string): void {
this.query = text;
this.notifyItemsChanged(); // Tell host to re-fetch items
}
getItems(): IListItem[] {
return allItems.filter(item => item.title.includes(this.query));
}
}
```
### ContentPageBase
```typescript
class MyContentPage extends ContentPageBase implements IContentPage {
readonly id = 'my-content';
readonly name = 'Content';
readonly title = 'My Content Page';
getContent(): Content[] {
return [
{ type: 'markdown', body: '# Hello\n\nThis is **markdown** content.' },
];
}
}
```
### InvokableCommandBase
```typescript
class MyCommand extends InvokableCommandBase implements IInvokableCommand {
readonly id = 'my-command';
readonly name = 'Do Something';
invoke(): CommandResult {
// Do work...
return { kind: 'showToast', args: { message: 'Done!' } };
}
}
```
---
## Built-In Commands
| Class | Purpose | `invoke()` returns |
|-------|---------|-------------------|
| `NoOpCommand` | Does nothing | `{ kind: 'keepOpen' }` |
| `OpenUrlCommand` | Opens a URL in the default browser | `{ kind: 'dismiss' }` |
| `CopyTextCommand` | Copies text to clipboard | Toast with copy confirmation |
| `ConfirmableCommand` | Shows a confirmation dialog | `{ kind: 'confirm', args: {...} }` |
---
## Icon Helpers
```typescript
import { iconFromGlyph, iconFromBase64, iconFromUrl, iconFromFile } from '@microsoft/cmdpal-sdk';
// Font glyph (Segoe Fluent Icons)
const icon = iconFromGlyph('\uE91B');
// Base64-encoded image data
const icon = iconFromBase64('iVBORw0KGgoAAAANSUhEUg...');
// Fetch image from URL (async — downloads and encodes as base64)
const icon = await iconFromUrl('https://example.com/icon.png');
// Read local file (async — reads and encodes as base64)
const icon = await iconFromFile('./assets/icon.png');
```
---
## Runtime API
### ExtensionHost
Static bridge for communicating with the CmdPal host.
```typescript
import { ExtensionHost } from '@microsoft/cmdpal-sdk';
// Logging
ExtensionHost.log('Something happened');
ExtensionHost.log('Error occurred', 'error');
// Status bar
ExtensionHost.showStatus('Loading...', 'info', { isIndeterminate: true });
ExtensionHost.hideStatus('loading-id');
// Clipboard
ExtensionHost.copyToClipboard('Hello, clipboard!');
```
### Activation
```typescript
import { activate, startJsonRpcServer } from '@microsoft/cmdpal-sdk';
// Standard activation pattern
startJsonRpcServer(() => {
return new MyProvider();
});
```
### Notifications
```typescript
import { sendNotification } from '@microsoft/cmdpal-sdk';
// Tell the host that a list page's items have changed
sendNotification('listPage/itemsChanged', { pageId: 'my-list' });
// Tell the host that a command's properties changed
sendNotification('command/propChanged', {
commandId: 'my-fallback',
properties: { displayTitle: 'Search: query text' },
});
```
---
## C# Toolkit Equivalence
| C# Toolkit | TypeScript SDK |
|------------|----------------|
| `ListPage` | `ListPageBase` |
| `DynamicListPage` | `DynamicListPageBase` |
| `ContentPage` | `ContentPageBase` |
| `InvokableCommand` | `InvokableCommandBase` |
| `CommandItem` | `CommandItemBase` |
| `ListItem` | `ListItemBase` |
| `FallbackCommandItem` | `FallbackCommandItemBase` |
| `Separator` | `Separator` |
| `NoOpCommand` | `NoOpCommand` |
| `OpenUrlCommand` | `OpenUrlCommand` |
| `CopyTextCommand` | `CopyTextCommand` |
| `ConfirmableCommand` | `ConfirmableCommand` |
| `Settings`/`SettingsPage` | `Settings` (auto-generates `IContentPage`) |
| `ToggleSetting` | `ToggleSetting` |
| `TextSetting` | `TextSetting` |
| `ChoiceSetSetting` | `ChoiceSetSetting` |
| `IconHelpers.FromRelativePath` | `iconFromFile` |
| `IconInfo.FromStream` | `iconFromBase64` / `iconFromUrl` |

View File

@@ -0,0 +1,509 @@
# 03 - JSON-RPC Protocol Specification
## Overview
Communication between the CmdPal host and JavaScript extensions uses **JSON-RPC 2.0** over **stdio** with **LSP-style `Content-Length` framing**.
### Framing Format
Every message (request, response, and notification) is preceded by a header:
```http
Content-Length: <byte-count>\r\n
\r\n
<UTF-8 JSON body>
```
Where `<byte-count>` is the byte length (not character length) of the JSON body.
### Connection Properties
| Property | Value |
|----------|-------|
| Transport | stdin (host→extension), stdout (extension→host) |
| Encoding | UTF-8 |
| Protocol | JSON-RPC 2.0 |
| Request timeout | 10 seconds |
| Concurrency | Requests are serialized (one at a time); notifications can interleave |
### Message Types
| Type | Has `id` | Has `method` | Direction |
|------|----------|-------------|-----------|
| Request | ✅ | ✅ | Host → Extension |
| Response | ✅ | ❌ | Extension → Host |
| Notification | ❌ | ✅ | Either direction |
---
## Host → Extension Requests
### `initialize`
Called once after the Node.js process starts. The extension should initialize its provider and return capabilities.
**Parameters:**
```json
{
"extensionId": "my-extension"
}
```
**Response:**
```json
{
"capabilities": ["commands"]
}
```
---
### `provider/getTopLevelCommands`
Fetches the extension's top-level command items (shown in the main CmdPal list).
**Parameters:** `null`
**Response:** Array of command items:
```json
[
{
"id": "cmd-1",
"title": "My Command",
"displayName": "My Command",
"subtitle": "Does something useful",
"command": {
"id": "cmd-1",
"name": "My Command",
"icon": { "light": { "icon": "\uE8A5" } },
"pageType": "dynamicListPage"
},
"icon": { "light": { "icon": "\uE8A5" } },
"moreCommands": []
}
]
```
The `command` object includes a `pageType` field when the command represents a page:
- `"listPage"` — static list page
- `"dynamicListPage"` — search-enabled list page
- `"contentPage"` — rich content page
- Absent — invokable command (no page)
---
### `provider/getFallbackCommands`
Fetches commands that receive the user's search query when no other results match.
**Parameters:** `null`
**Response:** Array of fallback command items, or `null`:
```json
[
{
"id": "search-web",
"title": "Search the web",
"displayName": "Search the web",
"command": { "id": "search-web", "name": "Search the web" }
}
]
```
---
### `provider/getCommand`
Fetches a specific command/page by ID. Used when navigating to a page.
**Parameters:**
```json
{
"commandId": "my-page-id"
}
```
**Response:** Command object or `null`:
```json
{
"id": "my-page-id",
"name": "My Page",
"pageType": "listPage",
"icon": { "light": { "icon": "\uE8A5" } }
}
```
---
### `provider/getSettings`
Fetches the extension's settings page ID.
**Parameters:** `null`
**Response:**
```json
{
"id": "settings-page-id"
}
```
Or `null` if the extension has no settings.
---
### `command/invoke`
Invokes a command by ID.
**Parameters:**
```json
{
"commandId": "my-command-id"
}
```
**Response:** Command result:
```json
{
"Kind": 6,
"Args": {
"Message": "Operation complete!"
}
}
```
`Kind` values:
| Value | Name | Description |
|-------|------|-------------|
| 0 | Dismiss | Close CmdPal |
| 1 | GoHome | Navigate to home |
| 2 | GoBack | Navigate back |
| 3 | Hide | Hide CmdPal (keep state) |
| 4 | KeepOpen | Stay on current page |
| 5 | GoToPage | Navigate to page (requires `PageId` in args) |
| 6 | ShowToast | Show toast notification (requires `Message` in args) |
| 7 | Confirm | Show confirmation dialog |
---
### `listPage/getItems`
Fetches items for a list page.
**Parameters:**
```json
{
"pageId": "my-list-page"
}
```
**Response:**
```json
{
"items": [
{
"id": "item-1",
"title": "Item One",
"subtitle": "Description",
"section": "Group A",
"command": { "id": "item-1-cmd", "name": "Item One" },
"icon": { "light": { "icon": "\uE8A5" } },
"tags": [{ "text": "New", "foreground": { "hasValue": true, "color": { "r": 255, "g": 255, "b": 255, "a": 255 } } }],
"details": {
"title": "Item One Details",
"body": "**Rich** markdown description",
"metadata": [
{ "key": "Author", "data": { "type": "tags", "tags": [{ "text": "mjolley" }] } },
{ "key": "Link", "data": { "type": "link", "link": "https://github.com", "text": "GitHub" } }
]
},
"moreCommands": [
{
"command": { "id": "copy-cmd", "name": "Copy" },
"title": "Copy to clipboard",
"icon": { "light": { "icon": "\uE8C8" } }
}
]
}
]
}
```
Items with `_isSeparator: true` are rendered as visual separators:
```json
{
"title": "Section Header",
"section": "Section Header",
"_isSeparator": true,
"command": null
}
```
---
### `listPage/setSearchText`
Updates the search text for a dynamic list page.
**Parameters:**
```json
{
"pageId": "my-dynamic-page",
"searchText": "user query"
}
```
**Response:** `null`
The extension should update its internal state and send a `listPage/itemsChanged` notification when items are ready.
---
### `listPage/setFilter`
Updates the active filter for a list page.
**Parameters:**
```json
{
"pageId": "my-filtered-page",
"filterId": "recent"
}
```
**Response:** `null`
---
### `listPage/loadMore`
Requests additional items for infinite scroll.
**Parameters:**
```json
{
"pageId": "my-page"
}
```
**Response:** `null`
The extension should append items and send a `listPage/itemsChanged` notification.
---
### `fallback/updateQuery`
Updates the search query for a fallback command.
**Parameters:**
```json
{
"commandId": "search-fallback",
"query": "user typed text"
}
```
**Response:** `null`
The extension should update its internal state and send a `command/propChanged` notification to update the display title.
---
### `contentPage/getContent`
Fetches content for a content page.
**Parameters:**
```json
{
"pageId": "my-content-page"
}
```
**Response:** Array of content items:
```json
[
{
"type": "markdown",
"body": "# Hello\n\nMarkdown content"
},
{
"type": "image",
"image": { "light": { "data": "iVBORw0KGgo..." } },
"maxWidth": 600,
"maxHeight": 400
},
{
"type": "form",
"templateJson": "{...adaptive card JSON...}",
"dataJson": "{...data values...}"
}
]
```
---
### `form/submit`
Submits form data from a content page or form content.
**Parameters:**
```json
{
"pageId": "my-form-page",
"inputs": "{\"name\":\"John\"}",
"data": "{}"
}
```
**Response:** Command result (same format as `command/invoke`).
---
### `dispose`
Notification sent before the host kills the extension process.
**Parameters:** `null`
**Note:** This is a notification (no `id`), not a request. The extension should clean up resources.
---
## Extension → Host Notifications
### `listPage/itemsChanged`
Tells the host to re-fetch items for a list page.
```json
{
"jsonrpc": "2.0",
"method": "listPage/itemsChanged",
"params": {
"pageId": "my-list-page"
}
}
```
---
### `command/propChanged`
Tells the host that a command's properties have changed (e.g., fallback display title).
```json
{
"jsonrpc": "2.0",
"method": "command/propChanged",
"params": {
"commandId": "my-fallback",
"properties": {
"displayTitle": "Search: new query"
}
}
}
```
---
### `host/logMessage`
Sends a log message to the host's logging system.
```json
{
"jsonrpc": "2.0",
"method": "host/logMessage",
"params": {
"message": "Extension initialized successfully",
"state": 0
}
}
```
State values: 0 = Info, 1 = Success, 2 = Warning, 3 = Error
---
### `host/showStatus`
Shows a status message in the CmdPal status bar.
```json
{
"jsonrpc": "2.0",
"method": "host/showStatus",
"params": {
"message": {
"Message": "Loading data...",
"State": 0
},
"context": "extension"
}
}
```
---
### `host/hideStatus`
Hides a previously shown status message.
```json
{
"jsonrpc": "2.0",
"method": "host/hideStatus",
"params": {
"message": {
"Message": "Loading data...",
"State": 0
}
}
}
```
---
### `host/copyText`
Copies text to the system clipboard (since Node.js doesn't have clipboard access).
```json
{
"jsonrpc": "2.0",
"method": "host/copyText",
"params": {
"text": "Text to copy to clipboard"
}
}
```
---
## Protocol Summary Table
| Method | Direction | Type | Purpose |
|--------|-----------|------|---------|
| `initialize` | Host → Ext | Request | Initialize extension |
| `provider/getTopLevelCommands` | Host → Ext | Request | Get top-level commands |
| `provider/getFallbackCommands` | Host → Ext | Request | Get fallback commands |
| `provider/getCommand` | Host → Ext | Request | Get command by ID |
| `provider/getSettings` | Host → Ext | Request | Get settings page |
| `command/invoke` | Host → Ext | Request | Invoke a command |
| `listPage/getItems` | Host → Ext | Request | Get list page items |
| `listPage/setSearchText` | Host → Ext | Request | Update search query |
| `listPage/setFilter` | Host → Ext | Request | Update active filter |
| `listPage/loadMore` | Host → Ext | Request | Load more items |
| `fallback/updateQuery` | Host → Ext | Request | Update fallback query |
| `contentPage/getContent` | Host → Ext | Request | Get content page content |
| `form/submit` | Host → Ext | Request | Submit form data |
| `dispose` | Host → Ext | Notification | Clean up before exit |
| `listPage/itemsChanged` | Ext → Host | Notification | Items have changed |
| `command/propChanged` | Ext → Host | Notification | Command props changed |
| `host/logMessage` | Ext → Host | Notification | Log message |
| `host/showStatus` | Ext → Host | Notification | Show status bar message |
| `host/hideStatus` | Ext → Host | Notification | Hide status bar message |
| `host/copyText` | Ext → Host | Notification | Copy text to clipboard |

View File

@@ -0,0 +1,326 @@
# 04 - Manifest, Packaging, and Installation
## Extension Project Structure
A CmdPal JavaScript extension is a standard Node.js project. Extension metadata is declared in the `cmdpal` field of `package.json` (similar to how VS Code uses `contributes`):
```
my-extension/
├── package.json # Node.js manifest + "cmdpal" section (required)
├── dist/
│ └── index.js # Compiled entry point
├── src/
│ └── index.ts # TypeScript source
├── tsconfig.json # TypeScript config
├── node_modules/ # Dependencies (ideally bundled)
└── icon.png # Extension icon (optional)
```
The key files:
- **`package.json`** — Standard Node.js package manifest with an added `cmdpal` section for CmdPal-specific metadata
- **`dist/index.js`** — The compiled JavaScript entry point that CmdPal will execute
---
## `package.json` Schema
CmdPal discovers extensions by finding directories with a `package.json` that contains a `cmdpal` object. Top-level npm fields provide identity; the `cmdpal` section provides CmdPal-specific metadata.
### Full Example
```json
{
"name": "@microsoft/cmdpal-my-extension",
"version": "1.0.0",
"description": "A brief description of the extension",
"main": "dist/index.js",
"engines": {
"node": ">=18"
},
"cmdpal": {
"displayName": "My Extension",
"icon": "icon.png",
"publisher": "your-name",
"capabilities": ["commands"],
"debug": false,
"debugPort": 9230
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"@microsoft/cmdpal-sdk": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"keywords": ["cmdpal", "powertoys", "command-palette"]
}
```
### Field Reference
#### Top-level fields (standard npm)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | ✅ | Package identifier. Must be unique across installed extensions. Used as the extension ID. |
| `version` | `string` | ❌ | Semantic version string (e.g., `"1.0.0"`). |
| `description` | `string` | ❌ | Brief description shown in the extension gallery and settings. |
| `main` | `string` | ✅ | Relative path to the entry point JavaScript file. This is what `node` executes. |
| `engines.node` | `string` | ❌ | Node.js version requirement (e.g., `">=18"`). |
#### `cmdpal` section fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `displayName` | `string` | ❌ | Human-readable name shown in CmdPal UI. Falls back to `name` if not provided. |
| `icon` | `string` | ❌ | Icon glyph character (e.g., `"\uE943"`) or relative path to an icon file (PNG recommended). |
| `publisher` | `string` | ❌ | Author or publisher name. |
| `capabilities` | `string[]` | ❌ | List of capabilities the extension provides (e.g., `["commands"]`). |
| `debug` | `boolean` | ❌ | When `true`, starts Node.js with `--inspect` for debugger attachment. Default: `false`. |
| `debugPort` | `integer` | ❌ | Inspector port when `debug` is `true`. If not specified, auto-assigned starting at 9229. |
| `main` | `string` | ❌ | Optional override of the top-level `main` field (for packages where the CmdPal entry point differs from the npm main). |
### Validation Rules
A `package.json` is recognized as a CmdPal extension if:
1. It contains a `cmdpal` object (even if empty: `"cmdpal": {}`)
2. `name` is present and non-empty
3. Either `cmdpal.main` or top-level `main` resolves to an existing file
---
## Installation Directory
Extensions are installed to:
```
%LOCALAPPDATA%\Microsoft\PowerToys\CommandPalette\JSExtensions\
```
Each extension occupies its own subdirectory:
```
JSExtensions/
├── my-extension/
│ ├── package.json ← contains "cmdpal" section
│ ├── dist/
│ │ └── index.js
│ └── node_modules/
├── another-extension/
│ ├── package.json
│ └── ...
```
### Discovery
The `JsonRpcExtensionService` watches this directory with a `FileSystemWatcher`:
- **New directory with valid `package.json`** → extension is loaded automatically
- **Directory removed** → extension is unloaded, Node.js process is terminated
- **`*.js` file changed** within an extension → hot-reload (500ms debounce)
This means:
- Installing an extension = copying its directory to `JSExtensions/`
- Uninstalling = deleting the directory
- Updating = replacing files (hot-reload handles `*.js` changes)
---
## Development Setup
### Creating a New Extension
1. **Create the project directory:**
```bash
mkdir my-extension && cd my-extension
npm init -y
```
2. **Install the SDK:**
```bash
npm install @microsoft/cmdpal-sdk
```
3. **Add the `cmdpal` section to `package.json`:**
```json
{
"name": "my-extension",
"version": "1.0.0",
"description": "My awesome CmdPal extension",
"main": "dist/index.js",
"cmdpal": {
"displayName": "My Extension",
"debug": true
},
"dependencies": {
"@microsoft/cmdpal-sdk": "^1.0.0"
}
}
```
4. **Create `tsconfig.json`:**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["src/**/*"]
}
```
5. **Write your extension** in `src/index.ts` (see [05-getting-started.md](./05-getting-started.md))
6. **Build:**
```bash
npx tsc
```
### Development Installation
For development, symlink or copy your extension to the JSExtensions directory:
```powershell
# Option 1: Copy
Copy-Item -Recurse ./my-extension "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions\my-extension"
# Option 2: Junction link (recommended for development)
New-Item -ItemType Junction -Path "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions\my-extension" -Target (Resolve-Path ./my-extension)
```
With a junction link, changes to your source files are reflected immediately (after build). The `*.js` file watcher triggers hot-reload automatically.
### Debugging
1. Set `"debug": true` in the `cmdpal` section of `package.json`
2. Optionally set `"debugPort": 9230` (or any available port)
3. Open Chrome DevTools: `chrome://inspect` or attach VS Code's debugger
4. The Node.js process starts with `--inspect=0.0.0.0:<port>`, ready for debugger attachment
---
## Production Packaging
### npm Package Structure
Extensions are distributed as standard npm packages. The recommended `package.json`:
```json
{
"name": "@publisher/cmdpal-my-extension",
"version": "1.0.0",
"description": "My CmdPal extension",
"main": "dist/index.js",
"cmdpal": {
"displayName": "My Extension",
"icon": "icon.png",
"publisher": "your-name",
"capabilities": ["commands"]
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"files": [
"dist/",
"icon.png"
],
"dependencies": {
"@microsoft/cmdpal-sdk": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"keywords": ["cmdpal", "powertoys", "command-palette"],
"engines": {
"node": ">=18"
}
}
```
### Naming Convention
Recommended npm package naming: `@publisher/cmdpal-<name>` or `cmdpal-<name>`.
The `cmdpal-` prefix helps with discoverability and could be used for future npm-based discovery.
---
## Extension Gallery Integration
### Gallery Manifest Entry
The existing CmdPal extension gallery pulls from a manifest that lists available extensions. For JavaScript extensions, the manifest entry includes npm package details:
```json
{
"name": "my-extension",
"displayName": "My Extension",
"description": "Does amazing things",
"publisher": "your-name",
"version": "1.0.0",
"icon": "https://example.com/icon.png",
"type": "jsonrpc",
"npm": {
"package": "@publisher/cmdpal-my-extension",
"registry": "https://registry.npmjs.org"
}
}
```
The `type: "jsonrpc"` field distinguishes JavaScript extensions from COM-based extensions.
### Installation Flow
When a user clicks "Install" for a JavaScript extension in the gallery:
1. CmdPal runs `npm install <package>` in the JSExtensions directory
2. The package is installed to `JSExtensions/<name>/`
3. `FileSystemWatcher` detects the new directory → extension loads automatically
4. The extension appears in the main CmdPal list
### Uninstallation Flow
When a user clicks "Uninstall":
1. CmdPal terminates the extension's Node.js process
2. The extension directory is deleted from `JSExtensions/`
3. `FileSystemWatcher` detects the removal → extension is unloaded
---
## Security Considerations
### Process Isolation
Each JavaScript extension runs in its own Node.js process:
- Separate memory space
- Separate event loop
- No direct access to other extensions or CmdPal internals
- Communication only through the JSON-RPC protocol
### Permissions
Currently, JavaScript extensions have the same permissions as the Node.js process:
- File system access
- Network access
- Process spawning
Future considerations:
- Extension permission declarations in `package.json` `cmdpal` section
- User consent prompts for sensitive permissions
- Sandboxing via Node.js `--experimental-policy` or similar mechanisms
### Trust Model
- Extensions installed from the gallery are implicitly trusted by the user
- Sideloaded extensions (copied to JSExtensions/) have no verification

View File

@@ -0,0 +1,484 @@
# 05 — Getting Started: Build Your First CmdPal Extension
This guide walks you through building a CmdPal JavaScript extension from scratch. By the end, you'll have a working extension with a searchable list page, a content page, and settings.
---
## Prerequisites
- **Node.js 18+** installed and on your PATH
- **PowerToys** with Command Palette enabled
- A text editor (VS Code recommended)
---
## Step 1: Scaffold the Project
```bash
mkdir my-first-extension && cd my-first-extension
npm init -y
npm install @microsoft/cmdpal-sdk
npm install --save-dev typescript
```
Create `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["src/**/*"]
}
```
Add the `cmdpal` section to your `package.json`:
```json
{
"name": "my-first-extension",
"version": "1.0.0",
"description": "A tutorial CmdPal extension",
"main": "dist/index.js",
"cmdpal": {
"displayName": "My First Extension",
"debug": true
},
"dependencies": {
"@microsoft/cmdpal-sdk": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
```
---
## Step 2: Create a Simple Command
Create `src/index.ts`:
```typescript
import {
CommandProvider,
Command,
ListPage,
ListItem,
CommandResultKind,
run,
iconFromGlyph,
} from "@microsoft/cmdpal-sdk";
// Define a simple command that shows a toast
class GreetCommand extends Command {
constructor() {
super({
id: "greet",
name: "Say Hello",
icon: iconFromGlyph("\uE76E"), // Checkmark icon
});
}
async invoke(): Promise<{ kind: CommandResultKind }> {
return {
kind: CommandResultKind.ShowToast,
args: { Message: "Hello from my extension!" },
};
}
}
// Define the main list page
class MainPage extends ListPage {
constructor() {
super({
id: "main-page",
name: "My First Extension",
icon: iconFromGlyph("\uE8A5"),
});
}
async getItems(): Promise<ListItem[]> {
return [
new ListItem({
title: "Say Hello",
subtitle: "Shows a greeting toast",
command: new GreetCommand(),
icon: iconFromGlyph("\uE76E"),
}),
];
}
}
// Define the provider
class MyProvider extends CommandProvider {
constructor() {
super("my-first-extension", "My First Extension");
}
getTopLevelCommands() {
return [
new ListItem({
title: "My First Extension",
subtitle: "A tutorial extension",
command: new MainPage(),
icon: iconFromGlyph("\uE8A5"),
}),
];
}
}
// Start the extension
run(new MyProvider());
```
---
## Step 3: Build and Install
```bash
# Build
npx tsc
# Install (junction link for development)
$extensionsDir = "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions"
New-Item -ItemType Junction -Path "$extensionsDir\my-first-extension" -Target (Resolve-Path .)
```
Open CmdPal — you should see "My First Extension" in the list. Click it to see your command, then click "Say Hello" to see the toast!
---
## Step 4: Add a Dynamic List Page (Search)
Let's add a searchable list that filters items as you type:
```typescript
import {
DynamicListPage,
// ... other imports
} from "@microsoft/cmdpal-sdk";
class SearchablePage extends DynamicListPage {
private allItems = [
{ title: "Apple", emoji: "🍎" },
{ title: "Banana", emoji: "🍌" },
{ title: "Cherry", emoji: "🍒" },
{ title: "Dragon Fruit", emoji: "🐉" },
{ title: "Elderberry", emoji: "🫐" },
];
constructor() {
super({
id: "searchable-page",
name: "Fruit Search",
icon: iconFromGlyph("\uE721"), // Search icon
placeholderText: "Search fruits...",
});
}
async getItems(): Promise<ListItem[]> {
const query = this.searchText?.toLowerCase() ?? "";
return this.allItems
.filter((item) => item.title.toLowerCase().includes(query))
.map(
(item) =>
new ListItem({
title: `${item.emoji} ${item.title}`,
subtitle: "A delicious fruit",
command: new ToastCommand(item.title),
})
);
}
}
class ToastCommand extends Command {
private fruit: string;
constructor(fruit: string) {
super({ id: `toast-${fruit}`, name: `Select ${fruit}` });
this.fruit = fruit;
}
async invoke() {
return {
kind: CommandResultKind.ShowToast,
args: { Message: `You selected ${this.fruit}!` },
};
}
}
```
Add the searchable page to your `MainPage.getItems()`:
```typescript
new ListItem({
title: "Fruit Search",
subtitle: "Search through a list of fruits",
command: new SearchablePage(),
icon: iconFromGlyph("\uE721"),
}),
```
---
## Step 5: Add a Content Page
Content pages display rich content — markdown, images, and forms:
```typescript
import {
ContentPage,
MarkdownContent,
ImageContent,
FormContent,
iconFromUrl,
// ... other imports
} from "@microsoft/cmdpal-sdk";
class AboutPage extends ContentPage {
constructor() {
super({
id: "about-page",
name: "About",
icon: iconFromGlyph("\uE946"), // Info icon
});
}
async getContent() {
return [
new MarkdownContent({
body: `# My First Extension
This extension was built with the CmdPal TypeScript SDK.
## Features
- **Simple commands** with toast notifications
- **Searchable lists** with dynamic filtering
- **Rich content** with markdown and images
`,
}),
];
}
}
```
---
## Step 6: Add Settings
Extensions can have a settings page using Adaptive Cards:
```typescript
import {
FormPage,
// ... other imports
} from "@microsoft/cmdpal-sdk";
class SettingsPage extends FormPage {
constructor() {
super({
id: "settings",
name: "Settings",
icon: iconFromGlyph("\uE713"),
});
}
async getContent() {
return [
new FormContent({
templateJson: JSON.stringify({
type: "AdaptiveCard",
body: [
{
type: "Input.Text",
id: "greeting",
label: "Custom Greeting",
placeholder: "Enter a greeting...",
value: "Hello",
},
{
type: "Input.Toggle",
id: "showEmoji",
title: "Show emoji in results",
value: "true",
},
],
actions: [
{
type: "Action.Submit",
title: "Save",
},
],
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
}),
dataJson: JSON.stringify({
greeting: "Hello",
showEmoji: "true",
}),
}),
];
}
async onFormSubmit(inputs: Record<string, string>) {
// Save settings (e.g., to a file or state)
console.log("Settings saved:", inputs);
return {
kind: CommandResultKind.ShowToast,
args: { Message: "Settings saved!" },
};
}
}
```
Register the settings page in your provider:
```typescript
class MyProvider extends CommandProvider {
constructor() {
super("my-first-extension", "My First Extension");
}
getSettings() {
return new SettingsPage();
}
// ... getTopLevelCommands
}
```
---
## Step 7: Add Context Commands and Details
List items can have context menu commands and a details panel:
```typescript
import {
CopyTextCommand,
OpenUrlCommand,
Details,
DetailsMetadata,
TagsData,
Tag,
LinkData,
// ... other imports
} from "@microsoft/cmdpal-sdk";
// In your getItems():
new ListItem({
title: "GitHub",
subtitle: "Open GitHub in your browser",
command: new OpenUrlCommand({
id: "open-github",
name: "Open GitHub",
url: "https://github.com",
}),
icon: iconFromGlyph("\uE774"),
tags: [
new Tag({ text: "Web", foreground: { r: 100, g: 200, b: 255, a: 255 } }),
],
details: new Details({
title: "GitHub",
body: "The world's leading software development platform.",
heroImage: iconFromGlyph("\uE774"),
metadata: [
new DetailsMetadata({
key: "URL",
data: new LinkData({
link: "https://github.com",
text: "github.com",
}),
}),
new DetailsMetadata({
key: "Tags",
data: new TagsData({
tags: [new Tag({ text: "Development" }), new Tag({ text: "Git" })],
}),
}),
],
}),
moreCommands: [
{
title: "Copy URL",
command: new CopyTextCommand({
id: "copy-github-url",
name: "Copy URL",
text: "https://github.com",
}),
icon: iconFromGlyph("\uE8C8"),
},
],
});
```
---
## Step 8: Rebuild and Test
```bash
# Rebuild after changes
npx tsc
```
CmdPal watches for `*.js` file changes and hot-reloads automatically. After running `tsc`, your extension will reload within ~500ms.
---
## Debugging Tips
### Enable Debug Mode
Set `"debug": true` in the `cmdpal` section of your `package.json`. The Node.js process starts with `--inspect`, allowing you to attach a debugger.
### Attach VS Code Debugger
Add to `.vscode/launch.json`:
```json
{
"type": "node",
"request": "attach",
"name": "Attach to CmdPal Extension",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
```
### View Logs
Use `host.log()` in your extension to send messages to the CmdPal log:
```typescript
// Inside any command or page method:
host.log("Fetching items...", MessageState.Info);
host.log("Error occurred!", MessageState.Error);
```
### Common Issues
| Issue | Solution |
|-------|----------|
| Extension not showing | Check `package.json` has a `cmdpal` section and valid `name` + `main` |
| Blank page | Check `getItems()` or `getContent()` is returning data |
| Command does nothing | Ensure `invoke()` returns a valid `CommandResultKind` |
| Images not loading | Use `iconFromUrl()` or `iconFromBase64()` helpers |
| Settings crash | Ensure form template JSON is valid Adaptive Card schema |
---
## Next Steps
- Read the [TypeScript SDK Reference](./02-typescript-sdk.md) for the full API
- Read the [JSON-RPC Protocol Specification](./03-jsonrpc-protocol.md) to understand the wire format
- Check out the [Sample Extension](../src/modules/cmdpal/ext/SampleJSExtension/) for a comprehensive example
- Read the [Architecture Overview](./01-architecture.md) for how it all fits together

View File

@@ -0,0 +1,83 @@
# Command Palette JavaScript Extension System Design Specification
> **Status:** Draft — seeking community and internal feedback<br/>
> **Last updated:** 2006-06-17
## Table of Contents
| Document | Description |
|----------|-------------|
| [01 — Architecture Overview](01-architecture.md) | Process model, extension lifecycle, transport, and security |
| [02 — TypeScript SDK Reference](02-typescript-sdk.md) | Full API surface: types, base classes, helpers, and runtime |
| [03 — JSON-RPC Protocol](03-jsonrpc-protocol.md) | Complete protocol specification: methods, notifications, framing |
| [04 — Extension Manifest & Packaging](04-manifest-packaging.md) | `package.json` schema, project structure, distribution |
---
## Executive Summary
Command Palette (CmdPal) is extending its extension model beyond in-process WinRT/COM extensions to support **JavaScript and TypeScript extensions** that run as isolated Node.js processes, communicating with the host over JSON-RPC 2.0 via stdio.
### Goals
1. **Developer accessibility** — Let web developers build CmdPal extensions using familiar tools (TypeScript, npm, Node.js)
2. **Process isolation** — Extension crashes don't take down CmdPal; extensions can't corrupt host state
3. **Type-safe SDK** — Full TypeScript type definitions mirroring the C# toolkit surface
4. **Developer experience** — Hot-reload on file changes, debugger attachment, familiar project structure
5. **Feature parity** — JS extensions can create list pages, content pages, forms, grids, settings, and more
### Non-Goals (v1)
- Browser/WebView-based extension UI rendering
- Sandboxed filesystem access or permission model
- Extension marketplace / auto-update infrastructure
- Multi-language JSONRPC support beyond JavaScript/TypeScript (Python, Go, etc.)
### Architecture at a Glance
```mermaid
flowchart LR
subgraph Host["Command Palette (WinUI Host)"]
subgraph JS["JsonRpcExtensionService"]
A["JSExtensionWrapper (ext A)"] --> ARPC["JsonRpcConnection\n(stdio, LSP framing)"]
B["JSExtensionWrapper (ext B)"] --> BRPC["JsonRpcConnection\n(stdio, LSP framing)"]
N["... one process per extension"]
end
W["WinRTExtensionService\n(existing COM/WinRT extensions)"]
BI["BuiltInExtensionService\n(built-in extensions)"]
end
ARPC <-->|"JSON-RPC 2.0 over stdio"| NODEA["node ext-a.js\n(TS SDK runtime)"]
BRPC <-->|"JSON-RPC 2.0 over stdio"| NODEB["node ext-b.js\n(TS SDK runtime)"]
```
Each JS extension runs in its own Node.js process. The host spawns the process, establishes a JSON-RPC 2.0 connection over stdin/stdout with LSP-style `Content-Length` framing, sends an `initialize` request, and then queries the extension for commands, pages, and content as the user navigates.
### Key Design Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Process model | One Node.js process per extension | Isolation, independent crash recovery, independent debugging |
| Transport | stdio with LSP framing | No port conflicts, no network exposure, proven by LSP ecosystem |
| Protocol | JSON-RPC 2.0 | Standard, well-tooled, bidirectional |
| SDK language | TypeScript | Type safety, npm ecosystem, familiar to web developers |
| Entry point | `cmdpal` field in `package.json` | Simple, declarative, same pattern as VS Code's contributions |
| Icon data | Base64-encoded in JSON | No filesystem sharing needed, works with generated/fetched images |
| Hot-reload | FileSystemWatcher on `*.js` | Immediate feedback during development |
---
## Feedback Requested
We are seeking feedback on the following areas:
1. **API surface** — Are the base classes and types intuitive? What's missing?
2. **Extension lifecycle** — Is the initialize → query → dispose model sufficient?
3. **Manifest schema** — What additional fields would be useful?
4. **Distribution** — Should we support npm-based installation? Local-only? Both?
5. **Security** — What permission boundaries should exist for JS extensions?
6. **Developer experience** — What tooling (CLI scaffolding, debugging, testing) is most important?
7. **Performance** — Are there concerns about per-extension Node.js processes?
Please file issues with the tag `[CmdPal-JS-SDK]`.