fluxplugin 1.0.0 → 1.0.2

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 (63) hide show
  1. package/README.md +35 -10
  2. package/dist/{node/chunks/chunk-XPLGAEBH.js → browser/chunks/chunk-TL63576R.js} +106 -81
  3. package/dist/browser/chunks/{chunk-ZZ25CRMR.js → chunk-VXQJIDXW.js} +479 -6
  4. package/dist/browser/index.js +5 -1
  5. package/dist/browser/manager-ui.js +93 -18
  6. package/dist/browser/react.js +34 -4
  7. package/dist/cli.js +955 -318
  8. package/dist/node/chunks/chunk-3FH6NAJG.js +38 -0
  9. package/dist/node/chunks/{chunk-BZ7NZF7I.js → chunk-C7VWZZOX.js} +259 -44
  10. package/dist/{browser/chunks/chunk-Q75AOCXD.js → node/chunks/chunk-EVX2O65A.js} +185 -2
  11. package/dist/node/chunks/chunk-WSFCRVEQ.js +9 -0
  12. package/dist/node/chunks/tailwind-TJQVTBG5.js +9 -0
  13. package/dist/node/cli-api.js +663 -288
  14. package/dist/node/index.js +6 -1
  15. package/dist/node/main.js +19 -2
  16. package/dist/node/preload.js +2 -1
  17. package/dist/types/cli/build.d.ts +1 -1
  18. package/dist/types/cli/config.d.ts +3 -0
  19. package/dist/types/cli/host-files.generated.d.ts +2 -0
  20. package/dist/types/cli/host-templates.d.ts +3 -0
  21. package/dist/types/cli/host.d.ts +10 -1
  22. package/dist/types/cli/package.d.ts +18 -0
  23. package/dist/types/cli/publish.d.ts +23 -0
  24. package/dist/types/cli/store.d.ts +7 -0
  25. package/dist/types/cli/tailwind.d.ts +10 -0
  26. package/dist/types/cli/templates.d.ts +2 -0
  27. package/dist/types/core/index.d.ts +1 -0
  28. package/dist/types/core/manager.d.ts +2 -0
  29. package/dist/types/core/manifest.d.ts +9 -0
  30. package/dist/types/core/notifications.d.ts +48 -0
  31. package/dist/types/core/plugin.d.ts +7 -0
  32. package/dist/types/core/protocol.d.ts +27 -0
  33. package/dist/types/main/framework.d.ts +7 -1
  34. package/dist/types/main/github-registry.d.ts +49 -0
  35. package/dist/types/main/index.d.ts +2 -0
  36. package/dist/types/main/registry.d.ts +30 -0
  37. package/dist/types/main/stores.d.ts +29 -0
  38. package/dist/types/react/base-css.d.ts +1 -1
  39. package/dist/types/react/icons.d.ts +12 -0
  40. package/dist/types/react/index.d.ts +5 -0
  41. package/dist/types/react/markdown.d.ts +8 -0
  42. package/dist/types/react/os.d.ts +5 -0
  43. package/dist/types/react/runtime.d.ts +2 -1
  44. package/dist/types/react/theme.d.ts +53 -0
  45. package/dist/types/react/titlebar.d.ts +28 -0
  46. package/docs/CUSTOMIZING.md +257 -0
  47. package/docs/PLUGIN-AUTHORS.md +36 -9
  48. package/docs/RELEASE.md +1 -1
  49. package/docs/SECURITY.md +5 -0
  50. package/docs/STORE.md +123 -0
  51. package/docs/TROUBLESHOOTING.md +35 -0
  52. package/docs/screenshots/command-palette.png +0 -0
  53. package/docs/screenshots/dashboard.png +0 -0
  54. package/docs/screenshots/electron-app.png +0 -0
  55. package/docs/screenshots/electron-plugin-page.png +0 -0
  56. package/docs/screenshots/electron-titlebar.png +0 -0
  57. package/docs/screenshots/plugin-details.png +0 -0
  58. package/docs/screenshots/plugin-manager.png +0 -0
  59. package/docs/screenshots/plugin-video.png +0 -0
  60. package/docs/screenshots/stats.png +0 -0
  61. package/docs/screenshots/store-available.png +0 -0
  62. package/docs/screenshots/tailwind-dark.png +0 -0
  63. package/package.json +1 -2
@@ -0,0 +1,38 @@
1
+ // src/cli/tailwind.ts
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ function resolveTailwindCli(startDirs) {
7
+ for (const dir of startDirs) {
8
+ try {
9
+ const pj = createRequire(path.join(dir, "package.json")).resolve("@tailwindcss/cli/package.json");
10
+ const pkg = JSON.parse(readFileSync(pj, "utf8"));
11
+ const rel = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin ?? {})[0];
12
+ if (rel) return path.join(path.dirname(pj), rel);
13
+ } catch {
14
+ }
15
+ }
16
+ return void 0;
17
+ }
18
+ function compileTailwind(opts, log = console.log) {
19
+ const cli = resolveTailwindCli([opts.cwd, ...opts.searchFrom ?? [], process.cwd()]);
20
+ if (!cli || !existsSync(cli)) throw new Error('Tailwind is not installed: run "npm install -D tailwindcss @tailwindcss/cli"');
21
+ const args = [cli, "-i", opts.input, "-o", opts.output];
22
+ if (!opts.watch) {
23
+ const r = spawnSync(process.execPath, args, { cwd: opts.cwd, encoding: "utf8" });
24
+ if (r.status !== 0) throw new Error(`tailwind failed: ${(r.stderr || r.stdout || "").trim().split("\n").slice(-3).join(" ")}`);
25
+ log(`tailwind \u2192 ${path.relative(opts.cwd, opts.output)}`);
26
+ return () => {
27
+ };
28
+ }
29
+ const child = spawn(process.execPath, [...args, "--watch=always"], { cwd: opts.cwd, stdio: ["ignore", "ignore", "pipe"] });
30
+ child.stderr?.on("data", (d) => /error/i.test(String(d)) && log("tailwind: " + String(d).trim()));
31
+ log(`tailwind watching ${path.relative(opts.cwd, opts.input)}`);
32
+ return () => void child.kill();
33
+ }
34
+
35
+ export {
36
+ resolveTailwindCli,
37
+ compileTailwind
38
+ };
@@ -9,12 +9,13 @@ import {
9
9
  core_exports,
10
10
  errorMessage,
11
11
  serializeError,
12
- validateManifest
13
- } from "./chunk-XPLGAEBH.js";
12
+ validateManifest,
13
+ videoEmbedUrl
14
+ } from "./chunk-EVX2O65A.js";
14
15
 
15
16
  // src/main/framework.ts
16
- import { watch } from "node:fs";
17
- import path6 from "node:path";
17
+ import { promises as fsp, watch } from "node:fs";
18
+ import path7 from "node:path";
18
19
 
19
20
  // src/main/discover.ts
20
21
  import { promises as fs2 } from "node:fs";
@@ -341,7 +342,7 @@ async function extractTarGz(buf, dest) {
341
342
  }
342
343
 
343
344
  // src/main/registry.ts
344
- async function readManifest(dir) {
345
+ async function readPluginManifest(dir) {
345
346
  const raw = JSON.parse(await fs5.readFile(path5.join(dir, MANIFEST_FILE), "utf8"));
346
347
  const v = validateManifest(raw);
347
348
  if (!v.manifest) throw new Error(`invalid manifest: ${v.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
@@ -358,6 +359,34 @@ var toEntry = (m, location, source, sha256) => ({
358
359
  source,
359
360
  sha256
360
361
  });
362
+ var RUNTIME_DIRS = /* @__PURE__ */ new Set(["dist", "assets", "images", "docs"]);
363
+ var RUNTIME_FILES = /^(plugin\.json|readme(\.[a-z]+)?|license(\.[a-z]+)?|changelog(\.[a-z]+)?|icon\.(png|svg|jpg|webp))$/i;
364
+ var SOURCE_FILE = /\.(tsx?|jsx|map|scss|sass|less)$/i;
365
+ async function copyPluginRuntime(src, dest) {
366
+ const copied = [];
367
+ const manifest = JSON.parse(await fs5.readFile(path5.join(src, MANIFEST_FILE), "utf8"));
368
+ const referenced = new Set(
369
+ [manifest["main"], manifest["preload"], manifest["renderer"], manifest["icon"], manifest["banner"], manifest["readme"], ...manifest["styles"] ?? [], ...manifest["screenshots"] ?? []].filter((x) => typeof x === "string").map((x) => path5.normalize(x).replace(/^(\.\/|\.\\)/, ""))
370
+ );
371
+ const walk2 = async (rel) => {
372
+ for (const e of await fs5.readdir(path5.join(src, rel), { withFileTypes: true })) {
373
+ const r = rel ? path5.join(rel, e.name) : e.name;
374
+ if (e.isDirectory()) {
375
+ if (rel === "" ? RUNTIME_DIRS.has(e.name) : true) await walk2(r);
376
+ } else if (e.isFile()) {
377
+ const top = r.split(path5.sep)[0];
378
+ const ok = referenced.has(r) || (rel === "" ? RUNTIME_FILES.test(e.name) : RUNTIME_DIRS.has(top) && !SOURCE_FILE.test(e.name));
379
+ if (!ok || SOURCE_FILE.test(e.name) && !referenced.has(r)) continue;
380
+ await fs5.mkdir(path5.dirname(path5.join(dest, r)), { recursive: true });
381
+ await fs5.copyFile(path5.join(src, r), path5.join(dest, r));
382
+ copied.push(r.split(path5.sep).join("/"));
383
+ }
384
+ }
385
+ };
386
+ await fs5.mkdir(dest, { recursive: true });
387
+ await walk2("");
388
+ return copied;
389
+ }
361
390
  async function copyDir(src, dest) {
362
391
  await fs5.mkdir(dest, { recursive: true });
363
392
  for (const e of await fs5.readdir(src, { withFileTypes: true })) {
@@ -368,6 +397,37 @@ async function copyDir(src, dest) {
368
397
  else if (e.isFile()) await fs5.copyFile(s, d);
369
398
  }
370
399
  }
400
+ function entriesFromIndex(index, baseUrl, source, query = "") {
401
+ const q = query.toLowerCase();
402
+ return (index.plugins ?? []).filter((p) => p && typeof p.id === "string" && typeof p.version === "string" && typeof p.url === "string").filter((p) => !q || `${p.id} ${p.name ?? ""} ${p.description ?? ""}`.toLowerCase().includes(q)).map((p) => ({
403
+ id: p.id,
404
+ name: p.name ?? p.id,
405
+ version: p.version,
406
+ description: p.description,
407
+ author: p.author,
408
+ permissions: p.permissions ?? [],
409
+ location: new URL(p.url, baseUrl).href,
410
+ source,
411
+ sha256: p.sha256,
412
+ homepage: p.homepage,
413
+ icon: p.icon
414
+ }));
415
+ }
416
+ async function pruneNonRuntime(dir) {
417
+ const removed = [];
418
+ const walk2 = async (rel) => {
419
+ for (const e of await fs5.readdir(path5.join(dir, rel), { withFileTypes: true })) {
420
+ const r = rel ? path5.join(rel, e.name) : e.name;
421
+ const abs = path5.join(dir, r);
422
+ if (e.isDirectory()) {
423
+ if (e.name === "node_modules" || rel === "" && (e.name === "src" || e.name === ".git")) await fs5.rm(abs, { recursive: true, force: true }), removed.push(r + "/");
424
+ else await walk2(r);
425
+ } else if (/\.(map|tsx?|scss|sass|less)$/i.test(e.name) && !/\.d\.ts$/i.test(e.name)) await fs5.rm(abs, { force: true }), removed.push(r);
426
+ }
427
+ };
428
+ await walk2("");
429
+ return removed;
430
+ }
371
431
  var LocalRegistry = class {
372
432
  constructor(dir, name = `local:${dir}`) {
373
433
  this.dir = dir;
@@ -387,7 +447,7 @@ var LocalRegistry = class {
387
447
  for (const n of names.sort()) {
388
448
  const root = path5.join(this.dir, n);
389
449
  try {
390
- const { manifest } = await readManifest(root);
450
+ const { manifest } = await readPluginManifest(root);
391
451
  if (!q || `${manifest.id} ${manifest.name} ${manifest.description ?? ""}`.toLowerCase().includes(q)) out.push(toEntry(manifest, root, this.name));
392
452
  } catch {
393
453
  }
@@ -395,8 +455,8 @@ var LocalRegistry = class {
395
455
  return out;
396
456
  }
397
457
  async fetch(entry, dest) {
398
- await copyDir(entry.location, dest);
399
- return (await readManifest(dest)).manifest;
458
+ await copyPluginRuntime(entry.location, dest);
459
+ return (await readPluginManifest(dest)).manifest;
400
460
  }
401
461
  };
402
462
  var HttpRegistry = class {
@@ -409,21 +469,10 @@ var HttpRegistry = class {
409
469
  fetchImpl;
410
470
  name;
411
471
  async search(query = "") {
412
- const res = await this.fetchImpl(new URL("index.json", this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/"));
413
- if (!res.ok) throw new Error(`registry responded ${res.status}`);
414
- const j = await res.json();
415
- const q = query.toLowerCase();
416
- return (j.plugins ?? []).filter((p) => !q || `${p["id"]} ${p["name"]} ${p["description"] ?? ""}`.toLowerCase().includes(q)).map((p) => ({
417
- id: String(p["id"]),
418
- name: String(p["name"] ?? p["id"]),
419
- version: String(p["version"]),
420
- description: p["description"],
421
- author: p["author"],
422
- permissions: p["permissions"] ?? [],
423
- location: new URL(String(p["url"]), this.baseUrl).href,
424
- source: this.name,
425
- sha256: p["sha256"]
426
- }));
472
+ const base = this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/";
473
+ const res = await this.fetchImpl(new URL("index.json", base));
474
+ if (!res.ok) throw new Error(`store responded ${res.status}`);
475
+ return entriesFromIndex(await res.json(), base, this.name, query);
427
476
  }
428
477
  async fetch(entry, dest) {
429
478
  const res = await this.fetchImpl(entry.location);
@@ -433,7 +482,7 @@ var HttpRegistry = class {
433
482
  throw new Error("downloaded archive does not match the registry checksum");
434
483
  }
435
484
  await extractTarGz(buf, dest);
436
- return (await readManifest(dest)).manifest;
485
+ return (await readPluginManifest(dest)).manifest;
437
486
  }
438
487
  };
439
488
  var PluginInstaller = class {
@@ -456,7 +505,7 @@ var PluginInstaller = class {
456
505
  const out = [];
457
506
  for (const n of await fs5.readdir(this.pluginsDir).catch(() => [])) {
458
507
  try {
459
- out.push((await readManifest(path5.join(this.pluginsDir, n))).manifest);
508
+ out.push((await readPluginManifest(path5.join(this.pluginsDir, n))).manifest);
460
509
  } catch {
461
510
  }
462
511
  }
@@ -473,11 +522,12 @@ var PluginInstaller = class {
473
522
  await fs5.mkdir(path5.dirname(stage), { recursive: true });
474
523
  try {
475
524
  const manifest = await registry.fetch(entry, stage);
525
+ await pruneNonRuntime(stage);
476
526
  if (manifest.id !== entry.id) throw new Error(`registry entry "${entry.id}" contains plugin "${manifest.id}"`);
477
527
  const target = this.dirOf(entry.id);
478
528
  let previousVersion;
479
529
  try {
480
- previousVersion = (await readManifest(target)).manifest.version;
530
+ previousVersion = (await readPluginManifest(target)).manifest.version;
481
531
  const bk = path5.join(this.backupDir, entry.id, previousVersion);
482
532
  await fs5.rm(bk, { recursive: true, force: true });
483
533
  await fs5.mkdir(path5.dirname(bk), { recursive: true });
@@ -495,7 +545,7 @@ var PluginInstaller = class {
495
545
  /** Removes the plugin directory (backups are kept so the user can still roll back/reinstall). */
496
546
  async uninstall(id) {
497
547
  const dir = this.dirOf(id);
498
- const { manifest } = await readManifest(dir);
548
+ const { manifest } = await readPluginManifest(dir);
499
549
  const bk = path5.join(this.backupDir, id, manifest.version);
500
550
  await fs5.rm(bk, { recursive: true, force: true });
501
551
  await fs5.mkdir(path5.dirname(bk), { recursive: true });
@@ -513,7 +563,7 @@ var PluginInstaller = class {
513
563
  const dir = this.dirOf(id);
514
564
  let current;
515
565
  try {
516
- current = (await readManifest(dir)).manifest.version;
566
+ current = (await readPluginManifest(dir)).manifest.version;
517
567
  const bk = path5.join(this.backupDir, id, current);
518
568
  await fs5.rm(bk, { recursive: true, force: true });
519
569
  await fs5.rename(dir, bk);
@@ -530,19 +580,132 @@ var PluginInstaller = class {
530
580
  }
531
581
  };
532
582
 
583
+ // src/main/stores.ts
584
+ import path6 from "node:path";
585
+
586
+ // src/main/github-registry.ts
587
+ import crypto3 from "node:crypto";
588
+ var DEFAULT_TOPIC = "fluxplugin-plugin";
589
+ var MAX_PACKAGE_BYTES = 100 * 1024 * 1024;
590
+ var isLoopback = (h) => h === "127.0.0.1" || h === "localhost" || h === "[::1]";
591
+ function assertSafeUrl(u) {
592
+ const url = new URL(u);
593
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) throw new Error(`refusing non-https URL: ${u}`);
594
+ return url;
595
+ }
596
+ var GitHubRegistry = class {
597
+ constructor(o = {}) {
598
+ this.o = o;
599
+ this.name = o.name ?? (o.repo ? `github:${o.repo}` : `github:topic/${o.topic ?? DEFAULT_TOPIC}`);
600
+ }
601
+ o;
602
+ name;
603
+ cache = /* @__PURE__ */ new Map();
604
+ get api() {
605
+ return (this.o.apiBase ?? "https://api.github.com").replace(/\/$/, "");
606
+ }
607
+ get raw() {
608
+ return (this.o.rawBase ?? "https://raw.githubusercontent.com").replace(/\/$/, "");
609
+ }
610
+ trusted(url) {
611
+ const hosts = /* @__PURE__ */ new Set(["api.github.com", "github.com", "raw.githubusercontent.com", new URL(this.api).hostname, new URL(this.raw).hostname]);
612
+ return hosts.has(url.hostname);
613
+ }
614
+ async get(u, accept = "application/vnd.github+json") {
615
+ const url = assertSafeUrl(u);
616
+ const headers = { accept, "user-agent": "fluxplugin" };
617
+ if (this.o.token && this.trusted(url)) headers["authorization"] = `Bearer ${this.o.token}`;
618
+ const res = await (this.o.fetchImpl ?? fetch)(url, { headers, redirect: "follow", signal: AbortSignal.timeout(2e4) });
619
+ if (res.status === 403 || res.status === 429) {
620
+ if (res.headers.get("x-ratelimit-remaining") === "0" || res.status === 429) throw new Error("GitHub rate limit reached. Set GITHUB_TOKEN (any token, no scopes needed) to raise it, or retry later.");
621
+ }
622
+ if (!res.ok) throw new Error(`GitHub responded ${res.status} for ${url.pathname}`);
623
+ return res;
624
+ }
625
+ async json(u) {
626
+ return await (await this.get(u)).json();
627
+ }
628
+ async search(query = "") {
629
+ const hit = this.cache.get(query);
630
+ if (hit && Date.now() - hit.at < 6e4) return hit.entries;
631
+ const entries = this.o.repo ? await this.catalog(query) : await this.discover(query);
632
+ this.cache.set(query, { at: Date.now(), entries });
633
+ return entries;
634
+ }
635
+ async catalog(query) {
636
+ const ref = this.o.ref ?? "HEAD";
637
+ const base = `${this.raw}/${this.o.repo}/${ref}/`;
638
+ const index = await this.json(new URL(this.o.indexPath ?? "index.json", base).href);
639
+ return entriesFromIndex(index, base, this.name, query);
640
+ }
641
+ async discover(query) {
642
+ const q = `topic:${this.o.topic ?? DEFAULT_TOPIC}${query ? ` ${query} in:name,description,readme` : ""}`;
643
+ const found = await this.json(`${this.api}/search/repositories?q=${encodeURIComponent(q)}&per_page=30&sort=updated`);
644
+ const out = [];
645
+ const queue = [...found.items ?? []];
646
+ const worker = async () => {
647
+ for (let repo = queue.shift(); repo; repo = queue.shift()) {
648
+ try {
649
+ const rel = await this.json(`${this.api}/repos/${repo.full_name}/releases/latest`);
650
+ const descAsset = rel.assets?.find((a) => a.name.endsWith(".fluxplugin.json"));
651
+ if (!descAsset) continue;
652
+ const d = await (await this.get(descAsset.browser_download_url, "application/json")).json();
653
+ const pkg = rel.assets?.find((a) => a.name === d.file);
654
+ if (!d || typeof d.id !== "string" || typeof d.version !== "string" || !pkg || !/^[0-9a-f]{64}$/.test(d.sha256 ?? "")) continue;
655
+ out.push({ id: d.id, name: d.name ?? d.id, version: d.version, description: d.description, author: d.author, permissions: d.permissions ?? [], location: pkg.browser_download_url, source: this.name, sha256: d.sha256, homepage: repo.html_url, stars: repo.stargazers_count });
656
+ } catch (e) {
657
+ if (/rate limit/i.test(e.message)) throw e;
658
+ }
659
+ }
660
+ };
661
+ await Promise.all(Array.from({ length: 5 }, worker));
662
+ return out.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0) || a.id.localeCompare(b.id));
663
+ }
664
+ async fetch(entry, dest) {
665
+ const res = await this.get(entry.location, "application/octet-stream");
666
+ const len = Number(res.headers.get("content-length") ?? 0);
667
+ if (len > MAX_PACKAGE_BYTES) throw new Error("package is too large");
668
+ const buf = Buffer.from(await res.arrayBuffer());
669
+ if (buf.length > MAX_PACKAGE_BYTES) throw new Error("package is too large");
670
+ if (!entry.sha256) throw new Error("this store entry has no checksum; refusing to install an unverifiable package");
671
+ if (crypto3.createHash("sha256").update(buf).digest("hex") !== entry.sha256.replace(/^sha256-/, "")) throw new Error("downloaded package does not match the published checksum");
672
+ await extractTarGz(buf, dest);
673
+ return (await readPluginManifest(dest)).manifest;
674
+ }
675
+ };
676
+
677
+ // src/main/stores.ts
678
+ var DEFAULT_STORE = { type: "github", topic: DEFAULT_TOPIC };
679
+ function createRegistries(specs = [DEFAULT_STORE], opts = {}) {
680
+ const env = opts.env ?? process.env;
681
+ const token = env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
682
+ return specs.map((s) => {
683
+ switch (s.type) {
684
+ case "github":
685
+ return new GitHubRegistry({ ...s, token, fetchImpl: opts.fetchImpl });
686
+ case "http":
687
+ return new HttpRegistry(s.url, opts.fetchImpl);
688
+ case "local":
689
+ return new LocalRegistry(path6.resolve(opts.baseDir ?? process.cwd(), s.path));
690
+ default:
691
+ throw new Error(`unknown store type: ${JSON.stringify(s)}`);
692
+ }
693
+ });
694
+ }
695
+
533
696
  // src/main/framework.ts
534
697
  var MainFramework = class {
535
698
  constructor(options) {
536
699
  this.options = options;
537
700
  this.logs = new LogStore(5e3);
538
- this.state = new JsonStateFile(path6.join(options.dataDir, "plugins-state.json"));
701
+ this.state = new JsonStateFile(path7.join(options.dataDir, "plugins-state.json"));
539
702
  this.permissions = new PermissionManager({
540
703
  autoGrant: !options.requireApproval,
541
704
  onChange: (id, snap) => void this.state.update((s) => ({ approved: { ...s.approved, [id]: snap.granted } })).catch(() => void 0)
542
705
  });
543
706
  this.manager = new PluginManager({
544
707
  runtime: "main",
545
- storage: new FsStorageBackend(path6.join(options.dataDir, "storage")),
708
+ storage: new FsStorageBackend(path7.join(options.dataDir, "storage")),
546
709
  permissions: this.permissions,
547
710
  logs: this.logs,
548
711
  frameworkVersion: options.frameworkVersion,
@@ -554,11 +717,12 @@ var MainFramework = class {
554
717
  ...options.manager
555
718
  });
556
719
  this.manager.ipc.setTransport({ broadcast: (c, p, t) => this.broadcast(c, p, t) });
557
- this.installer = new PluginInstaller(options.installDir ?? options.pluginDirs[0], path6.join(options.dataDir, "backups"), options.registries ?? []);
720
+ this.installer = new PluginInstaller(options.installDir ?? options.pluginDirs[0], path7.join(options.dataDir, "backups"), options.registries ?? createRegistries(options.stores));
558
721
  this.manager.subscribe(() => {
559
722
  this.revision++;
560
723
  this.broadcast(MGMT.changed, { revision: this.revision });
561
724
  });
725
+ this.manager.notifications.onPush((n) => this.broadcast("flux.notification", n));
562
726
  this.registerManagementApi();
563
727
  }
564
728
  options;
@@ -612,13 +776,13 @@ var MainFramework = class {
612
776
  try {
613
777
  const w = watch(dir, { recursive: true }, (_evt, filename) => {
614
778
  if (!filename) return;
615
- const parts = String(filename).split(path6.sep);
779
+ const parts = String(filename).split(path7.sep);
616
780
  if (parts.includes("node_modules") || parts.includes("src")) return;
617
781
  const folder = parts[0];
618
782
  clearTimeout(this.reloadTimers.get(folder));
619
783
  this.reloadTimers.set(
620
784
  folder,
621
- setTimeout(() => void this.hotReloadFolder(path6.join(dir, folder)), 150)
785
+ setTimeout(() => void this.hotReloadFolder(path7.join(dir, folder)), 150)
622
786
  );
623
787
  });
624
788
  this.watchers.push(w);
@@ -728,6 +892,7 @@ var MainFramework = class {
728
892
  h(MGMT.configSchema, ({ id }) => this.manager.getConfig(id)?.describe() ?? null);
729
893
  h(MGMT.logs, ({ id }) => this.logs.query(id ? { source: id } : {}).slice(-200));
730
894
  h(MGMT.diagnostics, ({ id }) => this.manager.diagnostics(id));
895
+ h(MGMT.details, ({ id }) => this.details(id));
731
896
  h(MGMT.search, ({ query }) => this.installer.search(query));
732
897
  h(MGMT.updates, () => this.installer.updates());
733
898
  h(MGMT.install, async ({ entry }) => this.installAndLoad(entry));
@@ -745,6 +910,43 @@ var MainFramework = class {
745
910
  return r;
746
911
  });
747
912
  }
913
+ /** Store-page data for one plugin: icon, banner, screenshots, video and the README (Markdown). */
914
+ async details(id) {
915
+ const m = this.manager.manifest(id);
916
+ const root = this.manager.info(id).root;
917
+ const url = this.options.assetUrl ?? ((pid, rel) => `fluxplugin://${pid}/${rel.replace(/^\.\//, "")}`);
918
+ let readme;
919
+ if (root) {
920
+ const file = path7.resolve(root, m.readme ?? "README.md");
921
+ if (file.startsWith(path7.resolve(root) + path7.sep)) {
922
+ try {
923
+ const st = await fsp.stat(file);
924
+ if (st.size <= 2e5) readme = await fsp.readFile(file, "utf8");
925
+ } catch {
926
+ }
927
+ }
928
+ }
929
+ const author = typeof m.author === "string" ? m.author : m.author?.name;
930
+ const repository = typeof m.repository === "string" ? m.repository : m.repository?.url;
931
+ return {
932
+ id,
933
+ name: m.name,
934
+ version: m.version,
935
+ description: m.description,
936
+ author,
937
+ license: m.license,
938
+ homepage: m.homepage,
939
+ repository,
940
+ categories: m.categories ?? [],
941
+ keywords: m.keywords ?? [],
942
+ links: m.links ?? {},
943
+ iconUrl: m.icon ? url(id, m.icon) : void 0,
944
+ bannerUrl: m.banner ? url(id, m.banner) : void 0,
945
+ screenshotUrls: (m.screenshots ?? []).map((s) => url(id, s)),
946
+ video: m.video ? { url: m.video, embedUrl: videoEmbedUrl(m.video) } : void 0,
947
+ readme
948
+ };
949
+ }
748
950
  requireConfig(id) {
749
951
  const c = this.manager.getConfig(id);
750
952
  if (!c) throw new Error(`plugin "${id}" has no configuration (activate it first)`);
@@ -764,7 +966,7 @@ var MainFramework = class {
764
966
 
765
967
  // src/main/adapters.ts
766
968
  import http from "node:http";
767
- import path7 from "node:path";
969
+ import path8 from "node:path";
768
970
  import { promises as fs6 } from "node:fs";
769
971
  function attachElectron(fw, electron) {
770
972
  const { ipcMain, webContents } = electron;
@@ -798,6 +1000,11 @@ var MIME = {
798
1000
  ".svg": "image/svg+xml",
799
1001
  ".png": "image/png",
800
1002
  ".jpg": "image/jpeg",
1003
+ ".jpeg": "image/jpeg",
1004
+ ".webp": "image/webp",
1005
+ ".gif": "image/gif",
1006
+ ".avif": "image/avif",
1007
+ ".ico": "image/x-icon",
801
1008
  ".woff2": "font/woff2",
802
1009
  ".map": "application/json"
803
1010
  };
@@ -813,12 +1020,12 @@ async function resolvePluginAsset(fw, url) {
813
1020
  const root = fw.manager.info(id).root;
814
1021
  if (!root) return null;
815
1022
  const rel = decodeURIComponent(u.pathname).replace(/^\/+/, "");
816
- const file = path7.resolve(root, rel);
817
- if (!file.startsWith(path7.resolve(root) + path7.sep)) return null;
1023
+ const file = path8.resolve(root, rel);
1024
+ if (!file.startsWith(path8.resolve(root) + path8.sep)) return null;
818
1025
  const m = fw.manager.manifest(id);
819
- const main = m.main ? path7.resolve(root, m.main) : void 0;
820
- if (file === main || path7.basename(file) === "plugin.json") return null;
821
- const ext = path7.extname(file).toLowerCase();
1026
+ const main = m.main ? path8.resolve(root, m.main) : void 0;
1027
+ if (file === main || path8.basename(file) === "plugin.json") return null;
1028
+ const ext = path8.extname(file).toLowerCase();
822
1029
  if (!(ext in MIME)) return null;
823
1030
  try {
824
1031
  await fs6.access(file);
@@ -934,19 +1141,19 @@ function createLoopbackBridge(fw) {
934
1141
  }
935
1142
 
936
1143
  // src/main/serve-web.ts
937
- import path8 from "node:path";
1144
+ import path9 from "node:path";
938
1145
  import { promises as fs7 } from "node:fs";
939
1146
  var TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png" };
940
1147
  function serveWeb(fw, opts) {
941
- const dist = path8.resolve(opts.distDir);
1148
+ const dist = path9.resolve(opts.distDir);
942
1149
  return attachHttp(fw, {
943
1150
  port: opts.port ?? 5173,
944
1151
  host: opts.host,
945
1152
  fallback: async (req, res) => {
946
1153
  const rel = !req.url || req.url === "/" ? "index.html" : decodeURIComponent(req.url.split("?")[0]).replace(/^\/+/, "");
947
- const file = path8.resolve(dist, rel);
948
- const type = TYPES[path8.extname(file)];
949
- if (!type || !file.startsWith(dist + path8.sep)) return false;
1154
+ const file = path9.resolve(dist, rel);
1155
+ const type = TYPES[path9.extname(file)];
1156
+ if (!type || !file.startsWith(dist + path9.sep)) return false;
950
1157
  try {
951
1158
  res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
952
1159
  res.end(await fs7.readFile(file));
@@ -975,10 +1182,18 @@ export {
975
1182
  createTarGz,
976
1183
  readTarGz,
977
1184
  extractTarGz,
1185
+ readPluginManifest,
1186
+ copyPluginRuntime,
978
1187
  copyDir,
1188
+ entriesFromIndex,
1189
+ pruneNonRuntime,
979
1190
  LocalRegistry,
980
1191
  HttpRegistry,
981
1192
  PluginInstaller,
1193
+ DEFAULT_TOPIC,
1194
+ GitHubRegistry,
1195
+ DEFAULT_STORE,
1196
+ createRegistries,
982
1197
  MainFramework,
983
1198
  attachElectron,
984
1199
  PLUGIN_SCHEME,