@phoundry/phials-plugin-sdk 1.0.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.
@@ -0,0 +1,239 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+
4
+ /**
5
+ * File type categories for plugin matching and UI display
6
+ * Format: 'broad-category' or 'broad-category - specific'
7
+ */
8
+ type FileCategory =
9
+ // Images
10
+ | "image"
11
+ | "image - vector"
12
+ | "image - raw"
13
+ // Video
14
+ | "video"
15
+ // Audio
16
+ | "audio - lossless"
17
+ | "audio - lossy"
18
+ | "audio - midi"
19
+ // Documents
20
+ | "document"
21
+ | "ebook"
22
+ | "spreadsheet"
23
+ | "presentation"
24
+ // Code & Data
25
+ | "code"
26
+ | "code - data"
27
+ | "code - config"
28
+ | "code - markup"
29
+ // 3D & CAD
30
+ | "model"
31
+ | "cad"
32
+ // Archives & Packages
33
+ | "archive"
34
+ | "archive - disk"
35
+ | "archive - package"
36
+ // System
37
+ | "executable"
38
+ | "database"
39
+ | "font"
40
+ | "folder"
41
+ | "unknown";
42
+
43
+ /**
44
+ * Core file entry interface - file-type agnostic
45
+ * Represents a file or directory in the filesystem
46
+ */
47
+ interface FileEntry {
48
+ name: string;
49
+ path: string;
50
+ /**
51
+ * Host-resolved Iconify glyph. Public directory-listing APIs populate this
52
+ * so plugins share Phials' canonical file and folder icon policy.
53
+ */
54
+ icon?: string;
55
+ is_file: boolean;
56
+ is_dir: boolean;
57
+ is_vial: boolean;
58
+ /** Nested vial folder when listing inside a parent vial */
59
+ isChildVial?: boolean;
60
+ /** Listing node is a symlink or Windows directory junction */
61
+ is_symlink?: boolean;
62
+ /** Resolved absolute target when healthy; stored link text when broken */
63
+ symlink_target?: string | null;
64
+ symlink_broken?: boolean;
65
+ size: number;
66
+ created?: number | null;
67
+ modified?: number | null;
68
+ /** Raw metadata from filesystem/EXIF - parsed lazily by plugins */
69
+ exif_data: Record<string, string> | null;
70
+ /** Computed lazily based on extension */
71
+ mimeType?: string;
72
+ /** Computed lazily based on extension */
73
+ category?: FileCategory;
74
+ }
75
+
76
+ /**
77
+ * Sort modes available for file listings.
78
+ * Can be a standard property ('name', 'created', 'modified', 'size')
79
+ * or a collection property ID.
80
+ */
81
+ type SortMode = string;
82
+
83
+ /**
84
+ * Sort order direction
85
+ */
86
+ type SortOrder = "asc" | "desc";
87
+
88
+ /**
89
+ * View modes for file browser
90
+ */
91
+ type ViewMode =
92
+ | "details"
93
+ | "thumbnails"
94
+ | "column"
95
+ | "tree"
96
+ | "boards"
97
+ | "gallery"
98
+ | "calendar";
99
+
100
+ /** Calendar view zoom level */
101
+ type CalendarScope = "year" | "month" | "week" | "3day" | "day";
102
+
103
+ /** Built-in calendar date sources (`created`, `modified`) or a vial property id */
104
+ type CalendarDateSourceId = "created" | "modified" | (string & {});
105
+
106
+ /**
107
+ * Filter scope for directory listing (current folder vs flatten-with-filters).
108
+ */
109
+ type FilterScope = "current" | "flatten";
110
+
111
+ // ─── Multi-Sort Types ─────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * A single sort criterion for multi-level sorting
115
+ */
116
+ interface SortCriteria {
117
+ /** Property to sort by: 'name', 'size', 'created', 'modified', 'type', 'extension', or collection property ID */
118
+ property: string;
119
+ /** Sort direction */
120
+ order: SortOrder;
121
+ }
122
+
123
+ // ─── Multi-Filter Types ───────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Filter operators for text properties
127
+ */
128
+ type TextFilterOp =
129
+ | "contains"
130
+ | "not_contains"
131
+ | "equals"
132
+ | "not_equals"
133
+ | "starts_with"
134
+ | "ends_with"
135
+ | "is_empty"
136
+ | "is_not_empty";
137
+
138
+ /**
139
+ * Filter operators for number properties
140
+ */
141
+ type NumberFilterOp =
142
+ "eq" | "neq" | "lt" | "gt" | "lte" | "gte" | "is_empty" | "is_not_empty";
143
+
144
+ /**
145
+ * Filter operators for date properties
146
+ */
147
+ type DateFilterOp =
148
+ | "is"
149
+ | "is_not"
150
+ | "is_before"
151
+ | "is_after"
152
+ | "is_on_or_before"
153
+ | "is_on_or_after"
154
+ | "is_empty"
155
+ | "is_not_empty";
156
+
157
+ /**
158
+ * Filter operators for select/status properties
159
+ */
160
+ type SelectFilterOp = "is" | "is_not" | "is_empty" | "is_not_empty";
161
+
162
+ /**
163
+ * Filter operators for multi-select properties
164
+ */
165
+ type MultiSelectFilterOp =
166
+ "contains" | "not_contains" | "is_empty" | "is_not_empty";
167
+
168
+ /**
169
+ * Filter operators for boolean properties
170
+ */
171
+ type BooleanFilterOp = "is" | "is_not";
172
+
173
+ /** @deprecated Migrated to `is` + `true` on load */
174
+ type LegacyBooleanFilterOp = "is_true" | "is_false";
175
+
176
+ /**
177
+ * Union of all filter operators
178
+ */
179
+ type FilterOperator =
180
+ | TextFilterOp
181
+ | NumberFilterOp
182
+ | DateFilterOp
183
+ | SelectFilterOp
184
+ | MultiSelectFilterOp
185
+ | BooleanFilterOp
186
+ | LegacyBooleanFilterOp;
187
+
188
+ /**
189
+ * Size unit for file size filters
190
+ */
191
+ type SizeUnit = "B" | "KB" | "MB" | "GB";
192
+
193
+ /**
194
+ * A single filter condition
195
+ */
196
+ interface FilterCondition {
197
+ /** Unique identifier for the condition */
198
+ id: string;
199
+ /** Property to filter by: 'name', 'extension', 'size', 'created', 'modified', or collection property ID */
200
+ property: string;
201
+ /** Filter operator */
202
+ operator: FilterOperator;
203
+ /** Filter value (type depends on property type) */
204
+ value?: string | number | boolean | string[];
205
+ /** Size unit for file size filters (default: MB) */
206
+ sizeUnit?: SizeUnit;
207
+ }
208
+
209
+ /** Leaf node in a filter tree */
210
+ interface FilterConditionNode extends FilterCondition {
211
+ type: "condition";
212
+ }
213
+
214
+ /** Nested filter sub-group node */
215
+ interface FilterSubGroupNode {
216
+ type: "group";
217
+ /** Unique identifier for the sub-group */
218
+ id: string;
219
+ /** Logic operator for combining direct children */
220
+ logic: "and" | "or";
221
+ /** Child conditions and nested sub-groups */
222
+ children: FilterNode[];
223
+ }
224
+
225
+ /** Discriminated node in a filter tree */
226
+ type FilterNode = FilterConditionNode | FilterSubGroupNode;
227
+
228
+ /** Root filter group id sentinel - `null` means the root group in tree APIs */
229
+ type FilterGroupId = string | null;
230
+
231
+ /**
232
+ * Filter tree root: conditions and nested sub-groups combined with AND/OR logic
233
+ */
234
+ interface FilterGroup {
235
+ /** Logic operator for combining direct children */
236
+ logic: "and" | "or";
237
+ /** Child conditions and nested sub-groups */
238
+ children: FilterNode[];
239
+ }
@@ -0,0 +1,307 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+ /**
4
+ * Human-readable descriptions for each permission
5
+ */
6
+ export const PERMISSION_DESCRIPTIONS = {
7
+ "filesystem.read": "Read files from your filesystem",
8
+ "filesystem.write": "Write and delete files on your filesystem",
9
+ "clipboard.read": "Read content from your clipboard",
10
+ "clipboard.write": "Write content to your clipboard",
11
+ "network.fetch": "Make network requests to external servers",
12
+ "workspace-folders.read": "Read Workspace Folder schemas and values",
13
+ "workspace-folders.write": "Change Workspace Folder schemas and values",
14
+ };
15
+ /**
16
+ * Risk level for each permission (for UI display)
17
+ */
18
+ export const PERMISSION_RISK = {
19
+ "filesystem.read": "low",
20
+ "filesystem.write": "high",
21
+ "clipboard.read": "medium",
22
+ "clipboard.write": "low",
23
+ "network.fetch": "medium",
24
+ "workspace-folders.read": "medium",
25
+ "workspace-folders.write": "high",
26
+ };
27
+ const MANIFEST_FIELDS = new Set([
28
+ "id",
29
+ "name",
30
+ "version",
31
+ "minAppVersion",
32
+ "pluginApiVersion",
33
+ "author",
34
+ "description",
35
+ "authorUrl",
36
+ "repository",
37
+ "icons",
38
+ "permissions",
39
+ ]);
40
+ const VALID_PERMISSIONS = new Set([
41
+ "filesystem.read",
42
+ "filesystem.write",
43
+ "clipboard.read",
44
+ "clipboard.write",
45
+ "network.fetch",
46
+ "workspace-folders.read",
47
+ "workspace-folders.write",
48
+ ]);
49
+ const IMPLIED_PERMISSION_PAIRS = [
50
+ ["filesystem.write", "filesystem.read"],
51
+ ["workspace-folders.write", "workspace-folders.read"],
52
+ ];
53
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
54
+ const ICONIFY_ICON_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
55
+ /**
56
+ * Validate a plugin ID format
57
+ * Must be in format: vendor.plugin-name (lowercase, alphanumeric with hyphens)
58
+ */
59
+ export function validatePluginId(id) {
60
+ const pattern = /^[a-z][a-z0-9]*\.[a-z][a-z0-9-]*[a-z0-9]$/;
61
+ return pattern.test(id) && !id.startsWith("phials.");
62
+ }
63
+ /**
64
+ * Validate complete SemVer 2.0.0 syntax.
65
+ */
66
+ export function validateSemver(version) {
67
+ return SEMVER_PATTERN.test(version);
68
+ }
69
+ /**
70
+ * Compare two semver versions
71
+ * Returns: -1 if a < b, 0 if a == b, 1 if a > b
72
+ */
73
+ export function compareSemver(a, b) {
74
+ if (!validateSemver(a) || !validateSemver(b)) {
75
+ throw new Error("compareSemver requires valid SemVer values");
76
+ }
77
+ const parseVersion = (value) => {
78
+ const match = SEMVER_PATTERN.exec(value);
79
+ return {
80
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
81
+ pre: match[4]?.split(".") ?? [],
82
+ };
83
+ };
84
+ const left = parseVersion(a);
85
+ const right = parseVersion(b);
86
+ for (let i = 0; i < 3; i++) {
87
+ const aVal = left.core[i] ?? 0;
88
+ const bVal = right.core[i] ?? 0;
89
+ if (aVal < bVal)
90
+ return -1;
91
+ if (aVal > bVal)
92
+ return 1;
93
+ }
94
+ if (left.pre.length === 0 && right.pre.length === 0)
95
+ return 0;
96
+ if (left.pre.length === 0)
97
+ return 1;
98
+ if (right.pre.length === 0)
99
+ return -1;
100
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
101
+ const aPart = left.pre[i];
102
+ const bPart = right.pre[i];
103
+ if (aPart === undefined)
104
+ return -1;
105
+ if (bPart === undefined)
106
+ return 1;
107
+ if (aPart === bPart)
108
+ continue;
109
+ const aNumeric = /^\d+$/.test(aPart);
110
+ const bNumeric = /^\d+$/.test(bPart);
111
+ if (aNumeric && bNumeric) {
112
+ return Number(aPart) < Number(bPart) ? -1 : 1;
113
+ }
114
+ if (aNumeric !== bNumeric)
115
+ return aNumeric ? -1 : 1;
116
+ return aPart < bPart ? -1 : 1;
117
+ }
118
+ return 0;
119
+ }
120
+ /**
121
+ * Check if a version satisfies a minimum version requirement
122
+ */
123
+ export function satisfiesMinVersion(version, minVersion) {
124
+ return compareSemver(version, minVersion) >= 0;
125
+ }
126
+ export function definePluginManifest(manifest) {
127
+ const result = validateManifest(manifest);
128
+ if (!result.valid) {
129
+ throw new Error(`Invalid plugin manifest: ${result.errors.join("; ")}`);
130
+ }
131
+ return Object.freeze({
132
+ ...manifest,
133
+ permissions: manifest.permissions ?
134
+ Object.freeze([...manifest.permissions])
135
+ : undefined,
136
+ icons: manifest.icons ? Object.freeze([...manifest.icons]) : undefined,
137
+ });
138
+ }
139
+ export function manifestIdentity(manifest) {
140
+ return {
141
+ id: manifest.id,
142
+ version: manifest.version,
143
+ minAppVersion: manifest.minAppVersion,
144
+ pluginApiVersion: manifest.pluginApiVersion,
145
+ };
146
+ }
147
+ export function validateIdentityProjections(identity, projections) {
148
+ const errors = [];
149
+ for (const projection of projections) {
150
+ for (const field of [
151
+ "id",
152
+ "version",
153
+ "minAppVersion",
154
+ "pluginApiVersion",
155
+ ]) {
156
+ const value = projection[field];
157
+ if (value !== undefined && value !== identity[field]) {
158
+ errors.push(`${projection.source}.${field} "${value}" does not match manifest "${identity[field]}"`);
159
+ }
160
+ }
161
+ }
162
+ return { valid: errors.length === 0, errors };
163
+ }
164
+ export function definePlugin(manifest, definition) {
165
+ const validManifest = definePluginManifest(manifest);
166
+ return Object.freeze({
167
+ ...definition,
168
+ id: validManifest.id,
169
+ name: validManifest.name,
170
+ version: validManifest.version,
171
+ });
172
+ }
173
+ /**
174
+ * Supported public plugin API contract for this app build.
175
+ * Keep in sync with `SUPPORTED_PLUGIN_API_VERSION` in `src-tauri/src/lib.rs`.
176
+ */
177
+ export const SUPPORTED_PLUGIN_API_VERSION = "1.0.0";
178
+ /**
179
+ * Validate a plugin manifest
180
+ */
181
+ export function validateManifest(manifest) {
182
+ const errors = [];
183
+ if (!manifest || typeof manifest !== "object") {
184
+ return { valid: false, errors: ["Manifest must be an object"] };
185
+ }
186
+ const m = manifest;
187
+ const unknownFields = Object.keys(m).filter((field) => !MANIFEST_FIELDS.has(field));
188
+ if (unknownFields.length > 0) {
189
+ errors.push(`Unknown manifest field(s): ${unknownFields.join(", ")}`);
190
+ }
191
+ // Required fields
192
+ if (typeof m.id !== "string" || !m.id.trim()) {
193
+ errors.push('Missing or invalid "id" field');
194
+ }
195
+ else if (!validatePluginId(m.id)) {
196
+ errors.push('Invalid or reserved "id". Use lowercase vendor.plugin-name and do not use the "phials." namespace');
197
+ }
198
+ if (typeof m.name !== "string" || !m.name.trim()) {
199
+ errors.push('Missing or invalid "name" field');
200
+ }
201
+ if (typeof m.version !== "string" || !m.version.trim()) {
202
+ errors.push('Missing or invalid "version" field');
203
+ }
204
+ else if (!validateSemver(m.version)) {
205
+ errors.push('Invalid "version" format. Must be semver (e.g., "1.0.0")');
206
+ }
207
+ if (typeof m.minAppVersion !== "string" || !m.minAppVersion.trim()) {
208
+ errors.push('Missing or invalid "minAppVersion" field');
209
+ }
210
+ else if (!validateSemver(m.minAppVersion)) {
211
+ errors.push('Invalid "minAppVersion" format. Must be semver (e.g., "0.1.0")');
212
+ }
213
+ if (typeof m.pluginApiVersion !== "string" || !m.pluginApiVersion.trim()) {
214
+ errors.push('Missing or invalid "pluginApiVersion" field');
215
+ }
216
+ else if (m.pluginApiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
217
+ errors.push(`Invalid "pluginApiVersion": expected exactly "${SUPPORTED_PLUGIN_API_VERSION}"`);
218
+ }
219
+ if (typeof m.author !== "string" || !m.author.trim()) {
220
+ errors.push('Missing or invalid "author" field');
221
+ }
222
+ if (typeof m.description !== "string" || !m.description.trim()) {
223
+ errors.push('Missing or invalid "description" field');
224
+ }
225
+ // Optional fields
226
+ for (const field of ["authorUrl", "repository"]) {
227
+ const value = m[field];
228
+ if (value === undefined)
229
+ continue;
230
+ if (typeof value !== "string" || !value.trim()) {
231
+ errors.push(`Invalid "${field}" field - must be a non-empty HTTPS URL`);
232
+ continue;
233
+ }
234
+ try {
235
+ const url = new URL(value);
236
+ if (url.protocol !== "https:" || url.username || url.password) {
237
+ errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
238
+ }
239
+ }
240
+ catch {
241
+ errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
242
+ }
243
+ }
244
+ if (m.icons !== undefined) {
245
+ if (!Array.isArray(m.icons)) {
246
+ errors.push('Invalid "icons" field - must be an array');
247
+ }
248
+ else {
249
+ const seen = new Set();
250
+ for (const icon of m.icons) {
251
+ if (typeof icon !== "string" ||
252
+ !icon.trim() ||
253
+ !ICONIFY_ICON_PATTERN.test(icon)) {
254
+ errors.push(`Invalid icon: "${String(icon)}"`);
255
+ }
256
+ else if (seen.has(icon)) {
257
+ errors.push(`Duplicate icon: "${icon}"`);
258
+ }
259
+ else {
260
+ seen.add(icon);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ if (m.permissions !== undefined) {
266
+ if (!Array.isArray(m.permissions)) {
267
+ errors.push('Invalid "permissions" field - must be an array');
268
+ }
269
+ else {
270
+ const seen = new Set();
271
+ for (const p of m.permissions) {
272
+ if (!VALID_PERMISSIONS.has(p)) {
273
+ errors.push(`Invalid permission: "${p}"`);
274
+ }
275
+ else if (seen.has(p)) {
276
+ errors.push(`Duplicate permission: "${p}"`);
277
+ }
278
+ else {
279
+ seen.add(p);
280
+ }
281
+ }
282
+ for (const [write, read] of IMPLIED_PERMISSION_PAIRS) {
283
+ if (seen.has(write) && seen.has(read)) {
284
+ errors.push(`Redundant permissions: "${write}" already implies "${read}"`);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return { valid: errors.length === 0, errors };
290
+ }
291
+ /**
292
+ * Parse and validate a manifest JSON string
293
+ */
294
+ export function parseManifest(json) {
295
+ let parsed;
296
+ try {
297
+ parsed = JSON.parse(json);
298
+ }
299
+ catch {
300
+ return { manifest: null, errors: ["Invalid JSON"] };
301
+ }
302
+ const result = validateManifest(parsed);
303
+ if (!result.valid) {
304
+ return { manifest: null, errors: result.errors };
305
+ }
306
+ return { manifest: parsed, errors: [] };
307
+ }