@lapis-notes/api 0.1.4 → 0.2.0

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.
Files changed (188) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +19 -0
  3. package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
  4. package/dist/__tests__/fixtures/memory-storage.json +309 -0
  5. package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
  6. package/dist/__tests__/fixtures/projection-query.json +195 -0
  7. package/dist/__tests__/fixtures/projection-values.json +60 -0
  8. package/dist/__tests__/fixtures/search-preparation.json +1513 -0
  9. package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
  10. package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
  11. package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
  12. package/dist/__tests__/fixtures/search-storage.json +409 -0
  13. package/dist/__tests__/fixtures/task-projection.json +428 -0
  14. package/dist/__tests__/indexed-mutation-support.js +262 -0
  15. package/dist/__tests__/projection-query-support.js +40 -0
  16. package/dist/__tests__/search-retrieval-support.js +47 -0
  17. package/dist/app-database-host.d.ts +1 -0
  18. package/dist/app-shell-plugin-persistence.d.ts +3 -2
  19. package/dist/app-shell-plugin-persistence.js +37 -3
  20. package/dist/cache.svelte.d.ts +8 -1
  21. package/dist/cache.svelte.js +103 -5
  22. package/dist/command.svelte.js +29 -2
  23. package/dist/components/editor/editor.css +4 -0
  24. package/dist/components/editor/editor.svelte +21 -9
  25. package/dist/components/editor/embedded-editor-surface.js +3 -1
  26. package/dist/components/editor/inline-title-alignment.d.ts +3 -0
  27. package/dist/components/editor/inline-title-alignment.js +57 -0
  28. package/dist/configuration.svelte.d.ts +270 -206
  29. package/dist/configuration.svelte.js +161 -86
  30. package/dist/context.svelte.d.ts +21 -3
  31. package/dist/context.svelte.js +63 -20
  32. package/dist/diagnostics.d.ts +8 -0
  33. package/dist/diagnostics.js +6 -0
  34. package/dist/editor-save-context.d.ts +12 -1
  35. package/dist/editor-save-context.js +32 -1
  36. package/dist/editor-view-registry.d.ts +10 -0
  37. package/dist/editor-view-registry.js +44 -4
  38. package/dist/editor.svelte.js +128 -37
  39. package/dist/embed-registry.d.ts +10 -0
  40. package/dist/embed-registry.js +15 -0
  41. package/dist/file-editing-failures.d.ts +2 -0
  42. package/dist/file-editing-failures.js +29 -0
  43. package/dist/file-editing-native-issues.d.ts +19 -0
  44. package/dist/file-editing-native-issues.js +65 -0
  45. package/dist/file-editing-sessions.d.ts +56 -8
  46. package/dist/file-editing-sessions.js +185 -15
  47. package/dist/file-manager.d.ts +8 -1
  48. package/dist/file-manager.js +37 -3
  49. package/dist/file-state-store.d.ts +5 -0
  50. package/dist/file-state-store.js +34 -0
  51. package/dist/file-text-conflicts.d.ts +25 -0
  52. package/dist/file-text-conflicts.js +35 -0
  53. package/dist/file-text-mutation.d.ts +13 -0
  54. package/dist/file-text-mutation.js +100 -0
  55. package/dist/file-text-persistence.d.ts +36 -0
  56. package/dist/file-text-persistence.js +234 -0
  57. package/dist/file-text-state-writer.d.ts +34 -0
  58. package/dist/file-text-state-writer.js +183 -0
  59. package/dist/files-configuration.d.ts +20 -0
  60. package/dist/files-configuration.js +47 -0
  61. package/dist/generated/plugin-host-modules.schema.generated.d.ts +87 -37
  62. package/dist/generated/plugin-host-modules.schema.generated.js +102 -37
  63. package/dist/glob.d.ts +3 -0
  64. package/dist/glob.js +2 -1
  65. package/dist/icons/changes.d.ts +3 -0
  66. package/dist/icons/changes.js +16 -0
  67. package/dist/icons/index.d.ts +5 -1
  68. package/dist/icons/index.js +44 -3
  69. package/dist/icons/registry.d.ts +35 -0
  70. package/dist/icons/registry.js +154 -0
  71. package/dist/icons/safe-svg.d.ts +2 -0
  72. package/dist/icons/safe-svg.js +44 -0
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.js +3 -1
  75. package/dist/lapis-extension.d.ts +6 -2
  76. package/dist/lapis-extension.js +2 -0
  77. package/dist/metadata.svelte.js +45 -2
  78. package/dist/native-background-indexing.d.ts +48 -0
  79. package/dist/native-background-indexing.js +208 -0
  80. package/dist/native-database-protocol.d.ts +29 -0
  81. package/dist/native-database-protocol.js +1 -0
  82. package/dist/native-editing-intent-lease.d.ts +17 -0
  83. package/dist/native-editing-intent-lease.js +188 -0
  84. package/dist/native-editing-intents-protocol.d.ts +70 -0
  85. package/dist/native-editing-intents-protocol.js +1 -0
  86. package/dist/native-editing-intents.d.ts +8 -0
  87. package/dist/native-editing-intents.js +1 -0
  88. package/dist/native-editing-problems.d.ts +3 -0
  89. package/dist/native-editing-problems.js +180 -0
  90. package/dist/native-editing-recovery.d.ts +24 -0
  91. package/dist/native-editing-recovery.js +198 -0
  92. package/dist/native-editing-session.d.ts +51 -0
  93. package/dist/native-editing-session.js +251 -0
  94. package/dist/native-history-index-protocol.d.ts +80 -0
  95. package/dist/native-history-index-protocol.js +1 -0
  96. package/dist/native-history-index.d.ts +6 -0
  97. package/dist/native-history-index.js +1 -0
  98. package/dist/native-indexing-protocol.d.ts +63 -0
  99. package/dist/native-indexing-protocol.js +1 -0
  100. package/dist/native-participant-protocol.d.ts +67 -0
  101. package/dist/native-participant-protocol.js +3 -0
  102. package/dist/native-queue-protocol.d.ts +73 -0
  103. package/dist/native-queue-protocol.js +3 -0
  104. package/dist/native-tasks-protocol.d.ts +264 -0
  105. package/dist/native-tasks-protocol.js +1 -0
  106. package/dist/native-tasks.d.ts +35 -0
  107. package/dist/native-tasks.js +79 -0
  108. package/dist/native-turn-protocol.d.ts +81 -0
  109. package/dist/native-turn-protocol.js +1 -0
  110. package/dist/native-turns.d.ts +29 -0
  111. package/dist/native-turns.js +2 -0
  112. package/dist/plugin-asset-server.js +3 -2
  113. package/dist/plugin-dependency-resolver.d.ts +1 -1
  114. package/dist/plugin-distribution/compatibility.d.ts +1 -1
  115. package/dist/plugin-distribution/compatibility.js +10 -5
  116. package/dist/plugin-distribution/installed-plugin-state.js +2 -1
  117. package/dist/plugin-distribution/installer.d.ts +6 -0
  118. package/dist/plugin-distribution/installer.js +90 -37
  119. package/dist/plugin-distribution/manager.d.ts +4 -1
  120. package/dist/plugin-distribution/manager.js +12 -5
  121. package/dist/plugin-distribution/registry-cache.js +2 -1
  122. package/dist/plugin-distribution/registry-source-state.js +2 -1
  123. package/dist/plugin-distribution/schemas.d.ts +26 -8
  124. package/dist/plugin-distribution/schemas.js +2 -1
  125. package/dist/plugin-distribution/types.d.ts +4 -0
  126. package/dist/plugin-manager.d.ts +20 -4
  127. package/dist/plugin-manager.js +228 -64
  128. package/dist/plugin-runtime-entry.d.ts +1 -1
  129. package/dist/plugin-runtime-entry.js +16 -0
  130. package/dist/plugin.d.ts +18 -2
  131. package/dist/plugin.js +93 -39
  132. package/dist/registry-schema/catalog-v1.d.ts +60 -9
  133. package/dist/registry-schema/catalog-v1.js +2 -1
  134. package/dist/registry-schema/catalog-v2.d.ts +60 -9
  135. package/dist/registry-schema/catalog-v2.js +1 -1
  136. package/dist/search-result-provider.d.ts +95 -0
  137. package/dist/search-result-provider.js +42 -0
  138. package/dist/status-bar.svelte.d.ts +26 -20
  139. package/dist/status-bar.svelte.js +1 -0
  140. package/dist/storage/app-database.d.ts +6 -2
  141. package/dist/storage/app-database.js +41 -34
  142. package/dist/storage/desktop-native.d.ts +5 -3
  143. package/dist/storage/desktop-native.js +59 -6
  144. package/dist/storage/directory-watcher.d.ts +27 -0
  145. package/dist/storage/directory-watcher.js +201 -0
  146. package/dist/storage/fs.d.ts +14 -27
  147. package/dist/storage/fs.js +2 -168
  148. package/dist/storage/index-projection.js +33 -15
  149. package/dist/storage/memory-vault-adapter.d.ts +2 -0
  150. package/dist/storage/memory-vault-adapter.js +14 -1
  151. package/dist/storage/path.d.ts +6 -2
  152. package/dist/storage/path.js +8 -2
  153. package/dist/storage/projection-query-candidates.d.ts +3 -0
  154. package/dist/storage/projection-query-candidates.js +69 -0
  155. package/dist/storage/remote-app-database.d.ts +2 -2
  156. package/dist/storage/remote-app-database.js +1 -1
  157. package/dist/storage/runtime.d.ts +3 -0
  158. package/dist/storage/runtime.js +3 -0
  159. package/dist/storage/search-embedding-record.d.ts +3 -0
  160. package/dist/storage/search-embedding-record.js +18 -0
  161. package/dist/storage/turso-app-database.d.ts +2 -0
  162. package/dist/storage/turso-app-database.js +67 -30
  163. package/dist/storage/vault-listing.d.ts +3 -0
  164. package/dist/storage/vault-listing.js +36 -0
  165. package/dist/storage/vault-reconciliation.d.ts +19 -0
  166. package/dist/storage/vault-reconciliation.js +137 -0
  167. package/dist/storage/vault-session.d.ts +8 -0
  168. package/dist/storage/vault-session.js +10 -2
  169. package/dist/storage/vault.svelte.d.ts +53 -4
  170. package/dist/storage/vault.svelte.js +223 -87
  171. package/dist/sync-host.d.ts +12 -3
  172. package/dist/sync-host.js +5 -1
  173. package/dist/vault-change-views.d.ts +4 -0
  174. package/dist/vault-change-views.js +43 -0
  175. package/dist/vault-changes.d.ts +37 -0
  176. package/dist/vault-changes.js +228 -0
  177. package/dist/view.svelte.d.ts +182 -149
  178. package/dist/view.svelte.js +11 -2
  179. package/dist/workspace-close-guard.d.ts +3 -0
  180. package/dist/workspace-close-guard.js +35 -0
  181. package/dist/workspace-host-internal.js +10 -2
  182. package/dist/workspace-layout-normalizer.d.ts +4 -2
  183. package/dist/workspace-layout-normalizer.js +5 -5
  184. package/dist/workspace-view-document.d.ts +3 -0
  185. package/dist/workspace-view-document.js +12 -0
  186. package/dist/workspace.svelte.d.ts +13 -2
  187. package/dist/workspace.svelte.js +371 -82
  188. package/package.json +45 -8
@@ -7,7 +7,7 @@ import { Plugin, type PluginConstructor, type PluginManifest, type PluginSource
7
7
  import { type HostedPluginCapability } from "./plugin-capability-facade";
8
8
  import { type PluginDependencyResolver, type PluginDependencyResolverFactory } from "./plugin-dependency-resolver";
9
9
  import { type PluginAssetServer } from "./plugin-asset-server";
10
- import type { PluginProvenance } from "./plugin-distribution/types";
10
+ import type { PluginProvenance, PluginHostRestrictions } from "./plugin-distribution/types";
11
11
  import { EventDispatcher } from "./events";
12
12
  import type { SyncHostCapabilities } from "./sync-host";
13
13
  export interface PluginLoader {
@@ -39,6 +39,7 @@ export interface CommunityPluginExecutionHost {
39
39
  evaluate(request: CommunityPluginEvaluationRequest): Promise<unknown>;
40
40
  }
41
41
  export interface PluginManagerOptions {
42
+ unavailablePlugins?: PluginHostRestrictions;
42
43
  communityPluginHost?: CommunityPluginExecutionHost;
43
44
  createCommunityPluginDependencyResolver?: PluginDependencyResolverFactory;
44
45
  pluginAssetServer?: PluginAssetServer;
@@ -164,6 +165,7 @@ export interface PluginEvents {
164
165
  "plugin-loaded": [plugin: Plugin];
165
166
  "plugin-enabled": [plugin: Plugin];
166
167
  "plugin-disabled": [plugin: Plugin];
168
+ "plugin-unloaded": [pluginId: string];
167
169
  "community-plugin-config-pruned": [pluginIds: string[]];
168
170
  "plugin-error": [id: string, message: string, error?: any];
169
171
  }
@@ -179,6 +181,15 @@ export declare class PluginManager extends EventDispatcher<PluginEvents> {
179
181
  get corePlugins(): Plugin[];
180
182
  get communityPlugins(): Plugin[];
181
183
  isPluginEnabled(pluginId: string): boolean;
184
+ /** Registered application profiles own their identities, even when disabled. */
185
+ isStaticPlugin(pluginId: string): boolean;
186
+ /** Release an external registration before the installer removes its code. */
187
+ unloadPlugin(pluginId: string): Promise<boolean>;
188
+ /** Retire a loaded module while preserving its configured startup choice. */
189
+ preparePluginReplacement(pluginId: string): Promise<{
190
+ reloadRequired: boolean;
191
+ rollback(): Promise<void>;
192
+ } | undefined>;
182
193
  get corePluginEntries(): CorePluginListEntry[];
183
194
  get communityPluginHostId(): string;
184
195
  get pluginAssetServer(): PluginAssetServer | undefined;
@@ -215,11 +226,12 @@ export declare class PluginManager extends EventDispatcher<PluginEvents> {
215
226
  private resolveExternalPluginSource;
216
227
  private readPluginListConfig;
217
228
  private readCorePluginStateConfig;
218
- private savePluginListConfig;
219
- private saveCorePluginStateConfig;
229
+ private queuePluginState;
220
230
  private saveCommunityPluginState;
221
231
  private saveCorePluginState;
222
- flushPendingConfigWrites(): void;
232
+ flushPendingConfigWrites(): Promise<void>;
233
+ private readPluginState;
234
+ private restoreCorePluginState;
223
235
  private isCorePluginEnabled;
224
236
  private pruneMissingConfiguredCommunityPlugins;
225
237
  private enableConfiguredIndexedExtensions;
@@ -230,8 +242,11 @@ export declare class PluginManager extends EventDispatcher<PluginEvents> {
230
242
  }): Promise<Plugin | null>;
231
243
  private registerPlugin;
232
244
  private syncLegacyPluginData;
245
+ private syncLegacyPluginDataNow;
233
246
  enablePlugin(pluginId: string): Promise<boolean>;
247
+ private enablePluginNow;
234
248
  disablePlugin(pluginId: string): Promise<boolean>;
249
+ private disablePluginNow;
235
250
  restartPlugin(pluginId: string): Promise<boolean>;
236
251
  private capturePluginOwnedLeaves;
237
252
  private replacePluginOwnedLeaves;
@@ -295,6 +310,7 @@ export declare class PluginManager extends EventDispatcher<PluginEvents> {
295
310
  private getSelectedCodeEntry;
296
311
  private getSelectedCommunityModulePath;
297
312
  private getSelectedLapisRuntimeEntry;
313
+ private isTauriSession;
298
314
  private getHostIdForManifest;
299
315
  private getHostIdForIndexedExtension;
300
316
  private getSelectedRuntimeEntryForManifest;
@@ -10,7 +10,7 @@ import { scanBarePluginDependencySpecifiers } from "./plugin-dependency-scanner"
10
10
  import pluginHostModuleRegistry from "./generated/plugin-host-modules.schema.generated";
11
11
  import { InstalledPluginStateStore } from "./plugin-distribution/installed-plugin-state";
12
12
  import { dirname, getAdapterVaultId, joinPath, normalizePath } from "./storage";
13
- import { debounce } from "lodash-es";
13
+ import { readFileState, saveFileState } from "./file-state-store";
14
14
  import { EventDispatcher } from "./events";
15
15
  function formatPluginError(error) {
16
16
  return error instanceof Error ? error.message : String(error);
@@ -396,6 +396,7 @@ export class PluginManager extends EventDispatcher {
396
396
  #defaultDisabledCorePlugins = new Set();
397
397
  #corePluginStyles = new Map();
398
398
  #corePluginDistribution = new Map();
399
+ #pendingReloadPlugins = new Set();
399
400
  #installedPluginProvenance = new Map();
400
401
  #installedPluginStateStore;
401
402
  #basesViewRegistrations = new Map();
@@ -412,6 +413,7 @@ export class PluginManager extends EventDispatcher {
412
413
  #deferredIndexedExtensions = new Map();
413
414
  #communityPluginHost;
414
415
  #pluginAssetServer;
416
+ #unavailablePlugins;
415
417
  #createSyncHostCapabilities;
416
418
  #revokeSyncHostCapabilities;
417
419
  constructor(app, pluginsPath, adapter, options = {}) {
@@ -423,6 +425,7 @@ export class PluginManager extends EventDispatcher {
423
425
  const dependencyResolver = options.createCommunityPluginDependencyResolver?.(this.dependencies) ??
424
426
  this.dependencies;
425
427
  this.#pluginAssetServer = options.pluginAssetServer;
428
+ this.#unavailablePlugins = { ...options.unavailablePlugins };
426
429
  this.#createSyncHostCapabilities = options.createSyncHostCapabilities;
427
430
  this.#revokeSyncHostCapabilities = options.revokeSyncHostCapabilities;
428
431
  const commonJsHost = options.communityPluginHost ??
@@ -438,7 +441,7 @@ export class PluginManager extends EventDispatcher {
438
441
  if (event.origin === "plugin-save") {
439
442
  return;
440
443
  }
441
- void this.syncLegacyPluginData(event.pluginId);
444
+ void this.syncLegacyPluginData(event.pluginId, event.value === undefined && event.prev !== undefined);
442
445
  });
443
446
  }
444
447
  getPluginFolder() {
@@ -453,6 +456,55 @@ export class PluginManager extends EventDispatcher {
453
456
  isPluginEnabled(pluginId) {
454
457
  return Boolean(this.plugins.get(pluginId)?.enabled);
455
458
  }
459
+ /** Registered application profiles own their identities, even when disabled. */
460
+ isStaticPlugin(pluginId) {
461
+ return (this.#corePluginDistribution.has(pluginId) ||
462
+ this.#systemExtensionManifests.has(pluginId));
463
+ }
464
+ /** Release an external registration before the installer removes its code. */
465
+ async unloadPlugin(pluginId) {
466
+ if (this.isStaticPlugin(pluginId))
467
+ return false;
468
+ const plugin = this.plugins.get(pluginId);
469
+ if (plugin && !(await this.disablePlugin(pluginId)))
470
+ return false;
471
+ this.disableManifestOnlyExtension(pluginId);
472
+ this.plugins.delete(pluginId);
473
+ this.#lapisExtensionIndex.delete(pluginId);
474
+ this.#communityPluginDiagnostics.delete(pluginId);
475
+ this.#communityPluginDependencyDiagnostics.delete(pluginId);
476
+ this.#communityPluginRuntimeExecutionDiagnostics.delete(pluginId);
477
+ this.#installedPluginProvenance.delete(pluginId);
478
+ if (plugin)
479
+ this.#pendingReloadPlugins.add(pluginId);
480
+ if (this.#configuredCommunityPlugins.delete(pluginId))
481
+ this.saveCommunityPluginState();
482
+ await this.flushPendingConfigWrites();
483
+ this.emit("plugin-unloaded", pluginId);
484
+ return true;
485
+ }
486
+ /** Retire a loaded module while preserving its configured startup choice. */
487
+ async preparePluginReplacement(pluginId) {
488
+ if (this.isStaticPlugin(pluginId))
489
+ throw new Error(`Plugin ${pluginId} is registered by the application`);
490
+ const plugin = this.plugins.get(pluginId);
491
+ if (!plugin)
492
+ return undefined;
493
+ const wasEnabled = plugin.enabled;
494
+ const wasPendingReload = this.#pendingReloadPlugins.has(pluginId);
495
+ if (!(await this.disablePluginNow(pluginId, false)))
496
+ throw new Error(`Plugin ${pluginId} could not finish replacement teardown`);
497
+ this.#pendingReloadPlugins.add(pluginId);
498
+ return {
499
+ reloadRequired: true,
500
+ rollback: async () => {
501
+ if (!wasPendingReload)
502
+ this.#pendingReloadPlugins.delete(pluginId);
503
+ if (wasEnabled && !(await this.enablePlugin(pluginId)))
504
+ throw new Error(`Plugin ${pluginId} could not restore its previous runtime`);
505
+ },
506
+ };
507
+ }
456
508
  get corePluginEntries() {
457
509
  const runtimePlugins = this.corePlugins.map((plugin) => ({
458
510
  manifest: plugin.manifest,
@@ -461,9 +513,7 @@ export class PluginManager extends EventDispatcher {
461
513
  source: plugin.source,
462
514
  provenance: plugin.provenance,
463
515
  distribution: this.#corePluginDistribution.get(plugin.manifest.id) ??
464
- (plugin.source === "official"
465
- ? "first-party-external"
466
- : "bundled"),
516
+ (plugin.source === "official" ? "first-party-external" : "bundled"),
467
517
  errorMessage: plugin.errorMessage,
468
518
  }));
469
519
  const runtimeSystemPlugins = [...this.plugins.values()].filter((plugin) => plugin.source === "system");
@@ -575,9 +625,7 @@ export class PluginManager extends EventDispatcher {
575
625
  }
576
626
  const registeredPlugin = this.registerPlugin(plugin, {
577
627
  source: "core",
578
- provenance: distribution === "first-party-external"
579
- ? "official"
580
- : "bundled",
628
+ provenance: distribution === "first-party-external" ? "official" : "bundled",
581
629
  required,
582
630
  basePath: pluginPath,
583
631
  });
@@ -756,6 +804,8 @@ export class PluginManager extends EventDispatcher {
756
804
  });
757
805
  await this.activateIndexedExtensionsForWorkspace();
758
806
  this.#loaded = true;
807
+ if (this.app.nativeTasks)
808
+ await this.app.nativeTasks.load();
759
809
  this.emit("plugins-loaded");
760
810
  await this.activateByEvent("onStartupFinished");
761
811
  }, {
@@ -890,14 +940,21 @@ export class PluginManager extends EventDispatcher {
890
940
  async readPluginListConfig(filename, options = {}) {
891
941
  const configPath = this.getPluginStatePath(filename);
892
942
  try {
893
- const exists = await this.adapter.exists(configPath);
894
- if (!exists) {
943
+ const data = await this.readPluginState(configPath, (text) => {
944
+ const value = JSON.parse(text || "[]");
945
+ if (!Array.isArray(value) || value.some((id) => typeof id !== "string"))
946
+ throw new Error("Invalid community plugin configuration");
947
+ this.#configuredCommunityPlugins = new Set(value);
948
+ });
949
+ if (data === null) {
895
950
  if (options.createIfMissing) {
896
- await this.adapter.write(configPath, "[]");
951
+ if (this.app.vault.supportsConditionalTextWrites)
952
+ await saveFileState(this.app, configPath, "[]");
953
+ else
954
+ await this.adapter.write(configPath, "[]");
897
955
  }
898
956
  return [];
899
957
  }
900
- const data = await this.adapter.read(configPath);
901
958
  const plugins = JSON.parse(data);
902
959
  if (Array.isArray(plugins)) {
903
960
  return plugins.map((it) => it.toString());
@@ -911,11 +968,9 @@ export class PluginManager extends EventDispatcher {
911
968
  async readCorePluginStateConfig() {
912
969
  const configPath = this.getPluginStatePath("core-plugins.json");
913
970
  try {
914
- const exists = await this.adapter.exists(configPath);
915
- if (!exists) {
971
+ const data = await this.readPluginState(configPath, (text) => this.restoreCorePluginState(text));
972
+ if (data === null)
916
973
  return { disabled: [], enabled: [] };
917
- }
918
- const data = await this.adapter.read(configPath);
919
974
  const config = JSON.parse(data);
920
975
  if (Array.isArray(config)) {
921
976
  return {
@@ -939,51 +994,80 @@ export class PluginManager extends EventDispatcher {
939
994
  }
940
995
  return { disabled: [], enabled: [] };
941
996
  }
942
- async savePluginListConfig(filename, pluginIds) {
943
- const configPath = this.getPluginStatePath(filename);
944
- try {
945
- const file = this.app.vault.getFileByPath(configPath);
946
- const payload = JSON.stringify([...pluginIds], null, 2);
947
- if (file) {
948
- await this.app.vault.modify(file, payload);
949
- return;
950
- }
951
- await this.app.vault.create(configPath, payload);
952
- }
953
- catch (error) {
954
- this.reportPluginError(configPath, `Failed to save plugin config ${configPath}: ${formatPluginError(error)}`, error);
955
- }
956
- }
957
- async saveCorePluginStateConfig() {
958
- const configPath = this.getPluginStatePath("core-plugins.json");
997
+ #pendingConfigWrites = new Set();
998
+ queuePluginState(filename, value) {
999
+ const path = this.getPluginStatePath(filename);
1000
+ const payload = JSON.stringify(value, null, 2);
1001
+ const operation = this.app.vault.supportsConditionalTextWrites
1002
+ ? saveFileState(this.app, path, payload)
1003
+ : (async () => {
1004
+ const file = this.app.vault.getFileByPath(path);
1005
+ if (file)
1006
+ await this.app.vault.modify(file, payload);
1007
+ else
1008
+ await this.app.vault.create(path, payload);
1009
+ })();
1010
+ this.#pendingConfigWrites.add(operation);
1011
+ void operation.then(() => this.#pendingConfigWrites.delete(operation), (error) => {
1012
+ this.#pendingConfigWrites.delete(operation);
1013
+ this.reportPluginError(path, `Failed to save plugin config ${path}: ${formatPluginError(error)}`, error);
1014
+ });
1015
+ return this.app.vault.supportsConditionalTextWrites
1016
+ ? operation
1017
+ : operation.catch(() => undefined);
1018
+ }
1019
+ saveCommunityPluginState = () => {
1020
+ this.queuePluginState("community-plugins.json", [
1021
+ ...this.#configuredCommunityPlugins,
1022
+ ]);
1023
+ };
1024
+ saveCorePluginState = () => {
1025
+ this.queuePluginState("core-plugins.json", this.#enabledCorePlugins.size === 0
1026
+ ? [...this.#disabledCorePlugins]
1027
+ : {
1028
+ disabled: [...this.#disabledCorePlugins],
1029
+ enabled: [...this.#enabledCorePlugins],
1030
+ });
1031
+ };
1032
+ async flushPendingConfigWrites() {
1033
+ if (this.app.vault.supportsConditionalTextWrites)
1034
+ await this.app.fileEditingSessions.settleOperations();
1035
+ await Promise.all([...this.#pendingConfigWrites]);
1036
+ }
1037
+ async readPluginState(path, reset) {
1038
+ if (this.app.vault.supportsConditionalTextWrites)
1039
+ return readFileState(this.app, path, reset);
1040
+ return (await this.adapter.exists(path)) ? this.adapter.read(path) : null;
1041
+ }
1042
+ async restoreCorePluginState(text) {
1043
+ const value = JSON.parse(text || "[]");
1044
+ if (!Array.isArray(value) && !isRecord(value))
1045
+ throw new Error("Invalid core plugin configuration");
1046
+ const disabled = Array.isArray(value) ? value : (value.disabled ?? []);
1047
+ const enabled = Array.isArray(value) ? [] : (value.enabled ?? []);
1048
+ if (!Array.isArray(disabled) ||
1049
+ !Array.isArray(enabled) ||
1050
+ [...disabled, ...enabled].some((id) => typeof id !== "string"))
1051
+ throw new Error("Invalid core plugin configuration");
1052
+ this.#disabledCorePlugins = new Set(disabled);
1053
+ this.#enabledCorePlugins = new Set(enabled);
1054
+ const loaded = this.#loaded;
1055
+ this.#loaded = false;
959
1056
  try {
960
- const file = this.app.vault.getFileByPath(configPath);
961
- const payload = this.#enabledCorePlugins.size === 0
962
- ? JSON.stringify([...this.#disabledCorePlugins], null, 2)
963
- : JSON.stringify({
964
- disabled: [...this.#disabledCorePlugins],
965
- enabled: [...this.#enabledCorePlugins],
966
- }, null, 2);
967
- if (file) {
968
- await this.app.vault.modify(file, payload);
969
- return;
1057
+ for (const plugin of this.corePlugins) {
1058
+ if (plugin.required)
1059
+ continue;
1060
+ const success = this.isCorePluginEnabled(plugin.id)
1061
+ ? await this.enablePlugin(plugin.id)
1062
+ : await this.disablePlugin(plugin.id);
1063
+ if (!success)
1064
+ throw new Error(`Could not restore plugin ${plugin.id}`);
970
1065
  }
971
- await this.app.vault.create(configPath, payload);
972
1066
  }
973
- catch (error) {
974
- this.reportPluginError(configPath, `Failed to save plugin config ${configPath}: ${formatPluginError(error)}`, error);
1067
+ finally {
1068
+ this.#loaded = loaded;
975
1069
  }
976
1070
  }
977
- saveCommunityPluginState = debounce(() => {
978
- return void this.savePluginListConfig("community-plugins.json", this.#configuredCommunityPlugins);
979
- }, 500);
980
- saveCorePluginState = debounce(() => {
981
- return void this.saveCorePluginStateConfig();
982
- }, 500);
983
- flushPendingConfigWrites() {
984
- this.saveCommunityPluginState.flush();
985
- this.saveCorePluginState.flush();
986
- }
987
1071
  isCorePluginEnabled(pluginId) {
988
1072
  if (this.#disabledCorePlugins.has(pluginId)) {
989
1073
  return false;
@@ -1016,7 +1100,9 @@ export class PluginManager extends EventDispatcher {
1016
1100
  for (const pluginId of missingPluginIds) {
1017
1101
  this.#configuredCommunityPlugins.delete(pluginId);
1018
1102
  }
1019
- await this.savePluginListConfig("community-plugins.json", this.#configuredCommunityPlugins);
1103
+ await this.queuePluginState("community-plugins.json", [
1104
+ ...this.#configuredCommunityPlugins,
1105
+ ]);
1020
1106
  this.emit("community-plugin-config-pruned", missingPluginIds);
1021
1107
  return missingPluginIds;
1022
1108
  }
@@ -1072,6 +1158,15 @@ export class PluginManager extends EventDispatcher {
1072
1158
  const manifestContent = await this.adapter.read(manifestPath);
1073
1159
  manifest = JSON.parse(manifestContent);
1074
1160
  pluginId = manifest.id || pluginId;
1161
+ if (this.isStaticPlugin(pluginId)) {
1162
+ throw new Error(`Plugin id ${pluginId} is registered by the application`);
1163
+ }
1164
+ if (Object.hasOwn(this.#unavailablePlugins, manifest.id)) {
1165
+ throw new Error(`Plugin ${manifest.id} is unavailable: ${this.#unavailablePlugins[manifest.id]}`);
1166
+ }
1167
+ if (this.#pendingReloadPlugins.has(manifest.id)) {
1168
+ throw new Error(`Reload the app to load updated plugin ${manifest.id}`);
1169
+ }
1075
1170
  span.setAttribute("plugin.id", manifest.id);
1076
1171
  manifest.dir ??= pluginPath;
1077
1172
  const runtimeSource = options.source !== undefined
@@ -1124,6 +1219,14 @@ export class PluginManager extends EventDispatcher {
1124
1219
  if (!(plugin instanceof Plugin)) {
1125
1220
  throw new Error(`Plugin ${manifest.id} does not extend the Plugin base class`);
1126
1221
  }
1222
+ const installed = await this.#installedPluginStateStore.get(manifest.id);
1223
+ if (installed?.restartRequired &&
1224
+ installed.installedVersion === manifest.version) {
1225
+ await this.#installedPluginStateStore.upsert({
1226
+ ...installed,
1227
+ restartRequired: false,
1228
+ });
1229
+ }
1127
1230
  return this.registerPlugin(plugin, {
1128
1231
  source: runtimeSource.source,
1129
1232
  provenance: runtimeSource.provenance,
@@ -1132,8 +1235,9 @@ export class PluginManager extends EventDispatcher {
1132
1235
  }
1133
1236
  catch (error) {
1134
1237
  const message = `Failed to load plugin from ${pluginPath}: ${formatPluginError(error)}`;
1135
- this.recordCommunityPluginFailure(pluginId, manifest, message);
1136
- this.reportPluginError(pluginPath, message, error);
1238
+ if (!this.isStaticPlugin(pluginId))
1239
+ this.recordCommunityPluginFailure(pluginId, manifest, message);
1240
+ this.reportPluginError(pluginId, message, error);
1137
1241
  return null;
1138
1242
  }
1139
1243
  }, { slowThresholdMs: 200 });
@@ -1200,7 +1304,13 @@ export class PluginManager extends EventDispatcher {
1200
1304
  void this.syncLegacyPluginData(plugin.manifest.id);
1201
1305
  return plugin;
1202
1306
  }
1203
- async syncLegacyPluginData(pluginId) {
1307
+ syncLegacyPluginData(pluginId, removeMissing = false) {
1308
+ const run = () => this.syncLegacyPluginDataNow(pluginId, removeMissing);
1309
+ return this.app.vault.supportsConditionalTextWrites
1310
+ ? this.app.fileEditingSessions.track(run)
1311
+ : run();
1312
+ }
1313
+ async syncLegacyPluginDataNow(pluginId, removeMissing) {
1204
1314
  const plugin = this.plugins.get(pluginId);
1205
1315
  if (!plugin) {
1206
1316
  return;
@@ -1211,6 +1321,9 @@ export class PluginManager extends EventDispatcher {
1211
1321
  }
1212
1322
  try {
1213
1323
  if (!this.app.configuration.hasPluginData(pluginId)) {
1324
+ // Registration is not a deletion: loadData still needs this legacy file.
1325
+ if (!removeMissing)
1326
+ return;
1214
1327
  const file = this.app.vault.getFileByPath(path);
1215
1328
  if (file) {
1216
1329
  await this.app.vault.delete(file, true);
@@ -1218,6 +1331,10 @@ export class PluginManager extends EventDispatcher {
1218
1331
  return;
1219
1332
  }
1220
1333
  const payload = JSON.stringify(this.app.configuration.getPluginData(pluginId), null, 2);
1334
+ if (this.app.vault.supportsConditionalTextWrites) {
1335
+ await saveFileState(this.app, path, payload);
1336
+ return;
1337
+ }
1221
1338
  const file = this.app.vault.getFileByPath(path);
1222
1339
  if (file) {
1223
1340
  await this.app.vault.modify(file, payload);
@@ -1230,7 +1347,17 @@ export class PluginManager extends EventDispatcher {
1230
1347
  this.reportPluginError(pluginId, `Failed to sync legacy plugin data for ${pluginId}: ${formatPluginError(error)}`, error);
1231
1348
  }
1232
1349
  }
1233
- async enablePlugin(pluginId) {
1350
+ enablePlugin(pluginId) {
1351
+ const run = () => this.enablePluginNow(pluginId);
1352
+ return this.#loaded && this.app.vault.supportsConditionalTextWrites
1353
+ ? this.app.fileEditingSessions.track(run)
1354
+ : run();
1355
+ }
1356
+ async enablePluginNow(pluginId) {
1357
+ if (this.#pendingReloadPlugins.has(pluginId)) {
1358
+ this.reportPluginError(pluginId, `Reload the app to activate updated plugin ${pluginId}`);
1359
+ return false;
1360
+ }
1234
1361
  const plugin = this.plugins.get(pluginId);
1235
1362
  if (!plugin) {
1236
1363
  const indexedExtension = this.#lapisExtensionIndex.get(pluginId);
@@ -1351,7 +1478,13 @@ export class PluginManager extends EventDispatcher {
1351
1478
  slowThresholdMs: 300,
1352
1479
  });
1353
1480
  }
1354
- async disablePlugin(pluginId) {
1481
+ disablePlugin(pluginId) {
1482
+ const run = () => this.disablePluginNow(pluginId);
1483
+ return this.#loaded && this.app.vault.supportsConditionalTextWrites
1484
+ ? this.app.fileEditingSessions.track(run)
1485
+ : run();
1486
+ }
1487
+ async disablePluginNow(pluginId, persist = true) {
1355
1488
  const plugin = this.plugins.get(pluginId);
1356
1489
  if (!plugin) {
1357
1490
  const indexedExtension = this.#lapisExtensionIndex.get(pluginId);
@@ -1361,7 +1494,7 @@ export class PluginManager extends EventDispatcher {
1361
1494
  return false;
1362
1495
  }
1363
1496
  this.disableManifestOnlyExtension(pluginId);
1364
- if (this.#loaded) {
1497
+ if (this.#loaded && persist) {
1365
1498
  if (indexedExtension.source === "community" ||
1366
1499
  indexedExtension.source === "official") {
1367
1500
  this.#configuredCommunityPlugins.delete(pluginId);
@@ -1412,7 +1545,7 @@ export class PluginManager extends EventDispatcher {
1412
1545
  this.unloadPluginCSS(pluginId);
1413
1546
  this.updatePluginDiagnosticsFromPlugin(plugin);
1414
1547
  this.emit("plugin-disabled", plugin);
1415
- if (this.#loaded) {
1548
+ if (this.#loaded && persist) {
1416
1549
  if (plugin.source === "community" || plugin.source === "official") {
1417
1550
  this.#configuredCommunityPlugins.delete(pluginId);
1418
1551
  this.saveCommunityPluginState();
@@ -1444,6 +1577,8 @@ export class PluginManager extends EventDispatcher {
1444
1577
  });
1445
1578
  }
1446
1579
  async restartPlugin(pluginId) {
1580
+ if (this.#pendingReloadPlugins.has(pluginId))
1581
+ return this.enablePlugin(pluginId);
1447
1582
  let plugin = this.plugins.get(pluginId);
1448
1583
  if (!plugin) {
1449
1584
  plugin =
@@ -1613,6 +1748,20 @@ export class PluginManager extends EventDispatcher {
1613
1748
  throw new Error(`Plugin ${manifest.id} can only run in the desktop app`);
1614
1749
  }
1615
1750
  if (isExternalPluginSource(source)) {
1751
+ const services = manifest.requiredNativeServices;
1752
+ if (services !== undefined) {
1753
+ if (!Array.isArray(services) ||
1754
+ services.some((id) => typeof id !== "string" || !id.trim()))
1755
+ throw new Error(`Plugin ${manifest.id} declares invalid required native services`);
1756
+ const capabilities = getNativeDesktopBridge()?.capabilities;
1757
+ for (const id of services) {
1758
+ const service = capabilities?.[id];
1759
+ if (!service)
1760
+ throw new Error(`Plugin ${manifest.id} requires unsupported native service: ${id}`);
1761
+ if (service.status !== "available")
1762
+ throw new Error(`Plugin ${manifest.id} requires disabled native service: ${id}`);
1763
+ }
1764
+ }
1616
1765
  if (requiresWorkspaceTrust(manifest) &&
1617
1766
  !this.app.workspaceTrust.trusted) {
1618
1767
  throw new Error(trustFailureMessage(manifest));
@@ -2647,11 +2796,19 @@ export class PluginManager extends EventDispatcher {
2647
2796
  }
2648
2797
  return selected?.path ?? null;
2649
2798
  }
2799
+ isTauriSession() {
2800
+ return ((this.app.session?.runtime ?? getNativeDesktopBridge()?.runtime) ===
2801
+ "tauri-desktop");
2802
+ }
2650
2803
  getHostIdForManifest(manifest) {
2804
+ if (this.isTauriSession())
2805
+ return "tauri-desktop";
2651
2806
  return (this.#communityPluginHost.hostIdForManifest?.(manifest) ??
2652
2807
  this.#communityPluginHost.id);
2653
2808
  }
2654
2809
  getHostIdForIndexedExtension(indexedExtension) {
2810
+ if (this.isTauriSession())
2811
+ return "tauri-desktop";
2655
2812
  return isSidecarHost(this.#communityPluginHost.id) &&
2656
2813
  (indexedExtension.runtime?.desktop ||
2657
2814
  indexedExtension.runtime?.trustedDesktop ||
@@ -2705,6 +2862,8 @@ export class PluginManager extends EventDispatcher {
2705
2862
  return this.getRequestedPluginExecutionHostForHostId(this.getHostIdForManifest(manifest));
2706
2863
  }
2707
2864
  getRequestedPluginExecutionHostForHostId(hostId) {
2865
+ if (hostId === "tauri-desktop")
2866
+ return "tauri-desktop";
2708
2867
  if (isSidecarHost(hostId)) {
2709
2868
  return "electron-sidecar";
2710
2869
  }
@@ -2802,6 +2961,9 @@ function runtimeMatchesHost(runtime, hostId) {
2802
2961
  if (runtime === hostId) {
2803
2962
  return true;
2804
2963
  }
2964
+ if (hostId === "tauri-desktop") {
2965
+ return ["renderer", "workspace", "browser", "pwa", "web"].includes(runtime);
2966
+ }
2805
2967
  if (hostId === "renderer") {
2806
2968
  return ["renderer", "browser", "pwa", "web"].includes(runtime);
2807
2969
  }
@@ -2825,6 +2987,8 @@ function isSidecarHost(hostId) {
2825
2987
  }
2826
2988
  function hostPlatformMatchesDependencyPlatform(host, platform) {
2827
2989
  switch (host) {
2990
+ case "tauri-desktop":
2991
+ return platform === "web";
2828
2992
  case "electron-sidecar":
2829
2993
  return platform === "electron-sidecar";
2830
2994
  case "electron-renderer":
@@ -1,6 +1,6 @@
1
1
  import type { LapisPluginModuleFormat } from "./lapis-extension";
2
2
  import type { PluginManifest } from "./plugin";
3
- export type PluginExecutionHostId = "workspace" | "electron-renderer" | "electron-sidecar";
3
+ export type PluginExecutionHostId = "workspace" | "tauri-desktop" | "electron-renderer" | "electron-sidecar";
4
4
  export interface SelectedPluginRuntimeEntry {
5
5
  host: PluginExecutionHostId;
6
6
  path: string;
@@ -5,6 +5,22 @@ export function selectPluginRuntimeEntry(options) {
5
5
  const structuredEntry = lapis
6
6
  ? selectStructuredEntry(lapis, options.requestedHost)
7
7
  : null;
8
+ // Tauri executes portable browser ESM only. Never select a compatibility
9
+ // fallback or an Electron/native service entry, even when main.js exists.
10
+ if (options.requestedHost === "tauri-desktop") {
11
+ const entry = lapis?.runtime?.entries?.workspace;
12
+ if (!options.supportsEsm || entry?.format !== "esm")
13
+ return null;
14
+ return {
15
+ host: "tauri-desktop",
16
+ path: entry.path,
17
+ format: "esm",
18
+ sharedDependencies: entry.sharedDependencies ??
19
+ getSharedDependencyDefaults(lapis, "workspace"),
20
+ requiresReloadOnUpdate: entry.requiresReloadOnUpdate,
21
+ source: "lapis-runtime-entry",
22
+ };
23
+ }
8
24
  if (!lapis) {
9
25
  if (!options.hasMainJs) {
10
26
  return null;