fluxplugin 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.
Files changed (70) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +50 -0
  3. package/dist/browser/chunks/chunk-Q75AOCXD.js +2544 -0
  4. package/dist/browser/chunks/chunk-ZZ25CRMR.js +759 -0
  5. package/dist/browser/index.js +138 -0
  6. package/dist/browser/manager-ui.js +165 -0
  7. package/dist/browser/react.js +73 -0
  8. package/dist/cli.js +1635 -0
  9. package/dist/node/chunks/chunk-BZ7NZF7I.js +990 -0
  10. package/dist/node/chunks/chunk-XPLGAEBH.js +2623 -0
  11. package/dist/node/cli-api.js +843 -0
  12. package/dist/node/index.js +138 -0
  13. package/dist/node/main.js +61 -0
  14. package/dist/node/preload.js +56 -0
  15. package/dist/types/cli/bin.d.ts +1 -0
  16. package/dist/types/cli/build.d.ts +7 -0
  17. package/dist/types/cli/cli.d.ts +2 -0
  18. package/dist/types/cli/config.d.ts +9 -0
  19. package/dist/types/cli/host-templates.d.ts +6 -0
  20. package/dist/types/cli/host.d.ts +16 -0
  21. package/dist/types/cli/index.d.ts +6 -0
  22. package/dist/types/cli/shared-modules.d.ts +8 -0
  23. package/dist/types/cli/templates.d.ts +10 -0
  24. package/dist/types/core/api.d.ts +11 -0
  25. package/dist/types/core/common.d.ts +69 -0
  26. package/dist/types/core/deps.d.ts +34 -0
  27. package/dist/types/core/events.d.ts +58 -0
  28. package/dist/types/core/hooks.d.ts +55 -0
  29. package/dist/types/core/index.d.ts +18 -0
  30. package/dist/types/core/ipc.d.ts +148 -0
  31. package/dist/types/core/manager.d.ts +155 -0
  32. package/dist/types/core/manifest.d.ts +88 -0
  33. package/dist/types/core/permissions.d.ts +45 -0
  34. package/dist/types/core/plugin.d.ts +214 -0
  35. package/dist/types/core/protocol.d.ts +86 -0
  36. package/dist/types/core/registry.d.ts +88 -0
  37. package/dist/types/core/routes.d.ts +105 -0
  38. package/dist/types/core/schema.d.ts +99 -0
  39. package/dist/types/core/semver.d.ts +16 -0
  40. package/dist/types/core/slots.d.ts +75 -0
  41. package/dist/types/core/storage.d.ts +55 -0
  42. package/dist/types/core/styles.d.ts +34 -0
  43. package/dist/types/main/adapters.d.ts +81 -0
  44. package/dist/types/main/discover.d.ts +21 -0
  45. package/dist/types/main/framework.d.ts +78 -0
  46. package/dist/types/main/fs-storage.d.ts +33 -0
  47. package/dist/types/main/index.d.ts +8 -0
  48. package/dist/types/main/registry.d.ts +75 -0
  49. package/dist/types/main/security.d.ts +24 -0
  50. package/dist/types/main/serve-web.d.ts +14 -0
  51. package/dist/types/main/tar.d.ts +10 -0
  52. package/dist/types/manager-ui/index.d.ts +7 -0
  53. package/dist/types/preload/index.d.ts +27 -0
  54. package/dist/types/react/base-css.d.ts +2 -0
  55. package/dist/types/react/components.d.ts +89 -0
  56. package/dist/types/react/http-bridge.d.ts +10 -0
  57. package/dist/types/react/index.d.ts +6 -0
  58. package/dist/types/react/palette.d.ts +10 -0
  59. package/dist/types/react/router.d.ts +47 -0
  60. package/dist/types/react/runtime.d.ts +99 -0
  61. package/docs/ARCHITECTURE.md +31 -0
  62. package/docs/PLUGIN-AUTHORS.md +11 -0
  63. package/docs/RELEASE.md +20 -0
  64. package/docs/SECURITY.md +18 -0
  65. package/docs/screenshots/command-palette.png +0 -0
  66. package/docs/screenshots/dashboard.png +0 -0
  67. package/docs/screenshots/plugin-details.png +0 -0
  68. package/docs/screenshots/plugin-manager.png +0 -0
  69. package/docs/screenshots/stats.png +0 -0
  70. package/package.json +112 -0
@@ -0,0 +1,2544 @@
1
+ // src/core/common.ts
2
+ var toDisposable = (fn) => {
3
+ let done = false;
4
+ return {
5
+ dispose() {
6
+ if (done) return;
7
+ done = true;
8
+ fn();
9
+ }
10
+ };
11
+ };
12
+ var DisposableStore = class {
13
+ items = [];
14
+ disposed = false;
15
+ add(d) {
16
+ if (this.disposed) d.dispose();
17
+ else this.items.push(d);
18
+ return d;
19
+ }
20
+ get size() {
21
+ return this.items.length;
22
+ }
23
+ dispose() {
24
+ this.disposed = true;
25
+ const items = this.items.splice(0).reverse();
26
+ for (const d of items) {
27
+ try {
28
+ d.dispose();
29
+ } catch {
30
+ }
31
+ }
32
+ }
33
+ /** Re-arms the store so it can be reused after a deactivate/activate cycle. */
34
+ reset() {
35
+ this.dispose();
36
+ this.disposed = false;
37
+ }
38
+ };
39
+ var FrameworkError = class extends Error {
40
+ constructor(message, code, pluginId, options) {
41
+ super(message, options);
42
+ this.code = code;
43
+ this.pluginId = pluginId;
44
+ this.name = new.target.name;
45
+ }
46
+ code;
47
+ pluginId;
48
+ };
49
+ var PermissionDeniedError = class extends FrameworkError {
50
+ constructor(pluginId, permission, api) {
51
+ super(`Plugin "${pluginId}" is not allowed to use ${api} (missing permission "${permission}")`, "FLUX_PERMISSION_DENIED", pluginId);
52
+ this.permission = permission;
53
+ }
54
+ permission;
55
+ };
56
+ var LifecycleError = class extends FrameworkError {
57
+ constructor(pluginId, phase, message, cause) {
58
+ super(`[${pluginId}] ${phase}: ${message}`, "FLUX_LIFECYCLE", pluginId, { cause });
59
+ this.phase = phase;
60
+ }
61
+ phase;
62
+ };
63
+ var TimeoutError = class extends FrameworkError {
64
+ constructor(what, ms, pluginId) {
65
+ super(`${what} timed out after ${ms}ms`, "FLUX_TIMEOUT", pluginId);
66
+ }
67
+ };
68
+ var HookCancelledError = class extends FrameworkError {
69
+ constructor(operation, reason) {
70
+ super(`Operation "${operation}" was cancelled by a hook${reason ? `: ${reason}` : ""}`, "FLUX_HOOK_CANCELLED");
71
+ this.operation = operation;
72
+ this.reason = reason;
73
+ }
74
+ operation;
75
+ reason;
76
+ };
77
+ function withTimeout(p, ms, what, pluginId) {
78
+ if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve(p);
79
+ return new Promise((resolve, reject) => {
80
+ const t = setTimeout(() => reject(new TimeoutError(what, ms, pluginId)), ms);
81
+ Promise.resolve(p).then(
82
+ (v) => {
83
+ clearTimeout(t);
84
+ resolve(v);
85
+ },
86
+ (e) => {
87
+ clearTimeout(t);
88
+ reject(e);
89
+ }
90
+ );
91
+ });
92
+ }
93
+ var LogStore = class {
94
+ constructor(capacity = 2e3, sink) {
95
+ this.capacity = capacity;
96
+ this.sink = sink;
97
+ }
98
+ capacity;
99
+ sink;
100
+ entries = [];
101
+ listeners = /* @__PURE__ */ new Set();
102
+ write(entry) {
103
+ this.entries.push(entry);
104
+ if (this.entries.length > this.capacity) this.entries.splice(0, this.entries.length - this.capacity);
105
+ this.sink?.(entry);
106
+ for (const l of this.listeners) {
107
+ try {
108
+ l(entry);
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ logger(source) {
114
+ const mk = (level) => (message, data) => this.write({ level, source, message, time: Date.now(), data });
115
+ return { debug: mk("debug"), info: mk("info"), warn: mk("warn"), error: mk("error") };
116
+ }
117
+ query(filter = {}) {
118
+ return this.entries.filter((e) => (!filter.source || e.source === filter.source) && (!filter.level || e.level === filter.level));
119
+ }
120
+ subscribe(l) {
121
+ this.listeners.add(l);
122
+ return toDisposable(() => this.listeners.delete(l));
123
+ }
124
+ clear(source) {
125
+ this.entries = source ? this.entries.filter((e) => e.source !== source) : [];
126
+ }
127
+ };
128
+ function errorMessage(e) {
129
+ return e instanceof Error ? e.message : String(e);
130
+ }
131
+
132
+ // src/core/schema.ts
133
+ var ValidationError = class extends Error {
134
+ constructor(issues, what = "value") {
135
+ super(`Invalid ${what}: ` + issues.map((i) => `${i.path || "<root>"}: ${i.message}`).join("; "));
136
+ this.issues = issues;
137
+ this.name = "ValidationError";
138
+ }
139
+ issues;
140
+ };
141
+ var ok = (value) => ({ ok: true, value });
142
+ var fail = (path, message) => ({ ok: false, issues: [{ path, message }] });
143
+ var join = (p, k) => p ? typeof k === "number" ? `${p}[${k}]` : `${p}.${k}` : String(k);
144
+ function make(kind, parse, describe) {
145
+ const self = {
146
+ kind,
147
+ parse: (input, path = "") => parse(input, path),
148
+ describe,
149
+ optional() {
150
+ return make(
151
+ kind,
152
+ (i, p) => i === void 0 ? ok(void 0) : parse(i, p),
153
+ () => ({ ...describe(), optional: true })
154
+ );
155
+ },
156
+ default(value) {
157
+ return make(
158
+ kind,
159
+ (i, p) => i === void 0 ? ok(value) : parse(i, p),
160
+ () => ({ ...describe(), default: value, optional: true })
161
+ );
162
+ }
163
+ };
164
+ return self;
165
+ }
166
+ var s = {
167
+ string(o = {}) {
168
+ return make(
169
+ "string",
170
+ (i, p) => {
171
+ if (typeof i !== "string") return fail(p, "expected string");
172
+ if (o.min !== void 0 && i.length < o.min) return fail(p, `must have at least ${o.min} characters`);
173
+ if (o.max !== void 0 && i.length > o.max) return fail(p, `must have at most ${o.max} characters`);
174
+ if (o.pattern && !o.pattern.test(i)) return fail(p, `does not match ${o.pattern}`);
175
+ return ok(i);
176
+ },
177
+ () => ({ type: "string", title: o.title, description: o.description, min: o.min, max: o.max })
178
+ );
179
+ },
180
+ number(o = {}) {
181
+ return make(
182
+ "number",
183
+ (i, p) => {
184
+ if (typeof i !== "number" || Number.isNaN(i)) return fail(p, "expected number");
185
+ if (o.int && !Number.isInteger(i)) return fail(p, "expected integer");
186
+ if (o.min !== void 0 && i < o.min) return fail(p, `must be >= ${o.min}`);
187
+ if (o.max !== void 0 && i > o.max) return fail(p, `must be <= ${o.max}`);
188
+ return ok(i);
189
+ },
190
+ () => ({ type: "number", title: o.title, description: o.description, min: o.min, max: o.max })
191
+ );
192
+ },
193
+ boolean(o = {}) {
194
+ return make(
195
+ "boolean",
196
+ (i, p) => typeof i === "boolean" ? ok(i) : fail(p, "expected boolean"),
197
+ () => ({ type: "boolean", ...o })
198
+ );
199
+ },
200
+ unknown() {
201
+ return make(
202
+ "unknown",
203
+ (i) => ok(i),
204
+ () => ({ type: "unknown" })
205
+ );
206
+ },
207
+ literal(v) {
208
+ return make(
209
+ "literal",
210
+ (i, p) => i === v ? ok(v) : fail(p, `expected ${JSON.stringify(v)}`),
211
+ () => ({ type: "literal", enum: [v] })
212
+ );
213
+ },
214
+ enum(values, o = {}) {
215
+ return make(
216
+ "enum",
217
+ (i, p) => typeof i === "string" && values.includes(i) ? ok(i) : fail(p, `expected one of ${values.join(", ")}`),
218
+ () => ({ type: "enum", enum: values, ...o })
219
+ );
220
+ },
221
+ array(item, o = {}) {
222
+ return make(
223
+ "array",
224
+ (i, p) => {
225
+ if (!Array.isArray(i)) return fail(p, "expected array");
226
+ if (o.min !== void 0 && i.length < o.min) return fail(p, `must have at least ${o.min} items`);
227
+ if (o.max !== void 0 && i.length > o.max) return fail(p, `must have at most ${o.max} items`);
228
+ const out = [];
229
+ const issues = [];
230
+ i.forEach((el, idx) => {
231
+ const r = item.parse(el, join(p, idx));
232
+ if (r.ok) out.push(r.value);
233
+ else issues.push(...r.issues);
234
+ });
235
+ return issues.length ? { ok: false, issues } : ok(out);
236
+ },
237
+ () => ({ type: "array", items: item.describe() })
238
+ );
239
+ },
240
+ record(value) {
241
+ return make(
242
+ "record",
243
+ (i, p) => {
244
+ if (typeof i !== "object" || i === null || Array.isArray(i)) return fail(p, "expected object");
245
+ const out = {};
246
+ const issues = [];
247
+ for (const [k, v] of Object.entries(i)) {
248
+ const r = value.parse(v, join(p, k));
249
+ if (r.ok) out[k] = r.value;
250
+ else issues.push(...r.issues);
251
+ }
252
+ return issues.length ? { ok: false, issues } : ok(out);
253
+ },
254
+ () => ({ type: "record", items: value.describe() })
255
+ );
256
+ },
257
+ object(shape, o = {}) {
258
+ return make(
259
+ "object",
260
+ (i, p) => {
261
+ if (typeof i !== "object" || i === null || Array.isArray(i)) return fail(p, "expected object");
262
+ const src = i;
263
+ const out = {};
264
+ const issues = [];
265
+ for (const [k, sch] of Object.entries(shape)) {
266
+ const r = sch.parse(src[k], join(p, k));
267
+ if (r.ok) {
268
+ if (r.value !== void 0) out[k] = r.value;
269
+ } else issues.push(...r.issues);
270
+ }
271
+ if (o.strict) {
272
+ for (const k of Object.keys(src)) if (!(k in shape)) issues.push({ path: join(p, k), message: "unknown property" });
273
+ }
274
+ return issues.length ? { ok: false, issues } : ok(out);
275
+ },
276
+ () => {
277
+ const properties = {};
278
+ const required = [];
279
+ for (const [k, sch] of Object.entries(shape)) {
280
+ const d = sch.describe();
281
+ properties[k] = d;
282
+ if (!d.optional) required.push(k);
283
+ }
284
+ return { type: "object", title: o.title, description: o.description, properties, required };
285
+ }
286
+ );
287
+ },
288
+ /** Accepts a value if any of the schemas matches. */
289
+ union(...options) {
290
+ return make(
291
+ "union",
292
+ (i, p) => {
293
+ for (const opt of options) {
294
+ const r = opt.parse(i, p);
295
+ if (r.ok) return r;
296
+ }
297
+ return fail(p, "does not match any allowed shape");
298
+ },
299
+ () => ({ type: "union" })
300
+ );
301
+ },
302
+ /** Escape hatch for values that cannot be validated structurally (functions, React components...). */
303
+ custom(check, message = "invalid value") {
304
+ return make(
305
+ "custom",
306
+ (i, p) => check(i) ? ok(i) : fail(p, message),
307
+ () => ({ type: "custom" })
308
+ );
309
+ },
310
+ fn() {
311
+ return s.custom((v) => typeof v === "function", "expected function");
312
+ }
313
+ };
314
+ function parseOrThrow(schema, input, what) {
315
+ const r = schema.parse(input);
316
+ if (!r.ok) throw new ValidationError(r.issues, what);
317
+ return r.value;
318
+ }
319
+
320
+ // src/core/semver.ts
321
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
322
+ function parseVersion(v) {
323
+ const m = SEMVER_RE.exec(v.trim());
324
+ if (!m) return null;
325
+ return {
326
+ major: Number(m[1]),
327
+ minor: Number(m[2]),
328
+ patch: Number(m[3]),
329
+ prerelease: m[4] ? m[4].split(".") : []
330
+ };
331
+ }
332
+ function isValidVersion(v) {
333
+ return parseVersion(v) !== null;
334
+ }
335
+ function cmpIdent(a, b) {
336
+ const an = /^\d+$/.test(a);
337
+ const bn = /^\d+$/.test(b);
338
+ if (an && bn) return Math.sign(Number(a) - Number(b));
339
+ if (an) return -1;
340
+ if (bn) return 1;
341
+ return a < b ? -1 : a > b ? 1 : 0;
342
+ }
343
+ function compareVersions(a, b) {
344
+ const x = parseVersion(a);
345
+ const y = parseVersion(b);
346
+ if (!x || !y) throw new Error(`Invalid version: ${!x ? a : b}`);
347
+ for (const k of ["major", "minor", "patch"]) {
348
+ if (x[k] !== y[k]) return x[k] < y[k] ? -1 : 1;
349
+ }
350
+ if (!x.prerelease.length && !y.prerelease.length) return 0;
351
+ if (!x.prerelease.length) return 1;
352
+ if (!y.prerelease.length) return -1;
353
+ const len = Math.max(x.prerelease.length, y.prerelease.length);
354
+ for (let i = 0; i < len; i++) {
355
+ const p = x.prerelease[i];
356
+ const q = y.prerelease[i];
357
+ if (p === void 0) return -1;
358
+ if (q === void 0) return 1;
359
+ const c = cmpIdent(p, q);
360
+ if (c !== 0) return c;
361
+ }
362
+ return 0;
363
+ }
364
+ function parsePartial(token) {
365
+ const m = /^v?(\d+|x|X|\*)(?:\.(\d+|x|X|\*))?(?:\.(\d+|x|X|\*))?(?:-([0-9A-Za-z.-]+))?$/.exec(token);
366
+ if (!m) return null;
367
+ const num = (x) => x === void 0 || /^[xX*]$/.test(x) ? void 0 : Number(x);
368
+ const major = num(m[1]);
369
+ if (major === void 0) return { major: -1 };
370
+ return { major, minor: num(m[2]), patch: num(m[3]), pre: m[4] };
371
+ }
372
+ var ver = (a, b, c, pre) => `${a}.${b}.${c}${pre ? `-${pre}` : ""}`;
373
+ function expand(token) {
374
+ token = token.trim();
375
+ if (token === "" || token === "*" || token === "x" || token === "X") return [{ op: ">=", version: "0.0.0" }];
376
+ const prefix = /^(\^|~|>=|<=|>|<|=)?\s*(.+)$/.exec(token);
377
+ if (!prefix) return null;
378
+ const sym = prefix[1] ?? "";
379
+ const p = parsePartial(prefix[2]);
380
+ if (!p) return null;
381
+ if (p.major === -1) return [{ op: ">=", version: "0.0.0" }];
382
+ const { major, minor, patch, pre } = p;
383
+ const lo = ver(major, minor ?? 0, patch ?? 0, pre);
384
+ switch (sym) {
385
+ case "^": {
386
+ let hi;
387
+ if (major > 0 || minor === void 0) hi = ver(major + 1, 0, 0);
388
+ else if (minor > 0 || patch === void 0) hi = ver(0, minor + 1, 0);
389
+ else hi = ver(0, 0, patch + 1);
390
+ return [
391
+ { op: ">=", version: lo },
392
+ { op: "<", version: hi }
393
+ ];
394
+ }
395
+ case "~": {
396
+ const hi = minor === void 0 ? ver(major + 1, 0, 0) : ver(major, minor + 1, 0);
397
+ return [
398
+ { op: ">=", version: lo },
399
+ { op: "<", version: hi }
400
+ ];
401
+ }
402
+ case ">=":
403
+ return [{ op: ">=", version: lo }];
404
+ case ">":
405
+ if (minor === void 0) return [{ op: ">=", version: ver(major + 1, 0, 0) }];
406
+ if (patch === void 0) return [{ op: ">=", version: ver(major, minor + 1, 0) }];
407
+ return [{ op: ">", version: lo }];
408
+ case "<=":
409
+ if (minor === void 0) return [{ op: "<", version: ver(major + 1, 0, 0) }];
410
+ if (patch === void 0) return [{ op: "<", version: ver(major, minor + 1, 0) }];
411
+ return [{ op: "<=", version: lo }];
412
+ case "<":
413
+ return [{ op: "<", version: lo }];
414
+ default: {
415
+ if (minor === void 0)
416
+ return [
417
+ { op: ">=", version: ver(major, 0, 0) },
418
+ { op: "<", version: ver(major + 1, 0, 0) }
419
+ ];
420
+ if (patch === void 0)
421
+ return [
422
+ { op: ">=", version: ver(major, minor, 0) },
423
+ { op: "<", version: ver(major, minor + 1, 0) }
424
+ ];
425
+ return [{ op: "=", version: lo }];
426
+ }
427
+ }
428
+ }
429
+ function tokenize(set) {
430
+ const raw = set.trim().split(/\s+/).filter(Boolean);
431
+ const out = [];
432
+ for (let i = 0; i < raw.length; i++) {
433
+ const t = raw[i];
434
+ if (/^(\^|~|>=|<=|>|<|=)$/.test(t) && raw[i + 1]) out.push(t + raw[++i]);
435
+ else out.push(t);
436
+ }
437
+ return out;
438
+ }
439
+ function isValidRange(range) {
440
+ try {
441
+ for (const set of range.split("||")) for (const tok of tokenize(set)) if (!expand(tok)) return false;
442
+ return true;
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+ function satisfies(version, range) {
448
+ if (!isValidVersion(version)) return false;
449
+ const v = parseVersion(version);
450
+ for (const set of range.split("||")) {
451
+ const tokens = tokenize(set);
452
+ const comparators = [];
453
+ let valid = true;
454
+ for (const t of tokens.length ? tokens : ["*"]) {
455
+ const e = expand(t);
456
+ if (!e) {
457
+ valid = false;
458
+ break;
459
+ }
460
+ comparators.push(...e);
461
+ }
462
+ if (!valid) continue;
463
+ const allOk = comparators.every((c) => {
464
+ const r = compareVersions(version, c.version);
465
+ switch (c.op) {
466
+ case ">=":
467
+ return r >= 0;
468
+ case ">":
469
+ return r > 0;
470
+ case "<=":
471
+ return r <= 0;
472
+ case "<":
473
+ return r < 0;
474
+ case "=":
475
+ return r === 0;
476
+ }
477
+ });
478
+ const preOk = v.prerelease.length === 0 || comparators.some((c) => {
479
+ const cv = parseVersion(c.version);
480
+ return cv.prerelease.length > 0 && cv.major === v.major && cv.minor === v.minor && cv.patch === v.patch;
481
+ });
482
+ if (allOk && preOk) return true;
483
+ }
484
+ return false;
485
+ }
486
+
487
+ // src/core/manifest.ts
488
+ var FRAMEWORK_VERSION = "1.0.0";
489
+ var FRAMEWORK_ENGINE_KEY = "fluxplugin";
490
+ var BUILTIN_PERMISSIONS = [
491
+ "filesystem.read",
492
+ "filesystem.write",
493
+ "network",
494
+ "database",
495
+ "ipc",
496
+ "window",
497
+ "notifications",
498
+ "storage",
499
+ "services",
500
+ "menus",
501
+ "commands",
502
+ "routes",
503
+ "ui",
504
+ "events",
505
+ "hooks",
506
+ "styles",
507
+ "clipboard",
508
+ "shell"
509
+ ];
510
+ var ID_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
511
+ var ACTIVATION_RE = /^(\*|onStartup|onCommand:.+|onRoute:.+|onEvent:.+|onService:.+|onSlot:.+|onIPC:.+)$/;
512
+ var permissionSchema = s.custom(
513
+ (v) => typeof v === "string" && (BUILTIN_PERMISSIONS.includes(v) || /^x\.[a-z0-9.-]+$/.test(v)),
514
+ `unknown permission (built-ins: ${BUILTIN_PERMISSIONS.join(", ")}; custom ones must start with "x.")`
515
+ );
516
+ var versionRange = s.custom((v) => typeof v === "string" && isValidRange(v), "expected a valid semver range");
517
+ var menuLocations = ["application", "context", "sidebar", "toolbar", "commandPalette", "tray", "settings"];
518
+ var manifestSchema = s.object({
519
+ id: s.string({ pattern: ID_RE, min: 3, max: 128, description: "Unique reverse-DNS style identifier, lowercase" }),
520
+ name: s.string({ min: 1, max: 128 }),
521
+ version: s.custom((v) => typeof v === "string" && isValidVersion(v), "expected a valid semver version"),
522
+ description: s.string().optional(),
523
+ author: s.union(s.string(), s.object({ name: s.string(), email: s.string().optional(), url: s.string().optional() })).optional(),
524
+ homepage: s.string().optional(),
525
+ repository: s.union(s.string(), s.object({ type: s.string().optional(), url: s.string() })).optional(),
526
+ license: s.string().optional(),
527
+ icon: s.string().optional(),
528
+ /** Entry executed in the Electron main process. */
529
+ main: s.string().optional(),
530
+ /** Entry executed in the preload context (sandbox-compatible, no Node). */
531
+ preload: s.string().optional(),
532
+ /** ESM entry loaded by the renderer. */
533
+ renderer: s.string().optional(),
534
+ styles: s.array(s.string()).optional(),
535
+ tailwind: s.object({
536
+ /** Globs (relative to the plugin root) scanned by Tailwind so utility classes are never purged. */
537
+ content: s.array(s.string()).optional(),
538
+ safelist: s.array(s.string()).optional(),
539
+ /** Path of a pre-compiled CSS file produced by the plugin's own Tailwind build. */
540
+ precompiled: s.string().optional()
541
+ }).optional(),
542
+ activationEvents: s.array(s.string({ pattern: ACTIVATION_RE })).default([]),
543
+ deactivationEvents: s.array(s.string()).default([]),
544
+ permissions: s.array(permissionSchema).default([]),
545
+ dependencies: s.record(versionRange).default({}),
546
+ optionalDependencies: s.record(versionRange).default({}),
547
+ peerDependencies: s.record(versionRange).default({}),
548
+ engines: s.record(versionRange).default({}),
549
+ /** Static, declarative metadata mirrored by the code-level registrations (lets the host display plugins without activating them). */
550
+ contributes: s.object({
551
+ routes: s.array(s.object({ path: s.string(), title: s.string().optional(), icon: s.string().optional(), permission: permissionSchema.optional() })).optional(),
552
+ commands: s.array(s.object({ id: s.string(), title: s.string(), category: s.string().optional() })).optional(),
553
+ menus: s.array(s.object({ location: s.enum(menuLocations), id: s.string(), label: s.string(), command: s.string().optional() })).optional(),
554
+ services: s.array(s.string()).optional(),
555
+ ipc: s.array(s.string()).optional(),
556
+ hooks: s.array(s.string()).optional(),
557
+ slots: s.array(s.string()).optional(),
558
+ configuration: s.unknown().optional()
559
+ }).default({}),
560
+ /** Optional integrity hash (sha256-<hex>) of the main bundle. Verified before load when present. */
561
+ integrity: s.record(s.string({ pattern: /^sha256-[0-9a-f]{64}$/ })).optional(),
562
+ signature: s.object({ algorithm: s.literal("ed25519"), publicKey: s.string(), value: s.string() }).optional()
563
+ });
564
+ function validateManifest(raw, opts = {}) {
565
+ const r = manifestSchema.parse(raw);
566
+ if (!r.ok) return { ok: false, issues: r.issues, warnings: [] };
567
+ const manifest = r.value;
568
+ const issues = [];
569
+ const warnings = [];
570
+ const fw = opts.frameworkVersion ?? FRAMEWORK_VERSION;
571
+ const required = manifest.engines?.[FRAMEWORK_ENGINE_KEY];
572
+ if (required && !satisfies(fw, required)) {
573
+ issues.push({ path: `engines.${FRAMEWORK_ENGINE_KEY}`, message: `requires framework ${required}, running ${fw}` });
574
+ }
575
+ if (!manifest.main && !manifest.renderer && !manifest.preload) {
576
+ issues.push({ path: "", message: "at least one of main, preload or renderer entry is required" });
577
+ }
578
+ if (!required) warnings.push(`engines.${FRAMEWORK_ENGINE_KEY} is not declared; compatibility cannot be verified`);
579
+ for (const dep of Object.keys(manifest.dependencies ?? {})) {
580
+ if (dep === manifest.id) issues.push({ path: `dependencies.${dep}`, message: "a plugin cannot depend on itself" });
581
+ }
582
+ return { ok: issues.length === 0, manifest: issues.length ? void 0 : manifest, issues, warnings };
583
+ }
584
+
585
+ // src/core/events.ts
586
+ function matchesPattern(pattern, name) {
587
+ if (pattern === "*" || pattern === "**") return true;
588
+ if (pattern === name) return true;
589
+ if (!pattern.includes("*")) return false;
590
+ const p = pattern.split(".");
591
+ const n = name.split(".");
592
+ for (let i = 0; i < p.length; i++) {
593
+ const seg = p[i];
594
+ if (seg === "*") {
595
+ if (i === p.length - 1) return n.length > i;
596
+ if (n[i] === void 0) return false;
597
+ continue;
598
+ }
599
+ if (n[i] !== seg) return false;
600
+ }
601
+ return p.length === n.length;
602
+ }
603
+ var EventBus = class {
604
+ constructor(onError = () => {
605
+ }) {
606
+ this.onError = onError;
607
+ }
608
+ onError;
609
+ entries = [];
610
+ seq = 0;
611
+ on(pattern, listener, opts = {}) {
612
+ const entry = {
613
+ pattern,
614
+ segments: pattern.split("."),
615
+ listener,
616
+ priority: opts.priority ?? 0,
617
+ once: opts.once ?? false,
618
+ seq: this.seq++
619
+ };
620
+ this.entries.push(entry);
621
+ return toDisposable(() => {
622
+ const i = this.entries.indexOf(entry);
623
+ if (i >= 0) this.entries.splice(i, 1);
624
+ });
625
+ }
626
+ once(pattern, listener, opts = {}) {
627
+ return this.on(pattern, listener, { ...opts, once: true });
628
+ }
629
+ off(pattern, listener) {
630
+ this.entries = this.entries.filter((e) => !(e.pattern === pattern && e.listener === listener));
631
+ }
632
+ listenerCount(pattern) {
633
+ return pattern ? this.entries.filter((e) => e.pattern === pattern).length : this.entries.length;
634
+ }
635
+ removeAll() {
636
+ this.entries = [];
637
+ }
638
+ matching(name) {
639
+ return this.entries.filter((e) => matchesPattern(e.pattern, name)).sort((a, b) => b.priority - a.priority || a.seq - b.seq);
640
+ }
641
+ emit(name, payload, source) {
642
+ const ctx = this.makeCtx(name, payload, source);
643
+ const result = { cancelled: false, errors: [], delivered: 0 };
644
+ for (const entry of this.matching(name)) {
645
+ if (ctx.cancelled) break;
646
+ if (entry.once) this.removeEntry(entry);
647
+ try {
648
+ const r = entry.listener(payload, ctx);
649
+ result.delivered++;
650
+ if (r && typeof r.then === "function") {
651
+ r.catch((e) => this.fail(e, name, entry.pattern, result));
652
+ }
653
+ } catch (e) {
654
+ this.fail(e, name, entry.pattern, result);
655
+ }
656
+ }
657
+ result.cancelled = ctx.cancelled;
658
+ return result;
659
+ }
660
+ async emitAsync(name, payload, source) {
661
+ const ctx = this.makeCtx(name, payload, source);
662
+ const result = { cancelled: false, errors: [], delivered: 0 };
663
+ for (const entry of this.matching(name)) {
664
+ if (ctx.cancelled) break;
665
+ if (entry.once) this.removeEntry(entry);
666
+ try {
667
+ await entry.listener(payload, ctx);
668
+ result.delivered++;
669
+ } catch (e) {
670
+ this.fail(e, name, entry.pattern, result);
671
+ }
672
+ }
673
+ result.cancelled = ctx.cancelled;
674
+ return result;
675
+ }
676
+ waitFor(pattern, timeoutMs = 0) {
677
+ return new Promise((resolve, reject) => {
678
+ const sub = this.once(pattern, (p) => {
679
+ if (t) clearTimeout(t);
680
+ resolve(p);
681
+ });
682
+ const t = timeoutMs > 0 ? setTimeout(() => (sub.dispose(), reject(new Error(`waitFor("${pattern}") timed out`))), timeoutMs) : void 0;
683
+ });
684
+ }
685
+ removeEntry(e) {
686
+ const i = this.entries.indexOf(e);
687
+ if (i >= 0) this.entries.splice(i, 1);
688
+ }
689
+ fail(e, name, pattern, result) {
690
+ result.errors.push(e);
691
+ try {
692
+ this.onError(e, name, pattern);
693
+ } catch {
694
+ }
695
+ }
696
+ makeCtx(name, payload, source) {
697
+ const ctx = {
698
+ name,
699
+ payload,
700
+ source,
701
+ cancelled: false,
702
+ cancel() {
703
+ ctx.cancelled = true;
704
+ }
705
+ };
706
+ return ctx;
707
+ }
708
+ };
709
+
710
+ // src/core/hooks.ts
711
+ var KINDS = ["before", "after", "around", "transform", "validate", "filter"];
712
+ function split(name) {
713
+ const i = name.indexOf(".");
714
+ const kind = i > 0 ? name.slice(0, i) : "";
715
+ if (!KINDS.includes(kind) || i === name.length - 1) {
716
+ throw new Error(`Invalid hook name "${name}": expected "<${KINDS.join("|")}>.<operation>"`);
717
+ }
718
+ return { kind, operation: name.slice(i + 1) };
719
+ }
720
+ var HookSystem = class {
721
+ constructor(onError = () => {
722
+ }) {
723
+ this.onError = onError;
724
+ }
725
+ onError;
726
+ entries = [];
727
+ seq = 0;
728
+ use(name, handler, opts = {}) {
729
+ const { kind, operation } = split(name);
730
+ const entry = { kind, operation, handler, priority: opts.priority ?? 0, seq: this.seq++, owner: opts.owner };
731
+ this.entries.push(entry);
732
+ return toDisposable(() => {
733
+ const i = this.entries.indexOf(entry);
734
+ if (i >= 0) this.entries.splice(i, 1);
735
+ });
736
+ }
737
+ count(kind, operation) {
738
+ return this.entries.filter((e) => (!kind || e.kind === kind) && (!operation || e.operation === operation)).length;
739
+ }
740
+ list() {
741
+ return this.entries.map(({ kind, operation, priority, owner }) => ({ kind, operation, priority, owner }));
742
+ }
743
+ get(kind, operation) {
744
+ return this.entries.filter((e) => e.kind === kind && e.operation === operation).sort((a, b) => b.priority - a.priority || a.seq - b.seq);
745
+ }
746
+ async run(operation, input, exec) {
747
+ const ctx = {
748
+ operation,
749
+ input,
750
+ result: void 0,
751
+ cancelled: false,
752
+ cancelReason: void 0,
753
+ data: /* @__PURE__ */ new Map(),
754
+ cancel(reason) {
755
+ ctx.cancelled = true;
756
+ ctx.cancelReason = reason;
757
+ }
758
+ };
759
+ const guard = () => {
760
+ if (ctx.cancelled) throw new HookCancelledError(operation, ctx.cancelReason);
761
+ };
762
+ for (const kind of ["validate", "before"]) {
763
+ for (const h of this.get(kind, operation)) {
764
+ await h.handler(ctx);
765
+ guard();
766
+ }
767
+ }
768
+ const arounds = this.get("around", operation);
769
+ const core = async () => ctx.result = await exec(ctx.input, ctx);
770
+ const composed = arounds.reduceRight(
771
+ (next, h) => async () => {
772
+ guard();
773
+ const r = await h.handler(ctx, next);
774
+ if (r !== void 0) ctx.result = r;
775
+ return ctx.result;
776
+ },
777
+ core
778
+ );
779
+ await composed();
780
+ guard();
781
+ for (const h of this.get("transform", operation)) {
782
+ const r = await h.handler(ctx);
783
+ if (r !== void 0) ctx.result = r;
784
+ }
785
+ for (const h of this.get("after", operation)) {
786
+ try {
787
+ await h.handler(ctx);
788
+ } catch (e) {
789
+ this.onError(e, `after.${operation}`, h.owner);
790
+ }
791
+ }
792
+ return ctx.result;
793
+ }
794
+ /** Runs `filter.<name>` handlers as a value pipeline. A failing filter is skipped and reported. */
795
+ async filter(name, value, extra) {
796
+ let current = value;
797
+ for (const h of this.get("filter", name)) {
798
+ try {
799
+ const r = await h.handler(current, extra);
800
+ if (r !== void 0) current = r;
801
+ } catch (e) {
802
+ this.onError(e, `filter.${name}`, h.owner);
803
+ }
804
+ }
805
+ return current;
806
+ }
807
+ /** Synchronous variant for render-time filters (async filters are ignored with a reported error). */
808
+ filterSync(name, value, extra) {
809
+ let current = value;
810
+ for (const h of this.get("filter", name)) {
811
+ try {
812
+ const r = h.handler(current, extra);
813
+ if (r && typeof r.then === "function") throw new Error("async filter used in filterSync()");
814
+ if (r !== void 0) current = r;
815
+ } catch (e) {
816
+ this.onError(e, `filter.${name}`, h.owner);
817
+ }
818
+ }
819
+ return current;
820
+ }
821
+ removeByOwner(owner) {
822
+ this.entries = this.entries.filter((e) => e.owner !== owner);
823
+ }
824
+ };
825
+
826
+ // src/core/permissions.ts
827
+ var PermissionManager = class {
828
+ constructor(opts = {}) {
829
+ this.opts = opts;
830
+ this.autoGrant = opts.autoGrant ?? true;
831
+ for (const [id, perms] of Object.entries(opts.approved ?? {})) this.granted.set(id, new Set(perms));
832
+ }
833
+ opts;
834
+ declared = /* @__PURE__ */ new Map();
835
+ granted = /* @__PURE__ */ new Map();
836
+ listeners = /* @__PURE__ */ new Set();
837
+ autoGrant;
838
+ /** Restores grants persisted from a previous session (call before `declare`). */
839
+ restore(approved) {
840
+ for (const [id, perms] of Object.entries(approved)) this.granted.set(id, new Set(perms));
841
+ }
842
+ /** Registers a plugin's requested permissions (called when its manifest is loaded). */
843
+ declare(pluginId, permissions) {
844
+ const set = new Set(permissions);
845
+ this.declared.set(pluginId, set);
846
+ const granted = this.granted.get(pluginId) ?? /* @__PURE__ */ new Set();
847
+ if (this.autoGrant) for (const p of set) granted.add(p);
848
+ for (const p of [...granted]) if (!set.has(p)) granted.delete(p);
849
+ this.granted.set(pluginId, granted);
850
+ this.notify(pluginId);
851
+ }
852
+ forget(pluginId) {
853
+ this.declared.delete(pluginId);
854
+ this.granted.delete(pluginId);
855
+ }
856
+ has(pluginId, permission) {
857
+ return this.granted.get(pluginId)?.has(permission) ?? false;
858
+ }
859
+ hasAll(pluginId, permissions) {
860
+ return permissions.every((p) => this.has(pluginId, p));
861
+ }
862
+ assert(pluginId, permission, api = permission) {
863
+ if (!this.has(pluginId, permission)) throw new PermissionDeniedError(pluginId, permission, api);
864
+ }
865
+ grant(pluginId, permission) {
866
+ if (!this.declared.get(pluginId)?.has(permission)) {
867
+ throw new Error(`Plugin "${pluginId}" did not declare permission "${permission}"`);
868
+ }
869
+ const set = this.granted.get(pluginId) ?? /* @__PURE__ */ new Set();
870
+ set.add(permission);
871
+ this.granted.set(pluginId, set);
872
+ this.notify(pluginId);
873
+ }
874
+ grantAll(pluginId) {
875
+ for (const p of this.declared.get(pluginId) ?? []) this.grant(pluginId, p);
876
+ }
877
+ revoke(pluginId, permission) {
878
+ this.granted.get(pluginId)?.delete(permission);
879
+ this.notify(pluginId);
880
+ }
881
+ snapshot(pluginId) {
882
+ const declared = [...this.declared.get(pluginId) ?? []];
883
+ const granted = [...this.granted.get(pluginId) ?? []];
884
+ return { declared, granted, denied: declared.filter((p) => !granted.includes(p)) };
885
+ }
886
+ onChange(l) {
887
+ this.listeners.add(l);
888
+ return toDisposable(() => this.listeners.delete(l));
889
+ }
890
+ notify(pluginId) {
891
+ this.opts.onChange?.(pluginId, this.snapshot(pluginId));
892
+ for (const l of this.listeners) l(pluginId);
893
+ }
894
+ };
895
+
896
+ // src/core/registry.ts
897
+ var Registry = class {
898
+ constructor(kind) {
899
+ this.kind = kind;
900
+ }
901
+ kind;
902
+ table = /* @__PURE__ */ new Map();
903
+ subscribers = /* @__PURE__ */ new Set();
904
+ _version = 0;
905
+ get version() {
906
+ return this._version;
907
+ }
908
+ add(key, item, owner) {
909
+ if (this.table.has(key)) {
910
+ const existing = this.table.get(key);
911
+ throw new FrameworkError(
912
+ `${this.kind} "${key}" is already registered${existing.owner ? ` by "${existing.owner}"` : ""}`,
913
+ "FLUX_DUPLICATE",
914
+ owner
915
+ );
916
+ }
917
+ const entry = { item, owner };
918
+ this.table.set(key, entry);
919
+ this.changed();
920
+ return toDisposable(() => {
921
+ if (this.table.get(key) === entry) {
922
+ this.table.delete(key);
923
+ this.changed();
924
+ }
925
+ });
926
+ }
927
+ has(key) {
928
+ return this.table.has(key);
929
+ }
930
+ get(key) {
931
+ return this.table.get(key)?.item;
932
+ }
933
+ ownerOf(key) {
934
+ return this.table.get(key)?.owner;
935
+ }
936
+ list() {
937
+ return [...this.table.values()].map((e) => e.item);
938
+ }
939
+ listByOwner(owner) {
940
+ return [...this.table.values()].filter((e) => e.owner === owner).map((e) => e.item);
941
+ }
942
+ unregisterByOwner(owner) {
943
+ let n = 0;
944
+ for (const [k, e] of this.table) {
945
+ if (e.owner === owner) {
946
+ this.table.delete(k);
947
+ n++;
948
+ }
949
+ }
950
+ if (n) this.changed();
951
+ return n;
952
+ }
953
+ clear() {
954
+ this.table.clear();
955
+ this.changed();
956
+ }
957
+ /** Subscribes to any change. Returns an unsubscribe function compatible with `useSyncExternalStore`. */
958
+ subscribe = (listener) => {
959
+ this.subscribers.add(listener);
960
+ return () => void this.subscribers.delete(listener);
961
+ };
962
+ changed() {
963
+ this._version++;
964
+ for (const l of [...this.subscribers]) {
965
+ try {
966
+ l();
967
+ } catch {
968
+ }
969
+ }
970
+ }
971
+ };
972
+ var CommandRegistry = class extends Registry {
973
+ /** Set by the plugin manager: activates plugins that declared `onCommand:<id>` before execution. */
974
+ beforeExecute;
975
+ /** Observer used for diagnostics / hooks. */
976
+ onExecuted;
977
+ constructor() {
978
+ super("Command");
979
+ }
980
+ register(command, owner) {
981
+ return this.add(command.id, command, owner);
982
+ }
983
+ async execute(id, ...args) {
984
+ if (!this.has(id)) await this.beforeExecute?.(id);
985
+ const cmd = this.get(id);
986
+ if (!cmd) throw new FrameworkError(`Unknown command "${id}"`, "FLUX_UNKNOWN_COMMAND");
987
+ if (cmd.when && !cmd.when()) throw new FrameworkError(`Command "${id}" is not available right now`, "FLUX_COMMAND_DISABLED");
988
+ try {
989
+ const r = await cmd.execute(...args);
990
+ this.onExecuted?.(id);
991
+ return r;
992
+ } catch (e) {
993
+ this.onExecuted?.(id, e);
994
+ throw e;
995
+ }
996
+ }
997
+ /** Commands sorted for display in a palette. */
998
+ palette(query = "") {
999
+ const q = query.trim().toLowerCase();
1000
+ return this.list().filter((c) => (!c.when || c.when()) && (!q || `${c.category ?? ""} ${c.title} ${c.id}`.toLowerCase().includes(q))).sort((a, b) => (a.category ?? "").localeCompare(b.category ?? "") || a.title.localeCompare(b.title));
1001
+ }
1002
+ /** Finds the command bound to a keyboard shortcut. */
1003
+ byKeybinding(binding) {
1004
+ const norm = (b) => b.toLowerCase().replace(/\s+/g, "").split("+").sort().join("+");
1005
+ return this.list().find((c) => c.keybinding && norm(c.keybinding) === norm(binding));
1006
+ }
1007
+ };
1008
+ var MenuRegistry = class extends Registry {
1009
+ constructor() {
1010
+ super("Menu item");
1011
+ }
1012
+ register(item, owner) {
1013
+ return this.add(`${item.location}:${item.id}`, item, owner);
1014
+ }
1015
+ items(location) {
1016
+ return this.list().filter((m) => m.location === location && (!m.when || m.when())).sort((a, b) => (a.group ?? "").localeCompare(b.group ?? "") || (a.order ?? 100) - (b.order ?? 100) || a.label.localeCompare(b.label));
1017
+ }
1018
+ };
1019
+ var ServiceRegistry = class extends Registry {
1020
+ /** Set by the plugin manager: activates plugins declaring `onService:<name>` on first lookup. */
1021
+ beforeResolve;
1022
+ constructor() {
1023
+ super("Service");
1024
+ }
1025
+ register(name, implementation, owner) {
1026
+ return this.add(name, { id: name, implementation }, owner);
1027
+ }
1028
+ /** Synchronous lookup — returns undefined when the providing plugin is not active. */
1029
+ resolve(name) {
1030
+ return this.get(name)?.implementation;
1031
+ }
1032
+ /** Looks a service up, activating its provider lazily if necessary. */
1033
+ async resolveAsync(name) {
1034
+ if (!this.has(name)) await this.beforeResolve?.(name);
1035
+ const s2 = this.get(name);
1036
+ if (!s2) throw new FrameworkError(`Service "${name}" is not available`, "FLUX_UNKNOWN_SERVICE");
1037
+ return s2.implementation;
1038
+ }
1039
+ };
1040
+
1041
+ // src/core/routes.ts
1042
+ function normalizePath(path) {
1043
+ const clean = "/" + path.split("?")[0].split("#")[0].split("/").filter(Boolean).join("/");
1044
+ return clean;
1045
+ }
1046
+ function toSegments(fullPath) {
1047
+ return fullPath.split("/").filter(Boolean).map((s2) => s2 === "*" ? { type: "splat" } : s2.startsWith(":") ? { type: "param", name: s2.slice(1) } : { type: "static", value: s2 });
1048
+ }
1049
+ function scoreOf(segs) {
1050
+ return segs.reduce((n, s2) => n + (s2.type === "static" ? 100 : s2.type === "param" ? 10 : 1), segs.length);
1051
+ }
1052
+ function joinPaths(base, child) {
1053
+ if (child.startsWith("/")) return normalizePath(child);
1054
+ return normalizePath(`${base}/${child}`);
1055
+ }
1056
+ var RouteRegistry = class extends Registry {
1057
+ /** Host-provided values handed to guards (e.g. `{ isAuthenticated: true }`). */
1058
+ guardContext = {};
1059
+ /** Set by the plugin manager to lazily activate plugins declaring `onRoute:<path>`. */
1060
+ beforeResolve;
1061
+ /** Called for every route with `meta.menu`: lets the manager create sidebar entries automatically. */
1062
+ onRouteRegistered;
1063
+ layouts = /* @__PURE__ */ new Map();
1064
+ constructor() {
1065
+ super("Route");
1066
+ }
1067
+ registerLayout(id, component) {
1068
+ if (this.layouts.has(id)) throw new FrameworkError(`Layout "${id}" is already registered`, "FLUX_DUPLICATE");
1069
+ this.layouts.set(id, component);
1070
+ this.bump();
1071
+ return toDisposable(() => {
1072
+ this.layouts.delete(id);
1073
+ this.bump();
1074
+ });
1075
+ }
1076
+ getLayout(id) {
1077
+ return this.layouts.get(id);
1078
+ }
1079
+ bump() {
1080
+ this.changed();
1081
+ }
1082
+ /** Registers a route (and its children). Returns one disposable removing all of them. */
1083
+ register(def, owner) {
1084
+ const disposables = [];
1085
+ const walk = (d, parent) => {
1086
+ if (!d.component && !d.lazy && !d.children?.length) {
1087
+ throw new FrameworkError(`Route "${d.path}" needs a component, a lazy loader or children`, "FLUX_INVALID_ROUTE", owner);
1088
+ }
1089
+ const fullPath = parent ? joinPaths(parent.fullPath, d.path) : normalizePath(d.path);
1090
+ const segments = toSegments(fullPath);
1091
+ const resolved = {
1092
+ fullPath,
1093
+ definition: d,
1094
+ owner,
1095
+ parents: parent ? [...parent.parents, parent] : [],
1096
+ segments,
1097
+ score: scoreOf(segments)
1098
+ };
1099
+ disposables.push(this.add(`${owner ?? ""}|${fullPath}`, resolved, owner));
1100
+ const extra = this.onRouteRegistered?.(resolved, owner);
1101
+ if (extra) disposables.push(extra);
1102
+ for (const child of d.children ?? []) walk(child, resolved);
1103
+ };
1104
+ try {
1105
+ walk(def, void 0);
1106
+ } catch (e) {
1107
+ for (const d of disposables.reverse()) d.dispose();
1108
+ throw e;
1109
+ }
1110
+ return toDisposable(() => {
1111
+ for (const d of disposables.reverse()) d.dispose();
1112
+ });
1113
+ }
1114
+ /** Finds the best route for a concrete pathname. */
1115
+ match(pathname) {
1116
+ const target = normalizePath(pathname).split("/").filter(Boolean).map((s2) => {
1117
+ try {
1118
+ return decodeURIComponent(s2);
1119
+ } catch {
1120
+ return s2;
1121
+ }
1122
+ });
1123
+ let best;
1124
+ for (const route of this.list()) {
1125
+ const def = route.definition;
1126
+ if (!def.component && !def.lazy) continue;
1127
+ const params = {};
1128
+ let ok2 = true;
1129
+ const segs = route.segments;
1130
+ for (let i = 0; i < segs.length; i++) {
1131
+ const seg = segs[i];
1132
+ if (seg.type === "splat") {
1133
+ params["*"] = target.slice(i).join("/");
1134
+ if (target.length < i) ok2 = false;
1135
+ break;
1136
+ }
1137
+ const t = target[i];
1138
+ if (t === void 0) {
1139
+ ok2 = false;
1140
+ break;
1141
+ }
1142
+ if (seg.type === "static" && seg.value !== t) {
1143
+ ok2 = false;
1144
+ break;
1145
+ }
1146
+ if (seg.type === "param") params[seg.name] = t;
1147
+ if (i === segs.length - 1 && target.length > segs.length) ok2 = false;
1148
+ }
1149
+ if (ok2 && segs.length === 0 && target.length > 0) ok2 = false;
1150
+ if (!ok2) continue;
1151
+ if (!best || route.score > best.route.score) best = { route, params, chain: [...route.parents, route] };
1152
+ }
1153
+ return best;
1154
+ }
1155
+ /** Same as `match` but first lets lazily-activated plugins register their routes. */
1156
+ async matchAsync(pathname) {
1157
+ const first = this.match(pathname);
1158
+ if (first) return first;
1159
+ await this.beforeResolve?.(normalizePath(pathname));
1160
+ return this.match(pathname);
1161
+ }
1162
+ /** Evaluates `access`, then every guard from the root layout down to the route itself. */
1163
+ async checkGuards(m, pathname) {
1164
+ for (const r of m.chain) {
1165
+ const base = { path: normalizePath(pathname), params: m.params, route: m.route, host: this.guardContext };
1166
+ if (r.definition.access === "protected" && !this.guardContext["isAuthenticated"]) {
1167
+ return { allow: false, redirect: this.guardContext["loginPath"] ?? "/login", reason: "authentication required" };
1168
+ }
1169
+ for (const g of r.definition.guards ?? []) {
1170
+ let res;
1171
+ try {
1172
+ res = await g(base);
1173
+ } catch (e) {
1174
+ return { allow: false, reason: `guard threw: ${e instanceof Error ? e.message : String(e)}` };
1175
+ }
1176
+ if (typeof res === "string") return { allow: false, redirect: res, reason: "redirected by guard" };
1177
+ if (res === false) return { allow: false, reason: "blocked by guard" };
1178
+ }
1179
+ }
1180
+ return { allow: true };
1181
+ }
1182
+ breadcrumbs(m) {
1183
+ const out = [];
1184
+ for (const r of m.chain) {
1185
+ const b = r.definition.meta?.breadcrumb ?? r.definition.meta?.title;
1186
+ if (!b) continue;
1187
+ const label = typeof b === "function" ? b(m.params) : b;
1188
+ const path = r.fullPath.replace(/:([A-Za-z0-9_]+)/g, (_, k) => encodeURIComponent(m.params[k] ?? `:${k}`));
1189
+ out.push({ label, path });
1190
+ }
1191
+ return out;
1192
+ }
1193
+ };
1194
+
1195
+ // src/core/slots.ts
1196
+ var SlotRegistry = class extends Registry {
1197
+ counter = 0;
1198
+ /** Set by the plugin manager to activate plugins declaring `onSlot:<name>`. */
1199
+ onSlotRendered;
1200
+ declared = /* @__PURE__ */ new Set();
1201
+ constructor() {
1202
+ super("Slot contribution");
1203
+ }
1204
+ /** Hosts may declare their slots so typos in plugins are caught early. Declaring is optional. */
1205
+ declareSlot(name) {
1206
+ this.declared.add(name);
1207
+ }
1208
+ declaredSlots() {
1209
+ return [...this.declared];
1210
+ }
1211
+ register(c, owner, opts = {}) {
1212
+ if (opts.strict && this.declared.size > 0 && !this.declared.has(c.slot)) {
1213
+ throw new FrameworkError(`Unknown UI slot "${c.slot}"`, "FLUX_UNKNOWN_SLOT", owner);
1214
+ }
1215
+ const id = c.id ?? `${owner ?? "anon"}#${c.slot}#${this.counter++}`;
1216
+ const entry = { id, slot: c.slot, component: c.component, mode: c.mode ?? "append", priority: c.priority ?? 0, props: c.props, owner, when: c.when };
1217
+ return this.add(`${c.slot}::${id}`, entry, owner);
1218
+ }
1219
+ resolve(slot, props = {}) {
1220
+ this.onSlotRendered?.(slot);
1221
+ const all = this.list().filter((c) => c.slot === slot && (!c.when || safe(() => c.when(props)))).sort((a, b) => b.priority - a.priority);
1222
+ const pick = (mode) => all.filter((c) => c.mode === mode);
1223
+ return {
1224
+ before: [...pick("prepend"), ...pick("before")],
1225
+ replacement: pick("replace")[0],
1226
+ after: [...pick("after"), ...pick("append")]
1227
+ };
1228
+ }
1229
+ /** Slots that currently have at least one contribution. */
1230
+ activeSlots() {
1231
+ return [...new Set(this.list().map((c) => c.slot))];
1232
+ }
1233
+ };
1234
+ function safe(fn) {
1235
+ try {
1236
+ return fn();
1237
+ } catch {
1238
+ return false;
1239
+ }
1240
+ }
1241
+ var ExtensionPointRegistry = class extends Registry {
1242
+ points = /* @__PURE__ */ new Map();
1243
+ counter = 0;
1244
+ constructor() {
1245
+ super("Extension point contribution");
1246
+ }
1247
+ declare(def) {
1248
+ if (this.points.has(def.name)) throw new FrameworkError(`Extension point "${def.name}" is already declared`, "FLUX_DUPLICATE");
1249
+ this.points.set(def.name, def);
1250
+ return {
1251
+ dispose: () => {
1252
+ this.points.delete(def.name);
1253
+ }
1254
+ };
1255
+ }
1256
+ definition(name) {
1257
+ return this.points.get(name);
1258
+ }
1259
+ declaredPoints() {
1260
+ return [...this.points.values()];
1261
+ }
1262
+ contribute(point, value, owner, priority = 0) {
1263
+ const def = this.points.get(point);
1264
+ if (!def) throw new FrameworkError(`Unknown extension point "${point}"`, "FLUX_UNKNOWN_EXTENSION_POINT", owner);
1265
+ for (const [key, v] of Object.entries(value)) {
1266
+ if (!(key in def.contributions)) {
1267
+ throw new FrameworkError(`Extension point "${point}" does not accept "${key}" (allowed: ${Object.keys(def.contributions).join(", ")})`, "FLUX_INVALID_CONTRIBUTION", owner);
1268
+ }
1269
+ const schema = def.contributions[key];
1270
+ if (schema) {
1271
+ for (const item of Array.isArray(v) ? v : [v]) {
1272
+ const r = schema.parse(item, `${point}.${key}`);
1273
+ if (!r.ok) throw new FrameworkError(`Invalid contribution to "${point}.${key}": ${r.issues.map((i) => i.message).join("; ")}`, "FLUX_INVALID_CONTRIBUTION", owner);
1274
+ }
1275
+ }
1276
+ }
1277
+ return this.add(`${point}::${owner ?? "anon"}::${this.counter++}`, { point, priority, value }, owner);
1278
+ }
1279
+ /** Merged, priority-ordered items contributed under `key` of `point`. */
1280
+ items(point, key) {
1281
+ return this.list().filter((c) => c.point === point && key in c.value).sort((a, b) => b.priority - a.priority).flatMap((c) => {
1282
+ const v = c.value[key];
1283
+ return Array.isArray(v) ? v : [v];
1284
+ });
1285
+ }
1286
+ };
1287
+
1288
+ // src/core/styles.ts
1289
+ var StyleRegistry = class extends Registry {
1290
+ n = 0;
1291
+ constructor() {
1292
+ super("Style");
1293
+ }
1294
+ register(input, owner) {
1295
+ if (!input.css && !input.href) throw new Error("style needs `css` or `href`");
1296
+ const id = input.id ?? `style-${this.n++}`;
1297
+ const entry = { id, css: input.css, href: input.href, priority: input.priority ?? 100, scoped: input.scoped ?? false, owner };
1298
+ return this.add(`${owner}::${id}`, entry, owner);
1299
+ }
1300
+ /** Entries in cascade order. */
1301
+ ordered() {
1302
+ return this.list().map((e, i) => ({ e, i })).sort((a, b) => a.e.priority - b.e.priority || a.i - b.i).map((x) => x.e);
1303
+ }
1304
+ };
1305
+ function scopeCss(css, pluginId) {
1306
+ return `@scope ([data-fluxplugin="${pluginId.replace(/"/g, "")}"]) {
1307
+ ${css}
1308
+ }`;
1309
+ }
1310
+
1311
+ // src/core/ipc.ts
1312
+ function defineIpcContract(c) {
1313
+ return Object.freeze({ ...c });
1314
+ }
1315
+ function defineIPC(def) {
1316
+ const contract = Object.freeze({
1317
+ name: def.name,
1318
+ input: def.input,
1319
+ output: def.output,
1320
+ permission: def.permission
1321
+ });
1322
+ return { ...contract, handler: def.handler, contract };
1323
+ }
1324
+ var RESERVED = /^flux\./;
1325
+ var IpcError = class extends FrameworkError {
1326
+ constructor(message, code, channel) {
1327
+ super(message, code);
1328
+ this.channel = channel;
1329
+ }
1330
+ channel;
1331
+ };
1332
+ function serializeError(e) {
1333
+ if (e instanceof ValidationError) return { code: "FLUX_VALIDATION", message: e.message, issues: e.issues };
1334
+ if (e instanceof FrameworkError) return { code: e.code, message: e.message };
1335
+ return { code: "FLUX_HANDLER_ERROR", message: errorMessage(e) };
1336
+ }
1337
+ var IpcRegistry = class extends Registry {
1338
+ oneWay = /* @__PURE__ */ new Map();
1339
+ transport;
1340
+ /** Hook for the manager: ensures the owner plugin is active (`onIPC:<name>`) and checks caller permissions. */
1341
+ beforeInvoke;
1342
+ /** Permission check provided by the manager. */
1343
+ assertCallerPermission;
1344
+ /** Observer used for diagnostics. */
1345
+ onCall;
1346
+ constructor() {
1347
+ super("IPC handler");
1348
+ }
1349
+ setTransport(t) {
1350
+ this.transport = t;
1351
+ }
1352
+ handle(a, handler, owner, opts = {}) {
1353
+ const c = typeof a === "string" ? { name: a, ...opts } : a;
1354
+ if (RESERVED.test(c.name) && owner !== "host") {
1355
+ throw new FrameworkError(`IPC names starting with "flux." are reserved for the framework`, "FLUX_RESERVED", owner);
1356
+ }
1357
+ return this.add(c.name, { name: c.name, handler, input: c.input, output: c.output, permission: c.permission, owner }, owner);
1358
+ }
1359
+ /** Registers a full `IpcDefinition` (contract + handler). */
1360
+ register(def, owner) {
1361
+ return this.handle(def.contract, def.handler, owner);
1362
+ }
1363
+ /** Dispatches a call: validate input → permission → handler → validate output. */
1364
+ async invoke(name, input, opts = {}) {
1365
+ if (!this.has(name)) await this.beforeInvoke?.(name, opts);
1366
+ const entry = this.table.get(name)?.item;
1367
+ if (!entry) throw new IpcError(`No handler registered for "${name}"`, "FLUX_NO_HANDLER", name);
1368
+ const caller = opts.caller ?? "host";
1369
+ const started = Date.now();
1370
+ try {
1371
+ if (entry.permission && caller !== "host") this.assertCallerPermission?.(caller, entry.permission, name);
1372
+ let value = input;
1373
+ if (entry.input) {
1374
+ const r = entry.input.parse(input);
1375
+ if (!r.ok) throw new ValidationError(r.issues, `input of "${name}"`);
1376
+ value = r.value;
1377
+ }
1378
+ const out = await entry.handler(value, { pluginId: entry.owner, caller, senderId: opts.senderId });
1379
+ if (entry.output) {
1380
+ const r = entry.output.parse(out);
1381
+ if (!r.ok) throw new ValidationError(r.issues, `output of "${name}"`);
1382
+ this.onCall?.({ name, owner: entry.owner, caller, ms: Date.now() - started });
1383
+ return r.value;
1384
+ }
1385
+ this.onCall?.({ name, owner: entry.owner, caller, ms: Date.now() - started });
1386
+ return out;
1387
+ } catch (e) {
1388
+ this.onCall?.({ name, owner: entry.owner, caller, ms: Date.now() - started, error: e });
1389
+ throw e;
1390
+ }
1391
+ }
1392
+ /** Fire-and-forget messages from renderer/other plugins to main (`ipc.on`). */
1393
+ on(name, fn, owner) {
1394
+ let set = this.oneWay.get(name);
1395
+ if (!set) this.oneWay.set(name, set = /* @__PURE__ */ new Set());
1396
+ const entry = { fn, owner };
1397
+ set.add(entry);
1398
+ return toDisposable(() => set.delete(entry));
1399
+ }
1400
+ /** Delivers a one-way message to main-side listeners. */
1401
+ dispatch(name, payload, opts = {}) {
1402
+ const set = this.oneWay.get(name);
1403
+ if (!set) return 0;
1404
+ let n = 0;
1405
+ for (const l of [...set]) {
1406
+ try {
1407
+ l.fn(payload, { pluginId: l.owner, caller: opts.caller ?? "host", senderId: opts.senderId });
1408
+ n++;
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ return n;
1413
+ }
1414
+ /** Main → renderer push. */
1415
+ send(channel, payload, target) {
1416
+ this.transport?.broadcast(channel, payload, target);
1417
+ }
1418
+ removeListenersByOwner(owner) {
1419
+ for (const set of this.oneWay.values()) for (const l of [...set]) if (l.owner === owner) set.delete(l);
1420
+ }
1421
+ };
1422
+ var preloadMember = {
1423
+ invoke: (channel) => ({ kind: "invoke", channel }),
1424
+ send: (channel) => ({ kind: "send", channel }),
1425
+ event: (channel) => ({ kind: "event", channel })
1426
+ };
1427
+ var NAMESPACE_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1428
+ var FORBIDDEN_NS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype", "flux", "__fluxplugin"]);
1429
+ var PreloadRegistry = class extends Registry {
1430
+ constructor() {
1431
+ super("Preload API");
1432
+ }
1433
+ expose(namespace, members, owner) {
1434
+ if (!NAMESPACE_RE.test(namespace) || FORBIDDEN_NS.has(namespace)) {
1435
+ throw new FrameworkError(`Invalid preload namespace "${namespace}"`, "FLUX_INVALID_NAMESPACE", owner);
1436
+ }
1437
+ const spec = {};
1438
+ for (const [key, m] of Object.entries(members)) {
1439
+ if (!NAMESPACE_RE.test(key) || FORBIDDEN_NS.has(key)) {
1440
+ throw new FrameworkError(`Invalid preload member "${namespace}.${key}"`, "FLUX_INVALID_NAMESPACE", owner);
1441
+ }
1442
+ spec[key] = typeof m === "string" ? { kind: "invoke", channel: m } : m;
1443
+ }
1444
+ return this.add(namespace, { id: namespace, namespace, spec, owner }, owner);
1445
+ }
1446
+ /** JSON-serializable manifest sent to the preload bridge. */
1447
+ manifest() {
1448
+ const out = {};
1449
+ for (const e of this.list()) out[e.namespace] = e.spec;
1450
+ return out;
1451
+ }
1452
+ };
1453
+
1454
+ // src/core/storage.ts
1455
+ var MemoryStorageBackend = class {
1456
+ data = /* @__PURE__ */ new Map();
1457
+ async read(ns) {
1458
+ return structuredClone(this.data.get(ns) ?? {});
1459
+ }
1460
+ async write(ns, data) {
1461
+ this.data.set(ns, structuredClone(data));
1462
+ }
1463
+ async remove(ns) {
1464
+ this.data.delete(ns);
1465
+ }
1466
+ };
1467
+ var CONFIG_KEY = "__fluxplugin_config__";
1468
+ function createPluginStorage(backend, namespace) {
1469
+ let queue = Promise.resolve();
1470
+ const exclusive = (fn) => {
1471
+ const run = queue.then(fn, fn);
1472
+ queue = run.catch(() => void 0);
1473
+ return run;
1474
+ };
1475
+ const guard = (key) => {
1476
+ if (typeof key !== "string" || key.length === 0) throw new Error("Storage key must be a non-empty string");
1477
+ if (key === CONFIG_KEY || key === "__proto__") throw new Error(`Storage key "${key}" is reserved`);
1478
+ };
1479
+ return {
1480
+ async get(key, fallback) {
1481
+ guard(key);
1482
+ const data = await exclusive(() => backend.read(namespace));
1483
+ return Object.prototype.hasOwnProperty.call(data, key) ? structuredClone(data[key]) : fallback;
1484
+ },
1485
+ async set(key, value) {
1486
+ guard(key);
1487
+ await exclusive(async () => {
1488
+ const data = await backend.read(namespace);
1489
+ data[key] = structuredClone(value);
1490
+ await backend.write(namespace, data);
1491
+ });
1492
+ },
1493
+ async delete(key) {
1494
+ guard(key);
1495
+ await exclusive(async () => {
1496
+ const data = await backend.read(namespace);
1497
+ delete data[key];
1498
+ await backend.write(namespace, data);
1499
+ });
1500
+ },
1501
+ async keys() {
1502
+ const data = await exclusive(() => backend.read(namespace));
1503
+ return Object.keys(data).filter((k) => k !== CONFIG_KEY);
1504
+ },
1505
+ async clear() {
1506
+ await exclusive(async () => {
1507
+ const data = await backend.read(namespace);
1508
+ await backend.write(namespace, CONFIG_KEY in data ? { [CONFIG_KEY]: data[CONFIG_KEY] } : {});
1509
+ });
1510
+ }
1511
+ };
1512
+ }
1513
+ function createPluginConfig(backend, namespace, def, onChanged) {
1514
+ const version = def.version ?? 1;
1515
+ const listeners = /* @__PURE__ */ new Set();
1516
+ let queue = Promise.resolve();
1517
+ const exclusive = (fn) => {
1518
+ const run = queue.then(fn, fn);
1519
+ queue = run.catch(() => void 0);
1520
+ return run;
1521
+ };
1522
+ const defaults = () => {
1523
+ const r = def.schema.parse({});
1524
+ if (!r.ok) throw new ValidationError(r.issues, `default configuration of "${namespace}" (every field needs a default or to be optional)`);
1525
+ return r.value;
1526
+ };
1527
+ const load = async () => {
1528
+ const data = await backend.read(namespace);
1529
+ const stored = data[CONFIG_KEY];
1530
+ if (!stored) return defaults();
1531
+ let values = stored.values ?? {};
1532
+ if (stored.version < version && def.migrate) values = def.migrate(values, stored.version);
1533
+ const r = def.schema.parse(values);
1534
+ if (r.ok) return r.value;
1535
+ return defaults();
1536
+ };
1537
+ const save = async (values) => {
1538
+ const data = await backend.read(namespace);
1539
+ data[CONFIG_KEY] = { version, values };
1540
+ await backend.write(namespace, data);
1541
+ };
1542
+ const emit = (next, prev) => {
1543
+ onChanged?.(next, prev);
1544
+ for (const l of [...listeners]) {
1545
+ try {
1546
+ l(next, prev);
1547
+ } catch {
1548
+ }
1549
+ }
1550
+ };
1551
+ return {
1552
+ get: () => exclusive(load),
1553
+ set: (patch) => exclusive(async () => {
1554
+ const prev = await load();
1555
+ const r = def.schema.parse({ ...prev, ...patch });
1556
+ if (!r.ok) throw new ValidationError(r.issues, `configuration of "${namespace}"`);
1557
+ await save(r.value);
1558
+ emit(r.value, prev);
1559
+ return r.value;
1560
+ }),
1561
+ reset: () => exclusive(async () => {
1562
+ const prev = await load();
1563
+ const next = defaults();
1564
+ await save(next);
1565
+ emit(next, prev);
1566
+ return next;
1567
+ }),
1568
+ onChange(l) {
1569
+ listeners.add(l);
1570
+ return toDisposable(() => listeners.delete(l));
1571
+ },
1572
+ describe: () => def.schema.describe()
1573
+ };
1574
+ }
1575
+ function createPluginState() {
1576
+ const data = /* @__PURE__ */ new Map();
1577
+ const listeners = /* @__PURE__ */ new Set();
1578
+ const notify = (k, v) => {
1579
+ for (const l of [...listeners]) {
1580
+ if (l.key !== void 0 && l.key !== k) continue;
1581
+ try {
1582
+ l.fn(k, v);
1583
+ } catch {
1584
+ }
1585
+ }
1586
+ };
1587
+ return {
1588
+ get: ((key, fallback) => data.has(key) ? data.get(key) : fallback),
1589
+ set(key, value) {
1590
+ if (Object.is(data.get(key), value) && data.has(key)) return;
1591
+ data.set(key, value);
1592
+ notify(key, value);
1593
+ },
1594
+ delete(key) {
1595
+ if (data.delete(key)) notify(key, void 0);
1596
+ },
1597
+ subscribe(fn, key) {
1598
+ const l = { fn, key };
1599
+ listeners.add(l);
1600
+ return toDisposable(() => listeners.delete(l));
1601
+ },
1602
+ snapshot: () => Object.fromEntries(data)
1603
+ };
1604
+ }
1605
+
1606
+ // src/core/deps.ts
1607
+ function buildDependencyGraph(manifests) {
1608
+ const map = /* @__PURE__ */ new Map();
1609
+ for (const m of manifests) map.set(m.id, m);
1610
+ const issues = [];
1611
+ const edges = /* @__PURE__ */ new Map();
1612
+ const blocked = /* @__PURE__ */ new Set();
1613
+ for (const m of map.values()) {
1614
+ const out = [];
1615
+ for (const kind of ["dependencies", "peerDependencies"]) {
1616
+ for (const [dep, range] of Object.entries(m[kind] ?? {})) {
1617
+ const target = map.get(dep);
1618
+ if (!target) {
1619
+ issues.push({ type: "missing", plugin: m.id, dependency: dep, range, kind });
1620
+ blocked.add(m.id);
1621
+ continue;
1622
+ }
1623
+ if (!satisfies(target.version, range)) {
1624
+ issues.push({ type: "version", plugin: m.id, dependency: dep, range, found: target.version });
1625
+ blocked.add(m.id);
1626
+ continue;
1627
+ }
1628
+ out.push(dep);
1629
+ }
1630
+ }
1631
+ for (const [dep, range] of Object.entries(m.optionalDependencies ?? {})) {
1632
+ const target = map.get(dep);
1633
+ if (target && satisfies(target.version, range)) out.push(dep);
1634
+ }
1635
+ edges.set(m.id, out);
1636
+ }
1637
+ const state = /* @__PURE__ */ new Map();
1638
+ const order = [];
1639
+ const stack = [];
1640
+ const visit = (id) => {
1641
+ const st = state.get(id) ?? 0;
1642
+ if (st === 2) return;
1643
+ if (st === 1) {
1644
+ const path = [...stack.slice(stack.indexOf(id)), id];
1645
+ issues.push({ type: "cycle", plugin: id, path });
1646
+ for (const p of path) blocked.add(p);
1647
+ return;
1648
+ }
1649
+ state.set(id, 1);
1650
+ stack.push(id);
1651
+ for (const dep of edges.get(id) ?? []) visit(dep);
1652
+ stack.pop();
1653
+ state.set(id, 2);
1654
+ order.push(id);
1655
+ };
1656
+ for (const id of [...map.keys()].sort()) visit(id);
1657
+ let changed = true;
1658
+ while (changed) {
1659
+ changed = false;
1660
+ for (const [id, deps] of edges) {
1661
+ if (!blocked.has(id) && deps.some((d) => blocked.has(d))) {
1662
+ blocked.add(id);
1663
+ changed = true;
1664
+ }
1665
+ }
1666
+ }
1667
+ return { manifests: map, edges, issues, blocked, order: order.filter((id) => !blocked.has(id)) };
1668
+ }
1669
+ function activationOrderFor(graph, id) {
1670
+ const out = [];
1671
+ const seen = /* @__PURE__ */ new Set();
1672
+ const walk = (n) => {
1673
+ if (seen.has(n)) return;
1674
+ seen.add(n);
1675
+ for (const d of graph.edges.get(n) ?? []) walk(d);
1676
+ out.push(n);
1677
+ };
1678
+ walk(id);
1679
+ return out;
1680
+ }
1681
+ function dependentsOf(graph, id) {
1682
+ const reverse = /* @__PURE__ */ new Map();
1683
+ for (const [p, deps] of graph.edges) for (const d of deps) (reverse.get(d) ?? reverse.set(d, []).get(d)).push(p);
1684
+ const out = [];
1685
+ const seen = /* @__PURE__ */ new Set([id]);
1686
+ const walk = (n) => {
1687
+ for (const p of reverse.get(n) ?? []) {
1688
+ if (seen.has(p)) continue;
1689
+ seen.add(p);
1690
+ walk(p);
1691
+ out.push(p);
1692
+ }
1693
+ };
1694
+ walk(id);
1695
+ return out;
1696
+ }
1697
+ function describeIssue(i) {
1698
+ switch (i.type) {
1699
+ case "missing":
1700
+ return `${i.plugin} requires "${i.dependency}" (${i.range}) which is not installed`;
1701
+ case "version":
1702
+ return `${i.plugin} requires "${i.dependency}" ${i.range} but ${i.found} is installed`;
1703
+ case "cycle":
1704
+ return `dependency cycle: ${i.path.join(" \u2192 ")}`;
1705
+ }
1706
+ }
1707
+
1708
+ // src/core/plugin.ts
1709
+ function definePlugin(def) {
1710
+ return def;
1711
+ }
1712
+ var defineRoute = (r) => r;
1713
+ var defineCommand = (c) => c;
1714
+ var defineService = (name, impl) => ({ name, implementation: impl });
1715
+ var defineHook = (name, handler, priority) => ({ name, handler, priority });
1716
+ var defineEvent = (name, handler, opts = {}) => ({ name, handler, ...opts });
1717
+ var defineComponent = (c) => c;
1718
+ var defineMenu = (m) => m;
1719
+
1720
+ // src/core/manager.ts
1721
+ var DEFAULT_TIMEOUTS = { load: 15e3, initialize: 1e4, activate: 15e3, ready: 1e4, deactivate: 1e4, unload: 1e4 };
1722
+ var PluginManager = class {
1723
+ constructor(opts) {
1724
+ this.opts = opts;
1725
+ this.runtime = opts.runtime;
1726
+ this.logs = opts.logs ?? new LogStore();
1727
+ this.log = this.logs.logger("core");
1728
+ this.storageBackend = opts.storage ?? new MemoryStorageBackend();
1729
+ this.permissions = opts.permissions ?? new PermissionManager();
1730
+ this.timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
1731
+ this.events = new EventBus((e, name, pattern) => this.log.error(`event listener for "${pattern}" failed on "${name}": ${errorMessage(e)}`));
1732
+ this.hooks = new HookSystem((e, name, owner) => this.logs.logger(owner ?? "core").error(`hook "${name}" failed: ${errorMessage(e)}`));
1733
+ this.wireLazyActivation();
1734
+ this.permissions.onChange(() => this.changed());
1735
+ }
1736
+ opts;
1737
+ runtime;
1738
+ logs;
1739
+ permissions;
1740
+ events;
1741
+ hooks;
1742
+ commands = new CommandRegistry();
1743
+ menus = new MenuRegistry();
1744
+ routes = new RouteRegistry();
1745
+ services = new ServiceRegistry();
1746
+ slots = new SlotRegistry();
1747
+ extensionPoints = new ExtensionPointRegistry();
1748
+ ipc = new IpcRegistry();
1749
+ preload = new PreloadRegistry();
1750
+ styles = new StyleRegistry();
1751
+ storageBackend;
1752
+ records = /* @__PURE__ */ new Map();
1753
+ states = /* @__PURE__ */ new Map();
1754
+ hostApis = /* @__PURE__ */ new Map();
1755
+ timeouts;
1756
+ log;
1757
+ listeners = /* @__PURE__ */ new Set();
1758
+ _version = 0;
1759
+ disposedFlag = false;
1760
+ /* ------------------------------------------------------------- observers */
1761
+ get version() {
1762
+ return this._version;
1763
+ }
1764
+ subscribe = (l) => {
1765
+ this.listeners.add(l);
1766
+ return () => void this.listeners.delete(l);
1767
+ };
1768
+ changed() {
1769
+ this._version++;
1770
+ for (const l of [...this.listeners]) {
1771
+ try {
1772
+ l();
1773
+ } catch {
1774
+ }
1775
+ }
1776
+ }
1777
+ /* ------------------------------------------------------------ host APIs */
1778
+ /** Exposes a controlled capability to plugins (`context.host.api(name)`), optionally gated by a permission. */
1779
+ provide(name, factory, permission) {
1780
+ if (this.hostApis.has(name)) throw new FrameworkError(`Host API "${name}" already provided`, "FLUX_DUPLICATE");
1781
+ this.hostApis.set(name, { factory, permission });
1782
+ return toDisposable(() => this.hostApis.delete(name));
1783
+ }
1784
+ /* --------------------------------------------------- discovery / validate */
1785
+ /** Persisted state (disabled plugins) must be loaded before registering sources. */
1786
+ async init() {
1787
+ const disabled = await this.opts.stateStore?.load() ?? [];
1788
+ this.persistedDisabled = new Set(disabled);
1789
+ }
1790
+ persistedDisabled = /* @__PURE__ */ new Set();
1791
+ /**
1792
+ * Discovers + validates one plugin. Invalid plugins are recorded in the `error` state (so the UI can show why)
1793
+ * and never throw — one broken plugin must not stop the others.
1794
+ */
1795
+ register(source) {
1796
+ const v = validateManifest(source.manifest, { frameworkVersion: this.opts.frameworkVersion ?? FRAMEWORK_VERSION });
1797
+ const raw = source.manifest;
1798
+ const id = v.manifest?.id ?? (typeof raw?.id === "string" ? raw.id : `invalid-${this.records.size}`);
1799
+ if (this.records.has(id)) {
1800
+ this.log.warn(`plugin "${id}" is already registered; ignoring duplicate from ${source.origin ?? "unknown origin"}`);
1801
+ return this.info(id);
1802
+ }
1803
+ const manifest = v.manifest ?? {
1804
+ id,
1805
+ name: typeof raw?.name === "string" ? raw.name : id,
1806
+ version: typeof raw?.version === "string" ? raw.version : "0.0.0",
1807
+ activationEvents: [],
1808
+ deactivationEvents: [],
1809
+ permissions: [],
1810
+ dependencies: {},
1811
+ optionalDependencies: {},
1812
+ peerDependencies: {},
1813
+ engines: {},
1814
+ contributes: {}
1815
+ };
1816
+ const rec = {
1817
+ id,
1818
+ manifest,
1819
+ source,
1820
+ state: v.ok ? "validated" : "error",
1821
+ enabled: !this.persistedDisabled.has(id),
1822
+ store: new DisposableStore(),
1823
+ deactivators: [],
1824
+ lazy: [],
1825
+ crashes: [],
1826
+ warnings: v.warnings
1827
+ };
1828
+ if (!v.ok) {
1829
+ rec.error = { message: v.issues.map((i) => `${i.path || "<root>"}: ${i.message}`).join("; "), phase: "validate", time: Date.now() };
1830
+ this.logs.logger(id).error(`manifest validation failed: ${rec.error.message}`);
1831
+ } else {
1832
+ this.permissions.declare(id, manifest.permissions);
1833
+ if (!rec.enabled) rec.state = "disabled";
1834
+ }
1835
+ this.records.set(id, rec);
1836
+ if (v.ok && rec.enabled) this.installLazyProxies(rec);
1837
+ this.log.info(`discovered ${id}@${manifest.version} (${rec.state})`);
1838
+ this.events.emit("plugin.registered", { id }, "core");
1839
+ this.changed();
1840
+ return this.info(id);
1841
+ }
1842
+ async unregister(id) {
1843
+ const rec = this.records.get(id);
1844
+ if (!rec) return;
1845
+ await this.deactivate(id).catch(() => void 0);
1846
+ await this.unload(id).catch(() => void 0);
1847
+ for (const d of rec.lazy.splice(0)) d.dispose();
1848
+ this.permissions.forget(id);
1849
+ this.records.delete(id);
1850
+ this.events.emit("plugin.unregistered", { id }, "core");
1851
+ this.changed();
1852
+ }
1853
+ /* ----------------------------------------------------------- inspection */
1854
+ has(id) {
1855
+ return this.records.has(id);
1856
+ }
1857
+ list() {
1858
+ return [...this.records.keys()].sort().map((id) => this.info(id));
1859
+ }
1860
+ info(id) {
1861
+ const rec = this.mustGet(id);
1862
+ const m = rec.manifest;
1863
+ const graph = this.graph();
1864
+ const author = typeof m.author === "string" ? m.author : m.author?.name;
1865
+ return {
1866
+ id,
1867
+ name: m.name,
1868
+ version: m.version,
1869
+ description: m.description,
1870
+ author,
1871
+ state: rec.state,
1872
+ enabled: rec.enabled,
1873
+ runtime: this.runtime,
1874
+ permissions: this.permissions.snapshot(id),
1875
+ dependencies: { ...m.dependencies, ...m.peerDependencies, ...m.optionalDependencies },
1876
+ dependents: dependentsOf(graph, id),
1877
+ activationEvents: m.activationEvents ?? [],
1878
+ contributes: m.contributes ?? {},
1879
+ error: rec.error,
1880
+ crashes: rec.crashes.length,
1881
+ hasConfig: !!rec.definition?.config,
1882
+ root: rec.source.root,
1883
+ origin: rec.source.origin,
1884
+ warnings: rec.warnings
1885
+ };
1886
+ }
1887
+ /** Reactive per-plugin state store (same instance as `ctx.state`). Survives reloads. */
1888
+ stateOf(id) {
1889
+ let s2 = this.states.get(id);
1890
+ if (!s2) this.states.set(id, s2 = createPluginState());
1891
+ return s2;
1892
+ }
1893
+ /** Validated manifest (or the best-effort one for plugins rejected at validation). */
1894
+ manifest(id) {
1895
+ return this.mustGet(id).manifest;
1896
+ }
1897
+ getConfig(id) {
1898
+ return this.records.get(id)?.config;
1899
+ }
1900
+ /** Active plugin's context (for hosts and tests). */
1901
+ contextOf(id) {
1902
+ return this.records.get(id)?.context;
1903
+ }
1904
+ diagnostics(id) {
1905
+ const owned = (r) => r.listByOwner(id).length;
1906
+ return {
1907
+ info: this.info(id),
1908
+ logs: this.logs.query({ source: id }),
1909
+ contributions: {
1910
+ commands: owned(this.commands),
1911
+ menus: owned(this.menus),
1912
+ routes: owned(this.routes),
1913
+ services: owned(this.services),
1914
+ slots: owned(this.slots),
1915
+ extensions: owned(this.extensionPoints),
1916
+ ipc: owned(this.ipc),
1917
+ preload: owned(this.preload),
1918
+ styles: owned(this.styles)
1919
+ }
1920
+ };
1921
+ }
1922
+ mustGet(id) {
1923
+ const r = this.records.get(id);
1924
+ if (!r) throw new FrameworkError(`Unknown plugin "${id}"`, "FLUX_UNKNOWN_PLUGIN");
1925
+ return r;
1926
+ }
1927
+ graph() {
1928
+ return buildDependencyGraph([...this.records.values()].filter((r) => r.state !== "error" || r.error?.phase !== "validate").map((r) => r.manifest));
1929
+ }
1930
+ /* ------------------------------------------------------------ activation */
1931
+ /** Activates every plugin that wants to start with the application (`onStartup`, `*`, or no activation events). */
1932
+ async startup() {
1933
+ const graph = this.graph();
1934
+ for (const issue of graph.issues) this.log.warn(describeIssue(issue));
1935
+ const ids = graph.order.filter((id) => {
1936
+ const r = this.records.get(id);
1937
+ const ev = r.manifest.activationEvents ?? [];
1938
+ return r.enabled && (ev.length === 0 || ev.includes("onStartup") || ev.includes("*"));
1939
+ });
1940
+ for (const id of ids) await this.activate(id).catch(() => void 0);
1941
+ }
1942
+ /** Activates `id` and, first, everything it depends on. Rejects with a `LifecycleError` on failure. */
1943
+ async activate(id) {
1944
+ const rec = this.mustGet(id);
1945
+ if (!rec.enabled) throw new LifecycleError(id, "activate", "plugin is disabled");
1946
+ if (rec.state === "error" && rec.error?.phase === "validate") throw new LifecycleError(id, "validate", rec.error.message);
1947
+ const graph = this.graph();
1948
+ if (graph.blocked.has(id)) {
1949
+ const reasons = graph.issues.filter((i) => i.plugin === id || i.type === "cycle" && i.path.includes(id)).map(describeIssue);
1950
+ throw new LifecycleError(id, "activate", reasons.join("; ") || "a dependency cannot be activated");
1951
+ }
1952
+ await this.ensureReady(id, graph, []);
1953
+ }
1954
+ async ensureReady(id, graph, chain) {
1955
+ const rec = this.mustGet(id);
1956
+ if (rec.state === "ready") return;
1957
+ if (rec.inflight) return rec.inflight;
1958
+ if (!rec.enabled) throw new LifecycleError(id, "activate", "plugin is disabled");
1959
+ if (chain.includes(id)) throw new LifecycleError(id, "activate", `dependency cycle: ${[...chain, id].join(" \u2192 ")}`);
1960
+ const optional = new Set(Object.keys(rec.manifest.optionalDependencies ?? {}).filter((d) => !(d in (rec.manifest.dependencies ?? {})) && !(d in (rec.manifest.peerDependencies ?? {}))));
1961
+ const run = (async () => {
1962
+ for (const dep of graph.edges.get(id) ?? []) {
1963
+ try {
1964
+ await this.ensureReady(dep, graph, [...chain, id]);
1965
+ } catch (e) {
1966
+ if (optional.has(dep)) {
1967
+ this.logs.logger(id).warn(`optional dependency "${dep}" could not be activated: ${errorMessage(e)}`);
1968
+ continue;
1969
+ }
1970
+ const err = new LifecycleError(id, "activate", `required dependency "${dep}" failed: ${errorMessage(e)}`, e);
1971
+ await this.fail(rec, "activate", err, false);
1972
+ throw err;
1973
+ }
1974
+ }
1975
+ await this.bringUp(rec);
1976
+ })();
1977
+ rec.inflight = run;
1978
+ try {
1979
+ await run;
1980
+ } finally {
1981
+ rec.inflight = void 0;
1982
+ }
1983
+ }
1984
+ async phase(rec, phase, fn) {
1985
+ try {
1986
+ return await withTimeout(Promise.resolve().then(fn), this.timeouts[phase], `${phase} of "${rec.id}"`, rec.id);
1987
+ } catch (e) {
1988
+ if (e instanceof LifecycleError) throw e;
1989
+ throw new LifecycleError(rec.id, phase, errorMessage(e), e);
1990
+ }
1991
+ }
1992
+ setState(rec, state) {
1993
+ rec.state = state;
1994
+ this.events.emit("plugin.state", { id: rec.id, state }, "core");
1995
+ this.changed();
1996
+ }
1997
+ async bringUp(rec) {
1998
+ let phase = "load";
1999
+ try {
2000
+ if (!rec.definition) {
2001
+ const def2 = await this.phase(rec, "load", () => rec.source.load());
2002
+ if (!def2 || typeof def2 !== "object") throw new Error("entry did not export a plugin definition (use definePlugin)");
2003
+ if (def2.id && def2.id !== rec.id) throw new Error(`plugin id mismatch: manifest "${rec.id}" vs entry "${def2.id}"`);
2004
+ rec.definition = def2;
2005
+ rec.context = this.createContext(rec);
2006
+ this.setState(rec, "loaded");
2007
+ await this.phase(rec, "load", () => def2.onLoad?.(rec.context));
2008
+ }
2009
+ const def = rec.definition;
2010
+ const ctx = rec.context;
2011
+ phase = "initialize";
2012
+ this.setState(rec, "initialized");
2013
+ await this.phase(rec, "initialize", () => def.onInitialize?.(ctx));
2014
+ phase = "activate";
2015
+ this.removeLazyProxies(rec);
2016
+ await this.phase(rec, "activate", async () => {
2017
+ await this.applyDeclarative(rec);
2018
+ await (def.onActivate ?? def.activate)?.(ctx);
2019
+ });
2020
+ this.setState(rec, "activated");
2021
+ phase = "ready";
2022
+ await this.phase(rec, "ready", () => def.onReady?.(ctx));
2023
+ rec.error = void 0;
2024
+ this.setState(rec, "ready");
2025
+ this.logs.logger(rec.id).info("ready");
2026
+ this.events.emit("plugin.ready", { id: rec.id }, "core");
2027
+ } catch (e) {
2028
+ await this.fail(rec, phase, e, true);
2029
+ throw e;
2030
+ }
2031
+ }
2032
+ /** Records a failure, tears down everything the plugin registered and auto-disables repeat offenders. */
2033
+ async fail(rec, phase, e, cleanup) {
2034
+ const message = errorMessage(e);
2035
+ this.logs.logger(rec.id).error(`${phase} failed: ${message}`, e instanceof Error ? e.stack : void 0);
2036
+ if (cleanup) this.teardown(rec);
2037
+ rec.error = { message, phase, time: Date.now() };
2038
+ const now = Date.now();
2039
+ const windowMs = this.opts.crashWindowMs ?? 6e4;
2040
+ rec.crashes = [...rec.crashes.filter((t) => now - t < windowMs), now];
2041
+ this.events.emit("plugin.error", { id: rec.id, phase, message }, "core");
2042
+ if (rec.crashes.length >= (this.opts.maxCrashes ?? 3)) {
2043
+ rec.enabled = false;
2044
+ this.logs.logger(rec.id).warn(`disabled automatically after ${rec.crashes.length} failures`);
2045
+ await this.persistDisabled();
2046
+ this.setState(rec, "disabled");
2047
+ this.events.emit("plugin.autodisabled", { id: rec.id }, "core");
2048
+ } else {
2049
+ this.setState(rec, "error");
2050
+ }
2051
+ if (rec.enabled) this.installLazyProxies(rec);
2052
+ }
2053
+ /** Disposes every contribution of a plugin (store + owner-based sweep as a safety net). */
2054
+ teardown(rec) {
2055
+ rec.store.dispose();
2056
+ rec.store = new DisposableStore();
2057
+ rec.deactivators = [];
2058
+ this.sweep(rec.id);
2059
+ }
2060
+ sweep(owner) {
2061
+ this.commands.unregisterByOwner(owner);
2062
+ this.menus.unregisterByOwner(owner);
2063
+ this.routes.unregisterByOwner(owner);
2064
+ this.services.unregisterByOwner(owner);
2065
+ this.slots.unregisterByOwner(owner);
2066
+ this.extensionPoints.unregisterByOwner(owner);
2067
+ this.ipc.unregisterByOwner(owner);
2068
+ this.ipc.removeListenersByOwner(owner);
2069
+ this.preload.unregisterByOwner(owner);
2070
+ this.styles.unregisterByOwner(owner);
2071
+ this.hooks.removeByOwner(owner);
2072
+ }
2073
+ /* ---------------------------------------------------------- deactivation */
2074
+ /** Deactivates `id` after deactivating every active plugin that depends on it. Never throws for plugin errors. */
2075
+ async deactivate(id) {
2076
+ const rec = this.mustGet(id);
2077
+ const graph = this.graph();
2078
+ for (const dep of dependentsOf(graph, id)) {
2079
+ const d = this.records.get(dep);
2080
+ if (d && (d.state === "ready" || d.state === "activated" || d.state === "initialized" || d.state === "loaded")) await this.deactivateOne(d);
2081
+ }
2082
+ if (rec.inflight) await rec.inflight.catch(() => void 0);
2083
+ await this.deactivateOne(rec);
2084
+ }
2085
+ async deactivateOne(rec) {
2086
+ if (!["ready", "activated", "initialized", "loaded", "error"].includes(rec.state)) return;
2087
+ const log = this.logs.logger(rec.id);
2088
+ const def = rec.definition;
2089
+ const ctx = rec.context;
2090
+ if (def && ctx && rec.state !== "error") {
2091
+ try {
2092
+ await this.phase(rec, "deactivate", () => (def.onDeactivate ?? def.deactivate)?.(ctx));
2093
+ } catch (e) {
2094
+ log.error(`deactivate hook failed: ${errorMessage(e)}`);
2095
+ }
2096
+ }
2097
+ for (const fn of [...rec.deactivators].reverse()) {
2098
+ try {
2099
+ await fn();
2100
+ } catch (e) {
2101
+ log.error(`cleanup failed: ${errorMessage(e)}`);
2102
+ }
2103
+ }
2104
+ this.teardown(rec);
2105
+ this.setState(rec, "deactivated");
2106
+ if (rec.enabled) this.installLazyProxies(rec);
2107
+ this.events.emit("plugin.deactivated", { id: rec.id }, "core");
2108
+ }
2109
+ async unload(id) {
2110
+ const rec = this.mustGet(id);
2111
+ if (rec.state === "ready" || rec.state === "activated") await this.deactivate(id);
2112
+ const def = rec.definition;
2113
+ const ctx = rec.context;
2114
+ if (def && ctx) {
2115
+ try {
2116
+ await this.phase(rec, "unload", () => def.onUnload?.(ctx));
2117
+ } catch (e) {
2118
+ this.logs.logger(id).error(`unload hook failed: ${errorMessage(e)}`);
2119
+ }
2120
+ }
2121
+ rec.definition = void 0;
2122
+ rec.context = void 0;
2123
+ rec.config = void 0;
2124
+ if (rec.state !== "disabled" && !(rec.state === "error" && rec.error?.phase === "validate")) this.setState(rec, "unloaded");
2125
+ }
2126
+ async disable(id) {
2127
+ const rec = this.mustGet(id);
2128
+ await this.deactivate(id);
2129
+ await this.unload(id);
2130
+ rec.enabled = false;
2131
+ for (const d of rec.lazy.splice(0)) d.dispose();
2132
+ await this.persistDisabled();
2133
+ this.setState(rec, "disabled");
2134
+ }
2135
+ async enable(id, opts = {}) {
2136
+ const rec = this.mustGet(id);
2137
+ rec.enabled = true;
2138
+ rec.crashes = [];
2139
+ rec.error = void 0;
2140
+ await this.persistDisabled();
2141
+ this.setState(rec, "validated");
2142
+ this.installLazyProxies(rec);
2143
+ if (opts.activate ?? true) await this.activate(id);
2144
+ }
2145
+ /** Development hot reload: deactivate → unload → load a fresh module → activate (if it was active). */
2146
+ async reload(id) {
2147
+ const rec = this.mustGet(id);
2148
+ const wasActive = rec.state === "ready" || rec.state === "error" && rec.error?.phase !== "validate";
2149
+ await this.deactivate(id);
2150
+ await this.unload(id);
2151
+ rec.error = void 0;
2152
+ if (rec.enabled && rec.state !== "disabled") this.setState(rec, "validated");
2153
+ this.events.emit("plugin.reloading", { id }, "core");
2154
+ if (wasActive || rec.manifest.activationEvents?.includes("onStartup")) await this.activate(id);
2155
+ this.events.emit("plugin.reloaded", { id }, "core");
2156
+ }
2157
+ async persistDisabled() {
2158
+ const ids = [...this.records.values()].filter((r) => !r.enabled).map((r) => r.id);
2159
+ this.persistedDisabled = new Set(ids);
2160
+ try {
2161
+ await this.opts.stateStore?.save(ids);
2162
+ } catch (e) {
2163
+ this.log.error(`could not persist plugin state: ${errorMessage(e)}`);
2164
+ }
2165
+ }
2166
+ async dispose() {
2167
+ this.disposedFlag = true;
2168
+ const graph = this.graph();
2169
+ for (const id of [...graph.order].reverse()) await this.deactivate(id).catch(() => void 0);
2170
+ for (const id of this.records.keys()) await this.unload(id).catch(() => void 0);
2171
+ this.events.removeAll();
2172
+ }
2173
+ /* ------------------------------------------------- lazy activation events */
2174
+ /** Activates plugins whose manifest lists `event` (or `*`). Used internally and by hosts for custom events. */
2175
+ async trigger(event) {
2176
+ if (this.disposedFlag) return;
2177
+ const targets = [...this.records.values()].filter((r) => {
2178
+ if (!r.enabled || r.state === "ready" || r.state === "error" || r.state === "disabled") return false;
2179
+ if (r.inflight) return true;
2180
+ return (r.manifest.activationEvents ?? []).some((e) => e === event || this.eventMatches(e, event));
2181
+ });
2182
+ await Promise.all(targets.map((r) => this.activate(r.id).catch(() => void 0)));
2183
+ }
2184
+ eventMatches(declared, event) {
2185
+ const [kd, ...rd] = declared.split(":");
2186
+ const [ke, ...re] = event.split(":");
2187
+ if (kd !== ke) return false;
2188
+ const pd = rd.join(":");
2189
+ const pe = re.join(":");
2190
+ if (kd === "onRoute") return routePatternMatches(pd, pe);
2191
+ return matchesPattern(pd, pe);
2192
+ }
2193
+ wireLazyActivation() {
2194
+ this.commands.beforeExecute = (id) => this.trigger(`onCommand:${id}`);
2195
+ this.services.beforeResolve = (name) => this.trigger(`onService:${name}`);
2196
+ this.routes.beforeResolve = (path) => this.trigger(`onRoute:${path}`);
2197
+ this.slots.onSlotRendered = (slot) => void this.trigger(`onSlot:${slot}`);
2198
+ this.ipc.beforeInvoke = (name) => this.trigger(`onIPC:${name}`);
2199
+ this.ipc.assertCallerPermission = (caller, permission, channel) => this.permissions.assert(caller, permission, `IPC "${channel}"`);
2200
+ this.events.on("**", (_p, ctx) => {
2201
+ if (!ctx.name.startsWith("plugin.")) void this.trigger(`onEvent:${ctx.name}`);
2202
+ }, { priority: 1e3 });
2203
+ this.routes.onRouteRegistered = (route, owner) => {
2204
+ const menu = route.definition.meta?.menu;
2205
+ if (!menu || !owner) return;
2206
+ const meta = route.definition.meta;
2207
+ return this.menus.register(
2208
+ {
2209
+ id: `route:${route.fullPath}`,
2210
+ location: menu.location ?? "sidebar",
2211
+ label: menu.label ?? meta.title ?? route.fullPath,
2212
+ icon: meta.icon,
2213
+ path: route.fullPath,
2214
+ order: menu.order,
2215
+ group: menu.group
2216
+ },
2217
+ owner
2218
+ );
2219
+ };
2220
+ }
2221
+ /**
2222
+ * Before a plugin is active, its statically declared commands/menus (manifest `contributes`) are visible
2223
+ * as lightweight proxies: using one activates the plugin on demand. Replaced by the real registrations on activate.
2224
+ */
2225
+ installLazyProxies(rec) {
2226
+ if (rec.lazy.length) return;
2227
+ const c = rec.manifest.contributes ?? {};
2228
+ const owner = rec.id;
2229
+ for (const cmd of c.commands ?? []) {
2230
+ if (this.commands.has(cmd.id)) continue;
2231
+ rec.lazy.push(
2232
+ this.commands.register(
2233
+ {
2234
+ id: cmd.id,
2235
+ title: cmd.title,
2236
+ category: cmd.category,
2237
+ execute: async (...args) => {
2238
+ await this.activate(owner);
2239
+ return this.commands.execute(cmd.id, ...args);
2240
+ }
2241
+ },
2242
+ `${owner}#lazy`
2243
+ )
2244
+ );
2245
+ }
2246
+ for (const m of c.menus ?? []) {
2247
+ if (this.menus.has(`${m.location}:${m.id}`)) continue;
2248
+ rec.lazy.push(this.menus.register({ id: m.id, location: m.location, label: m.label, command: m.command }, `${owner}#lazy`));
2249
+ }
2250
+ }
2251
+ removeLazyProxies(rec) {
2252
+ for (const d of rec.lazy.splice(0)) d.dispose();
2253
+ }
2254
+ /* ------------------------------------------------- declarative contributions */
2255
+ async applyDeclarative(rec) {
2256
+ const def = rec.definition;
2257
+ const ctx = rec.context;
2258
+ const renderer = this.runtime === "renderer";
2259
+ const main = this.runtime === "main";
2260
+ for (const [id, component] of Object.entries(def.layouts ?? {})) if (renderer) ctx.routes.registerLayout(id, component);
2261
+ if (renderer) for (const r of def.routes ?? []) ctx.routes.register(r);
2262
+ for (const c of def.commands ?? []) ctx.commands.register(c);
2263
+ for (const m of def.menus ?? []) ctx.menus.register(m);
2264
+ for (const h of def.hooks ?? []) ctx.hooks.use(h.name, h.handler, { priority: h.priority });
2265
+ const services = typeof def.services === "function" ? def.services(ctx) : def.services;
2266
+ for (const [name, impl] of Object.entries(services ?? {})) ctx.services.register(name, impl);
2267
+ if (main) for (const i of def.ipc ?? []) ctx.ipc.define(i);
2268
+ for (const e of def.events ?? []) ctx.events.on(e.name, e.handler, { priority: e.priority, once: e.once });
2269
+ if (renderer) for (const c of def.components ?? []) ctx.ui.register(c);
2270
+ if (renderer) {
2271
+ for (const x of def.extensions ?? []) {
2272
+ try {
2273
+ ctx.ui.extend(x.point, { ...x.contribution, priority: x.priority });
2274
+ } catch (e) {
2275
+ ctx.logger.warn(`extension "${x.point}" skipped: ${errorMessage(e)}`);
2276
+ }
2277
+ }
2278
+ }
2279
+ if (renderer) for (const st of def.styles ?? []) ctx.styles.add(typeof st === "string" ? { css: st } : st);
2280
+ if (main) for (const [ns, members] of Object.entries(def.preload ?? {})) ctx.preload.expose(ns, members);
2281
+ }
2282
+ /* ------------------------------------------------------------- context */
2283
+ createContext(rec) {
2284
+ const id = rec.id;
2285
+ const store = () => rec.store;
2286
+ const track = (d) => store().add(d);
2287
+ const need = (permission, api) => this.permissions.assert(id, permission, api);
2288
+ const logger = this.logs.logger(id);
2289
+ const renderer = this.runtime === "renderer";
2290
+ const bridge = this.opts.ipcBridge;
2291
+ const def = rec.definition;
2292
+ const configDef = def.config ?? { schema: s.object({}) };
2293
+ const config = createPluginConfig(this.storageBackend, id, configDef, (next, prev) => {
2294
+ this.events.emit("plugin.config.changed", { id, next, prev }, id);
2295
+ this.changed();
2296
+ });
2297
+ rec.config = def.config ? config : void 0;
2298
+ const storage = createPluginStorage(this.storageBackend, id);
2299
+ const unsupported = (what) => {
2300
+ throw new FrameworkError(`${what} is not available in the ${this.runtime} process`, "FLUX_UNSUPPORTED_RUNTIME", id);
2301
+ };
2302
+ const ctx = {
2303
+ pluginId: id,
2304
+ manifest: rec.manifest,
2305
+ runtime: this.runtime,
2306
+ root: rec.source.root,
2307
+ logger,
2308
+ config,
2309
+ storage,
2310
+ state: this.stateOf(id),
2311
+ events: {
2312
+ on: (pattern, listener, opts) => (need("events", "events.on"), track(this.events.on(pattern, listener, opts))),
2313
+ once: (pattern, listener, opts) => (need("events", "events.once"), track(this.events.once(pattern, listener, opts))),
2314
+ emit: (name, payload, opts) => {
2315
+ need("events", "events.emit");
2316
+ const r = this.events.emit(name, payload, id);
2317
+ if (opts?.broadcast) this.opts.onBroadcastEvent?.(name, payload, id);
2318
+ return r;
2319
+ },
2320
+ emitAsync: (name, payload) => (need("events", "events.emitAsync"), this.events.emitAsync(name, payload, id))
2321
+ },
2322
+ commands: {
2323
+ register: (c) => (need("commands", "commands.register"), track(this.commands.register(c, id))),
2324
+ execute: (cid, ...args) => (need("commands", "commands.execute"), this.commands.execute(cid, ...args)),
2325
+ list: () => this.commands.list()
2326
+ },
2327
+ routes: {
2328
+ register: (r) => {
2329
+ need("routes", "routes.register");
2330
+ if (r.permission) need(r.permission, `route "${r.path}"`);
2331
+ return track(this.routes.register(r, id));
2332
+ },
2333
+ registerLayout: (lid, component) => (need("routes", "routes.registerLayout"), track(this.routes.registerLayout(lid, component)))
2334
+ },
2335
+ menus: { register: (m) => (need("menus", "menus.register"), track(this.menus.register(m, id))) },
2336
+ ui: {
2337
+ register: (c) => (need("ui", "ui.register"), track(this.slots.register(c, id, { strict: this.opts.strictSlots }))),
2338
+ extend: (point, contribution) => {
2339
+ need("ui", "ui.extend");
2340
+ const { mode, component, priority, ...data } = contribution;
2341
+ const parts = [];
2342
+ try {
2343
+ if (component !== void 0) parts.push(this.slots.register({ slot: point, component, mode: mode ?? "append", priority }, id));
2344
+ if (Object.keys(data).length) parts.push(this.extensionPoints.contribute(point, data, id, priority ?? 0));
2345
+ } catch (e) {
2346
+ for (const p of parts) p.dispose();
2347
+ throw e;
2348
+ }
2349
+ return track(toDisposable(() => parts.forEach((p) => p.dispose())));
2350
+ }
2351
+ },
2352
+ ipc: {
2353
+ handle: ((a, handler, opts) => {
2354
+ need("ipc", "ipc.handle");
2355
+ if (renderer) return unsupported("ipc.handle");
2356
+ return track(typeof a === "string" ? this.ipc.handle(a, handler, id, opts) : this.ipc.handle(a, handler, id));
2357
+ }),
2358
+ define: (d) => {
2359
+ need("ipc", "ipc.define");
2360
+ if (renderer) return unsupported("ipc.define");
2361
+ return track(this.ipc.register(d, id));
2362
+ },
2363
+ on: (name, handler) => {
2364
+ need("ipc", "ipc.on");
2365
+ if (renderer) return track(bridge ? bridge.on(name, (p) => handler(p, { pluginId: id, caller: "host" })) : toDisposable(() => {
2366
+ }));
2367
+ return track(this.ipc.on(name, handler, id));
2368
+ },
2369
+ send: (channel, payload) => {
2370
+ need("ipc", "ipc.send");
2371
+ if (renderer) bridge?.send(channel, payload, id);
2372
+ else this.ipc.send(channel, payload);
2373
+ },
2374
+ invoke: ((a, input) => {
2375
+ need("ipc", "ipc.invoke");
2376
+ const name = typeof a === "string" ? a : a.name;
2377
+ return renderer && bridge ? bridge.invoke(name, input, id) : this.ipc.invoke(name, input, { caller: id });
2378
+ })
2379
+ },
2380
+ preload: {
2381
+ expose: (ns, members) => {
2382
+ need("ipc", "preload.expose");
2383
+ if (renderer) return unsupported("preload.expose");
2384
+ return track(this.preload.expose(ns, members, id));
2385
+ }
2386
+ },
2387
+ services: {
2388
+ register: (name, impl) => (need("services", "services.register"), track(this.services.register(name, impl, id))),
2389
+ get: (name) => (need("services", "services.get"), this.services.resolve(name)),
2390
+ getAsync: (name) => (need("services", "services.getAsync"), this.services.resolveAsync(name))
2391
+ },
2392
+ hooks: {
2393
+ use: (name, handler, opts) => (need("hooks", "hooks.use"), track(this.hooks.use(name, handler, { ...opts, owner: id }))),
2394
+ run: (op, input, exec) => (need("hooks", "hooks.run"), this.hooks.run(op, input, exec)),
2395
+ filter: (name, value, extra) => (need("hooks", "hooks.filter"), this.hooks.filter(name, value, extra))
2396
+ },
2397
+ styles: { add: (st) => (need("styles", "styles.add"), track(this.styles.register(st, id))) },
2398
+ host: {
2399
+ has: (name) => this.hostApis.has(name),
2400
+ api: (name) => {
2401
+ const p = this.hostApis.get(name);
2402
+ if (!p) throw new FrameworkError(`Host API "${name}" is not provided by this application`, "FLUX_UNKNOWN_HOST_API", id);
2403
+ if (p.permission) need(p.permission, `host API "${name}"`);
2404
+ return p.factory({ pluginId: id, root: rec.source.root, manifest: rec.manifest });
2405
+ }
2406
+ },
2407
+ permissions: {
2408
+ has: (p) => this.permissions.has(id, p),
2409
+ assert: (p) => this.permissions.assert(id, p),
2410
+ list: () => this.permissions.snapshot(id)
2411
+ },
2412
+ lifecycle: {
2413
+ get state() {
2414
+ return rec.state;
2415
+ },
2416
+ onDeactivate: (fn) => void rec.deactivators.push(fn),
2417
+ subscriptions: { add: (d) => track(d) }
2418
+ }
2419
+ };
2420
+ return ctx;
2421
+ }
2422
+ };
2423
+ function routePatternMatches(pattern, path) {
2424
+ const p = normalizePath(pattern).split("/").filter(Boolean);
2425
+ const t = normalizePath(path).split("/").filter(Boolean);
2426
+ for (let i = 0; i < p.length; i++) {
2427
+ const seg = p[i];
2428
+ if (seg === "*") return true;
2429
+ if (t[i] === void 0) return false;
2430
+ if (seg.startsWith(":")) continue;
2431
+ if (seg !== t[i]) return false;
2432
+ }
2433
+ return p.length === t.length;
2434
+ }
2435
+
2436
+ // src/core/protocol.ts
2437
+ var CHANNELS = {
2438
+ invoke: "flux:invoke",
2439
+ send: "flux:send",
2440
+ event: "flux:event",
2441
+ bootstrap: "flux:bootstrap"
2442
+ };
2443
+ var MGMT = {
2444
+ list: "flux.plugins.list",
2445
+ enable: "flux.plugins.enable",
2446
+ disable: "flux.plugins.disable",
2447
+ reload: "flux.plugins.reload",
2448
+ grant: "flux.plugins.grant",
2449
+ revoke: "flux.plugins.revoke",
2450
+ uninstall: "flux.plugins.uninstall",
2451
+ install: "flux.plugins.install",
2452
+ update: "flux.plugins.update",
2453
+ rollback: "flux.plugins.rollback",
2454
+ search: "flux.plugins.search",
2455
+ updates: "flux.plugins.updates",
2456
+ configGet: "flux.plugins.config.get",
2457
+ configSet: "flux.plugins.config.set",
2458
+ configReset: "flux.plugins.config.reset",
2459
+ configSchema: "flux.plugins.config.schema",
2460
+ logs: "flux.plugins.logs",
2461
+ diagnostics: "flux.plugins.diagnostics",
2462
+ changed: "flux.plugins.changed"
2463
+ };
2464
+
2465
+ // src/core/api.ts
2466
+ function pluginApi(namespace) {
2467
+ const root = globalThis.plugins;
2468
+ const api = root?.[namespace];
2469
+ if (!api) throw new Error(`window.plugins.${namespace} is not available (plugin disabled, permission missing, or not exposed)`);
2470
+ return api;
2471
+ }
2472
+ function hasPluginApi(namespace) {
2473
+ return !!globalThis.plugins?.[namespace];
2474
+ }
2475
+
2476
+ export {
2477
+ toDisposable,
2478
+ DisposableStore,
2479
+ FrameworkError,
2480
+ PermissionDeniedError,
2481
+ LifecycleError,
2482
+ TimeoutError,
2483
+ HookCancelledError,
2484
+ withTimeout,
2485
+ LogStore,
2486
+ errorMessage,
2487
+ ValidationError,
2488
+ s,
2489
+ parseOrThrow,
2490
+ parseVersion,
2491
+ isValidVersion,
2492
+ compareVersions,
2493
+ isValidRange,
2494
+ satisfies,
2495
+ FRAMEWORK_VERSION,
2496
+ FRAMEWORK_ENGINE_KEY,
2497
+ BUILTIN_PERMISSIONS,
2498
+ menuLocations,
2499
+ manifestSchema,
2500
+ validateManifest,
2501
+ matchesPattern,
2502
+ EventBus,
2503
+ HookSystem,
2504
+ PermissionManager,
2505
+ Registry,
2506
+ CommandRegistry,
2507
+ MenuRegistry,
2508
+ ServiceRegistry,
2509
+ normalizePath,
2510
+ RouteRegistry,
2511
+ SlotRegistry,
2512
+ ExtensionPointRegistry,
2513
+ StyleRegistry,
2514
+ scopeCss,
2515
+ defineIpcContract,
2516
+ defineIPC,
2517
+ IpcError,
2518
+ serializeError,
2519
+ IpcRegistry,
2520
+ preloadMember,
2521
+ PreloadRegistry,
2522
+ MemoryStorageBackend,
2523
+ createPluginStorage,
2524
+ createPluginConfig,
2525
+ createPluginState,
2526
+ buildDependencyGraph,
2527
+ activationOrderFor,
2528
+ dependentsOf,
2529
+ describeIssue,
2530
+ definePlugin,
2531
+ defineRoute,
2532
+ defineCommand,
2533
+ defineService,
2534
+ defineHook,
2535
+ defineEvent,
2536
+ defineComponent,
2537
+ defineMenu,
2538
+ PluginManager,
2539
+ routePatternMatches,
2540
+ CHANNELS,
2541
+ MGMT,
2542
+ pluginApi,
2543
+ hasPluginApi
2544
+ };