@abraca/nuxt 2.0.10 → 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.
- package/dist/module.d.mts +68 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +99 -4
- package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
- package/dist/runtime/components/ACodeEditor.vue +268 -0
- package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
- package/dist/runtime/components/ADocumentTree.vue +52 -20
- package/dist/runtime/components/AEditor.d.vue.ts +20 -13
- package/dist/runtime/components/AEditor.vue +55 -2
- package/dist/runtime/components/AEditor.vue.d.ts +20 -13
- package/dist/runtime/components/ANodePanel.vue +64 -60
- package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
- package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
- package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
- package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
- package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
- package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
- package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
- package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
- package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
- package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
- package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
- package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
- package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
- package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
- package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
- package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
- package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
- package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
- package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
- package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
- package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
- package/dist/runtime/components/registry/APluginCard.vue +91 -0
- package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
- package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
- package/dist/runtime/components/registry/APluginDetail.vue +252 -0
- package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
- package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
- package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
- package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
- package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
- package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
- package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
- package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
- package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
- package/dist/runtime/components/shell/AUserMenu.vue +4 -0
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
- package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
- package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
- package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
- package/dist/runtime/composables/useCalendarView.d.ts +1 -1
- package/dist/runtime/composables/useChat.js +1 -0
- package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
- package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
- package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
- package/dist/runtime/composables/useDocEntryTyped.js +70 -0
- package/dist/runtime/composables/useEditorDragHandle.js +18 -0
- package/dist/runtime/composables/useEditorSuggestions.js +2 -1
- package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
- package/dist/runtime/composables/useInstalledPlugins.js +2 -12
- package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
- package/dist/runtime/composables/useMetaMenuItems.js +115 -0
- package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
- package/dist/runtime/composables/useMetaValidator.js +10 -0
- package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
- package/dist/runtime/composables/usePluginCatalog.js +234 -0
- package/dist/runtime/composables/useQuery.d.ts +79 -0
- package/dist/runtime/composables/useQuery.js +97 -0
- package/dist/runtime/composables/useSpaces.js +4 -5
- package/dist/runtime/composables/useTableView.d.ts +3 -3
- package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
- package/dist/runtime/composables/useTypedDoc.js +114 -0
- package/dist/runtime/composables/useWebRTC.js +44 -5
- package/dist/runtime/extensions/document-meta.js +5 -0
- package/dist/runtime/extensions/timeline.d.ts +11 -0
- package/dist/runtime/extensions/timeline.js +52 -0
- package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
- package/dist/runtime/extensions/views/TimelineView.vue +29 -0
- package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
- package/dist/runtime/locale.d.ts +2 -0
- package/dist/runtime/locale.js +2 -0
- package/dist/runtime/plugin-abracadabra.client.js +107 -6
- package/dist/runtime/plugin-registry.d.ts +11 -30
- package/dist/runtime/plugin-registry.js +2 -82
- package/dist/runtime/plugins/core.plugin.js +10 -4
- package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
- package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
- package/dist/runtime/server/utils/docCache.js +24 -3
- package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
- package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
- package/dist/runtime/types.d.ts +63 -46
- package/dist/runtime/utils/docTypes.d.ts +15 -0
- package/dist/runtime/utils/docTypes.js +20 -0
- package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
- package/dist/runtime/utils/loadCodeMirror.js +65 -0
- package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
- package/dist/runtime/utils/markdownToYjs.js +5 -440
- package/dist/runtime/utils/schemaSupport.d.ts +60 -0
- package/dist/runtime/utils/schemaSupport.js +40 -0
- package/dist/runtime/utils/yjsConvert.d.ts +1 -14
- package/dist/runtime/utils/yjsConvert.js +5 -331
- package/package.json +84 -23
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
}
|