@notehub.md/cli 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,547 @@
1
+ # Plugin Examples
2
+
3
+ Complete, working plugin examples you can use as templates.
4
+
5
+ ---
6
+
7
+ ## Example 1: Hello World (Minimal)
8
+
9
+ The simplest possible plugin.
10
+
11
+ ### `manifest.json`
12
+
13
+ ```json
14
+ {
15
+ "id": "hello-world",
16
+ "name": "Hello World",
17
+ "version": "1.0.0"
18
+ }
19
+ ```
20
+
21
+ ### `src/index.ts`
22
+
23
+ ```typescript
24
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
25
+
26
+ export default class HelloWorld extends NotehubPlugin {
27
+ async onload(ctx: PluginContext): Promise<void> {
28
+ await ctx.invokeApi('logger:info', 'HelloWorld', '👋 Hello from my first plugin!');
29
+
30
+ // Register a simple API
31
+ ctx.registerApi('hello:greet', (name: string) => {
32
+ return `Hello, ${name}!`;
33
+ });
34
+ }
35
+
36
+ async onunload(): Promise<void> {
37
+ console.log('Goodbye!');
38
+ }
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Example 2: Word Counter Widget
45
+
46
+ A widget that counts words in the current paragraph.
47
+
48
+ ### `manifest.json`
49
+
50
+ ```json
51
+ {
52
+ "id": "word-counter",
53
+ "name": "Word Counter Widget",
54
+ "version": "1.0.0"
55
+ }
56
+ ```
57
+
58
+ ### `src/index.ts`
59
+
60
+ ```typescript
61
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
62
+ import React from 'react';
63
+
64
+ // Widget component
65
+ const WordCounter: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
66
+ const text = match[1];
67
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0).length;
68
+ const chars = text.length;
69
+
70
+ return (
71
+ <span style={{
72
+ display: 'inline-flex',
73
+ gap: '12px',
74
+ padding: '4px 12px',
75
+ background: 'var(--nh-bg-surface)',
76
+ borderRadius: '4px',
77
+ fontSize: '12px',
78
+ color: 'var(--nh-text-muted)',
79
+ border: '1px solid var(--nh-border-subtle)',
80
+ }}>
81
+ <span>📝 {words} words</span>
82
+ <span>📏 {chars} chars</span>
83
+ </span>
84
+ );
85
+ };
86
+
87
+ export default class WordCounterPlugin extends NotehubPlugin {
88
+ async onload(ctx: PluginContext): Promise<void> {
89
+ // Match: {{count: any text here}}
90
+ await ctx.invokeApi(
91
+ 'editor:register-widget',
92
+ 'word-counter',
93
+ /\{\{count:\s*(.+?)\}\}/g,
94
+ WordCounter
95
+ );
96
+
97
+ await ctx.invokeApi('logger:info', 'WordCounter', 'Widget registered');
98
+ }
99
+
100
+ async onunload(): Promise<void> {}
101
+ }
102
+ ```
103
+
104
+ **Usage:**
105
+ ```markdown
106
+ {{count: This is a sample text with exactly ten words total}}
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Example 3: Settings Plugin
112
+
113
+ A plugin with configurable settings.
114
+
115
+ ### `manifest.json`
116
+
117
+ ```json
118
+ {
119
+ "id": "settings-demo",
120
+ "name": "Settings Demo",
121
+ "version": "1.0.0"
122
+ }
123
+ ```
124
+
125
+ ### `src/index.ts`
126
+
127
+ ```typescript
128
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
129
+
130
+ export default class SettingsDemo extends NotehubPlugin {
131
+ async onload(ctx: PluginContext): Promise<void> {
132
+ // Register settings tab
133
+ await ctx.invokeApi('settings:register-tab', {
134
+ id: 'settings-demo',
135
+ label: 'Demo Settings',
136
+ icon: 'sliders',
137
+ order: 100
138
+ });
139
+
140
+ // Register group
141
+ await ctx.invokeApi('settings:register-group', {
142
+ id: 'demo-general',
143
+ tabId: 'settings-demo',
144
+ label: 'General Options',
145
+ order: 0
146
+ });
147
+
148
+ // Register items
149
+ await ctx.invokeApi('settings:register-items', [
150
+ {
151
+ key: 'settings-demo.enabled',
152
+ type: 'toggle',
153
+ label: 'Enable feature',
154
+ description: 'Turn the demo feature on or off',
155
+ groupId: 'demo-general',
156
+ order: 0,
157
+ defaultValue: true
158
+ },
159
+ {
160
+ key: 'settings-demo.name',
161
+ type: 'text',
162
+ label: 'Your name',
163
+ placeholder: 'Enter your name...',
164
+ groupId: 'demo-general',
165
+ order: 1,
166
+ defaultValue: ''
167
+ },
168
+ {
169
+ key: 'settings-demo.count',
170
+ type: 'number',
171
+ label: 'Item count',
172
+ min: 1,
173
+ max: 100,
174
+ step: 1,
175
+ groupId: 'demo-general',
176
+ order: 2,
177
+ defaultValue: 10
178
+ },
179
+ {
180
+ key: 'settings-demo.mode',
181
+ type: 'select',
182
+ label: 'Display mode',
183
+ options: [
184
+ { label: 'Compact', value: 'compact' },
185
+ { label: 'Normal', value: 'normal' },
186
+ { label: 'Expanded', value: 'expanded' }
187
+ ],
188
+ groupId: 'demo-general',
189
+ order: 3,
190
+ defaultValue: 'normal'
191
+ },
192
+ {
193
+ key: 'settings-demo.color',
194
+ type: 'color',
195
+ label: 'Highlight color',
196
+ groupId: 'demo-general',
197
+ order: 4,
198
+ defaultValue: '#3b82f6'
199
+ }
200
+ ]);
201
+
202
+ // Read and use settings
203
+ const enabled = await ctx.invokeApi<boolean>('config:get', 'settings-demo.enabled', true);
204
+ const name = await ctx.invokeApi<string>('config:get', 'settings-demo.name', 'User');
205
+
206
+ await ctx.invokeApi('logger:info', 'SettingsDemo',
207
+ `Loaded! enabled=${enabled}, name="${name}"`);
208
+ }
209
+
210
+ async onunload(): Promise<void> {}
211
+ }
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Example 4: Context Menu Extension
217
+
218
+ Add custom actions to file explorer context menu.
219
+
220
+ ### `manifest.json`
221
+
222
+ ```json
223
+ {
224
+ "id": "quick-actions",
225
+ "name": "Quick Actions",
226
+ "version": "1.0.0"
227
+ }
228
+ ```
229
+
230
+ ### `src/index.ts`
231
+
232
+ ```typescript
233
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
234
+
235
+ interface FilePayload {
236
+ path: string;
237
+ isDirectory: boolean;
238
+ }
239
+
240
+ export default class QuickActionsPlugin extends NotehubPlugin {
241
+ private ctx!: PluginContext;
242
+
243
+ async onload(ctx: PluginContext): Promise<void> {
244
+ this.ctx = ctx;
245
+
246
+ await ctx.invokeApi(
247
+ 'context-menu:register',
248
+ 'explorer-item',
249
+ (payload: FilePayload) => this.buildMenu(payload)
250
+ );
251
+
252
+ await ctx.invokeApi('logger:info', 'QuickActions', 'Context menu registered');
253
+ }
254
+
255
+ private buildMenu(payload: FilePayload) {
256
+ const items = [];
257
+
258
+ // Only for markdown files
259
+ if (payload.path.endsWith('.md')) {
260
+ items.push({
261
+ type: 'action' as const,
262
+ id: 'qa-word-count',
263
+ label: 'Word Count',
264
+ icon: 'hash',
265
+ onClick: () => this.countWords(payload.path)
266
+ });
267
+
268
+ items.push({
269
+ type: 'action' as const,
270
+ id: 'qa-add-date',
271
+ label: 'Add Today\'s Date',
272
+ icon: 'calendar',
273
+ onClick: () => this.addDate(payload.path)
274
+ });
275
+ }
276
+
277
+ // For all files
278
+ items.push({ type: 'separator' as const });
279
+
280
+ items.push({
281
+ type: 'action' as const,
282
+ id: 'qa-copy-path',
283
+ label: 'Copy Path',
284
+ icon: 'copy',
285
+ onClick: () => navigator.clipboard.writeText(payload.path)
286
+ });
287
+
288
+ // Duplicate action
289
+ if (!payload.isDirectory) {
290
+ items.push({
291
+ type: 'action' as const,
292
+ id: 'qa-duplicate',
293
+ label: 'Duplicate File',
294
+ icon: 'files',
295
+ onClick: () => this.duplicateFile(payload.path)
296
+ });
297
+ }
298
+
299
+ return items;
300
+ }
301
+
302
+ private async countWords(path: string) {
303
+ const content = await this.ctx.invokeApi<string>('fs:read-text-file', path);
304
+ const words = content.split(/\s+/).filter(w => w.length > 0).length;
305
+ await this.ctx.invokeApi('dialog:alert', 'Word Count', `${words} words in this file`);
306
+ }
307
+
308
+ private async addDate(path: string) {
309
+ const content = await this.ctx.invokeApi<string>('fs:read-text-file', path);
310
+ const date = new Date().toISOString().split('T')[0];
311
+ const newContent = `---\ndate: ${date}\n---\n\n${content}`;
312
+ await this.ctx.invokeApi('fs:write-text-file', path, newContent);
313
+ await this.ctx.invokeApi('dialog:alert', 'Success', 'Date added to file');
314
+ }
315
+
316
+ private async duplicateFile(path: string) {
317
+ const content = await this.ctx.invokeApi<string>('fs:read-text-file', path);
318
+ const newPath = path.replace(/\.md$/, ' (copy).md');
319
+ await this.ctx.invokeApi('fs:write-text-file', newPath, content);
320
+ await this.ctx.invokeApi('dialog:alert', 'Success', `Created: ${newPath}`);
321
+ }
322
+
323
+ async onunload(): Promise<void> {}
324
+ }
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Example 5: Full-Featured Plugin
330
+
331
+ A complete plugin combining widgets, settings, and context menus.
332
+
333
+ ### `manifest.json`
334
+
335
+ ```json
336
+ {
337
+ "id": "task-tracker",
338
+ "name": "Task Tracker",
339
+ "version": "1.0.0"
340
+ }
341
+ ```
342
+
343
+ ### `src/index.ts`
344
+
345
+ ```typescript
346
+ import { NotehubPlugin, PluginContext } from '@notehub/api';
347
+ import React, { useState } from 'react';
348
+
349
+ // === Widget: Interactive Task Checkbox ===
350
+ const TaskWidget: React.FC<{ match: RegExpExecArray }> = ({ match }) => {
351
+ const status = match[1]; // 'x' or ' '
352
+ const text = match[2];
353
+ const [checked, setChecked] = useState(status === 'x');
354
+
355
+ return (
356
+ <span
357
+ onClick={() => setChecked(!checked)}
358
+ style={{
359
+ display: 'inline-flex',
360
+ alignItems: 'center',
361
+ gap: '8px',
362
+ padding: '4px 8px',
363
+ background: checked ? 'var(--nh-accent-primary)20' : 'var(--nh-bg-surface)',
364
+ borderRadius: '4px',
365
+ cursor: 'pointer',
366
+ transition: 'all 0.2s',
367
+ }}
368
+ >
369
+ <span style={{
370
+ width: '18px',
371
+ height: '18px',
372
+ borderRadius: '4px',
373
+ border: `2px solid ${checked ? 'var(--nh-accent-primary)' : 'var(--nh-border-subtle)'}`,
374
+ background: checked ? 'var(--nh-accent-primary)' : 'transparent',
375
+ display: 'flex',
376
+ alignItems: 'center',
377
+ justifyContent: 'center',
378
+ color: '#fff',
379
+ fontSize: '12px',
380
+ }}>
381
+ {checked && '✓'}
382
+ </span>
383
+ <span style={{
384
+ textDecoration: checked ? 'line-through' : 'none',
385
+ color: checked ? 'var(--nh-text-muted)' : 'var(--nh-text-primary)',
386
+ }}>
387
+ {text}
388
+ </span>
389
+ </span>
390
+ );
391
+ };
392
+
393
+ // === Plugin Class ===
394
+ export default class TaskTracker extends NotehubPlugin {
395
+ private ctx!: PluginContext;
396
+
397
+ async onload(ctx: PluginContext): Promise<void> {
398
+ this.ctx = ctx;
399
+
400
+ // 1. Register widget
401
+ await ctx.invokeApi(
402
+ 'editor:register-widget',
403
+ 'task-tracker:checkbox',
404
+ /\[([x ])\]\s+(.+?)(?=\n|$)/g,
405
+ TaskWidget
406
+ );
407
+
408
+ // 2. Register settings
409
+ await ctx.invokeApi('settings:register-tab', {
410
+ id: 'task-tracker',
411
+ label: 'Task Tracker',
412
+ icon: 'check-square',
413
+ order: 50
414
+ });
415
+
416
+ await ctx.invokeApi('settings:register-group', {
417
+ id: 'task-tracker-options',
418
+ tabId: 'task-tracker',
419
+ label: 'Options',
420
+ order: 0
421
+ });
422
+
423
+ await ctx.invokeApi('settings:register-items', [
424
+ {
425
+ key: 'task-tracker.style',
426
+ type: 'select',
427
+ label: 'Checkbox style',
428
+ options: [
429
+ { label: 'Round', value: 'round' },
430
+ { label: 'Square', value: 'square' }
431
+ ],
432
+ groupId: 'task-tracker-options',
433
+ order: 0,
434
+ defaultValue: 'square'
435
+ },
436
+ {
437
+ key: 'task-tracker.strikethrough',
438
+ type: 'toggle',
439
+ label: 'Strikethrough completed',
440
+ groupId: 'task-tracker-options',
441
+ order: 1,
442
+ defaultValue: true
443
+ }
444
+ ]);
445
+
446
+ // 3. Register context menu
447
+ await ctx.invokeApi(
448
+ 'context-menu:register',
449
+ 'explorer-item',
450
+ (payload: { path: string }) => {
451
+ if (!payload.path.endsWith('.md')) return [];
452
+
453
+ return [
454
+ {
455
+ type: 'action' as const,
456
+ id: 'tt-count-tasks',
457
+ label: 'Count Tasks',
458
+ icon: 'list-checks',
459
+ onClick: () => this.countTasks(payload.path)
460
+ }
461
+ ];
462
+ }
463
+ );
464
+
465
+ await ctx.invokeApi('logger:info', 'TaskTracker', 'Plugin loaded successfully!');
466
+ }
467
+
468
+ private async countTasks(path: string) {
469
+ const content = await this.ctx.invokeApi<string>('fs:read-text-file', path);
470
+ const total = (content.match(/\[[x ]\]/g) || []).length;
471
+ const done = (content.match(/\[x\]/g) || []).length;
472
+
473
+ await this.ctx.invokeApi(
474
+ 'dialog:alert',
475
+ 'Task Summary',
476
+ `${done}/${total} tasks completed (${Math.round(done/total*100)}%)`
477
+ );
478
+ }
479
+
480
+ async onunload(): Promise<void> {
481
+ await this.ctx.invokeApi('logger:info', 'TaskTracker', 'Plugin unloaded');
482
+ }
483
+ }
484
+ ```
485
+
486
+ **Usage in documents:**
487
+ ```markdown
488
+ ## Today's Tasks
489
+
490
+ [x] Review pull request
491
+ [ ] Write documentation
492
+ [ ] Deploy to production
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Build Configuration
498
+
499
+ ### `package.json`
500
+
501
+ ```json
502
+ {
503
+ "name": "my-plugin",
504
+ "version": "1.0.0",
505
+ "type": "module",
506
+ "scripts": {
507
+ "build": "esbuild src/index.ts --bundle --format=esm --outfile=main.js --external:@notehub/api --external:react --external:react-dom",
508
+ "watch": "npm run build -- --watch"
509
+ },
510
+ "devDependencies": {
511
+ "@notehub/api": "file:../path/to/notehub/packages/api",
512
+ "@types/react": "^18.2.0",
513
+ "esbuild": "^0.20.0",
514
+ "typescript": "^5.3.0"
515
+ }
516
+ }
517
+ ```
518
+
519
+ ### `tsconfig.json`
520
+
521
+ ```json
522
+ {
523
+ "compilerOptions": {
524
+ "target": "ES2020",
525
+ "module": "ESNext",
526
+ "moduleResolution": "bundler",
527
+ "lib": ["ES2020", "DOM"],
528
+ "jsx": "react-jsx",
529
+ "strict": true,
530
+ "esModuleInterop": true,
531
+ "skipLibCheck": true,
532
+ "declaration": false,
533
+ "outDir": "./dist"
534
+ },
535
+ "include": ["src/**/*"]
536
+ }
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Tips for Development
542
+
543
+ 1. **Use `npm run watch`** for automatic rebuilding
544
+ 2. **Notehub auto-reloads** plugins when files change
545
+ 3. **Open DevTools** (Ctrl+Shift+I) for console output
546
+ 4. **Use `logger:info`** for structured logging
547
+ 5. **Test incrementally** - add features one at a time
@@ -0,0 +1,125 @@
1
+ <h1 align="center">🔌 Notehub Plugin Developer Guide</h1>
2
+
3
+ <p align="center">
4
+ <em>Create powerful plugins for Notehub.md - the extensible note-taking app</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="#-quick-start">Quick Start</a> •
9
+ <a href="#-documentation">Documentation</a> •
10
+ <a href="#-examples">Examples</a> •
11
+ <a href="#-api-reference">API Reference</a>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ## 🚀 Quick Start
17
+
18
+ ### Option 1: Use the Plugin Generator (Recommended)
19
+
20
+ For **internal plugins** (part of the monorepo):
21
+
22
+ ```bash
23
+ pnpm gen:plugin
24
+ ```
25
+
26
+ This interactive CLI will:
27
+ 1. Ask for a plugin name (kebab-case, e.g., `my-feature`)
28
+ 2. Let you choose a category (`system`, `ui`, `features`)
29
+ 3. Generate the full plugin structure
30
+
31
+ **Output:**
32
+ ```
33
+ 🔌 Notehub.md Plugin Generator
34
+
35
+ ✔ Plugin name (kebab-case): word-counter
36
+ ✔ Select plugin category: features - User-facing features
37
+
38
+ 📦 Creating plugin: nh.features.word-counter
39
+ Path: packages/plugins/features/word-counter
40
+
41
+ ✅ Created: package.json
42
+ ✅ Created: tsconfig.json
43
+ ✅ Created: manifest.json
44
+ ✅ Created: src/index.ts
45
+
46
+ ✨ Plugin created successfully!
47
+ ```
48
+
49
+ ---
50
+
51
+ ### Option 2: Manual Setup (External Plugins)
52
+
53
+ For plugins that will be loaded at runtime from a vault:
54
+
55
+ #### 1. Create a plugin folder
56
+
57
+ ```bash
58
+ mkdir my-plugin && cd my-plugin
59
+ npm init -y
60
+ npm install @notehub/api typescript esbuild --save-dev
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 📚 Documentation
66
+
67
+ | Chapter | Description |
68
+ |---------|-------------|
69
+ | [Getting Started](01-getting-started.md) | Prerequisites, setup, first plugin |
70
+ | [Architecture](02-architecture.md) | Plugin lifecycle, EventBus, ApiBus |
71
+ | [API Reference](03-api-reference.md) | All 50+ API methods with examples |
72
+ | [Widgets](04-widgets.md) | Custom React components in notes |
73
+ | [Settings](05-settings.md) | Add configuration UI |
74
+ | [Context Menu](06-context-menu.md) | Right-click menu integration |
75
+ | [Examples](07-examples.md) | Complete working plugins |
76
+
77
+ ---
78
+
79
+ ## 💡 What Can Plugins Do?
80
+
81
+ | Feature | API |
82
+ |---------|-----|
83
+ | 📁 Read/write files | `fs:read-text-file`, `fs:write-text-file` |
84
+ | ⚙️ Save settings | `config:get`, `config:set` |
85
+ | 🎨 Register themes | `theme:register`, `theme:set` |
86
+ | 🧩 Create widgets | `editor:register-widget` |
87
+ | 📋 Context menus | `context-menu:register` |
88
+ | 💬 Show dialogs | `dialog:alert`, `dialog:confirm` |
89
+ | 📡 Subscribe to events | `ctx.subscribe()` |
90
+
91
+ ---
92
+
93
+ ## 🎯 Examples
94
+
95
+ ### Hello World
96
+ ```typescript
97
+ ctx.registerApi('hello:greet', (name: string) => `Hello, ${name}!`);
98
+ ```
99
+
100
+ ### Progress Bar Widget
101
+ ```typescript
102
+ await ctx.invokeApi('editor:register-widget', 'progress', /\[progress:(\d+)\]/g,
103
+ ({ match }) => <ProgressBar value={parseInt(match[1])} />);
104
+ ```
105
+
106
+ ### File Watcher
107
+ ```typescript
108
+ ctx.subscribe<{ path: string }>('explorer:file-selected', (payload) => {
109
+ console.log('Selected:', payload.path);
110
+ });
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 🔗 Links
116
+
117
+ - [Main README](../../README.md)
118
+ - [API Package](../../packages/api)
119
+ - [Example Plugins](../../packages/plugins)
120
+
121
+ ---
122
+
123
+ <p align="center">
124
+ <strong>Happy coding! 🎉</strong>
125
+ </p>