fluxplugin 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/dist/{node/chunks/chunk-XPLGAEBH.js → browser/chunks/chunk-TL63576R.js} +106 -81
- package/dist/browser/chunks/{chunk-ZZ25CRMR.js → chunk-VXQJIDXW.js} +479 -6
- package/dist/browser/index.js +5 -1
- package/dist/browser/manager-ui.js +93 -18
- package/dist/browser/react.js +34 -4
- package/dist/cli.js +869 -174
- package/dist/node/chunks/chunk-3FH6NAJG.js +38 -0
- package/dist/node/chunks/{chunk-BZ7NZF7I.js → chunk-C7VWZZOX.js} +259 -44
- package/dist/{browser/chunks/chunk-Q75AOCXD.js → node/chunks/chunk-EVX2O65A.js} +185 -2
- package/dist/node/chunks/chunk-WSFCRVEQ.js +9 -0
- package/dist/node/chunks/tailwind-TJQVTBG5.js +9 -0
- package/dist/node/cli-api.js +562 -129
- package/dist/node/index.js +6 -1
- package/dist/node/main.js +19 -2
- package/dist/node/preload.js +2 -1
- package/dist/types/cli/build.d.ts +1 -1
- package/dist/types/cli/config.d.ts +3 -0
- package/dist/types/cli/host-files.generated.d.ts +1 -0
- package/dist/types/cli/host-templates.d.ts +3 -0
- package/dist/types/cli/host.d.ts +8 -0
- package/dist/types/cli/package.d.ts +18 -0
- package/dist/types/cli/publish.d.ts +23 -0
- package/dist/types/cli/store.d.ts +7 -0
- package/dist/types/cli/tailwind.d.ts +10 -0
- package/dist/types/cli/templates.d.ts +2 -0
- package/dist/types/core/index.d.ts +1 -0
- package/dist/types/core/manager.d.ts +2 -0
- package/dist/types/core/manifest.d.ts +9 -0
- package/dist/types/core/notifications.d.ts +48 -0
- package/dist/types/core/plugin.d.ts +7 -0
- package/dist/types/core/protocol.d.ts +27 -0
- package/dist/types/main/framework.d.ts +7 -1
- package/dist/types/main/github-registry.d.ts +49 -0
- package/dist/types/main/index.d.ts +2 -0
- package/dist/types/main/registry.d.ts +30 -0
- package/dist/types/main/stores.d.ts +29 -0
- package/dist/types/react/base-css.d.ts +1 -1
- package/dist/types/react/icons.d.ts +12 -0
- package/dist/types/react/index.d.ts +5 -0
- package/dist/types/react/markdown.d.ts +8 -0
- package/dist/types/react/os.d.ts +5 -0
- package/dist/types/react/runtime.d.ts +2 -1
- package/dist/types/react/theme.d.ts +53 -0
- package/dist/types/react/titlebar.d.ts +28 -0
- package/docs/CUSTOMIZING.md +257 -0
- package/docs/PLUGIN-AUTHORS.md +36 -9
- package/docs/RELEASE.md +1 -1
- package/docs/SECURITY.md +5 -0
- package/docs/STORE.md +123 -0
- package/docs/TROUBLESHOOTING.md +35 -0
- package/docs/screenshots/command-palette.png +0 -0
- package/docs/screenshots/dashboard.png +0 -0
- package/docs/screenshots/electron-plugin-page.png +0 -0
- package/docs/screenshots/electron-titlebar.png +0 -0
- package/docs/screenshots/plugin-details.png +0 -0
- package/docs/screenshots/plugin-manager.png +0 -0
- package/docs/screenshots/plugin-video.png +0 -0
- package/docs/screenshots/stats.png +0 -0
- package/docs/screenshots/store-available.png +0 -0
- package/docs/screenshots/tailwind-dark.png +0 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
399
|
-
return (await
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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],
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
817
|
-
if (!file.startsWith(
|
|
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 ?
|
|
820
|
-
if (file === main ||
|
|
821
|
-
const ext =
|
|
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
|
|
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 =
|
|
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 =
|
|
948
|
-
const type = TYPES[
|
|
949
|
-
if (!type || !file.startsWith(dist +
|
|
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,
|