@abraca/nuxt 2.0.11 → 2.3.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 (130) hide show
  1. package/dist/module.d.mts +68 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +99 -4
  4. package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
  5. package/dist/runtime/components/ACodeEditor.vue +268 -0
  6. package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
  7. package/dist/runtime/components/ADocumentTree.vue +52 -20
  8. package/dist/runtime/components/AEditor.d.vue.ts +20 -13
  9. package/dist/runtime/components/AEditor.vue +55 -2
  10. package/dist/runtime/components/AEditor.vue.d.ts +20 -13
  11. package/dist/runtime/components/ANodePanel.vue +64 -60
  12. package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
  13. package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
  14. package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
  15. package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
  16. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  17. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  18. package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
  19. package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
  20. package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
  21. package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
  22. package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
  23. package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
  24. package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
  25. package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
  26. package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
  27. package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
  28. package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
  29. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  30. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  31. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  32. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  33. package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
  34. package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
  35. package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
  36. package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
  37. package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
  38. package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
  39. package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
  40. package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
  41. package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
  42. package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
  43. package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
  44. package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
  45. package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
  46. package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
  47. package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
  48. package/dist/runtime/components/registry/APluginCard.vue +91 -0
  49. package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
  50. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
  51. package/dist/runtime/components/registry/APluginDetail.vue +252 -0
  52. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
  53. package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
  54. package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
  55. package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
  56. package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
  57. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  58. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  59. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
  60. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
  61. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
  62. package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
  63. package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
  64. package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
  65. package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
  66. package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
  67. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +5 -2
  68. package/dist/runtime/components/shell/AUserMenu.vue +4 -0
  69. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +5 -2
  70. package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
  71. package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
  72. package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
  73. package/dist/runtime/composables/useCalendarView.d.ts +1 -1
  74. package/dist/runtime/composables/useChat.js +1 -0
  75. package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
  76. package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
  77. package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
  78. package/dist/runtime/composables/useDocEntryTyped.js +70 -0
  79. package/dist/runtime/composables/useEditorDragHandle.js +18 -0
  80. package/dist/runtime/composables/useEditorSuggestions.js +2 -1
  81. package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
  82. package/dist/runtime/composables/useInstalledPlugins.js +2 -12
  83. package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
  84. package/dist/runtime/composables/useMetaMenuItems.js +115 -0
  85. package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
  86. package/dist/runtime/composables/useMetaValidator.js +10 -0
  87. package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
  88. package/dist/runtime/composables/usePluginCatalog.js +234 -0
  89. package/dist/runtime/composables/useQuery.d.ts +79 -0
  90. package/dist/runtime/composables/useQuery.js +97 -0
  91. package/dist/runtime/composables/useSpaces.js +4 -5
  92. package/dist/runtime/composables/useTableView.d.ts +3 -3
  93. package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
  94. package/dist/runtime/composables/useTypedDoc.js +114 -0
  95. package/dist/runtime/composables/useWebRTC.js +44 -5
  96. package/dist/runtime/extensions/document-meta.js +5 -0
  97. package/dist/runtime/extensions/timeline.d.ts +11 -0
  98. package/dist/runtime/extensions/timeline.js +52 -0
  99. package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
  100. package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
  101. package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
  102. package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
  103. package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
  104. package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
  105. package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
  106. package/dist/runtime/extensions/views/TimelineView.vue +29 -0
  107. package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
  108. package/dist/runtime/locale.d.ts +2 -0
  109. package/dist/runtime/locale.js +2 -0
  110. package/dist/runtime/plugin-abracadabra.client.js +107 -6
  111. package/dist/runtime/plugin-registry.d.ts +11 -30
  112. package/dist/runtime/plugin-registry.js +2 -82
  113. package/dist/runtime/plugins/core.plugin.js +10 -4
  114. package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
  115. package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
  116. package/dist/runtime/server/utils/docCache.js +24 -3
  117. package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
  118. package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
  119. package/dist/runtime/types.d.ts +63 -46
  120. package/dist/runtime/utils/docTypes.d.ts +15 -0
  121. package/dist/runtime/utils/docTypes.js +20 -0
  122. package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
  123. package/dist/runtime/utils/loadCodeMirror.js +65 -0
  124. package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
  125. package/dist/runtime/utils/markdownToYjs.js +5 -440
  126. package/dist/runtime/utils/schemaSupport.d.ts +60 -0
  127. package/dist/runtime/utils/schemaSupport.js +40 -0
  128. package/dist/runtime/utils/yjsConvert.d.ts +1 -14
  129. package/dist/runtime/utils/yjsConvert.js +5 -331
  130. package/package.json +84 -23
@@ -73,7 +73,8 @@ export function useEditorSuggestions(options = {}) {
73
73
  ...extEnabled("mathBlock") ? [{ kind: "mathBlock", label: "Math (block)", icon: "i-lucide-square-sigma", description: "Display-mode LaTeX equation", keywords: ["katex", "latex", "equation", "formula", "math"] }] : [],
74
74
  ...extEnabled("mathInline") ? [{ kind: "mathInline", label: "Math (inline)", icon: "i-lucide-sigma", description: "Inline LaTeX expression", keywords: ["katex", "latex", "inline math"] }] : [],
75
75
  ...extEnabled("diff") ? [{ kind: "diff", label: "Diff", icon: "i-lucide-git-compare", description: "Side-by-side text diff", keywords: ["compare", "changes", "git"] }] : [],
76
- ...extEnabled("svgEmbed") ? [{ kind: "svgEmbed", label: "SVG embed", icon: "i-lucide-image", description: "Inline SVG (sanitized)", keywords: ["svg", "vector", "icon", "diagram"] }] : []
76
+ ...extEnabled("svgEmbed") ? [{ kind: "svgEmbed", label: "SVG embed", icon: "i-lucide-image", description: "Inline SVG (sanitized)", keywords: ["svg", "vector", "icon", "diagram"] }] : [],
77
+ ...extEnabled("timeline") ? [{ kind: "timeline", label: "Timeline", icon: "i-lucide-git-commit-vertical", description: "Vertical event timeline", keywords: ["events", "history", "milestones"] }] : []
77
78
  ]
78
79
  ];
79
80
  try {
@@ -1,24 +1,6 @@
1
- export interface ExternalPluginEntry {
2
- url: string;
3
- /** Plugin name from the loaded bundle's `plugin.name` field */
4
- name: string;
5
- label?: string;
6
- version?: string;
7
- description?: string;
8
- enabled: boolean;
9
- /** Error message from the last load attempt, if any */
10
- error?: string;
11
- installedAt: number;
12
- }
13
- /**
14
- * Normalizes user-entered shorthand to a full CDN URL.
15
- *
16
- * Supported formats:
17
- * - `npm:package-name[@version]` -> jsDelivr npm CDN
18
- * - `github:user/repo[@branch]` -> jsDelivr GitHub CDN
19
- * - Any other string is returned as-is (assumed to be a full URL)
20
- */
21
- export declare function normalizePluginUrl(input: string): string;
1
+ import { normalizePluginUrl, type ExternalPluginEntry } from '@abraca/plugin';
2
+ export { normalizePluginUrl };
3
+ export type { ExternalPluginEntry };
22
4
  export declare function useInstalledPlugins(): {
23
5
  entries: import("@vueuse/shared").RemovableRef<ExternalPluginEntry[]>;
24
6
  install: (rawUrl: string) => void;
@@ -1,18 +1,8 @@
1
1
  import { useLocalStorage } from "@vueuse/core";
2
+ import { normalizePluginUrl } from "@abraca/plugin";
3
+ export { normalizePluginUrl };
2
4
  const STORAGE_KEY = "abracadabra_external_plugins";
3
5
  const DISABLED_BUILTINS_KEY = "abracadabra_disabled_builtins";
4
- export function normalizePluginUrl(input) {
5
- const trimmed = input.trim();
6
- if (trimmed.startsWith("npm:")) {
7
- const pkg = trimmed.slice(4);
8
- return `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;
9
- }
10
- if (trimmed.startsWith("github:")) {
11
- const repo = trimmed.slice(7);
12
- return `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;
13
- }
14
- return trimmed;
15
- }
16
6
  export function useInstalledPlugins() {
17
7
  const entries = useLocalStorage(STORAGE_KEY, []);
18
8
  const disabledBuiltins = useLocalStorage(DISABLED_BUILTINS_KEY, []);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * useMetaMenuItems — unified "Add Property" menu items for documentMeta.
3
+ *
4
+ * Used by:
5
+ * - The (+) button inside DocumentMetaView
6
+ * - The drag-handle menu on the documentMeta row
7
+ *
8
+ * Returns two groups:
9
+ * [0] Schema properties (from `editor.storage.metaField.configFields`)
10
+ * [1] Standard properties (from `META_FIELD_DEFINITIONS`)
11
+ *
12
+ * Items already present are marked with a check trailing icon. Schema fields
13
+ * focus the existing chip instead of duplicating; standard fields insert a
14
+ * new chip (their `metaKey` is timestamped, so duplicates are intentional)
15
+ * unless they're singleton field types (rating, icon, color, members).
16
+ *
17
+ * Ported 1:1 from cou-sh/app/composables/useMetaMenuItems.ts.
18
+ */
19
+ import type { Editor } from '@tiptap/vue-3';
20
+ import type { DropdownMenuItem } from '@nuxt/ui';
21
+ export declare function buildMetaMenuItems(editor: Editor): DropdownMenuItem[][];
@@ -0,0 +1,115 @@
1
+ import { META_FIELD_DEFINITIONS } from "../utils/metaFieldDefinitions.js";
2
+ import { schemaFieldToAttrs } from "../extensions/meta-field.js";
3
+ function collectPresentFields(editor) {
4
+ const present = [];
5
+ const doc = editor.state.doc;
6
+ doc.forEach((node, offset) => {
7
+ if (node.type.name !== "documentMeta") return;
8
+ node.forEach((child, childOffset) => {
9
+ if (child.type.name !== "metaField") return;
10
+ present.push({
11
+ fieldType: child.attrs.fieldType ?? "",
12
+ metaKey: child.attrs.metaKey ?? "",
13
+ startKey: child.attrs.startKey ?? "",
14
+ endKey: child.attrs.endKey ?? "",
15
+ latKey: child.attrs.latKey ?? "",
16
+ lngKey: child.attrs.lngKey ?? "",
17
+ userDefined: !!child.attrs.userDefined,
18
+ pos: offset + 1 + childOffset
19
+ });
20
+ });
21
+ });
22
+ return present;
23
+ }
24
+ function schemaFieldKey(field) {
25
+ if (field.type === "daterange" || field.type === "timerange")
26
+ return `${field.type}:${String(field.startKey)}|${String(field.endKey)}`;
27
+ if (field.type === "datetimerange")
28
+ return `${field.type}:${String(field.startKey)}|${String(field.endKey)}`;
29
+ if (field.type === "location")
30
+ return `${field.type}:${String(field.latKey)}|${String(field.lngKey)}`;
31
+ if ("key" in field) return `${field.type}:${String(field.key)}`;
32
+ return field.type;
33
+ }
34
+ function presentFieldKey(p) {
35
+ if (p.fieldType === "daterange" || p.fieldType === "timerange" || p.fieldType === "datetimerange")
36
+ return `${p.fieldType}:${p.startKey}|${p.endKey}`;
37
+ if (p.fieldType === "location")
38
+ return `${p.fieldType}:${p.latKey}|${p.lngKey}`;
39
+ return `${p.fieldType}:${p.metaKey}`;
40
+ }
41
+ function focusExistingChip(editor, pos) {
42
+ try {
43
+ const { node } = editor.view.domAtPos(pos);
44
+ const el = node instanceof Element ? node : node.parentElement;
45
+ if (!el) return;
46
+ el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
47
+ el.classList.add("meta-chip-flash");
48
+ setTimeout(() => el.classList.remove("meta-chip-flash"), 900);
49
+ } catch {
50
+ }
51
+ }
52
+ export function buildMetaMenuItems(editor) {
53
+ const storage = editor.storage?.metaField;
54
+ const configFields = storage?.configFields ?? [];
55
+ const present = collectPresentFields(editor);
56
+ const presentKeys = new Set(present.map(presentFieldKey));
57
+ const presentFieldTypes = new Set(present.map((p) => p.fieldType));
58
+ const groups = [];
59
+ if (configFields.length) {
60
+ groups.push(configFields.map((f) => {
61
+ const key = schemaFieldKey(f);
62
+ const already = presentKeys.has(key);
63
+ const existing = already ? present.find((p) => presentFieldKey(p) === key) : void 0;
64
+ return {
65
+ label: f.label || ("key" in f ? String(f.key) : f.type),
66
+ icon: "i-lucide-settings-2",
67
+ trailingIcon: already ? "i-lucide-check" : void 0,
68
+ onSelect: () => {
69
+ if (already && existing) {
70
+ focusExistingChip(editor, existing.pos);
71
+ return;
72
+ }
73
+ const attrs = schemaFieldToAttrs(f);
74
+ signalAutoOpen(editor, attrs);
75
+ editor.commands.insertMetaField(attrs);
76
+ }
77
+ };
78
+ }));
79
+ }
80
+ groups.push(META_FIELD_DEFINITIONS.map((def) => {
81
+ const sampleAttrs = def.buildAttrs();
82
+ const already = presentFieldTypes.has(String(sampleAttrs.fieldType));
83
+ const existing = already ? present.find((p) => p.fieldType === sampleAttrs.fieldType) : void 0;
84
+ return {
85
+ label: def.label,
86
+ icon: def.icon,
87
+ trailingIcon: already ? "i-lucide-check" : void 0,
88
+ onSelect: () => {
89
+ if (already && existing && isSingletonFieldType(String(sampleAttrs.fieldType))) {
90
+ focusExistingChip(editor, existing.pos);
91
+ return;
92
+ }
93
+ const attrs = def.buildAttrs();
94
+ signalAutoOpen(editor, attrs);
95
+ editor.commands.insertMetaField(attrs);
96
+ }
97
+ };
98
+ }));
99
+ return groups;
100
+ }
101
+ function isSingletonFieldType(fieldType) {
102
+ return fieldType === "rating" || fieldType === "icon" || fieldType === "colorPreset" || fieldType === "colorPicker" || fieldType === "members";
103
+ }
104
+ function isPopoverFieldType(fieldType) {
105
+ return fieldType === "colorPreset" || fieldType === "colorPicker" || fieldType === "icon" || fieldType === "location" || fieldType === "select" || fieldType === "multiselect" || fieldType === "members" || fieldType === "textarea" || fieldType === "date" || fieldType === "datetime" || fieldType === "time" || fieldType === "daterange" || fieldType === "datetimerange";
106
+ }
107
+ function signalAutoOpen(editor, attrs) {
108
+ const storage = editor.storage?.metaField;
109
+ if (!storage) return;
110
+ const fieldType = String(attrs.fieldType ?? "");
111
+ if (!isPopoverFieldType(fieldType)) return;
112
+ const marker = String(attrs.metaKey ?? attrs.startKey ?? attrs.latKey ?? "");
113
+ if (!marker) return;
114
+ storage.pendingOpen = `${fieldType}:${marker}`;
115
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * useMetaValidator — ad-hoc meta validation for UI forms.
3
+ *
4
+ * Returns a single `validate(typeName, meta)` function that delegates to
5
+ * whichever registry was attached via the `abracadabra:before-boot` hook.
6
+ * Returns `{ ok: true, value }` for unknown doc-types (Rule 4).
7
+ *
8
+ * Intended for properties-panel inputs and similar UI surfaces that
9
+ * want a non-throwing pre-write check. For the throwing path, use
10
+ * `useTypedDoc(...)` (which validates inside `update`/`set`).
11
+ */
12
+ import { type SchemaRegistryLike } from './useAbracadabraSchema.js';
13
+ export interface MetaValidationFailure {
14
+ ok: false;
15
+ errors: ReadonlyArray<{
16
+ path: ReadonlyArray<PropertyKey>;
17
+ message: string;
18
+ code?: string;
19
+ }>;
20
+ }
21
+ export interface MetaValidationOk {
22
+ ok: true;
23
+ value: unknown;
24
+ }
25
+ export declare function useMetaValidator(_explicitSchema?: SchemaRegistryLike): {
26
+ validate: (typeName: string, meta: unknown) => MetaValidationOk | MetaValidationFailure;
27
+ };
@@ -0,0 +1,10 @@
1
+ import { useAbracadabraSchema } from "./useAbracadabraSchema.js";
2
+ export function useMetaValidator(_explicitSchema) {
3
+ const { validateMeta } = useAbracadabraSchema();
4
+ return {
5
+ validate(typeName, meta) {
6
+ const result = validateMeta(typeName, meta);
7
+ return result;
8
+ }
9
+ };
10
+ }
@@ -0,0 +1,161 @@
1
+ import type { PluginManifest, PluginCapability } from '@abraca/plugin';
2
+ export interface CatalogPlugin {
3
+ id: string;
4
+ name: string | null;
5
+ description: string;
6
+ repository: string | null;
7
+ homepage: string | null;
8
+ categories: string[];
9
+ pricing: string;
10
+ status: string;
11
+ latest_version: string | null;
12
+ owner_login: string | null;
13
+ created_at: string;
14
+ updated_at: string;
15
+ }
16
+ export interface CatalogVersionSummary {
17
+ version: string;
18
+ integrity: string;
19
+ status: string;
20
+ github_tag: string | null;
21
+ submitted_at: string;
22
+ scanned_at: string | null;
23
+ }
24
+ export interface CatalogPluginDetail {
25
+ plugin: CatalogPlugin;
26
+ versions: CatalogVersionSummary[];
27
+ }
28
+ export interface CatalogVersionDetail {
29
+ plugin_id: string;
30
+ version: string;
31
+ integrity: string;
32
+ status: string;
33
+ artifact_url: string | null;
34
+ github_tag: string | null;
35
+ submitted_at: string;
36
+ scanned_at: string | null;
37
+ manifest: PluginManifest;
38
+ }
39
+ export interface CatalogListResult {
40
+ plugins: CatalogPlugin[];
41
+ pagination: {
42
+ limit: number;
43
+ offset: number;
44
+ has_more: boolean;
45
+ };
46
+ }
47
+ export interface CatalogListParams {
48
+ category?: string;
49
+ search?: string;
50
+ limit?: number;
51
+ offset?: number;
52
+ }
53
+ /**
54
+ * Mirrors `GET /plugins/policy` on the abracadabra server. Fields are
55
+ * stable across server versions; new fields are additive.
56
+ */
57
+ export interface ServerPluginPolicy {
58
+ registry_url: string;
59
+ allowlist: string[];
60
+ allow_user_install: boolean;
61
+ allow_unsafe_install: boolean;
62
+ auto_update: 'off' | 'patch' | 'minor' | 'major';
63
+ require_review_on_cap_growth: boolean;
64
+ }
65
+ /** One row in `checkUpdates()`'s report. */
66
+ export interface AutoUpdateOutcome {
67
+ pluginId: string;
68
+ fromVersion: string;
69
+ toVersion: string;
70
+ /**
71
+ * `applied` — version was bumped, artifact URL re-installed.
72
+ * `pending-review` — version is newer but introduces capabilities not
73
+ * present in the installed version; `require_review_on_cap_growth`
74
+ * is on, so the user must explicitly accept the new manifest.
75
+ * `skipped-policy` — bump fell outside `auto_update` (e.g. a minor
76
+ * bump while policy is `patch`).
77
+ * `skipped-blocked` — server policy blocks this plugin entirely now.
78
+ */
79
+ state: 'applied' | 'pending-review' | 'skipped-policy' | 'skipped-blocked';
80
+ /** Capabilities new in the upstream version vs the installed one. */
81
+ newCapabilities: PluginCapability[];
82
+ /** Set when state === 'pending-review' or skipped — actionable reason. */
83
+ reason?: string;
84
+ }
85
+ /**
86
+ * `policyDecisionFor` outcome. Drives every Install button across the UI —
87
+ * one helper, three rendered states, no scattered "if" chains.
88
+ */
89
+ export type PolicyDecision =
90
+ /** Plugin is in the server's `allowlist`. Install + auto-update silently. */
91
+ {
92
+ state: 'allowed';
93
+ reason?: undefined;
94
+ }
95
+ /** Not allowlisted but `allow_user_install` is true. Show capability disclosure. */
96
+ | {
97
+ state: 'gated';
98
+ reason?: undefined;
99
+ }
100
+ /** Not allowlisted and `allow_user_install` is false. Install button hidden. */
101
+ | {
102
+ state: 'blocked';
103
+ reason: string;
104
+ }
105
+ /** Policy hasn't been fetched yet — fall back to permissive (allowed). */
106
+ | {
107
+ state: 'unknown';
108
+ reason?: undefined;
109
+ };
110
+ export declare function usePluginCatalog(): {
111
+ /** Public catalog (latest page fetched). Refresh via `list()`. */
112
+ plugins: import("vue").ShallowRef<CatalogPlugin[], CatalogPlugin[]>;
113
+ /** Distinct category tags across `plugins`, alphabetical. */
114
+ categories: import("vue").ComputedRef<string[]>;
115
+ pagination: import("vue").Ref<{
116
+ limit: number;
117
+ offset: number;
118
+ has_more: boolean;
119
+ } | null, {
120
+ limit: number;
121
+ offset: number;
122
+ has_more: boolean;
123
+ } | {
124
+ limit: number;
125
+ offset: number;
126
+ has_more: boolean;
127
+ } | null>;
128
+ isLoading: import("vue").Ref<boolean, boolean>;
129
+ error: import("vue").Ref<Error | null, Error | null>;
130
+ list: (params?: CatalogListParams) => Promise<CatalogListResult>;
131
+ get: (id: string) => Promise<CatalogPluginDetail>;
132
+ getLatest: (id: string) => Promise<CatalogVersionDetail>;
133
+ getVersion: (id: string, version: string) => Promise<CatalogVersionDetail>;
134
+ install: (id: string) => Promise<CatalogVersionDetail>;
135
+ installFromUrl: (url: string) => Promise<void>;
136
+ isInstalled: (id: string) => boolean;
137
+ capabilitiesFor: (detail: CatalogVersionDetail | PluginManifest) => {
138
+ required: PluginCapability[];
139
+ optional: PluginCapability[];
140
+ };
141
+ /** Current server policy. `null` until `loadPolicy()` resolves. */
142
+ policy: import("vue").Ref<{
143
+ registry_url: string;
144
+ allowlist: string[];
145
+ allow_user_install: boolean;
146
+ allow_unsafe_install: boolean;
147
+ auto_update: "off" | "patch" | "minor" | "major";
148
+ require_review_on_cap_growth: boolean;
149
+ } | null, ServerPluginPolicy | {
150
+ registry_url: string;
151
+ allowlist: string[];
152
+ allow_user_install: boolean;
153
+ allow_unsafe_install: boolean;
154
+ auto_update: "off" | "patch" | "minor" | "major";
155
+ require_review_on_cap_growth: boolean;
156
+ } | null>;
157
+ loadPolicy: (serverUrl: string) => Promise<ServerPluginPolicy>;
158
+ /** Per-plugin install gate. Drives every Install button across the UI. */
159
+ policyDecisionFor: (pluginId: string) => PolicyDecision;
160
+ checkUpdates: () => Promise<AutoUpdateOutcome[]>;
161
+ };
@@ -0,0 +1,234 @@
1
+ import { ref, computed, shallowRef } from "vue";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { useInstalledPlugins } from "./useInstalledPlugins.js";
4
+ export function usePluginCatalog() {
5
+ const cfg = useRuntimeConfig();
6
+ const base = cfg.public.abracadabra?.pluginRegistry?.url || "http://127.0.0.1:8787";
7
+ const plugins = shallowRef([]);
8
+ const isLoading = ref(false);
9
+ const error = ref(null);
10
+ const pagination = ref(null);
11
+ const details = shallowRef({});
12
+ const policy = ref(null);
13
+ async function list(params = {}) {
14
+ isLoading.value = true;
15
+ error.value = null;
16
+ try {
17
+ const url = new URL(`${base.replace(/\/$/, "")}/v1/plugins`);
18
+ if (params.category) url.searchParams.set("category", params.category);
19
+ if (params.search) url.searchParams.set("search", params.search);
20
+ if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
21
+ if (typeof params.offset === "number") url.searchParams.set("offset", String(params.offset));
22
+ const result = await $fetch(url.toString());
23
+ plugins.value = result.plugins;
24
+ pagination.value = result.pagination;
25
+ return result;
26
+ } catch (e) {
27
+ error.value = e instanceof Error ? e : new Error(String(e));
28
+ throw error.value;
29
+ } finally {
30
+ isLoading.value = false;
31
+ }
32
+ }
33
+ async function get(id) {
34
+ if (details.value[id]) return details.value[id];
35
+ const detail = await $fetch(`${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}`);
36
+ details.value = { ...details.value, [id]: detail };
37
+ return detail;
38
+ }
39
+ async function getLatest(id) {
40
+ return await $fetch(
41
+ `${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/latest`
42
+ );
43
+ }
44
+ async function getVersion(id, version) {
45
+ return await $fetch(
46
+ `${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}`
47
+ );
48
+ }
49
+ const installed = useInstalledPlugins();
50
+ function isInstalled(id) {
51
+ return installed.entries.value.some((e) => e.name === id || e.url.includes(`/${id}/`));
52
+ }
53
+ async function install(id) {
54
+ const decision = policyDecisionFor(id);
55
+ if (decision.state === "blocked") {
56
+ throw new Error(`install refused by server policy: ${decision.reason}`);
57
+ }
58
+ const latest = await getLatest(id);
59
+ if (!latest.artifact_url) {
60
+ throw new Error(
61
+ `plugin "${id}" version ${latest.version} has no artifact URL \u2014 registry artifact storage not configured yet`
62
+ );
63
+ }
64
+ installed.install(latest.artifact_url);
65
+ installed.updateMeta(latest.artifact_url, {
66
+ name: id,
67
+ version: latest.version,
68
+ label: latest.manifest.name ?? id,
69
+ description: latest.manifest.description
70
+ });
71
+ return latest;
72
+ }
73
+ async function installFromUrl(url) {
74
+ const p = policy.value;
75
+ if (p && !p.allow_unsafe_install) {
76
+ throw new Error(
77
+ "install refused by server policy: out-of-registry URL/file installs are disabled"
78
+ );
79
+ }
80
+ installed.install(url);
81
+ }
82
+ async function checkUpdates() {
83
+ const p = policy.value;
84
+ if (p && p.auto_update === "off") return [];
85
+ const outcomes = [];
86
+ for (const entry of installed.entries.value) {
87
+ if (!entry.name || !entry.version) continue;
88
+ let latest;
89
+ try {
90
+ latest = await getLatest(entry.name);
91
+ } catch {
92
+ continue;
93
+ }
94
+ const decision = policyDecisionFor(entry.name);
95
+ if (decision.state === "blocked") {
96
+ outcomes.push({
97
+ pluginId: entry.name,
98
+ fromVersion: entry.version,
99
+ toVersion: latest.version,
100
+ state: "skipped-blocked",
101
+ newCapabilities: [],
102
+ reason: decision.reason
103
+ });
104
+ continue;
105
+ }
106
+ const bump = semverBump(entry.version, latest.version);
107
+ if (!bump) continue;
108
+ if (!bumpAllowed(bump, p?.auto_update ?? "patch")) {
109
+ outcomes.push({
110
+ pluginId: entry.name,
111
+ fromVersion: entry.version,
112
+ toVersion: latest.version,
113
+ state: "skipped-policy",
114
+ newCapabilities: [],
115
+ reason: `policy is '${p?.auto_update ?? "patch"}', would not apply '${bump}' bump`
116
+ });
117
+ continue;
118
+ }
119
+ let installedCaps = [];
120
+ try {
121
+ const installedDetail = await getVersion(entry.name, entry.version);
122
+ installedCaps = installedDetail.manifest.capabilities.required ?? [];
123
+ } catch {
124
+ }
125
+ const latestCaps = latest.manifest.capabilities.required ?? [];
126
+ const newCaps = latestCaps.filter((c) => !installedCaps.includes(c));
127
+ const requireReview = p?.require_review_on_cap_growth ?? true;
128
+ if (newCaps.length > 0 && requireReview) {
129
+ outcomes.push({
130
+ pluginId: entry.name,
131
+ fromVersion: entry.version,
132
+ toVersion: latest.version,
133
+ state: "pending-review",
134
+ newCapabilities: newCaps,
135
+ reason: "new capabilities requested"
136
+ });
137
+ continue;
138
+ }
139
+ if (latest.artifact_url) {
140
+ installed.uninstall(entry.url);
141
+ installed.install(latest.artifact_url);
142
+ installed.updateMeta(latest.artifact_url, {
143
+ name: entry.name,
144
+ version: latest.version,
145
+ label: latest.manifest.name ?? entry.name,
146
+ description: latest.manifest.description
147
+ });
148
+ }
149
+ outcomes.push({
150
+ pluginId: entry.name,
151
+ fromVersion: entry.version,
152
+ toVersion: latest.version,
153
+ state: "applied",
154
+ newCapabilities: newCaps
155
+ });
156
+ }
157
+ return outcomes;
158
+ }
159
+ function capabilitiesFor(detail) {
160
+ const m = "manifest" in detail ? detail.manifest : detail;
161
+ return {
162
+ required: [...m.capabilities.required ?? []],
163
+ optional: [...m.capabilities.optional ?? []]
164
+ };
165
+ }
166
+ async function loadPolicy(serverUrl) {
167
+ const url = `${serverUrl.replace(/\/$/, "")}/plugins/policy`;
168
+ const fetched = await $fetch(url);
169
+ policy.value = fetched;
170
+ return fetched;
171
+ }
172
+ function policyDecisionFor(pluginId) {
173
+ const p = policy.value;
174
+ if (!p) return { state: "unknown" };
175
+ if (p.allowlist.includes(pluginId)) return { state: "allowed" };
176
+ if (p.allow_user_install) return { state: "gated" };
177
+ return {
178
+ state: "blocked",
179
+ reason: "Your server only permits plugins from its allowlist."
180
+ };
181
+ }
182
+ const categories = computed(() => {
183
+ const set = /* @__PURE__ */ new Set();
184
+ for (const p of plugins.value) for (const c of p.categories) set.add(c);
185
+ return [...set].sort();
186
+ });
187
+ return {
188
+ /** Public catalog (latest page fetched). Refresh via `list()`. */
189
+ plugins,
190
+ /** Distinct category tags across `plugins`, alphabetical. */
191
+ categories,
192
+ pagination,
193
+ isLoading,
194
+ error,
195
+ list,
196
+ get,
197
+ getLatest,
198
+ getVersion,
199
+ install,
200
+ installFromUrl,
201
+ isInstalled,
202
+ capabilitiesFor,
203
+ /** Current server policy. `null` until `loadPolicy()` resolves. */
204
+ policy,
205
+ loadPolicy,
206
+ /** Per-plugin install gate. Drives every Install button across the UI. */
207
+ policyDecisionFor,
208
+ checkUpdates
209
+ };
210
+ }
211
+ function semverBump(from, to) {
212
+ const a = parseSemver(from);
213
+ const b = parseSemver(to);
214
+ if (!a || !b) return null;
215
+ if (b.major > a.major) return "major";
216
+ if (b.major < a.major) return null;
217
+ if (b.minor > a.minor) return "minor";
218
+ if (b.minor < a.minor) return null;
219
+ if (b.patch > a.patch) return "patch";
220
+ return null;
221
+ }
222
+ function parseSemver(v) {
223
+ const cleaned = v.replace(/^v/, "").split(/[-+]/)[0];
224
+ if (!cleaned) return null;
225
+ const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10));
226
+ if (parts.length !== 3 || parts.some((p) => Number.isNaN(p))) return null;
227
+ return { major: parts[0], minor: parts[1], patch: parts[2] };
228
+ }
229
+ function bumpAllowed(bump, policy) {
230
+ if (policy === "off") return false;
231
+ if (policy === "patch") return bump === "patch";
232
+ if (policy === "minor") return bump === "patch" || bump === "minor";
233
+ return true;
234
+ }