@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,403 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+
4
+ /**
5
+ * Plugin Manifest Schema
6
+ *
7
+ * Defines the structure and validation for external plugin manifests.
8
+ * Each external plugin must have a manifest.json that conforms to this schema.
9
+ */
10
+ // ─── Permission Types ─────────────────────────────────────────────────────────
11
+ /**
12
+ * Available permissions that plugins can request.
13
+ * `shell.execute` is intentionally omitted until a native-backed, reviewed path exists.
14
+ */
15
+ export type PluginPermission = "filesystem.read" | "filesystem.write" | "clipboard.read" | "clipboard.write" | "network.fetch" | "workspace-folders.read" | "workspace-folders.write";
16
+
17
+ /**
18
+ * Human-readable descriptions for each permission
19
+ */
20
+ export const PERMISSION_DESCRIPTIONS: Record<PluginPermission, string> = {
21
+ "filesystem.read": "Read files from your filesystem",
22
+ "filesystem.write": "Write and delete files on your filesystem",
23
+ "clipboard.read": "Read content from your clipboard",
24
+ "clipboard.write": "Write content to your clipboard",
25
+ "network.fetch": "Make network requests to external servers",
26
+ "workspace-folders.read": "Read Workspace Folder schemas and values",
27
+ "workspace-folders.write": "Change Workspace Folder schemas and values",
28
+ };
29
+
30
+ /**
31
+ * Risk level for each permission (for UI display)
32
+ */
33
+ export const PERMISSION_RISK: Record<PluginPermission, "low" | "medium" | "high"> = {
34
+ "filesystem.read": "low",
35
+ "filesystem.write": "high",
36
+ "clipboard.read": "medium",
37
+ "clipboard.write": "low",
38
+ "network.fetch": "medium",
39
+ "workspace-folders.read": "medium",
40
+ "workspace-folders.write": "high",
41
+ };
42
+
43
+ // ─── Manifest Types ───────────────────────────────────────────────────────────
44
+ /**
45
+ * Plugin manifest schema
46
+ */
47
+ export interface PluginManifest {
48
+ /** Unique plugin identifier (e.g., "vendor.plugin-name") */
49
+ id: string;
50
+ /** Human-readable name */
51
+ name: string;
52
+ /** Plugin version (semver format) */
53
+ version: string;
54
+ /** Minimum Phials app version required */
55
+ minAppVersion: string;
56
+ /**
57
+ * Public plugin API / SDK contract version this bundle targets (semver).
58
+ */
59
+ pluginApiVersion: string;
60
+ /** Plugin author name */
61
+ author: string;
62
+ /** Brief description of the plugin */
63
+ description: string;
64
+ /** Author's website or profile URL */
65
+ authorUrl?: string;
66
+ /** GitHub repository URL */
67
+ repository?: string;
68
+ /** Iconify icons to preload */
69
+ icons?: string[];
70
+ /** Required permissions */
71
+ permissions?: PluginPermission[];
72
+ }
73
+
74
+ export interface PluginIdentity {
75
+ id: string;
76
+ version: string;
77
+ minAppVersion: string;
78
+ pluginApiVersion: string;
79
+ }
80
+
81
+ export interface PluginIdentityProjection {
82
+ source: string;
83
+ id?: string;
84
+ version?: string;
85
+ minAppVersion?: string;
86
+ pluginApiVersion?: string;
87
+ }
88
+
89
+ export interface PluginCandidateIdentity extends PluginIdentity {
90
+ checksum: string;
91
+ }
92
+
93
+ // ─── Validation ───────────────────────────────────────────────────────────────
94
+ /**
95
+ * Validation result
96
+ */
97
+ export interface ValidationResult {
98
+ valid: boolean;
99
+ errors: string[];
100
+ }
101
+
102
+ const MANIFEST_FIELDS = new Set([
103
+ "id",
104
+ "name",
105
+ "version",
106
+ "minAppVersion",
107
+ "pluginApiVersion",
108
+ "author",
109
+ "description",
110
+ "authorUrl",
111
+ "repository",
112
+ "icons",
113
+ "permissions",
114
+ ]);
115
+
116
+ const VALID_PERMISSIONS = new Set<PluginPermission>([
117
+ "filesystem.read",
118
+ "filesystem.write",
119
+ "clipboard.read",
120
+ "clipboard.write",
121
+ "network.fetch",
122
+ "workspace-folders.read",
123
+ "workspace-folders.write",
124
+ ]);
125
+
126
+ const IMPLIED_PERMISSION_PAIRS: ReadonlyArray<readonly [
127
+ PluginPermission,
128
+ PluginPermission
129
+ ]> = [
130
+ ["filesystem.write", "filesystem.read"],
131
+ ["workspace-folders.write", "workspace-folders.read"],
132
+ ];
133
+
134
+ 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-]+)*))?$/;
135
+
136
+ const ICONIFY_ICON_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
137
+
138
+ /**
139
+ * Validate a plugin ID format
140
+ * Must be in format: vendor.plugin-name (lowercase, alphanumeric with hyphens)
141
+ */
142
+ export function validatePluginId(id: string): boolean {
143
+ const pattern = /^[a-z][a-z0-9]*\.[a-z][a-z0-9-]*[a-z0-9]$/;
144
+ return pattern.test(id) && !id.startsWith("phials.");
145
+ }
146
+
147
+ /**
148
+ * Validate complete SemVer 2.0.0 syntax.
149
+ */
150
+ export function validateSemver(version: string): boolean {
151
+ return SEMVER_PATTERN.test(version);
152
+ }
153
+
154
+ /**
155
+ * Compare two semver versions
156
+ * Returns: -1 if a < b, 0 if a == b, 1 if a > b
157
+ */
158
+ export function compareSemver(a: string, b: string): number {
159
+ if (!validateSemver(a) || !validateSemver(b)) {
160
+ throw new Error("compareSemver requires valid SemVer values");
161
+ }
162
+ const parseVersion = (value: string) => {
163
+ const match = SEMVER_PATTERN.exec(value)!;
164
+ return {
165
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
166
+ pre: match[4]?.split(".") ?? [],
167
+ };
168
+ };
169
+ const left = parseVersion(a);
170
+ const right = parseVersion(b);
171
+ for (let i = 0; i < 3; i++) {
172
+ const aVal = left.core[i] ?? 0;
173
+ const bVal = right.core[i] ?? 0;
174
+ if (aVal < bVal)
175
+ return -1;
176
+ if (aVal > bVal)
177
+ return 1;
178
+ }
179
+ if (left.pre.length === 0 && right.pre.length === 0)
180
+ return 0;
181
+ if (left.pre.length === 0)
182
+ return 1;
183
+ if (right.pre.length === 0)
184
+ return -1;
185
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
186
+ const aPart = left.pre[i];
187
+ const bPart = right.pre[i];
188
+ if (aPart === undefined)
189
+ return -1;
190
+ if (bPart === undefined)
191
+ return 1;
192
+ if (aPart === bPart)
193
+ continue;
194
+ const aNumeric = /^\d+$/.test(aPart);
195
+ const bNumeric = /^\d+$/.test(bPart);
196
+ if (aNumeric && bNumeric) {
197
+ return Number(aPart) < Number(bPart) ? -1 : 1;
198
+ }
199
+ if (aNumeric !== bNumeric)
200
+ return aNumeric ? -1 : 1;
201
+ return aPart < bPart ? -1 : 1;
202
+ }
203
+ return 0;
204
+ }
205
+
206
+ /**
207
+ * Check if a version satisfies a minimum version requirement
208
+ */
209
+ export function satisfiesMinVersion(version: string, minVersion: string): boolean {
210
+ return compareSemver(version, minVersion) >= 0;
211
+ }
212
+
213
+ export function definePluginManifest<const T extends PluginManifest>(manifest: T): Readonly<T> {
214
+ const result = validateManifest(manifest);
215
+ if (!result.valid) {
216
+ throw new Error(`Invalid plugin manifest: ${result.errors.join("; ")}`);
217
+ }
218
+ return Object.freeze({
219
+ ...manifest,
220
+ permissions: manifest.permissions ?
221
+ Object.freeze([...manifest.permissions])
222
+ : undefined,
223
+ icons: manifest.icons ? Object.freeze([...manifest.icons]) : undefined,
224
+ }) as Readonly<T>;
225
+ }
226
+
227
+ export function manifestIdentity(manifest: PluginManifest): PluginIdentity {
228
+ return {
229
+ id: manifest.id,
230
+ version: manifest.version,
231
+ minAppVersion: manifest.minAppVersion,
232
+ pluginApiVersion: manifest.pluginApiVersion,
233
+ };
234
+ }
235
+
236
+ export function validateIdentityProjections(identity: PluginIdentity, projections: readonly PluginIdentityProjection[]): ValidationResult {
237
+ const errors: string[] = [];
238
+ for (const projection of projections) {
239
+ for (const field of [
240
+ "id",
241
+ "version",
242
+ "minAppVersion",
243
+ "pluginApiVersion",
244
+ ] as const) {
245
+ const value = projection[field];
246
+ if (value !== undefined && value !== identity[field]) {
247
+ errors.push(`${projection.source}.${field} "${value}" does not match manifest "${identity[field]}"`);
248
+ }
249
+ }
250
+ }
251
+ return { valid: errors.length === 0, errors };
252
+ }
253
+
254
+ export function definePlugin(manifest: PluginManifest, definition: Omit<PhialsPlugin, "id" | "name" | "version">): PhialsPlugin {
255
+ const validManifest = definePluginManifest(manifest);
256
+ return Object.freeze({
257
+ ...definition,
258
+ id: validManifest.id,
259
+ name: validManifest.name,
260
+ version: validManifest.version,
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Supported public plugin API contract for this app build.
266
+ * Keep in sync with `SUPPORTED_PLUGIN_API_VERSION` in `src-tauri/src/lib.rs`.
267
+ */
268
+ export const SUPPORTED_PLUGIN_API_VERSION = "1.0.0" as const;
269
+
270
+ /**
271
+ * Validate a plugin manifest
272
+ */
273
+ export function validateManifest(manifest: unknown): ValidationResult {
274
+ const errors: string[] = [];
275
+ if (!manifest || typeof manifest !== "object") {
276
+ return { valid: false, errors: ["Manifest must be an object"] };
277
+ }
278
+ const m = manifest as Record<string, unknown>;
279
+ const unknownFields = Object.keys(m).filter((field) => !MANIFEST_FIELDS.has(field));
280
+ if (unknownFields.length > 0) {
281
+ errors.push(`Unknown manifest field(s): ${unknownFields.join(", ")}`);
282
+ }
283
+ // Required fields
284
+ if (typeof m.id !== "string" || !m.id.trim()) {
285
+ errors.push('Missing or invalid "id" field');
286
+ }
287
+ else if (!validatePluginId(m.id)) {
288
+ errors.push('Invalid or reserved "id". Use lowercase vendor.plugin-name and do not use the "phials." namespace');
289
+ }
290
+ if (typeof m.name !== "string" || !m.name.trim()) {
291
+ errors.push('Missing or invalid "name" field');
292
+ }
293
+ if (typeof m.version !== "string" || !m.version.trim()) {
294
+ errors.push('Missing or invalid "version" field');
295
+ }
296
+ else if (!validateSemver(m.version)) {
297
+ errors.push('Invalid "version" format. Must be semver (e.g., "1.0.0")');
298
+ }
299
+ if (typeof m.minAppVersion !== "string" || !m.minAppVersion.trim()) {
300
+ errors.push('Missing or invalid "minAppVersion" field');
301
+ }
302
+ else if (!validateSemver(m.minAppVersion)) {
303
+ errors.push('Invalid "minAppVersion" format. Must be semver (e.g., "0.1.0")');
304
+ }
305
+ if (typeof m.pluginApiVersion !== "string" || !m.pluginApiVersion.trim()) {
306
+ errors.push('Missing or invalid "pluginApiVersion" field');
307
+ }
308
+ else if (m.pluginApiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
309
+ errors.push(`Invalid "pluginApiVersion": expected exactly "${SUPPORTED_PLUGIN_API_VERSION}"`);
310
+ }
311
+ if (typeof m.author !== "string" || !m.author.trim()) {
312
+ errors.push('Missing or invalid "author" field');
313
+ }
314
+ if (typeof m.description !== "string" || !m.description.trim()) {
315
+ errors.push('Missing or invalid "description" field');
316
+ }
317
+ // Optional fields
318
+ for (const field of ["authorUrl", "repository"] as const) {
319
+ const value = m[field];
320
+ if (value === undefined)
321
+ continue;
322
+ if (typeof value !== "string" || !value.trim()) {
323
+ errors.push(`Invalid "${field}" field - must be a non-empty HTTPS URL`);
324
+ continue;
325
+ }
326
+ try {
327
+ const url = new URL(value);
328
+ if (url.protocol !== "https:" || url.username || url.password) {
329
+ errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
330
+ }
331
+ }
332
+ catch {
333
+ errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
334
+ }
335
+ }
336
+ if (m.icons !== undefined) {
337
+ if (!Array.isArray(m.icons)) {
338
+ errors.push('Invalid "icons" field - must be an array');
339
+ }
340
+ else {
341
+ const seen = new Set<string>();
342
+ for (const icon of m.icons) {
343
+ if (typeof icon !== "string" ||
344
+ !icon.trim() ||
345
+ !ICONIFY_ICON_PATTERN.test(icon)) {
346
+ errors.push(`Invalid icon: "${String(icon)}"`);
347
+ }
348
+ else if (seen.has(icon)) {
349
+ errors.push(`Duplicate icon: "${icon}"`);
350
+ }
351
+ else {
352
+ seen.add(icon);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ if (m.permissions !== undefined) {
358
+ if (!Array.isArray(m.permissions)) {
359
+ errors.push('Invalid "permissions" field - must be an array');
360
+ }
361
+ else {
362
+ const seen = new Set<PluginPermission>();
363
+ for (const p of m.permissions) {
364
+ if (!VALID_PERMISSIONS.has(p as PluginPermission)) {
365
+ errors.push(`Invalid permission: "${p}"`);
366
+ }
367
+ else if (seen.has(p as PluginPermission)) {
368
+ errors.push(`Duplicate permission: "${p}"`);
369
+ }
370
+ else {
371
+ seen.add(p as PluginPermission);
372
+ }
373
+ }
374
+ for (const [write, read] of IMPLIED_PERMISSION_PAIRS) {
375
+ if (seen.has(write) && seen.has(read)) {
376
+ errors.push(`Redundant permissions: "${write}" already implies "${read}"`);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ return { valid: errors.length === 0, errors };
382
+ }
383
+
384
+ /**
385
+ * Parse and validate a manifest JSON string
386
+ */
387
+ export function parseManifest(json: string): {
388
+ manifest: PluginManifest | null;
389
+ errors: string[];
390
+ } {
391
+ let parsed: unknown;
392
+ try {
393
+ parsed = JSON.parse(json);
394
+ }
395
+ catch {
396
+ return { manifest: null, errors: ["Invalid JSON"] };
397
+ }
398
+ const result = validateManifest(parsed);
399
+ if (!result.valid) {
400
+ return { manifest: null, errors: result.errors };
401
+ }
402
+ return { manifest: parsed as PluginManifest, errors: [] };
403
+ }
@@ -0,0 +1,41 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+
4
+ /**
5
+ * Module System Type Definitions
6
+ *
7
+ * Types for the modular panel system that allows Navigator, Preview, Terminal,
8
+ * and other components to be placed in any panel position with tabs or splits.
9
+ */
10
+ // ─── Module Types ─────────────────────────────────────────────────────────────
11
+ /**
12
+ * Built-in module type identifiers.
13
+ * Plugin-provided modules use their full plugin ID (e.g., 'vendor.module.custom')
14
+ */
15
+ type ModuleType = "navigator" | "preview" | "terminal" | string;
16
+
17
+ /**
18
+ * Panel positions in the layout (side/bottom panels only).
19
+ */
20
+ type PanelPosition = "left" | "right" | "bottom";
21
+
22
+ /**
23
+ * All positions where a module can be placed, including center (tab-based).
24
+ */
25
+ type ModulePosition = PanelPosition | "center";
26
+
27
+ // ─── Module Instance ──────────────────────────────────────────────────────────
28
+ /**
29
+ * A single module instance configuration.
30
+ * Multiple instances of the same module type may exist (if allowMultiple is true).
31
+ */
32
+ interface ModuleInstance {
33
+ /** Unique instance ID */
34
+ id: string;
35
+ /** Module type (references ModuleProvider.id) */
36
+ type: ModuleType;
37
+ /** Custom title override (uses provider name if not set) */
38
+ title?: string;
39
+ /** Module-specific persisted state */
40
+ state?: unknown;
41
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@phoundry/phials-plugin-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Public plugin SDK contract for Phials",
5
+ "type": "module",
6
+ "types": "./phials-plugin-sdk.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./phials-plugin-sdk.d.ts"
10
+ },
11
+ "./manifest": {
12
+ "types": "./manifest-schema.ts",
13
+ "import": "./manifest-schema.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "*.d.ts",
18
+ "manifest-schema.ts",
19
+ "manifest-schema.js"
20
+ ],
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }
@@ -0,0 +1,71 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+
4
+ /**
5
+ * Stable reactive projection of one Explorer pane.
6
+ *
7
+ * The host owns the backing pane and keeps these readonly projections current.
8
+ * Acquire a pane explicitly through `api.explorer` and do not retain it after
9
+ * the owning plugin API is invalidated.
10
+ */
11
+ interface PluginPaneContext {
12
+ readonly id: string;
13
+ readonly listing: PluginPaneListing;
14
+ readonly selection: PluginPaneSelection;
15
+ readonly navigation: PluginPaneNavigation;
16
+ readonly view: PluginPaneView;
17
+ readonly workspaceFolder: PluginPaneWorkspaceFolder | null;
18
+ }
19
+
20
+ interface PluginPaneListing {
21
+ readonly loading: boolean;
22
+ readonly entries: readonly FileEntry[];
23
+ readonly failures: readonly PluginFileFailure[];
24
+ refresh(): Promise<void>;
25
+ }
26
+
27
+ interface PluginPaneSelection {
28
+ readonly entries: readonly FileEntry[];
29
+ readonly paths: readonly string[];
30
+ set(paths: readonly string[]): void;
31
+ selectAll(): void;
32
+ clear(): void;
33
+ }
34
+
35
+ interface PluginPaneNavigation {
36
+ readonly currentPath: string | null;
37
+ readonly canGoBack: boolean;
38
+ readonly canGoForward: boolean;
39
+ readonly canGoUp: boolean;
40
+ navigateTo(path: string): Promise<void>;
41
+ openPath(path: string): Promise<void>;
42
+ back(): Promise<void>;
43
+ forward(): Promise<void>;
44
+ up(): Promise<void>;
45
+ }
46
+
47
+ interface PluginPaneView {
48
+ readonly mode: string;
49
+ readonly itemSize: number | null;
50
+ readonly columns: readonly PluginPaneColumn[];
51
+ readonly sorting: readonly PluginPaneSort[];
52
+ readonly options: Readonly<Record<string, JsonValue>>;
53
+ }
54
+
55
+ interface PluginPaneColumn {
56
+ readonly id: string;
57
+ readonly visible: boolean;
58
+ readonly width: number;
59
+ readonly order: number;
60
+ }
61
+
62
+ interface PluginPaneSort {
63
+ readonly property: string;
64
+ readonly order: "asc" | "desc";
65
+ }
66
+
67
+ interface PluginPaneWorkspaceFolder {
68
+ readonly id: string;
69
+ readonly rootPath: string;
70
+ readonly available: boolean;
71
+ }
@@ -0,0 +1,11 @@
1
+ // @generated from phials - do not edit
2
+ // Source graph: phials/scripts/lib/public-sdk-manifest.mjs
3
+
4
+ /// <reference path="./pane-context.generated.d.ts" />
5
+ /// <reference path="./public-contract.generated.d.ts" />
6
+ /// <reference path="./plugin-types.generated.d.ts" />
7
+ /// <reference path="./command-types.generated.d.ts" />
8
+ /// <reference path="./file-types.generated.d.ts" />
9
+ /// <reference path="./shortcuts-types.generated.d.ts" />
10
+ /// <reference path="./events-types.generated.d.ts" />
11
+ /// <reference path="./module-types.generated.d.ts" />