diffwiki-core 0.4.0 → 0.5.0-rc.202607230329.fb70592

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.
package/dist/index.d.cts CHANGED
@@ -1,10 +1,20 @@
1
- type CollectionType = 'global' | 'repo';
1
+ import { SearchDoc } from './bm25.cjs';
2
+ export { RankableFields, rankDocs, tokenize } from './bm25.cjs';
3
+ import { LogLevel, Logger } from '@logtape/logtape';
4
+
5
+ type CollectionType = 'global' | 'repo' | 'external';
2
6
  interface CollectionEntry {
3
7
  name: string;
4
8
  type: CollectionType;
5
- /** Absolute path to the collection's directory. */
9
+ /** Absolute path to the collection's content directory. */
6
10
  path: string;
7
11
  createdAt: string;
12
+ /** External only: stable git repo id (see `repoId`). Identity + dedupe key. */
13
+ id?: string;
14
+ /** External only: absolute repo root (branch/behind + open-at-root). */
15
+ repoRoot?: string;
16
+ /** External only: content dir relative to `repoRoot` (default "docs"). */
17
+ docsDir?: string;
8
18
  }
9
19
  interface Registry {
10
20
  version: number;
@@ -12,6 +22,7 @@ interface Registry {
12
22
  }
13
23
  interface Config {
14
24
  defaultCollection?: string;
25
+ defaultSearch?: string;
15
26
  }
16
27
  interface Article {
17
28
  title: string;
@@ -50,6 +61,142 @@ interface ArticleContent {
50
61
  /** Absolute path to the file on disk. */
51
62
  path: string;
52
63
  }
64
+ type PluginKind = 'search';
65
+ declare const NATIVE_ENGINE = "native";
66
+ interface QueryOptions {
67
+ engine?: string;
68
+ collection?: string;
69
+ type?: string;
70
+ limit?: number;
71
+ }
72
+ interface QueryHit {
73
+ collection: string;
74
+ path: string;
75
+ title: string;
76
+ tags: string[];
77
+ snippet?: string;
78
+ score?: number;
79
+ docid?: string;
80
+ }
81
+ type DiagnosticLevel = 'ok' | 'warn' | 'error';
82
+ interface Diagnostic {
83
+ level: DiagnosticLevel;
84
+ message: string;
85
+ }
86
+ interface PluginRecord {
87
+ name: string;
88
+ kind: PluginKind;
89
+ command: string[];
90
+ version?: string;
91
+ /** Capabilities cached at install/register time via the `describe` op. */
92
+ capabilities?: PluginCapabilities;
93
+ enabled: boolean;
94
+ managed?: boolean;
95
+ }
96
+ interface PluginRegistryFile {
97
+ version: number;
98
+ plugins: PluginRecord[];
99
+ }
100
+ /** Capabilities a plugin advertises via the `describe` op, cached in the registry. */
101
+ interface PluginCapabilities {
102
+ /** Search types the plugin supports (e.g. `['keyword', 'semantic', 'hybrid']`). */
103
+ searchTypes: string[];
104
+ /** Whether the plugin supports filtering results by collection. */
105
+ collectionFilter: boolean;
106
+ /** Whether the plugin handles lifecycle events (article/collection mutations). */
107
+ events: boolean;
108
+ }
109
+ interface PluginReadiness {
110
+ toolInstalled: boolean;
111
+ modelsPresent: boolean;
112
+ indexBuilt: boolean;
113
+ docsIndexed: number;
114
+ embeddingsPending: number;
115
+ firstSearchCost: 'instant' | 'model-load' | 'model-download';
116
+ }
117
+ interface PluginHealth {
118
+ ready: boolean;
119
+ readiness?: PluginReadiness;
120
+ diagnostics: Diagnostic[];
121
+ }
122
+ interface SearchMode {
123
+ plugin: string;
124
+ type: string;
125
+ label: string;
126
+ }
127
+ type PluginEvent = 'collection-added' | 'collection-removed' | 'article-created' | 'article-updated' | 'article-removed';
128
+ /**
129
+ * Host-side facade for a search plugin. Each method is backed by a one-shot
130
+ * subprocess invocation -- there is no persistent process and no `close()`.
131
+ */
132
+ interface SearchProvider {
133
+ /** Plugin name. */
134
+ name: string;
135
+ /** Plugin capabilities (cached from the `describe` op). */
136
+ capabilities: PluginCapabilities;
137
+ /** Run a search query and return hits with absolute paths. */
138
+ handleSearch(term: string, collections: CollectionEntry[], opts?: QueryOptions): Promise<QueryHit[]>;
139
+ /** Rebuild the search index. Returns the number of documents indexed. */
140
+ reindex(collections: CollectionEntry[], opts?: {
141
+ embed?: boolean;
142
+ }): Promise<number>;
143
+ /** Emit a lifecycle event to the plugin (best-effort). */
144
+ emitEvent(event: PluginEvent, collection: string, relPath: string | undefined, collections: CollectionEntry[]): Promise<void>;
145
+ /** Check plugin health and readiness. */
146
+ health(collections: CollectionEntry[]): Promise<PluginHealth>;
147
+ }
148
+ interface NodeMeta {
149
+ branch?: string;
150
+ behind?: number;
151
+ }
152
+ interface TreeNode {
153
+ name: string;
154
+ title?: string;
155
+ path?: string;
156
+ children?: TreeNode[];
157
+ /** Set only on top-level collection rows: git branch/behind badge data. */
158
+ meta?: NodeMeta;
159
+ }
160
+ interface CollectionInfo {
161
+ name: string;
162
+ type: CollectionType;
163
+ path: string;
164
+ branch?: string;
165
+ behind?: number;
166
+ }
167
+ interface CollectionTree {
168
+ collection: CollectionInfo;
169
+ nodes: TreeNode[];
170
+ }
171
+ interface TocItem {
172
+ slug: string;
173
+ text: string;
174
+ depth: number;
175
+ }
176
+ interface DocData {
177
+ coll: string;
178
+ slug: string;
179
+ title: string;
180
+ tags: string[];
181
+ html: string;
182
+ toc: TocItem[];
183
+ path: string;
184
+ }
185
+ interface ListingEntry {
186
+ name: string;
187
+ title: string;
188
+ path: string;
189
+ isDir: boolean;
190
+ }
191
+ type PageData = ({
192
+ kind: 'doc';
193
+ } & DocData) | {
194
+ kind: 'listing';
195
+ coll: string;
196
+ slug: string;
197
+ title: string;
198
+ entries: ListingEntry[];
199
+ };
53
200
 
54
201
  /** Base class for all expected, user-facing diffwiki failures. */
55
202
  declare class DiffwikiError extends Error {
@@ -70,6 +217,14 @@ declare class ArticleNotFoundError extends DiffwikiError {
70
217
  declare class InvalidTargetError extends DiffwikiError {
71
218
  constructor(target: string, expected: string);
72
219
  }
220
+ declare class PluginError extends DiffwikiError {
221
+ }
222
+ declare class WorktreeNotAllowedError extends DiffwikiError {
223
+ constructor(root: string);
224
+ }
225
+ declare class CollectionAlreadyAddedError extends DiffwikiError {
226
+ constructor(name: string);
227
+ }
73
228
 
74
229
  /** Root of all global diffwiki state. Overridable via DIFFWIKI_HOME (used by tests). */
75
230
  declare function resolveHome(): string;
@@ -77,6 +232,8 @@ declare function registryPath(): string;
77
232
  declare function configPath(): string;
78
233
  declare function collectionsDir(): string;
79
234
  declare function collectionDir(name: string): string;
235
+ declare function pluginsDir(): string;
236
+ declare function pluginsRegistryPath(): string;
80
237
 
81
238
  declare function readConfig(): Promise<Config>;
82
239
  declare function writeConfig(config: Config): Promise<void>;
@@ -110,6 +267,7 @@ declare function serializeArticle(article: SerializableArticle): string;
110
267
 
111
268
  declare function listCollections(): Promise<CollectionEntry[]>;
112
269
  declare function findCollection(name: string): Promise<CollectionEntry | undefined>;
270
+ declare function findCollectionById(id: string): Promise<CollectionEntry | undefined>;
113
271
  declare function createCollection(name: string): Promise<CollectionEntry>;
114
272
  /**
115
273
  * Remove a collection from the registry. For `global` collections we own the
@@ -147,26 +305,225 @@ interface InitOptions {
147
305
  wikiPath?: string;
148
306
  }
149
307
  declare function initRepoWiki(opts: InitOptions): Promise<CollectionEntry>;
308
+ interface AddExternalOptions {
309
+ cwd: string;
310
+ /** Content dir relative to the repo root. Default "docs". */
311
+ docsDir?: string;
312
+ }
313
+ /**
314
+ * Register the git repo containing `cwd` as an `external` collection: title from
315
+ * the git remote (fallback repo dir), content dir `docs/` by default, git-only
316
+ * (auto `git init`), unique by repo id, and refusing linked worktrees.
317
+ */
318
+ declare function addExternalCollection(opts: AddExternalOptions): Promise<CollectionEntry>;
319
+ /**
320
+ * The registered collection the current directory belongs to: an external
321
+ * collection whose repo id matches, else any collection whose content path (or
322
+ * repoRoot) contains `cwd`. Undefined when cwd is outside every known collection.
323
+ */
324
+ declare function resolveCwdCollection(cwd: string): Promise<CollectionEntry | undefined>;
325
+ interface CollectionGitStatus {
326
+ branch?: string;
327
+ ahead: number;
328
+ behind: number;
329
+ hasUpstream: boolean;
330
+ }
331
+ /**
332
+ * Branch + upstream status for an external collection, from its repo root.
333
+ * Undefined for non-external collections or when git is unavailable (e.g. during
334
+ * static generation) — callers warn + omit.
335
+ */
336
+ declare function collectionGitStatus(entry: CollectionEntry): Promise<CollectionGitStatus | undefined>;
150
337
 
151
- interface QueryHit {
152
- collection: string;
153
- path: string;
154
- title: string;
155
- tags: string[];
338
+ /** Absolute working-tree root, or undefined when `cwd` is not inside a git repo. */
339
+ declare function gitToplevel(cwd: string): Promise<string | undefined>;
340
+ /**
341
+ * True when `cwd` sits inside a *linked* worktree (not the primary working tree).
342
+ * In the primary tree `--absolute-git-dir` and `--git-common-dir` resolve to the
343
+ * same directory; in a linked worktree the git-dir is `<common>/worktrees/<name>`.
344
+ */
345
+ declare function isLinkedWorktree(cwd: string): Promise<boolean>;
346
+ /** Short repo name from the `origin` remote URL (basename, `.git` stripped), or undefined. */
347
+ declare function repoRemoteName(cwd: string): Promise<string | undefined>;
348
+ /**
349
+ * A stable identifier for the repo, independent of checkout location. Prefers the
350
+ * root (first) commit hash — identical across clones/worktrees; falls back to the
351
+ * normalised origin URL when there is no commit yet. Undefined when neither exists.
352
+ */
353
+ declare function repoId(cwd: string): Promise<string | undefined>;
354
+ /** Current short branch name, or undefined (detached HEAD / not a repo). */
355
+ declare function currentBranch(cwd: string): Promise<string | undefined>;
356
+ interface UpstreamStatus {
357
+ branch?: string;
358
+ ahead: number;
359
+ behind: number;
360
+ hasUpstream: boolean;
156
361
  }
157
362
  /**
158
- * DUMMY search: case-insensitive substring match over title, filename, and tags.
159
- * Real QMD-based semantic search replaces this later.
363
+ * Ahead/behind counts of the current branch versus its upstream. `hasUpstream` is
364
+ * false when no upstream is configured (then ahead/behind are 0).
160
365
  */
161
- declare function query(term: string, collection?: string): Promise<QueryHit[]>;
366
+ declare function upstreamStatus(cwd: string): Promise<UpstreamStatus>;
367
+ /** Ensure `cwd` is inside a git repo, running `git init` if not. Returns the toplevel. */
368
+ declare function ensureGitRepo(cwd: string): Promise<string>;
162
369
 
163
- type DiagnosticLevel = 'ok' | 'warn' | 'error';
164
- interface Diagnostic {
165
- level: DiagnosticLevel;
166
- message: string;
370
+ /** Map core article nodes to UI nav nodes, assigning each its route path. */
371
+ declare function toTreeNodes(coll: string, nodes: ArticleNode[]): TreeNode[];
372
+ declare function buildCollections(): Promise<CollectionInfo[]>;
373
+ declare function buildNavTree(): Promise<CollectionTree[]>;
374
+ /** Resolve a route to a rendered doc or a generated directory listing (or null). */
375
+ declare function resolvePage(coll: string, slug: string): Promise<PageData | null>;
376
+
377
+ /**
378
+ * Search query routing.
379
+ *
380
+ * `query()` is the primary search entry point. It resolves the search engine
381
+ * (explicit opt, config default, first enabled plugin, or native BM25), runs
382
+ * the search, and falls back to native on plugin failure. The built-in native
383
+ * search is a zero-dependency BM25-ranked full-text search across title, body,
384
+ * tags, and collection name.
385
+ *
386
+ * @module
387
+ */
388
+
389
+ /**
390
+ * Built-in JS-native search: BM25-ranked full-text search over an article's
391
+ * title, body, tags, and collection name -- across every registered collection's
392
+ * ABSOLUTE registry path (so in-repo wikis outside ~/.diffwiki are searched too).
393
+ * No `find`/OS shelling -- cross-platform and dependency-free.
394
+ *
395
+ * When `term` is empty, returns all articles unranked (no score) to preserve
396
+ * the "browse all" behavior.
397
+ */
398
+ declare function nativeSearch(term: string, collections: CollectionEntry[], collection?: string): Promise<QueryHit[]>;
399
+ /**
400
+ * Resolve the engine + type that `query()` would use when the caller passes no
401
+ * explicit engine/type. Reads cached capabilities from the registry -- no spawn.
402
+ */
403
+ declare function resolveDefaultSearchMode(): Promise<{
404
+ engine: string;
405
+ type: string;
406
+ }>;
407
+ /**
408
+ * The result of a {@link search} call: the hits plus the metadata a host UI
409
+ * needs to tell the user WHICH engine ran and whether it silently degraded.
410
+ *
411
+ * `fellBackToNative` is true whenever the resolved engine could not serve the
412
+ * search (no provider, or `handleSearch` threw) and native BM25 produced these
413
+ * hits instead. `error`, when set, is the captured message from the failing
414
+ * engine — a failing engine is thus distinguishable from an engine that
415
+ * legitimately found zero results.
416
+ */
417
+ interface SearchOutcome {
418
+ /** The hits to display (from the engine, or from native on fallback). */
419
+ hits: QueryHit[];
420
+ /** The engine that was resolved and attempted (may differ from what produced `hits`). */
421
+ engine: string;
422
+ /** True when native BM25 produced `hits` because the engine failed/was unavailable. */
423
+ fellBackToNative: boolean;
424
+ /** The captured failure message from the engine, when it errored. */
425
+ error?: string;
167
426
  }
427
+ /**
428
+ * Search articles via the resolved search engine, surfacing engine failures.
429
+ *
430
+ * Routing precedence: explicit `opts.engine` -> `config.defaultSearch` ->
431
+ * first enabled plugin -> native BM25.
432
+ *
433
+ * On ANY engine failure (no provider, or `handleSearch` throws) the error
434
+ * message is CAPTURED, native BM25 is run as a fallback, and the outcome
435
+ * reports `{ fellBackToNative: true, error }`. On success it reports
436
+ * `{ fellBackToNative: false }` with the engine's hits. This lets callers
437
+ * distinguish "the engine broke" from "the engine found nothing".
438
+ */
439
+ declare function search(term: string, opts?: QueryOptions): Promise<SearchOutcome>;
440
+ /**
441
+ * Search articles via the resolved search engine (native fallback on plugin
442
+ * failure). A thin wrapper over {@link search} that returns just the hits;
443
+ * engine errors are swallowed. Use {@link search} to surface those errors.
444
+ *
445
+ * Routing precedence: explicit `opts.engine` -> `config.defaultSearch` ->
446
+ * first enabled plugin -> native BM25.
447
+ */
448
+ declare function query(term: string, opts?: QueryOptions): Promise<QueryHit[]>;
449
+
450
+ /**
451
+ * Build the search index: one {@link SearchDoc} per article, carrying routing
452
+ * metadata (collection/slug/title/tags) AND the pre-tokenized fields BM25 ranks.
453
+ * The static exporter serialises this to `search-index.json`; the browser ranks it
454
+ * with the shared `rankDocs` (same algorithm as the server's native search), so
455
+ * keyword search works fully offline on the static site.
456
+ */
457
+ declare function buildSearchIndex(collection?: string): Promise<SearchDoc[]>;
458
+
459
+ /**
460
+ * Diagnostics and health checks.
461
+ *
462
+ * `doctor()` inspects the diffwiki home directory, registry, collections,
463
+ * config, and every registered plugin to produce a list of human-readable
464
+ * diagnostics at ok/warn/error severity levels.
465
+ *
466
+ * @module
467
+ */
468
+
469
+ /** Run all diagnostics and return the results. */
168
470
  declare function doctor(): Promise<Diagnostic[]>;
169
471
 
472
+ /**
473
+ * Centralized logging for diffwiki host code, built on LogTape.
474
+ *
475
+ * diffwiki-core (and the plugin SDK) are LIBRARIES: they obtain loggers via
476
+ * {@link getLogger} and emit records, but they NEVER call LogTape's
477
+ * `configure()`/`configureSync()`. Per the LogTape library-vs-app pattern, a
478
+ * library stays silent until the surrounding APPLICATION (the CLI or the web
479
+ * app) installs sinks. `getLogger` called before any `configure` is a safe
480
+ * no-op, so importing this module has no side effects.
481
+ *
482
+ * All diffwiki loggers live under the `['diffwiki', ...]` category so an app can
483
+ * route the whole tree with a single logger config entry. Plugin diagnostics
484
+ * are forwarded under `['diffwiki', 'plugin', <name>]` (see `plugins/host.ts`).
485
+ *
486
+ * [DEVIATION] The task brief lists the level name `warn`, but the installed
487
+ * `@logtape/logtape` (2.2.4) names that level `warning` (levels are
488
+ * `trace|debug|info|warning|error|fatal`). We follow the installed types:
489
+ * {@link DiffwikiLogLevel} uses `warning`, and {@link resolveLogLevel} defaults
490
+ * to `warning`. Note LogTape's Logger METHOD for that level is still `.warn()`.
491
+ *
492
+ * @module
493
+ */
494
+
495
+ /**
496
+ * A diffwiki log level. Identical to LogTape's `LogLevel`
497
+ * (`trace|debug|info|warning|error|fatal`); re-exported so host and app code can
498
+ * refer to it without importing LogTape directly.
499
+ */
500
+ type DiffwikiLogLevel = LogLevel;
501
+ /**
502
+ * Get a diffwiki logger under the shared `['diffwiki', ...]` category root.
503
+ *
504
+ * This is a thin wrapper over LogTape's `getLogger`. It does NOT configure
505
+ * LogTape — libraries stay silent until an app calls `configure()`. Passing a
506
+ * string scope is sugar for a single-element category array.
507
+ *
508
+ * @param scope - A scope name (e.g. `'query'`) or a nested category path
509
+ * (e.g. `['plugin', 'diffwiki-qmd']`).
510
+ * @returns A LogTape {@link Logger} under `['diffwiki', ...scope]`.
511
+ */
512
+ declare function getLogger(scope: string | string[]): Logger;
513
+ /**
514
+ * Resolve the diffwiki log level from the environment.
515
+ *
516
+ * Reads `DIFFWIKI_LOG` (one of `trace|debug|info|warning|error|fatal`, case
517
+ * insensitive) and returns it, falling back to the documented default
518
+ * (`warning`) when unset or unrecognized. Apps use this to pick the app-level
519
+ * sink's `lowestLevel` at startup.
520
+ *
521
+ * @param env - The environment to read (defaults to `process.env`). Injectable
522
+ * for tests.
523
+ * @returns The resolved {@link DiffwikiLogLevel}.
524
+ */
525
+ declare function resolveLogLevel(env?: NodeJS.ProcessEnv): DiffwikiLogLevel;
526
+
170
527
  /**
171
528
  * Build a nested ArticleNode[] from a flat list of relative file paths.
172
529
  * Pure — no I/O. Directories become branches; files become leaves whose `slug`
@@ -186,12 +543,6 @@ declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
186
543
  */
187
544
  declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
188
545
 
189
- /** A heading extracted for the table of contents (h2/h3). */
190
- interface TocItem {
191
- slug: string;
192
- text: string;
193
- depth: number;
194
- }
195
546
  /** The result of rendering an article body to HTML. */
196
547
  interface RenderedArticle {
197
548
  /** Fully-rendered, Shiki-highlighted HTML — mounted directly on the client (no eval). */
@@ -208,4 +559,166 @@ interface RenderedArticle {
208
559
  */
209
560
  declare function renderArticle(body: string): Promise<RenderedArticle>;
210
561
 
211
- export { type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, type CollectionEntry, CollectionExistsError, CollectionNotFoundError, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type InitOptions, InvalidTargetError, NoDefaultCollectionError, type ParsedArticle, type QueryHit, REPO_CONFIG_FILE, type Registry, type RenderedArticle, type RepoConfig, type SerializableArticle, type TocItem, addTags, buildArticleTree, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listArticleTree, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readArticle, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, renderArticle, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
562
+ /**
563
+ * Host-side plugin invocation and search-provider loading.
564
+ *
565
+ * `invokePlugin()` spawns a plugin subprocess, writes a single JSON request to
566
+ * stdin (then closes it), reads the JSON response from stdout, and lets the
567
+ * process exit. Progress/diagnostic logs arrive on STDERR as structured
568
+ * `{log:{level,message}}` JSON lines; the host forwards each to a LogTape logger
569
+ * scoped by plugin name and keeps the raw stderr for on-error context.
570
+ *
571
+ * `loadSearchProvider()` returns a `SearchProvider` backed by one-shot
572
+ * invocations — each method call spawns the plugin fresh. `listSearchModes()`
573
+ * reads cached capabilities from the registry rather than spawning each plugin.
574
+ *
575
+ * @module
576
+ */
577
+
578
+ /**
579
+ * Load a search provider backed by one-shot subprocess invocations. Each method
580
+ * on the returned `SearchProvider` spawns the plugin binary, sends one JSON
581
+ * request, and reads the response. Returns `null` if no matching enabled plugin
582
+ * is found or the plugin fails its `describe` handshake.
583
+ *
584
+ * @param name - Optional plugin name. If omitted, the first enabled search plugin is used.
585
+ */
586
+ declare function loadSearchProvider(name?: string): Promise<SearchProvider | null>;
587
+ /**
588
+ * List all available search modes, starting with native BM25. Reads cached
589
+ * capabilities from the plugin registry rather than spawning each plugin.
590
+ * Plugins without cached capabilities (registered before the SDK refactor) are
591
+ * skipped -- re-register to populate them.
592
+ */
593
+ declare function listSearchModes(): Promise<SearchMode[]>;
594
+
595
+ /**
596
+ * Plugin lifecycle management.
597
+ *
598
+ * Handles installing (from npm), registering (from an existing binary),
599
+ * removing, and listing plugins. Installation and registration both run
600
+ * a `describe` op to validate the plugin and cache its capabilities in
601
+ * the registry, then a best-effort `setup` op to provision the plugin.
602
+ *
603
+ * @module
604
+ */
605
+
606
+ /** Create the managed plugin root (an isolated npm project) and return its path. */
607
+ declare function ensurePluginRoot(): Promise<string>;
608
+ /** One provisioning step reported by a plugin's `setup` op. */
609
+ interface SetupStep {
610
+ step: string;
611
+ ok: boolean;
612
+ message?: string;
613
+ }
614
+ /** The captured result of a plugin's `setup` op. */
615
+ interface SetupOutcome {
616
+ /** Whether the plugin provisioned to a ready-to-search state. */
617
+ ready: boolean;
618
+ /** Per-step results (e.g. verify-qmd, add-collections, index). */
619
+ steps: SetupStep[];
620
+ }
621
+ /** The result of {@link installPlugin}: the registered record + the setup outcome (if setup ran). */
622
+ interface InstallOutcome {
623
+ record: PluginRecord;
624
+ /** The setup result, or undefined when setup was skipped (`opts.setup === false`). */
625
+ setup?: SetupOutcome;
626
+ }
627
+ /**
628
+ * Install an npm spec into the managed root, register the discovered search bin,
629
+ * run a `describe` to validate and cache capabilities, then (unless `opts.setup
630
+ * === false`) run the plugin's `setup` op so the user lands on a working search.
631
+ */
632
+ declare function installPlugin(spec: string, opts?: {
633
+ setup?: boolean;
634
+ prefetchModels?: boolean;
635
+ }): Promise<InstallOutcome>;
636
+ /**
637
+ * Register an existing executable as a search plugin. Validates by running
638
+ * `describe`, caches capabilities, then runs `setup` best-effort.
639
+ */
640
+ declare function registerPlugin(name: string, command: string[]): Promise<PluginRecord>;
641
+ /** Uninstall a managed plugin's package and deregister it. */
642
+ declare function removePlugin(name: string): Promise<void>;
643
+ /** List every registered plugin, marking whether it currently responds to `describe`. */
644
+ declare function listPlugins(): Promise<Array<PluginRecord & {
645
+ spawnable: boolean;
646
+ }>>;
647
+
648
+ /**
649
+ * Plugin lifecycle event emission.
650
+ *
651
+ * Fires a lifecycle event at the active search plugin -- best-effort and
652
+ * non-blocking. diffwiki emits events on every content mutation (article
653
+ * create/update/remove, collection add/remove). A plugin's `onEvent`
654
+ * handler can self-manage in response (e.g. incremental reindex).
655
+ *
656
+ * Events are an optimisation, not the correctness path: the mtime-staleness
657
+ * lazy reindex on `search` remains the guarantee, so a dropped event only
658
+ * costs a later lazy rebuild, never wrong results.
659
+ *
660
+ * @module
661
+ */
662
+
663
+ /**
664
+ * Emit a lifecycle event to the active search plugin.
665
+ *
666
+ * This function is designed to be called fire-and-forget: `void emitPluginEvent(...)`.
667
+ * It only invokes the plugin when the plugin has `capabilities.events === true`.
668
+ * Every error is swallowed -- a slow or absent plugin can neither stall nor
669
+ * fail a content mutation.
670
+ */
671
+ declare function emitPluginEvent(event: PluginEvent, collection: string, relPath?: string): Promise<void>;
672
+
673
+ /**
674
+ * Known plugin catalog.
675
+ *
676
+ * A built-in directory of official diffwiki plugins that users can install.
677
+ * This is discovery only -- installing goes through `installPlugin(spec)`.
678
+ *
679
+ * @module
680
+ */
681
+
682
+ /**
683
+ * A plugin diffwiki knows about and can recommend — the "app store" catalog.
684
+ * Discovery only; installing still goes through `installPlugin(spec)`.
685
+ */
686
+ interface KnownPlugin {
687
+ name: string;
688
+ kind: PluginKind;
689
+ /** The argument for `diffwiki plugin install <spec>` (the npm package name). */
690
+ spec: string;
691
+ description: string;
692
+ /** External tool the plugin drives, shown as a prerequisite hint. */
693
+ requires?: string;
694
+ }
695
+ /** Return a copy of the known/official plugins available to install. */
696
+ declare function listAvailablePlugins(): KnownPlugin[];
697
+
698
+ /**
699
+ * Plugin registry persistence.
700
+ *
701
+ * Manages the `~/.diffwiki/plugins.json` file that stores plugin records.
702
+ * Each record includes the plugin's name, kind, command argv, version,
703
+ * cached capabilities (populated at install/register time via `describe`),
704
+ * and enabled/managed flags.
705
+ *
706
+ * @module
707
+ */
708
+
709
+ /** Read the plugin registry from disk. Returns an empty registry if the file does not exist. */
710
+ declare function readPluginRegistry(): Promise<PluginRegistryFile>;
711
+ /** Write the plugin registry to disk, creating the home directory if needed. */
712
+ declare function writePluginRegistry(registry: PluginRegistryFile): Promise<void>;
713
+ /** Insert or replace the plugin record with the same name. */
714
+ declare function upsertPlugin(record: PluginRecord): Promise<void>;
715
+ /** Remove a plugin record by name. */
716
+ declare function deregisterPlugin(name: string): Promise<void>;
717
+ /** List all enabled plugins of a given kind. */
718
+ declare function listEnabledPlugins(kind: PluginKind): Promise<PluginRecord[]>;
719
+ /** Find a single enabled plugin by kind and optionally by name. Returns the first if name is omitted. */
720
+ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<PluginRecord | undefined>;
721
+ /** Find a plugin record by name (regardless of enabled state). */
722
+ declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
723
+
724
+ export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type InitOptions, type InstallOutcome, InvalidTargetError, type KnownPlugin, type ListingEntry, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };